Move to Edge runtime

This commit is contained in:
Catalin Pinte 2022-10-12 14:36:43 +03:00
parent cf1878e9f0
commit 483b608e0c
157 changed files with 3302 additions and 6728 deletions

View File

@ -47,14 +47,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@cfworker/uuid": "^1.12.4",
"@tsndr/cloudflare-worker-jwt": "^2.1.0",
"@vercel/commerce": "workspace:*", "@vercel/commerce": "workspace:*",
"@vercel/fetch": "^6.2.0",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"jsonwebtoken": "^8.5.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8"
"uuidv4": "^6.2.12",
"node-fetch": "^2.6.7"
}, },
"peerDependencies": { "peerDependencies": {
"next": "^12", "next": "^12",
@ -66,11 +65,10 @@
"@taskr/esnext": "^1.1.0", "@taskr/esnext": "^1.1.0",
"@taskr/watch": "^1.1.0", "@taskr/watch": "^1.1.0",
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"@types/jsonwebtoken": "^8.5.7",
"@types/lodash.debounce": "^4.0.6", "@types/lodash.debounce": "^4.0.6",
"@types/node": "^17.0.8", "@types/node": "^17.0.8",
"@types/react": "^18.0.14",
"@types/node-fetch": "^2.6.2", "@types/node-fetch": "^2.6.2",
"@types/react": "^18.0.14",
"lint-staged": "^12.1.7", "lint-staged": "^12.1.7",
"next": "^12.0.8", "next": "^12.0.8",
"prettier": "^2.5.1", "prettier": "^2.5.1",

View File

@ -1,16 +1,14 @@
// @ts-nocheck import type { CartEndpoint } from '.'
import type { BigcommerceCart } from '../../../types'
import { normalizeCart } from '../../../lib/normalize' import { normalizeCart } from '../../../lib/normalize'
import { parseCartItem } from '../../utils/parse-item' import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie' import getCartCookie from '../../utils/get-cart-cookie'
import type { CartEndpoint } from '.'
const addItem: CartEndpoint['handlers']['addItem'] = async ({ const addItem: CartEndpoint['handlers']['addItem'] = async ({
res,
body: { cartId, item }, body: { cartId, item },
config, config,
}) => { }) => {
if (!item.quantity) item.quantity = 1
const options = { const options = {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@ -20,22 +18,27 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
: {}), : {}),
}), }),
} }
const { data } = cartId const { data } = cartId
? await config.storeApiFetch( ? await config.storeApiFetch<{ data: BigcommerceCart }>(
`/v3/carts/${cartId}/items?include=line_items.physical_items.options,line_items.digital_items.options`, `/v3/carts/${cartId}/items?include=line_items.physical_items.options,line_items.digital_items.options`,
options options
) )
: await config.storeApiFetch( : await config.storeApiFetch<{ data: BigcommerceCart }>(
'/v3/carts?include=line_items.physical_items.options,line_items.digital_items.options', '/v3/carts?include=line_items.physical_items.options,line_items.digital_items.options',
options options
) )
// Create or update the cart cookie return {
res.setHeader( data: normalizeCart(data),
'Set-Cookie', headers: {
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge) 'Set-Cookie': getCartCookie(
) config.cartCookie,
res.status(200).json({ data: data ? normalizeCart(data) : null }) data.id,
config.cartCookieMaxAge
),
},
}
} }
export default addItem export default addItem

View File

@ -1,36 +1,41 @@
// @ts-nocheck import type { CartEndpoint } from '.'
import type { BigcommerceCart } from '../../../types'
import getCartCookie from '../../utils/get-cart-cookie'
import { normalizeCart } from '../../../lib/normalize' import { normalizeCart } from '../../../lib/normalize'
import { BigcommerceApiError } from '../../utils/errors' import { BigcommerceApiError } from '../../utils/errors'
import getCartCookie from '../../utils/get-cart-cookie'
import type { BigcommerceCart } from '../../../types'
import type { CartEndpoint } from '.'
// Return current cart info // Return current cart info
const getCart: CartEndpoint['handlers']['getCart'] = async ({ const getCart: CartEndpoint['handlers']['getCart'] = async ({
res,
body: { cartId }, body: { cartId },
config, config,
}) => { }) => {
let result: { data?: BigcommerceCart } = {}
if (cartId) { if (cartId) {
try { try {
result = await config.storeApiFetch( const result = await config.storeApiFetch<{
data?: BigcommerceCart
} | null>(
`/v3/carts/${cartId}?include=line_items.physical_items.options,line_items.digital_items.options` `/v3/carts/${cartId}?include=line_items.physical_items.options,line_items.digital_items.options`
) )
return {
data: result?.data ? normalizeCart(result.data) : null,
}
} catch (error) { } catch (error) {
if (error instanceof BigcommerceApiError && error.status === 404) { if (error instanceof BigcommerceApiError && error.status === 404) {
// Remove the cookie if it exists but the cart wasn't found return {
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie)) headers: { 'Set-Cookie': getCartCookie(config.cartCookie) },
}
} else { } else {
throw error throw error
} }
} }
} }
res.status(200).json({ return {
data: result.data ? normalizeCart(result.data) : null, data: null,
}) }
} }
export default getCart export default getCart

View File

@ -1,4 +1,4 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api' import { type GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart' import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
import type { CartSchema } from '@vercel/commerce/types/cart' import type { CartSchema } from '@vercel/commerce/types/cart'
import type { BigcommerceAPI } from '../..' import type { BigcommerceAPI } from '../..'
@ -19,7 +19,6 @@ export const handlers: CartEndpoint['handlers'] = {
} }
const cartApi = createEndpoint<CartAPI>({ const cartApi = createEndpoint<CartAPI>({
/* @ts-ignore */
handler: cartEndpoint, handler: cartEndpoint,
handlers, handlers,
}) })

View File

@ -1,9 +1,9 @@
import { normalizeCart } from '../../../lib/normalize'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartEndpoint } from '.' import type { CartEndpoint } from '.'
import { normalizeCart } from '../../../lib/normalize'
import getCartCookie from '../../utils/get-cart-cookie'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
res,
body: { cartId, itemId }, body: { cartId, itemId },
config, config,
}) => { }) => {
@ -11,18 +11,16 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`, `/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{ method: 'DELETE' } { method: 'DELETE' }
) )
const data = result?.data ?? null return {
data: result?.data ? normalizeCart(result.data) : null,
res.setHeader( headers: {
'Set-Cookie', 'Set-Cookie': result?.data
data ? // Update the cart cookie
? // Update the cart cookie getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) : // Remove the cart cookie if the cart was removed (empty items)
: // Remove the cart cookie if the cart was removed (empty items) getCartCookie(config.cartCookie),
getCartCookie(config.cartCookie) },
) }
res.status(200).json({ data: data ? normalizeCart(data) : null })
} }
export default removeItem export default removeItem

View File

@ -1,14 +1,15 @@
import type { CartEndpoint } from '.'
import type { BigcommerceCart } from '../../../types'
import { normalizeCart } from '../../../lib/normalize' import { normalizeCart } from '../../../lib/normalize'
import { parseCartItem } from '../../utils/parse-item' import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie' import getCartCookie from '../../utils/get-cart-cookie'
import type { CartEndpoint } from '.'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
res,
body: { cartId, itemId, item }, body: { cartId, itemId, item },
config, config,
}) => { }) => {
const { data } = await config.storeApiFetch<{ data?: any }>( const { data } = await config.storeApiFetch<{ data: BigcommerceCart }>(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`, `/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{ {
method: 'PUT', method: 'PUT',
@ -18,12 +19,16 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
} }
) )
// Update the cart cookie return {
res.setHeader( data: normalizeCart(data),
'Set-Cookie', headers: {
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) 'Set-Cookie': getCartCookie(
) config.cartCookie,
res.status(200).json({ data: normalizeCart(data) }) cartId,
config.cartCookieMaxAge
),
},
}
} }
export default updateItem export default updateItem

View File

@ -11,7 +11,6 @@ const LIMIT = 12
// Return current cart info // Return current cart info
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
res,
body: { search, categoryId, brandId, sort }, body: { search, categoryId, brandId, sort },
config, config,
commerce, commerce,
@ -73,7 +72,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
if (product) products.push(product) if (product) products.push(product)
}) })
res.status(200).json({ data: { products, found } }) return { data: { products, found } }
} }
export default getProducts export default getProducts

View File

@ -1,22 +1,21 @@
import type { CheckoutEndpoint } from '.' import type { CheckoutEndpoint } from '.'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import jwt from 'jsonwebtoken' import jwt from '@tsndr/cloudflare-worker-jwt'
import { uuid } from 'uuidv4' import { uuid } from '@cfworker/uuid'
import { NextResponse } from 'next/server'
const fullCheckout = true const fullCheckout = true
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req, req,
res,
config, config,
}) => { }) => {
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies.get(config.cartCookie)
const customerToken = cookies[config.customerCookie] const customerToken = cookies.get(config.customerCookie)
if (!cartId) { if (!cartId) {
res.redirect('/cart') return { redirectTo: '/cart' }
return
} }
const { data } = await config.storeApiFetch<any>( const { data } = await config.storeApiFetch<any>(
@ -31,8 +30,7 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
//if there is a customer create a jwt token //if there is a customer create a jwt token
if (!customerId) { if (!customerId) {
if (fullCheckout) { if (fullCheckout) {
res.redirect(data.checkout_url) return { redirectTo: data.checkout_url }
return
} }
} else { } else {
const dateCreated = Math.round(new Date().getTime() / 1000) const dateCreated = Math.round(new Date().getTime() / 1000)
@ -50,10 +48,9 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
algorithm: 'HS256', algorithm: 'HS256',
}) })
let checkouturl = `${config.storeUrl}/login/token/${token}` let checkouturl = `${config.storeUrl}/login/token/${token}`
console.log('checkouturl', checkouturl)
if (fullCheckout) { if (fullCheckout) {
res.redirect(checkouturl) return { redirectTo: checkouturl }
return
} }
} }
@ -83,10 +80,11 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
</html> </html>
` `
res.status(200) return new NextResponse(html, {
res.setHeader('Content-Type', 'text/html') headers: {
res.write(html) 'Content-Type': 'text/html',
res.end() },
})
} }
export default getCheckout export default getCheckout

View File

@ -1,5 +1,6 @@
import type { GetLoggedInCustomerQuery } from '../../../../schema' import type { GetLoggedInCustomerQuery } from '../../../../schema'
import type { CustomerEndpoint } from '.' import type { CustomerEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
export const getLoggedInCustomerQuery = /* GraphQL */ ` export const getLoggedInCustomerQuery = /* GraphQL */ `
query getLoggedInCustomer { query getLoggedInCustomer {
@ -25,29 +26,26 @@ export const getLoggedInCustomerQuery = /* GraphQL */ `
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']> export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
async ({ req, res, config }) => { async ({ req, config }) => {
const token = req.cookies[config.customerCookie] const token = req.cookies.get(config.customerCookie)
if (token) { if (token) {
const { data } = await config.fetch<GetLoggedInCustomerQuery>( const { data } = await config.fetch<GetLoggedInCustomerQuery>(
getLoggedInCustomerQuery, getLoggedInCustomerQuery,
undefined, undefined,
{ {
headers: { 'Set-Cookie': `${config.customerCookie}=${token}`,
cookie: `${config.customerCookie}=${token}`,
},
} }
) )
const { customer } = data const { customer } = data
if (!customer) { if (!customer) {
return res.status(400).json({ throw new CommerceAPIError('Customer not found', {
data: null, status: 404,
errors: [{ message: 'Customer not found', code: 'not_found' }],
}) })
} }
return res.status(200).json({ return {
data: { data: {
customer: { customer: {
id: String(customer.entityId), id: String(customer.entityId),
@ -59,10 +57,12 @@ const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
notes: customer.notes, notes: customer.notes,
}, },
}, },
}) }
} }
res.status(200).json({ data: null }) return {
data: null,
}
} }
export default getLoggedInCustomer export default getLoggedInCustomer

View File

@ -1,49 +1,36 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { LoginEndpoint } from '.' import type { LoginEndpoint } from '.'
import { NextResponse } from 'next/server'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
const invalidCredentials = /invalid credentials/i const invalidCredentials = /invalid credentials/i
const login: LoginEndpoint['handlers']['login'] = async ({ const login: LoginEndpoint['handlers']['login'] = async ({
res,
body: { email, password }, body: { email, password },
config, config,
commerce, commerce,
}) => { }) => {
// TODO: Add proper validations with something like Ajv
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// TODO: validate the password and email
// Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters.
try { try {
const res = new NextResponse(null)
await commerce.login({ variables: { email, password }, config, res }) await commerce.login({ variables: { email, password }, config, res })
return {
status: res.status,
headers: res.headers,
}
} catch (error) { } catch (error) {
// Check if the email and password didn't match an existing account // Check if the email and password didn't match an existing account
if ( if (error instanceof FetcherError) {
error instanceof FetcherError && throw new CommerceAPIError(
invalidCredentials.test(error.message) invalidCredentials.test(error.message)
) { ? 'Cannot find an account that matches the provided credentials'
return res.status(401).json({ : error.message,
data: null, { status: error.status || 401 }
errors: [ )
{ } else {
message: throw error
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
} }
throw error
} }
res.status(200).json({ data: null })
} }
export default login export default login

View File

@ -2,22 +2,24 @@ import { serialize } from 'cookie'
import type { LogoutEndpoint } from '.' import type { LogoutEndpoint } from '.'
const logout: LogoutEndpoint['handlers']['logout'] = async ({ const logout: LogoutEndpoint['handlers']['logout'] = async ({
res,
body: { redirectTo }, body: { redirectTo },
config, config,
}) => { }) => {
// Remove the cookie const headers = {
res.setHeader( 'Set-Cookie': serialize(config.customerCookie, '', {
'Set-Cookie', maxAge: -1,
serialize(config.customerCookie, '', { maxAge: -1, path: '/' }) path: '/',
) }),
// Only allow redirects to a relative URL
if (redirectTo?.startsWith('/')) {
res.redirect(redirectTo)
} else {
res.status(200).json({ data: null })
} }
return redirectTo
? {
redirectTo,
headers,
}
: {
headers,
}
} }
export default logout export default logout

View File

@ -1,23 +1,15 @@
import { BigcommerceApiError } from '../../utils/errors'
import type { SignupEndpoint } from '.' import type { SignupEndpoint } from '.'
import { NextResponse } from 'next/server'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { BigcommerceApiError } from '../../utils/errors'
const signup: SignupEndpoint['handlers']['signup'] = async ({ const signup: SignupEndpoint['handlers']['signup'] = async ({
res,
body: { firstName, lastName, email, password }, body: { firstName, lastName, email, password },
config, config,
commerce, commerce,
}) => { }) => {
// TODO: Add proper validations with something like Ajv
if (!(firstName && lastName && email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// TODO: validate the password and email
// Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters.
try { try {
await config.storeApiFetch('/v3/customers', { await config.storeApiFetch('/v3/customers', {
method: 'POST', method: 'POST',
@ -35,28 +27,26 @@ const signup: SignupEndpoint['handlers']['signup'] = async ({
} catch (error) { } catch (error) {
if (error instanceof BigcommerceApiError && error.status === 422) { if (error instanceof BigcommerceApiError && error.status === 422) {
const hasEmailError = '0.email' in error.data?.errors const hasEmailError = '0.email' in error.data?.errors
// If there's an error with the email, it most likely means it's duplicated // If there's an error with the email, it most likely means it's duplicated
if (hasEmailError) { if (hasEmailError) {
return res.status(400).json({ throw new CommerceAPIError('Email already in use', {
data: null, status: 400,
errors: [ code: 'duplicated_email',
{
message: 'The email is already in use',
code: 'duplicated_email',
},
],
}) })
} }
} else {
throw error
} }
throw error
} }
const res = new NextResponse()
// Login the customer right after creating it // Login the customer right after creating it
await commerce.login({ variables: { email, password }, res, config }) await commerce.login({ variables: { email, password }, res, config })
res.status(200).json({ data: null }) return {
headers: res.headers,
}
} }
export default signup export default signup

View File

@ -1,67 +1,53 @@
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import { parseWishlistItem } from '../../utils/parse-item' import { parseWishlistItem } from '../../utils/parse-item'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
res,
body: { customerToken, item }, body: { customerToken, item },
config, config,
commerce, commerce,
}) => { }) => {
if (!item) { const customerId =
return res.status(400).json({ customerToken && (await getCustomerId({ customerToken, config }))
data: null,
errors: [{ message: 'Missing item' }], if (!customerId) {
}) throw new Error('Invalid request. No CustomerId')
} }
try { let { wishlist } = await commerce.getCustomerWishlist({
const customerId = variables: { customerId },
customerToken && (await getCustomerId({ customerToken, config })) config,
})
if (!customerId) { if (!wishlist) {
throw new Error('Invalid request. No CustomerId') // If user has no wishlist, then let's create one with new item
} const { data } = await config.storeApiFetch<any>('/v3/wishlists', {
method: 'POST',
let { wishlist } = await commerce.getCustomerWishlist({ body: JSON.stringify({
variables: { customerId }, name: 'Next.js Commerce Wishlist',
config, is_public: false,
customer_id: Number(customerId),
items: [parseWishlistItem(item)],
}),
}) })
return {
if (!wishlist) { data,
// If user has no wishlist, then let's create one with new item
const { data } = await config.storeApiFetch('/v3/wishlists', {
method: 'POST',
body: JSON.stringify({
name: 'Next.js Commerce Wishlist',
is_public: false,
customer_id: Number(customerId),
items: [parseWishlistItem(item)],
}),
})
return res.status(200).json(data)
} }
// Existing Wishlist, let's add Item to Wishlist
const { data } = await config.storeApiFetch(
`/v3/wishlists/${wishlist.id}/items`,
{
method: 'POST',
body: JSON.stringify({
items: [parseWishlistItem(item)],
}),
}
)
// Returns Wishlist
return res.status(200).json(data)
} catch (err: any) {
res.status(500).json({
data: null,
errors: [{ message: err.message }],
})
} }
// Existing Wishlist, let's add Item to Wishlist
const { data } = await config.storeApiFetch<any>(
`/v3/wishlists/${wishlist.id}/items`,
{
method: 'POST',
body: JSON.stringify({
items: [parseWishlistItem(item)],
}),
}
)
// Returns Wishlist
return { data }
} }
export default addItem export default addItem

View File

@ -1,10 +1,10 @@
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import type { Wishlist } from '@vercel/commerce/types/wishlist' import type { Wishlist } from '@vercel/commerce/types/wishlist'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
// Return wishlist info // Return wishlist info
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
res,
body: { customerToken, includeProducts }, body: { customerToken, includeProducts },
config, config,
commerce, commerce,
@ -16,11 +16,7 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
customerToken && (await getCustomerId({ customerToken, config })) customerToken && (await getCustomerId({ customerToken, config }))
if (!customerId) { if (!customerId) {
// If the customerToken is invalid, then this request is too throw new CommerceAPIError('Wishlist not found', { status: 404 })
return res.status(404).json({
data: null,
errors: [{ message: 'Wishlist not found' }],
})
} }
const { wishlist } = await commerce.getCustomerWishlist({ const { wishlist } = await commerce.getCustomerWishlist({
@ -32,7 +28,7 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
result = { data: wishlist } result = { data: wishlist }
} }
res.status(200).json({ data: result.data ?? null }) return { data: result.data ?? null }
} }
export default getWishlist export default getWishlist

View File

@ -1,10 +1,10 @@
import type { Wishlist } from '@vercel/commerce/types/wishlist' import type { Wishlist } from '@vercel/commerce/types/wishlist'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
// Return wishlist info // Return wishlist info
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
res,
body: { customerToken, itemId }, body: { customerToken, itemId },
config, config,
commerce, commerce,
@ -20,10 +20,7 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
{} {}
if (!wishlist || !itemId) { if (!wishlist || !itemId) {
return res.status(400).json({ throw new CommerceAPIError('Wishlist not found', { status: 400 })
data: null,
errors: [{ message: 'Invalid request' }],
})
} }
const result = await config.storeApiFetch<{ data: Wishlist } | null>( const result = await config.storeApiFetch<{ data: Wishlist } | null>(
@ -32,7 +29,7 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
) )
const data = result?.data ?? null const data = result?.data ?? null
res.status(200).json({ data }) return { data }
} }
export default removeItem export default removeItem

View File

@ -1,4 +1,3 @@
import type { RequestInit } from '@vercel/fetch'
import { import {
CommerceAPI, CommerceAPI,
CommerceAPIConfig, CommerceAPIConfig,
@ -35,7 +34,14 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
storeUrl?: string storeUrl?: string
storeApiClientSecret?: string storeApiClientSecret?: string
storeHash?: string storeHash?: string
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T> storeApiFetch<T>(
endpoint: string,
options?: {
method?: string
body?: any
headers?: HeadersInit
}
): Promise<T>
} }
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL // GraphAPI const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL // GraphAPI

View File

@ -1,4 +1,3 @@
import type { ServerResponse } from 'http'
import type { import type {
OperationContext, OperationContext,
OperationOptions, OperationOptions,
@ -8,6 +7,7 @@ import type { LoginMutation } from '../../../schema'
import type { RecursivePartial } from '../utils/types' import type { RecursivePartial } from '../utils/types'
import concatHeader from '../utils/concat-cookie' import concatHeader from '../utils/concat-cookie'
import type { BigcommerceConfig, Provider } from '..' import type { BigcommerceConfig, Provider } from '..'
import type { NextResponse } from 'next/server'
export const loginMutation = /* GraphQL */ ` export const loginMutation = /* GraphQL */ `
mutation login($email: String!, $password: String!) { mutation login($email: String!, $password: String!) {
@ -23,14 +23,14 @@ export default function loginOperation({
async function login<T extends LoginOperation>(opts: { async function login<T extends LoginOperation>(opts: {
variables: T['variables'] variables: T['variables']
config?: BigcommerceConfig config?: BigcommerceConfig
res: ServerResponse res: NextResponse
}): Promise<T['data']> }): Promise<T['data']>
async function login<T extends LoginOperation>( async function login<T extends LoginOperation>(
opts: { opts: {
variables: T['variables'] variables: T['variables']
config?: BigcommerceConfig config?: BigcommerceConfig
res: ServerResponse res: NextResponse
} & OperationOptions } & OperationOptions
): Promise<T['data']> ): Promise<T['data']>
@ -42,7 +42,7 @@ export default function loginOperation({
}: { }: {
query?: string query?: string
variables: T['variables'] variables: T['variables']
res: ServerResponse res: NextResponse
config?: BigcommerceConfig config?: BigcommerceConfig
}): Promise<T['data']> { }): Promise<T['data']> {
config = commerce.getConfig(config) config = commerce.getConfig(config)
@ -64,10 +64,15 @@ export default function loginOperation({
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax') cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
} }
response.setHeader( const prevCookie = response.headers.get('Set-Cookie')
'Set-Cookie', const newCookie = concatHeader(prevCookie, cookie)
concatHeader(response.getHeader('Set-Cookie'), cookie)!
) if (newCookie) {
res.headers.set(
'Set-Cookie',
String(Array.isArray(newCookie) ? newCookie.join(',') : newCookie)
)
}
} }
return { return {

View File

@ -1,4 +1,4 @@
type Header = string | number | string[] | undefined type Header = string | number | string[] | undefined | null
export default function concatHeader(prev: Header, val: Header) { export default function concatHeader(prev: Header, val: Header) {
if (!val) return prev if (!val) return prev

View File

@ -1,5 +1,3 @@
import type { Response } from '@vercel/fetch'
// Used for GraphQL errors // Used for GraphQL errors
export class BigcommerceGraphQLError extends Error {} export class BigcommerceGraphQLError extends Error {}

View File

@ -1,19 +1,22 @@
import { FetcherError } from '@vercel/commerce/utils/errors' import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api' import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { BigcommerceConfig } from '../index' import type { BigcommerceConfig } from '../index'
import fetch from './fetch'
const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher = const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
(getConfig) => (getConfig) =>
async (query: string, { variables, preview } = {}, fetchOptions) => { async (
query: string,
{ variables, preview } = {},
options: { headers?: HeadersInit } = {}
): Promise<any> => {
// log.warn(query) // log.warn(query)
const config = getConfig() const config = getConfig()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${config.apiToken}`, Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers, ...options.headers,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({

View File

@ -1,11 +1,16 @@
import type { FetchOptions, Response } from '@vercel/fetch'
import type { BigcommerceConfig } from '../index' import type { BigcommerceConfig } from '../index'
import { BigcommerceApiError, BigcommerceNetworkError } from './errors' import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
import fetch from './fetch'
const fetchStoreApi = const fetchStoreApi =
<T>(getConfig: () => BigcommerceConfig) => <T>(getConfig: () => BigcommerceConfig) =>
async (endpoint: string, options?: FetchOptions): Promise<T> => { async (
endpoint: string,
options?: {
method?: string
body?: any
headers?: HeadersInit
}
): Promise<T> => {
const config = getConfig() const config = getConfig()
let res: Response let res: Response

View File

@ -20,9 +20,7 @@ async function getCustomerId({
getCustomerIdQuery, getCustomerIdQuery,
undefined, undefined,
{ {
headers: { 'Set-Cookie': `${config.customerCookie}=${customerToken}`,
cookie: `${config.customerCookie}=${customerToken}`,
},
} }
) )

View File

@ -33,7 +33,6 @@ export const handler: MutationHook<AddItemHook> = {
({ fetch }) => ({ fetch }) =>
() => { () => {
const { mutate } = useCart() const { mutate } = useCart()
return useCallback( return useCallback(
async function addItem(input) { async function addItem(input) {
const data = await fetch({ input }) const data = await fetch({ input })

View File

@ -1,4 +1,7 @@
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce' import {
getCommerceProvider,
useCommerce as useCoreCommerce,
} from '@vercel/commerce'
import { bigcommerceProvider, BigcommerceProvider } from './provider' import { bigcommerceProvider, BigcommerceProvider } from './provider'
export { bigcommerceProvider } export { bigcommerceProvider }

View File

@ -3,9 +3,9 @@ import type { Product } from '@vercel/commerce/types/product'
import type { Cart, LineItem } from '@vercel/commerce/types/cart' import type { Cart, LineItem } from '@vercel/commerce/types/cart'
import type { Category, Brand } from '@vercel/commerce/types/site' import type { Category, Brand } from '@vercel/commerce/types/site'
import type { BigcommerceCart, BCCategory, BCBrand } from '../types' import type { BigcommerceCart, BCCategory, BCBrand } from '../types'
import type { ProductNode } from '../api/operations/get-all-products'
import type { definitions } from '../api/definitions/store-content'
import { definitions } from '../api/definitions/store-content'
import update from './immutability'
import getSlug from './get-slug' import getSlug from './get-slug'
function normalizeProductOption(productOption: any) { function normalizeProductOption(productOption: any) {
@ -20,55 +20,44 @@ function normalizeProductOption(productOption: any) {
} }
} }
export function normalizeProduct(productNode: any): Product { export function normalizeProduct(productNode: ProductNode): Product {
const { const {
entityId: id, entityId: id,
productOptions, productOptions,
prices, prices,
path, path,
id: _, images,
options: _0, variants,
} = productNode } = productNode
return update(productNode, { return {
id: { $set: String(id) }, id: String(id),
images: { name: productNode.name,
$apply: ({ edges }: any) => description: productNode.description,
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({ images:
url: urlOriginal, images.edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
alt: altText, url: urlOriginal,
...rest, alt: altText,
})), ...rest,
}, })) || [],
variants: { path: `/${getSlug(path)}`,
$apply: ({ edges }: any) => variants:
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({ variants.edges?.map(
({ node: { entityId, productOptions, ...rest } }: any) => ({
id: String(entityId), id: String(entityId),
options: productOptions?.edges options: productOptions?.edges
? productOptions.edges.map(normalizeProductOption) ? productOptions.edges.map(normalizeProductOption)
: [], : [],
...rest, ...rest,
})), })
}, ) || [],
options: { options: productOptions?.edges?.map(normalizeProductOption) || [],
$set: productOptions.edges slug: path?.replace(/^\/+|\/+$/g, ''),
? productOptions?.edges.map(normalizeProductOption)
: [],
},
brand: {
$apply: (brand: any) => (brand?.id ? brand.id : null),
},
slug: {
$set: path?.replace(/^\/+|\/+$/g, ''),
},
price: { price: {
$set: { value: prices?.price.value,
value: prices?.price.value, currencyCode: prices?.price.currencyCode,
currencyCode: prices?.price.currencyCode,
},
}, },
$unset: ['entityId'], }
})
} }
export function normalizePage(page: definitions['page_Full']): Page { export function normalizePage(page: definitions['page_Full']): Page {
@ -122,7 +111,7 @@ function normalizeLineItem(item: any): LineItem {
listPrice: item.list_price, listPrice: item.list_price,
}, },
options: item.options, options: item.options,
path: item.url.split('/')[3], path: `/${item.url.split('/')[3]}`,
discounts: item.discounts.map((discount: any) => ({ discounts: item.discounts.map((discount: any) => ({
value: discount.discounted_amount, value: discount.discounted_amount,
})), })),

View File

@ -47,11 +47,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@vercel/fetch": "^6.2.0", "@vercel/edge": "^0.0.4",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"import-cwd": "^3.0.0", "import-cwd": "^3.0.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"node-fetch": "^2.6.7",
"swr": "^1.3.0", "swr": "^1.3.0",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },

View File

@ -1,54 +1,63 @@
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { CartSchema } from '../../types/cart' import type { CartSchema } from '../../types/cart'
import parse from '../utils/parse-output'
import validateHandlers from '../utils/validate-handlers' import validateHandlers from '../utils/validate-handlers'
import { getInput } from '../utils'
import { import {
getCartBodySchema, getCartBodySchema,
addItemBodySchema, addItemBodySchema,
updateItemBodySchema, updateItemBodySchema,
removeItemBodySchema, removeItemBodySchema,
cartSchema,
} from '../../schemas/cart' } from '../../schemas/cart'
const cartEndpoint: GetAPISchema< const cartEndpoint: GetAPISchema<
any, any,
CartSchema CartSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx const { req, handlers, config } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['getCart'], GET: handlers['getCart'],
POST: handlers['addItem'], POST: handlers['addItem'],
PUT: handlers['updateItem'], PUT: handlers['updateItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
const input = await getInput(req)
let output
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies.get(config.cartCookie)
// Return current cart info // Return current cart info
if (req.method === 'GET') { if (req.method === 'GET') {
const body = getCartBodySchema.parse({ cartId }) const body = getCartBodySchema.parse({ cartId })
return handlers['getCart']({ ...ctx, body }) output = await handlers['getCart']({ ...ctx, body })
} }
// Create or add an item to the cart // Create or add an item to the cart
if (req.method === 'POST') { if (req.method === 'POST') {
const body = addItemBodySchema.parse({ ...req.body, cartId }) const body = addItemBodySchema.parse({ ...input, cartId })
return handlers['addItem']({ ...ctx, body }) output = await handlers['addItem']({ ...ctx, body })
} }
// Update item in cart // Update item in cart
if (req.method === 'PUT') { if (req.method === 'PUT') {
const body = updateItemBodySchema.parse({ ...req.body, cartId }) const body = updateItemBodySchema.parse({ ...input, cartId })
return handlers['updateItem']({ ...ctx, body }) output = await handlers['updateItem']({ ...ctx, body })
} }
// Remove an item from the cart // Remove an item from the cart
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = removeItemBodySchema.parse({ ...req.body, cartId }) const body = removeItemBodySchema.parse({ ...input, cartId })
return handlers['removeItem']({ ...ctx, body }) return await handlers['removeItem']({ ...ctx, body })
} }
return output ? parse(output, cartSchema.nullish()) : { status: 405 }
} }
export default cartEndpoint export default cartEndpoint

View File

@ -1,25 +1,38 @@
import { searchProductBodySchema } from '../../../schemas/product'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import type { ProductsSchema } from '../../../types/product' import type { ProductsSchema } from '../../../types/product'
import validateHandlers from '../../utils/validate-handlers' import validateHandlers from '../../utils/validate-handlers'
import {
searchProductBodySchema,
searchProductsSchema,
} from '../../../schemas/product'
import parse from '../../utils/parse-output'
const productsEndpoint: GetAPISchema< const productsEndpoint: GetAPISchema<
any, any,
ProductsSchema ProductsSchema
>['endpoint']['handler'] = (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx const { req, handlers } = ctx
validateHandlers(req, res, { GET: handlers['getProducts'] }) validateHandlers(req, { GET: handlers['getProducts'] })
const { searchParams } = new URL(req.url)
const body = searchProductBodySchema.parse({ const body = searchProductBodySchema.parse({
search: req.query.search, search: searchParams.get('search') ?? undefined,
categoryId: req.query.categoryId, categoryId: searchParams.get('categoryId') ?? undefined,
brandId: req.query.brandId, brandId: searchParams.get('brandId') ?? undefined,
sort: req.query.sort, sort: searchParams.get('sort') ?? undefined,
}) })
return handlers['getProducts']({ ...ctx, body }) const res = await handlers['getProducts']({ ...ctx, body })
res.headers = {
'Cache-Control':
'max-age=0, s-maxage=3600, stale-while-revalidate=60, public',
...res.headers,
}
return parse(res, searchProductsSchema)
} }
export default productsEndpoint export default productsEndpoint

View File

@ -2,37 +2,46 @@ import type { GetAPISchema } from '..'
import type { CheckoutSchema } from '../../types/checkout' import type { CheckoutSchema } from '../../types/checkout'
import { import {
checkoutSchema,
getCheckoutBodySchema, getCheckoutBodySchema,
submitCheckoutBodySchema, submitCheckoutBodySchema,
} from '../../schemas/checkout' } from '../../schemas/checkout'
import validateHandlers from '../utils/validate-handlers' import validateHandlers from '../utils/validate-handlers'
import parse from '../utils/parse-output'
import { z } from 'zod'
import { getInput } from '../utils'
const checkoutEndpoint: GetAPISchema< const checkoutEndpoint: GetAPISchema<
any, any,
CheckoutSchema CheckoutSchema
>['endpoint']['handler'] = (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx const { req, handlers, config } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['getCheckout'], GET: handlers['getCheckout'],
POST: handlers['submitCheckout'], POST: handlers['submitCheckout'],
}) })
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies.get(config.cartCookie)!
const input = await getInput(req)
// Get checkout // Get checkout
if (req.method === 'GET') { if (req.method === 'GET') {
const body = getCheckoutBodySchema.parse({ ...req.body, cartId }) const body = getCheckoutBodySchema.parse({ ...input, cartId })
return handlers['getCheckout']({ ...ctx, body }) const res = await handlers['getCheckout']({ ...ctx, body })
return parse(res, checkoutSchema.optional().or(z.string()))
} }
// Create checkout // Create checkout
if (req.method === 'POST' && handlers['submitCheckout']) { if (req.method === 'POST' && handlers['submitCheckout']) {
const body = submitCheckoutBodySchema.parse({ ...req.body, cartId }) const body = submitCheckoutBodySchema.parse({ ...input, cartId })
return handlers['submitCheckout']({ ...ctx, body }) const res = await handlers['submitCheckout']({ ...ctx, body })
return parse(res, checkoutSchema.optional())
} }
return { status: 405 }
} }
export default checkoutEndpoint export default checkoutEndpoint

View File

@ -1,55 +1,69 @@
import type { CustomerAddressSchema } from '../../../types/customer/address' import type { CustomerAddressSchema } from '../../../types/customer/address'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import parse from '../../utils/parse-output'
import validateHandlers from '../../utils/validate-handlers' import validateHandlers from '../../utils/validate-handlers'
import { import {
addAddressBodySchema, addAddressBodySchema,
addressSchema,
deleteAddressBodySchema, deleteAddressBodySchema,
updateAddressBodySchema, updateAddressBodySchema,
} from '../../../schemas/customer' } from '../../../schemas/customer'
import { getInput } from '../../utils'
import { getCartBodySchema } from '../../../schemas/cart' import { getCartBodySchema } from '../../../schemas/cart'
// create a function that returns a function
const customerShippingEndpoint: GetAPISchema< const customerShippingEndpoint: GetAPISchema<
any, any,
CustomerAddressSchema CustomerAddressSchema
>['endpoint']['handler'] = (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx const { req, handlers, config } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['getAddresses'], GET: handlers['getAddresses'],
POST: handlers['addItem'], POST: handlers['addItem'],
PUT: handlers['updateItem'], PUT: handlers['updateItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
let output
const input = await getInput(req)
const { cookies } = req const { cookies } = req
// Cart id might be usefull for anonymous shopping // Cart id might be usefull for anonymous shopping
const cartId = cookies[config.cartCookie] const cartId = cookies.get(config.cartCookie)
// Return customer addresses // Return customer addresses
if (req.method === 'GET') { if (req.method === 'GET') {
const body = getCartBodySchema.parse({ cartId }) const body = getCartBodySchema.parse({ cartId })
return handlers['getAddresses']({ ...ctx, body }) return parse(
await handlers['getAddresses']({ ...ctx, body }),
addressSchema
)
} }
// Create or add an item to customer addresses list // Create or add an item to customer addresses list
if (req.method === 'POST') { if (req.method === 'POST') {
const body = addAddressBodySchema.parse({ ...req.body, cartId }) const body = addAddressBodySchema.parse({ ...input, cartId })
return handlers['addItem']({ ...ctx, body }) output = await handlers['addItem']({ ...ctx, body })
} }
// Update item in customer addresses list // Update item in customer addresses list
if (req.method === 'PUT') { if (req.method === 'PUT') {
const body = updateAddressBodySchema.parse({ ...req.body, cartId }) const body = updateAddressBodySchema.parse({ ...input, cartId })
return handlers['updateItem']({ ...ctx, body }) output = await handlers['updateItem']({ ...ctx, body })
} }
// Remove an item from customer addresses list // Remove an item from customer addresses list
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = deleteAddressBodySchema.parse({ ...req.body, cartId }) const body = deleteAddressBodySchema.parse({ ...input, cartId })
return handlers['removeItem']({ ...ctx, body }) return await handlers['removeItem']({ ...ctx, body })
} }
return output ? parse(output, addressSchema) : { status: 405 }
} }
export default customerShippingEndpoint export default customerShippingEndpoint

View File

@ -1,54 +1,68 @@
import type { CustomerCardSchema } from '../../../types/customer/card' import type { CustomerCardSchema } from '../../../types/customer/card'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import { z } from 'zod'
import parse from '../../utils/parse-output'
import validateHandlers from '../../utils/validate-handlers' import validateHandlers from '../../utils/validate-handlers'
import { import {
cardSchema,
addCardBodySchema, addCardBodySchema,
deleteCardBodySchema, deleteCardBodySchema,
updateCardBodySchema, updateCardBodySchema,
} from '../../../schemas/customer' } from '../../../schemas/customer'
import { getInput } from '../../utils'
const customerCardEndpoint: GetAPISchema< const customerCardEndpoint: GetAPISchema<
any, any,
CustomerCardSchema CustomerCardSchema
>['endpoint']['handler'] = (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx const { req, handlers, config } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['getCards'], GET: handlers['getCards'],
POST: handlers['addItem'], POST: handlers['addItem'],
PUT: handlers['updateItem'], PUT: handlers['updateItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
let output
const input = await getInput(req)
const { cookies } = req const { cookies } = req
// Cart id might be usefull for anonymous shopping // Cart id might be usefull for anonymous shopping
const cartId = cookies[config.cartCookie] const cartId = cookies.get(config.cartCookie)
// Create or add a card // Create or add a card
if (req.method === 'GET') { if (req.method === 'GET') {
const body = { ...req.body } const body = { ...input }
return handlers['getCards']({ ...ctx, body }) return parse(
await handlers['getCards']({ ...ctx, body }),
z.array(cardSchema).optional()
)
} }
// Create or add an item to customer cards // Create or add an item to customer cards
if (req.method === 'POST') { if (req.method === 'POST') {
const body = addCardBodySchema.parse({ ...req.body, cartId }) const body = addCardBodySchema.parse({ ...input, cartId })
return handlers['addItem']({ ...ctx, body }) output = await handlers['addItem']({ ...ctx, body })
} }
// Update item in customer cards // Update item in customer cards
if (req.method === 'PUT') { if (req.method === 'PUT') {
const body = updateCardBodySchema.parse({ ...req.body, cartId }) const body = updateCardBodySchema.parse({ ...input, cartId })
return handlers['updateItem']({ ...ctx, body }) output = await handlers['updateItem']({ ...ctx, body })
} }
// Remove an item from customer cards // Remove an item from customer cards
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = deleteCardBodySchema.parse({ ...req.body, cartId }) const body = deleteCardBodySchema.parse({ ...input, cartId })
return handlers['removeItem']({ ...ctx, body })
return await handlers['removeItem']({ ...ctx, body })
} }
return output ? parse(output, cardSchema.nullish()) : { status: 405 }
} }
export default customerCardEndpoint export default customerCardEndpoint

View File

@ -1,20 +1,25 @@
import type { CustomerSchema } from '../../../types/customer' import type { CustomerSchema } from '../../../types/customer'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import parse from '../../utils/parse-output'
import validateHandlers from '../../utils/validate-handlers' import validateHandlers from '../../utils/validate-handlers'
import { customerSchema } from '../../../schemas/customer'
const customerEndpoint: GetAPISchema< const customerEndpoint: GetAPISchema<
any, any,
CustomerSchema CustomerSchema
>['endpoint']['handler'] = (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx const { req, handlers } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['getLoggedInCustomer'], GET: handlers['getLoggedInCustomer'],
}) })
const body = null const body = null
return handlers['getLoggedInCustomer']({ ...ctx, body }) const output = await handlers['getLoggedInCustomer']({ ...ctx, body })
return output ? parse(output, customerSchema) : { status: 204 }
} }
export default customerEndpoint export default customerEndpoint

View File

@ -1,74 +1,80 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' import type { APIProvider, CommerceAPI, EndpointHandler } from '..'
import type { APIProvider, CommerceAPI } from '..'
import { normalizeError } from '../utils/errors' import { NextRequest, NextResponse } from 'next/server'
import { normalizeApiError } from '../utils/errors'
/** /**
* Handles the catch-all api endpoint for the Commerce API. * Next.js Commerce API endpoints handler. Based on the path, it will call the corresponding endpoint handler,
* exported from the `endpoints` folder of the provider.
* @param {CommerceAPI} commerce The Commerce API instance. * @param {CommerceAPI} commerce The Commerce API instance.
* @param endpoints An object containing the handlers for each endpoint. * @param endpoints An object containing the handlers for each endpoint.
* @returns JSON response with the data or error.
*/ */
export default function createEndpoints<P extends APIProvider>( export default function createEndpoints<P extends APIProvider>(
commerce: CommerceAPI<P>, commerce: CommerceAPI<P>,
endpoints: { endpoints: Record<string, (commerce: CommerceAPI<P>) => EndpointHandler>
[key: string]: (commerce: CommerceAPI<P>) => NextApiHandler
}
) { ) {
const paths = Object.keys(endpoints) const endpointsKeys = Object.keys(endpoints)
const handlers = endpointsKeys.reduce<Record<string, EndpointHandler>>(
const handlers = paths.reduce<Record<string, NextApiHandler>>( (acc, endpoint) =>
(acc, path) =>
Object.assign(acc, { Object.assign(acc, {
[path]: endpoints[path](commerce), [endpoint]: endpoints[endpoint](commerce),
}), }),
{} {}
) )
return async (req: NextApiRequest, res: NextApiResponse) => { return async (req: NextRequest) => {
try { try {
if (!req.query.commerce) { const { pathname } = new URL(req.url)
throw new Error(
'Invalid configuration. Please make sure that the /pages/api/commerce/[[...commerce]].ts route is configured correctly, and it passes the commerce instance.'
)
}
/** /**
* Get the url path * Get the current endpoint by removing the leading and trailing slash & base path.
* Csovers: /api/commerce/cart & /checkout
*/ */
const path = Array.isArray(req.query.commerce) const endpoint = pathname
? req.query.commerce.join('/') .replace('/api/commerce/', '')
: req.query.commerce .replace(/^\/|\/$/g, '')
// Check if the handler for this path exists and return a 404 if it doesn't // Check if the handler for this path exists and return a 404 if it doesn't
if (!paths.includes(path)) { if (!endpointsKeys.includes(endpoint)) {
throw new Error( throw new Error(
`Endpoint handler not implemented. Please use one of the available api endpoints: ${paths.join( `Endpoint "${endpoint}" not implemented. Please use one of the available api endpoints: ${endpointsKeys.join(
', ' ', '
)}` )}`
) )
} }
const data = await handlers[path](req, res)
// If the handler returns a value but the response hasn't been sent yet, send it
if (!res.headersSent && data) {
res.status(200).json({
data,
})
}
} catch (error) {
/** /**
* Return the error as a JSON response only if the response hasn't been sent yet * Executes the handler for this endpoint, provided by the provider,
* Eg. by the `isAllowedMethod` util returning a 405 status code * parses the input body and returns the parsed output
*/ */
if (!res.headersSent) { const output = await handlers[endpoint](req)
console.error(error)
const { status, data, errors } = normalizeError(error) // If the output is a NextResponse, return it directly (E.g. checkout page & validateMethod util)
res.status(status).json({ if (output instanceof NextResponse) {
data, return output
errors, }
// If the output contains a redirectTo property, return a NextResponse with the redirect
if (output.redirectTo) {
return NextResponse.redirect(output.redirectTo, {
headers: output.headers,
}) })
} }
const { data = null, errors, status, headers } = output
return NextResponse.json(
{ data, errors },
{
status,
headers,
}
)
} catch (error) {
const output = normalizeApiError(error)
return output instanceof NextResponse
? output
: NextResponse.json(output, { status: output.status ?? 500 })
} }
} }
} }

View File

@ -2,20 +2,23 @@ import type { GetAPISchema } from '..'
import type { LoginSchema } from '../../types/login' import type { LoginSchema } from '../../types/login'
import validateHandlers from '../utils/validate-handlers' import validateHandlers from '../utils/validate-handlers'
import { getInput } from '../utils'
import { loginBodySchema } from '../../schemas/auth' import { loginBodySchema } from '../../schemas/auth'
const loginEndpoint: GetAPISchema<any, LoginSchema>['endpoint']['handler'] = ( const loginEndpoint: GetAPISchema<
ctx any,
) => { LoginSchema
const { req, res, handlers } = ctx >['endpoint']['handler'] = async (ctx) => {
const { req, handlers } = ctx
validateHandlers(req, res, { validateHandlers(req, {
POST: handlers['login'], POST: handlers['login'],
GET: handlers['login'], GET: handlers['login'],
}) })
const input = await getInput(req)
const body = loginBodySchema.parse(req.body) const body = loginBodySchema.parse(input)
return handlers['login']({ ...ctx, body }) return await handlers['login']({ ...ctx, body })
} }
export default loginEndpoint export default loginEndpoint

View File

@ -3,23 +3,25 @@ import type { LogoutSchema } from '../../types/logout'
import { logoutBodySchema } from '../../schemas/auth' import { logoutBodySchema } from '../../schemas/auth'
import validateHandlers from '../utils/validate-handlers' import validateHandlers from '../utils/validate-handlers'
import { normalizeApiError } from '../utils/errors'
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] = ( const logoutEndpoint: GetAPISchema<
ctx any,
) => { LogoutSchema
const { req, res, handlers } = ctx >['endpoint']['handler'] = async (ctx) => {
const { req, handlers } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['logout'], GET: handlers['logout'],
}) })
const redirectTo = req.query.redirect_to const redirectTo = new URL(req.url).searchParams.get('redirectTo')
const body = logoutBodySchema.parse( const body = logoutBodySchema.parse(
typeof redirectTo === 'string' ? { redirectTo } : {} typeof redirectTo === 'string' ? { redirectTo } : {}
) )
return handlers['logout']({ ...ctx, body }) return await handlers['logout']({ ...ctx, body })
} }
export default logoutEndpoint export default logoutEndpoint

View File

@ -1,23 +1,27 @@
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { SignupSchema } from '../../types/signup' import type { SignupSchema } from '../../types/signup'
import { signupBodySchema } from '../../schemas/auth'
import validateHandlers from '../utils/validate-handlers' import validateHandlers from '../utils/validate-handlers'
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] = ( import { getInput } from '../utils'
ctx import { signupBodySchema } from '../../schemas/auth'
) => {
const { req, res, handlers, config } = ctx
validateHandlers(req, res, { const signupEndpoint: GetAPISchema<
any,
SignupSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, handlers, config } = ctx
validateHandlers(req, {
POST: handlers['signup'], POST: handlers['signup'],
}) })
const { cookies } = req
const cartId = cookies[config.cartCookie]
const body = signupBodySchema.parse({ ...req.body, cartId }) const input = await getInput(req)
return handlers['signup']({ ...ctx, body }) const { cookies } = req
const cartId = cookies.get(config.cartCookie)
const body = signupBodySchema.parse({ ...input, cartId })
return await handlers['signup']({ ...ctx, body })
} }
export default signupEndpoint export default signupEndpoint

View File

@ -3,47 +3,57 @@ import type { WishlistSchema } from '../../types/wishlist'
import validateHandlers from '../utils/validate-handlers' import validateHandlers from '../utils/validate-handlers'
import { getInput } from '../utils'
import { import {
getWishlistBodySchema, wishlistSchema,
addItemBodySchema, addItemBodySchema,
removeItemBodySchema, removeItemBodySchema,
getWishlistBodySchema,
} from '../../schemas/whishlist' } from '../../schemas/whishlist'
import parse from '../utils/parse-output'
const wishlistEndpoint: GetAPISchema< const wishlistEndpoint: GetAPISchema<
any, any,
WishlistSchema WishlistSchema
>['endpoint']['handler'] = (ctx) => { >['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx const { req, handlers, config } = ctx
validateHandlers(req, res, { validateHandlers(req, {
GET: handlers['getWishlist'], GET: handlers['getWishlist'],
POST: handlers['addItem'], POST: handlers['addItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
let output
const { cookies } = req const { cookies } = req
const customerToken = cookies[config.customerCookie] const input = await getInput(req)
const customerToken = cookies.get(config.customerCookie)
const products = new URL(req.url).searchParams.get('products')
// Return current wishlist info // Return current wishlist info
if (req.method === 'GET') { if (req.method === 'GET') {
const body = getWishlistBodySchema.parse({ const body = getWishlistBodySchema.parse({
customerToken, customerToken,
includeProducts: !!req.query.products, includeProducts: !!products,
}) })
return handlers['getWishlist']({ ...ctx, body }) output = await handlers['getWishlist']({ ...ctx, body })
} }
// Add an item to the wishlist // Add an item to the wishlist
if (req.method === 'POST') { if (req.method === 'POST') {
const body = addItemBodySchema.parse({ ...req.body, customerToken }) const body = addItemBodySchema.parse({ ...input, customerToken })
return handlers['addItem']({ ...ctx, body }) output = await handlers['addItem']({ ...ctx, body })
} }
// Remove an item from the wishlist // Remove an item from the wishlist
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = removeItemBodySchema.parse({ ...req.body, customerToken }) const body = removeItemBodySchema.parse({ ...input, customerToken })
return handlers['removeItem']({ ...ctx, body }) output = await handlers['removeItem']({ ...ctx, body })
} }
return output ? parse(output, wishlistSchema.optional()) : { status: 40 }
} }
export default wishlistEndpoint export default wishlistEndpoint

View File

@ -1,6 +1,6 @@
import type { NextApiHandler } from 'next' import type { NextRequest } from 'next/server'
import type { FetchOptions, Response } from '@vercel/fetch'
import type { APIEndpoint, APIHandler } from './utils/types' import type { APIEndpoint, APIHandler, APIResponse } from './utils/types'
import type { CartSchema } from '../types/cart' import type { CartSchema } from '../types/cart'
import type { CustomerSchema } from '../types/customer' import type { CustomerSchema } from '../types/customer'
import type { LoginSchema } from '../types/login' import type { LoginSchema } from '../types/login'
@ -12,6 +12,7 @@ import type { CheckoutSchema } from '../types/checkout'
import type { CustomerCardSchema } from '../types/customer/card' import type { CustomerCardSchema } from '../types/customer/card'
import type { CustomerAddressSchema } from '../types/customer/address' import type { CustomerAddressSchema } from '../types/customer/address'
import { geolocation } from '@vercel/edge'
import { withOperationCallback } from './utils/with-operation-callback' import { withOperationCallback } from './utils/with-operation-callback'
import { import {
@ -119,6 +120,8 @@ export function getCommerceApi<P extends APIProvider>(
return commerce return commerce
} }
export type EndpointHandler = (req: NextRequest) => Promise<APIResponse>
export function getEndpoint< export function getEndpoint<
P extends APIProvider, P extends APIProvider,
T extends GetAPISchema<any, any> T extends GetAPISchema<any, any>
@ -128,17 +131,16 @@ export function getEndpoint<
config?: P['config'] config?: P['config']
options?: T['schema']['endpoint']['options'] options?: T['schema']['endpoint']['options']
} }
): NextApiHandler { ): EndpointHandler {
const cfg = commerce.getConfig(context.config) const cfg = commerce.getConfig(context.config)
return function apiHandler(req) {
return function apiHandler(req, res) {
return context.handler({ return context.handler({
req, req,
res,
commerce, commerce,
config: cfg, config: cfg,
handlers: context.handlers, handlers: context.handlers,
options: context.options ?? {}, options: context.options ?? {},
geolocation: geolocation(req),
}) })
} }
} }
@ -151,7 +153,7 @@ export const createEndpoint =
config?: P['config'] config?: P['config']
options?: API['schema']['endpoint']['options'] options?: API['schema']['endpoint']['options']
} }
): NextApiHandler => { ): EndpointHandler => {
return getEndpoint(commerce, { ...endpoint, ...context }) return getEndpoint(commerce, { ...endpoint, ...context })
} }
@ -166,7 +168,7 @@ export interface CommerceAPIConfig {
fetch<Data = any, Variables = any>( fetch<Data = any, Variables = any>(
query: string, query: string,
queryData?: CommerceAPIFetchOptions<Variables>, queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: FetchOptions headers?: HeadersInit
): Promise<GraphQLFetcherResult<Data>> ): Promise<GraphQLFetcherResult<Data>>
} }
@ -175,8 +177,7 @@ export type GraphQLFetcher<
Variables = any Variables = any
> = ( > = (
query: string, query: string,
queryData?: CommerceAPIFetchOptions<Variables>, queryData?: CommerceAPIFetchOptions<Variables>
fetchOptions?: FetchOptions
) => Promise<Data> ) => Promise<Data>
export interface GraphQLFetcherResult<Data = any> { export interface GraphQLFetcherResult<Data = any> {

View File

@ -1,4 +1,3 @@
import type { ServerResponse } from 'http'
import type { LoginOperation } from '../types/login' import type { LoginOperation } from '../types/login'
import type { GetAllPagesOperation, GetPageOperation } from '../types/page' import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
import type { GetSiteInfoOperation } from '../types/site' import type { GetSiteInfoOperation } from '../types/site'
@ -9,6 +8,7 @@ import type {
GetProductOperation, GetProductOperation,
} from '../types/product' } from '../types/product'
import type { APIProvider, CommerceAPI } from '.' import type { APIProvider, CommerceAPI } from '.'
import { NextResponse } from 'next/server'
const noop = () => { const noop = () => {
throw new Error('Not implemented') throw new Error('Not implemented')
@ -44,14 +44,14 @@ export type Operations<P extends APIProvider> = {
<T extends LoginOperation>(opts: { <T extends LoginOperation>(opts: {
variables: T['variables'] variables: T['variables']
config?: P['config'] config?: P['config']
res: ServerResponse res: NextResponse
}): Promise<T['data']> }): Promise<T['data']>
<T extends LoginOperation>( <T extends LoginOperation>(
opts: { opts: {
variables: T['variables'] variables: T['variables']
config?: P['config'] config?: P['config']
res: ServerResponse res: NextResponse
} & OperationOptions } & OperationOptions
): Promise<T['data']> ): Promise<T['data']>
} }

View File

@ -1,14 +1,13 @@
import type { Response } from '@vercel/fetch' import { NextRequest, NextResponse } from 'next/server'
import { CommerceError } from '../../utils/errors' import { CommerceError } from '../../utils/errors'
import { ZodError } from 'zod' import { ZodError } from 'zod'
export class CommerceAPIError extends Error { export class CommerceAPIResponseError extends Error {
status: number status: number
res: Response res: NextResponse
data: any data: any
constructor(msg: string, res: Response, data?: any) { constructor(msg: string, res: NextResponse, data?: any) {
super(msg) super(msg)
this.name = 'CommerceApiError' this.name = 'CommerceApiError'
this.status = res.status this.status = res.status
@ -17,6 +16,23 @@ export class CommerceAPIError extends Error {
} }
} }
export class CommerceAPIError extends Error {
status: number
code: string
constructor(
msg: string,
options?: {
status?: number
code?: string
}
) {
super(msg)
this.name = 'CommerceApiError'
this.status = options?.status || 500
this.code = options?.code || 'api_error'
}
}
export class CommerceNetworkError extends Error { export class CommerceNetworkError extends Error {
constructor(msg: string) { constructor(msg: string) {
super(msg) super(msg)
@ -25,7 +41,7 @@ export class CommerceNetworkError extends Error {
} }
export const normalizeZodIssues = (issues: ZodError['issues']) => export const normalizeZodIssues = (issues: ZodError['issues']) =>
issues.map(({ path, message }) => `${message} at "${path.join('.')}"`) issues.map(({ path, message }) => `${message} at "${path.join('.')}" field`)
export const getOperationError = (operation: string, error: unknown) => { export const getOperationError = (operation: string, error: unknown) => {
if (error instanceof ZodError) { if (error instanceof ZodError) {
@ -41,29 +57,41 @@ export const getOperationError = (operation: string, error: unknown) => {
return error return error
} }
export const normalizeError = (error: unknown) => { export const normalizeApiError = (error: unknown, req?: NextRequest) => {
if (error instanceof CommerceAPIError) { if (error instanceof CommerceAPIResponseError && error.res) {
return { return error.res
status: error.status || 500,
data: error.data || null,
errors: [
{ message: 'An unexpected error ocurred with the Commerce API' },
],
}
} }
req?.url && console.log(req.url)
if (error instanceof ZodError) { if (error instanceof ZodError) {
const message = 'Validation error, please check the input data!'
const errors = normalizeZodIssues(error.issues).map((message) => ({
message,
}))
console.error(`${message}\n${errors.map((e) => e.message).join('\n')}`)
return { return {
status: 400, status: 400,
data: null, data: null,
message: errors,
'Validation error, please check the input data check errors property for more info', }
errors: normalizeZodIssues(error.issues).map((message) => ({ message })), }
console.error(error)
if (error instanceof CommerceAPIError) {
return {
errors: [
{
message: error.message,
code: error.code,
},
],
status: error.status,
} }
} }
return { return {
status: 500,
data: null, data: null,
errors: [{ message: 'An unexpected error ocurred' }], errors: [{ message: 'An unexpected error ocurred' }],
} }

View File

@ -0,0 +1,3 @@
import { NextRequest } from 'next/server'
export const getInput = (req: NextRequest) => req.json().catch(() => ({}))

View File

@ -1,40 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE'
export default function isAllowedMethod(
req: NextApiRequest,
res: NextApiResponse,
allowedMethods: HTTP_METHODS[]
) {
const methods = allowedMethods.includes('OPTIONS')
? allowedMethods
: [...allowedMethods, 'OPTIONS']
if (!req.method || !methods.includes(req.method)) {
res.status(405)
res.setHeader('Allow', methods.join(', '))
res.json({
errors: [
{
message: `You are not allowed to use the ${
req.method
} method for this route, please use one of the following methods: ${methods.join(
', '
)}`,
},
],
})
return false
}
if (req.method === 'OPTIONS') {
res.status(200)
res.setHeader('Allow', methods.join(', '))
res.setHeader('Content-Length', '0')
res.end()
return false
}
return true
}

View File

@ -0,0 +1,11 @@
import type { ZodSchema } from 'zod'
import { APIResponse } from './types'
export const parseOutput = <T>(res: APIResponse<T>, parser: ZodSchema) => {
if (res.data) {
res.data = parser.parse(res.data)
}
return res
}
export default parseOutput

View File

@ -1,14 +1,20 @@
import type { NextApiRequest, NextApiResponse } from 'next' import { NextRequest } from 'next/server'
import type { Geo } from '@vercel/edge'
import type { CommerceAPI } from '..' import type { CommerceAPI } from '..'
export type ErrorData = { message: string; code?: string } export type ErrorData = { message: string; code?: string }
export type APIResponse<Data = any> = export type APIResponse<Data = any> = {
| { data: Data; errors?: ErrorData[] } data?: Data
// If `data` doesn't include `null`, then `null` is only allowed on errors errors?: ErrorData[]
| (Data extends null status?: number
? { data: null; errors?: ErrorData[] } headers?: HeadersInit
: { data: null; errors: ErrorData[] }) /**
* @type {string}
* @example redirectTo: '/cart'
*/
redirectTo?: string
}
export type APIHandlerContext< export type APIHandlerContext<
C extends CommerceAPI, C extends CommerceAPI,
@ -16,15 +22,12 @@ export type APIHandlerContext<
Data = any, Data = any,
Options extends {} = {} Options extends {} = {}
> = { > = {
req: NextApiRequest req: NextRequest
res: NextApiResponse<APIResponse<Data>>
commerce: C commerce: C
config: C['provider']['config'] config: C['provider']['config']
handlers: H handlers: H
/**
* Custom configs that may be used by a particular handler
*/
options: Options options: Options
geolocation: Geo
} }
export type APIHandler< export type APIHandler<
@ -35,7 +38,7 @@ export type APIHandler<
Options extends {} = {} Options extends {} = {}
> = ( > = (
context: APIHandlerContext<C, H, Data, Options> & { body: Body } context: APIHandlerContext<C, H, Data, Options> & { body: Body }
) => void | Promise<void> ) => Promise<APIResponse<Data>>
export type APIHandlers<C extends CommerceAPI> = { export type APIHandlers<C extends CommerceAPI> = {
[k: string]: APIHandler<C, any, any, any, any> [k: string]: APIHandler<C, any, any, any, any>
@ -46,4 +49,6 @@ export type APIEndpoint<
H extends APIHandlers<C> = {}, H extends APIHandlers<C> = {},
Data = any, Data = any,
Options extends {} = {} Options extends {} = {}
> = (context: APIHandlerContext<C, H, Data, Options>) => void | Promise<void> > = (
context: APIHandlerContext<C, H, Data, Options>
) => Promise<APIResponse<Data>>

View File

@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextRequest } from 'next/server'
import isAllowedMethod, { HTTP_METHODS } from './is-allowed-method'
import validateMethod, { HTTP_METHODS } from './validate-method'
import { APIHandler } from './types' import { APIHandler } from './types'
/** /**
@ -11,8 +12,7 @@ import { APIHandler } from './types'
* @throws Error when the method is not allowed or the handler is not implemented. * @throws Error when the method is not allowed or the handler is not implemented.
*/ */
export default function validateHandlers( export default function validateHandlers(
req: NextApiRequest, req: NextRequest,
res: NextApiResponse,
allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> } allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> }
) { ) {
const methods = Object.keys(allowedOperations) as HTTP_METHODS[] const methods = Object.keys(allowedOperations) as HTTP_METHODS[]
@ -23,7 +23,5 @@ export default function validateHandlers(
return arr return arr
}, []) }, [])
if (!isAllowedMethod(req, res, allowedMethods)) { return validateMethod(req, allowedMethods)
throw new Error(`Method ${req.method} Not Allowed for this url: ${req.url}`)
}
} }

View File

@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server'
import { CommerceAPIResponseError } from './errors'
export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE'
export default function isAllowedMethod(
req: NextRequest,
allowedMethods: HTTP_METHODS[]
) {
const methods = allowedMethods.includes('OPTIONS')
? allowedMethods
: [...allowedMethods, 'OPTIONS']
if (!req.method || !methods.includes(req.method)) {
throw new CommerceAPIResponseError(
`The HTTP ${req.method} method is not supported at this route.`,
NextResponse.json(
{
errors: [
{
code: 'invalid_method',
message: `The HTTP ${req.method} method is not supported at this route.`,
},
],
},
{
status: 405,
headers: {
Allow: methods.join(', '),
},
}
)
)
}
if (req.method === 'OPTIONS') {
throw new CommerceAPIResponseError(
'This is a CORS preflight request.',
new NextResponse(null, {
status: 204,
headers: {
Allow: methods.join(', '),
'Content-Length': '0',
},
})
)
}
}

View File

@ -3,7 +3,7 @@ import { z } from 'zod'
export const loginBodySchema = z.object({ export const loginBodySchema = z.object({
redirectTo: z.string().optional(), redirectTo: z.string().optional(),
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string().min(7),
}) })
export const logoutBodySchema = z.object({ export const logoutBodySchema = z.object({
@ -13,6 +13,6 @@ export const logoutBodySchema = z.object({
export const signupBodySchema = z.object({ export const signupBodySchema = z.object({
firstName: z.string(), firstName: z.string(),
lastName: z.string(), lastName: z.string(),
email: z.string(), email: z.string().email(),
password: z.string(), password: z.string().min(7),
}) })

View File

@ -25,3 +25,78 @@ export const removeItemBodySchema = z.object({
cartId: z.string(), cartId: z.string(),
itemId: z.string(), itemId: z.string(),
}) })
export const discountSchema = z.object({
value: z.number(),
})
export const optionsSchema = z.object({
id: z.string().optional(),
name: z.string(),
value: z.string(),
})
export const cartProductVariantSchema = z.object({
id: z.string(),
sku: z.string().optional(),
name: z.string(),
price: z.number(),
listPrice: z.number(),
availableForSale: z.boolean().optional(),
requiresShipping: z.boolean().optional(),
image: z.object({
url: z.string(),
altText: z.string().optional(),
}),
weight: z
.object({
value: z.number(),
unit: z.string(),
})
.optional(),
height: z
.object({
value: z.number(),
unit: z.string(),
})
.optional(),
width: z
.object({
value: z.number(),
unit: z.string(),
})
.optional(),
depth: z
.object({
value: z.number(),
unit: z.string(),
})
.optional(),
})
export const cartLineItemSchema = z.object({
id: z.string(),
variantId: z.string(),
productId: z.string(),
name: z.string(),
quantity: z.number().min(1),
discounts: z.array(discountSchema).optional(),
path: z.string().startsWith('/').optional(),
variant: cartProductVariantSchema,
options: z.array(optionsSchema).optional(),
})
export const cartSchema = z.object({
id: z.string(),
customerId: z.string().optional(),
url: z.string().optional(),
email: z.string().optional(),
createdAt: z.string(),
currency: z.object({ code: z.string() }),
taxesIncluded: z.boolean(),
lineItems: z.array(cartLineItemSchema),
lineItemsSubtotalPrice: z.number(),
subtotalPrice: z.number(),
totalPrice: z.number(),
discounts: z.array(discountSchema).optional(),
})

View File

@ -1,4 +1,5 @@
import { z } from 'zod' import { z } from 'zod'
import { cartLineItemSchema } from './cart'
import { addressFieldsSchema, cardFieldsSchema } from './customer' import { addressFieldsSchema, cardFieldsSchema } from './customer'
export const getCheckoutBodySchema = z.object({ export const getCheckoutBodySchema = z.object({
@ -13,3 +14,12 @@ export const submitCheckoutBodySchema = z.object({
address: addressFieldsSchema, address: addressFieldsSchema,
}), }),
}) })
export const checkoutSchema = z.object({
hasPayment: z.boolean(),
hasShipping: z.boolean(),
addressId: z.string(),
payments: z.array(cardFieldsSchema).optional(),
cardId: z.string().optional(),
lineItems: z.array(cartLineItemSchema).optional(),
})

View File

@ -4,6 +4,22 @@ export const getCustomerAddressBodySchema = z.object({
cartId: z.string(), cartId: z.string(),
}) })
export const customerSchema = z.object({
id: z.string(),
firstName: z.string(),
lastName: z.string(),
email: z.string().optional(),
phone: z.string().optional(),
company: z.string().optional(),
notes: z.string().optional(),
acceptsMarketing: z.boolean().optional(),
})
export const addressSchema = z.object({
id: z.string(),
mask: z.string(),
})
export const addressFieldsSchema = z.object({ export const addressFieldsSchema = z.object({
type: z.string(), type: z.string(),
firstName: z.string(), firstName: z.string(),
@ -46,6 +62,11 @@ export const cardFieldsSchema = z.object({
country: z.string(), country: z.string(),
}) })
export const cardSchema = z.object({
id: z.string(),
mask: z.string(),
})
export const addCardBodySchema = z.object({ export const addCardBodySchema = z.object({
cartId: z.string(), cartId: z.string(),
item: cardFieldsSchema, item: cardFieldsSchema,

View File

@ -1,4 +1,4 @@
import { z } from 'zod' import { boolean, z } from 'zod'
export const productPriceSchema = z.object({ export const productPriceSchema = z.object({
value: z.number(), value: z.number(),
@ -58,3 +58,8 @@ export const searchProductBodySchema = z.object({
sort: z.string().optional(), sort: z.string().optional(),
locale: z.string().optional(), locale: z.string().optional(),
}) })
export const searchProductsSchema = z.object({
products: z.array(productSchema),
found: z.boolean(),
})

View File

@ -1,4 +1,18 @@
import { z } from 'zod' import { z } from 'zod'
import { productSchema } from './product'
export const wishlistSchemaItem = z.object({
id: z.string(),
productId: z.string(),
variantId: z.string(),
product: productSchema,
})
export const wishlistSchema = z.object({
id: z.string(),
items: z.array(wishlistSchemaItem),
token: z.string().optional(),
})
export const getWishlistBodySchema = z.object({ export const getWishlistBodySchema = z.object({
customerAccessToken: z.string(), customerAccessToken: z.string(),

View File

@ -199,14 +199,14 @@ export type CartHooks = {
} }
export type GetCartHook = { export type GetCartHook = {
data: Cart | null data: Cart | null | undefined
input: {} input: {}
fetcherInput: { cartId?: string } fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean } swrState: { isEmpty: boolean }
} }
export type AddItemHook = { export type AddItemHook = {
data: Cart data: Cart | null | undefined
input?: CartItemBody input?: CartItemBody
fetcherInput: CartItemBody fetcherInput: CartItemBody
body: { item: CartItemBody } body: { item: CartItemBody }
@ -251,6 +251,7 @@ export type GetCartHandler = GetCartHook & {
} }
export type AddItemHandler = AddItemHook & { export type AddItemHandler = AddItemHook & {
data: Cart | null | undefined
body: { cartId?: string } body: { cartId?: string }
} }

View File

@ -48,7 +48,7 @@ export interface CheckoutBody {
} }
export type SubmitCheckoutHook = { export type SubmitCheckoutHook = {
data: Checkout data: Checkout | null
input?: CheckoutBody input?: CheckoutBody
fetcherInput: CheckoutBody fetcherInput: CheckoutBody
body: { item: CheckoutBody } body: { item: CheckoutBody }

View File

@ -61,7 +61,7 @@ export type GetAddressesHook = {
} }
export type AddItemHook = { export type AddItemHook = {
data: Address data: Address | null
input?: AddressFields input?: AddressFields
fetcherInput: AddressFields fetcherInput: AddressFields
body: { item: AddressFields } body: { item: AddressFields }
@ -77,7 +77,7 @@ export type UpdateItemHook = {
} }
export type RemoveItemHook = { export type RemoveItemHook = {
data: Address | null | undefined data: Address | null
input: { item?: Address } input: { item?: Address }
fetcherInput: { itemId: string } fetcherInput: { itemId: string }
body: { itemId: string } body: { itemId: string }
@ -100,7 +100,6 @@ export type AddItemHandler = AddItemHook & {
} }
export type UpdateItemHandler = UpdateItemHook & { export type UpdateItemHandler = UpdateItemHook & {
data: Address
body: { cartId: string } body: { cartId: string }
} }

View File

@ -78,7 +78,7 @@ export type GetCardsHook = {
} }
export type AddItemHook = { export type AddItemHook = {
data: Card data: Card | null
input?: CardFields input?: CardFields
fetcherInput: CardFields fetcherInput: CardFields
body: { item: CardFields } body: { item: CardFields }
@ -86,7 +86,7 @@ export type AddItemHook = {
} }
export type UpdateItemHook = { export type UpdateItemHook = {
data: Card | null | undefined data: Card | null
input: { item?: CardFields; wait?: number } input: { item?: CardFields; wait?: number }
fetcherInput: { itemId: string; item: CardFields } fetcherInput: { itemId: string; item: CardFields }
body: { itemId: string; item: CardFields } body: { itemId: string; item: CardFields }
@ -94,7 +94,7 @@ export type UpdateItemHook = {
} }
export type RemoveItemHook = { export type RemoveItemHook = {
data: Card | null | undefined data: Card | null
input: { item?: Card } input: { item?: Card }
fetcherInput: { itemId: string } fetcherInput: { itemId: string }
body: { itemId: string } body: { itemId: string }
@ -116,7 +116,6 @@ export type AddItemHandler = AddItemHook & {
} }
export type UpdateItemHandler = UpdateItemHook & { export type UpdateItemHandler = UpdateItemHook & {
data: Card
body: { cartId: string } body: { cartId: string }
} }

View File

@ -55,7 +55,7 @@ export type HookFetcherOptions = { method?: string } & (
| { query?: string; url: string } | { query?: string; url: string }
) )
export type HookInputValue = string | number | boolean | undefined export type HookInputValue = string | number | boolean | null | undefined
export type HookSWRInput = [string, HookInputValue][] export type HookSWRInput = [string, HookInputValue][]

View File

@ -51,7 +51,7 @@
"@vercel/commerce": "workspace:*", "@vercel/commerce": "workspace:*",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"jsonwebtoken": "^8.5.1", "@tsndr/cloudflare-worker-jwt": "^2.1.0",
"lodash.debounce": "^4.0.8" "lodash.debounce": "^4.0.8"
}, },
"peerDependencies": { "peerDependencies": {
@ -66,7 +66,6 @@
"@types/chec__commerce.js": "^2.8.4", "@types/chec__commerce.js": "^2.8.4",
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/jsonwebtoken": "^8.5.7",
"@types/lodash.debounce": "^4.0.6", "@types/lodash.debounce": "^4.0.6",
"@types/node": "^17.0.8", "@types/node": "^17.0.8",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",

View File

@ -1 +1,3 @@
export default function noopApi(...args: any[]): void {} export default function getCheckout(...args: any[]) {
return Promise.resolve({ data: null })
}

View File

@ -5,7 +5,6 @@ import sdkFetcherFunction from '../../utils/sdk-fetch'
import { normalizeTestCheckout } from '../../../utils/normalize-checkout' import { normalizeTestCheckout } from '../../../utils/normalize-checkout'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({ const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
res,
body: { item, cartId }, body: { item, cartId },
config: { sdkFetch }, config: { sdkFetch },
}) => { }) => {
@ -38,7 +37,7 @@ const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
// Capture the order // Capture the order
await sdkFetcher('checkout', 'capture', checkoutToken, checkoutData) await sdkFetcher('checkout', 'capture', checkoutToken, checkoutData)
res.status(200).json({ data: null, errors: [] }) return { data: null }
} }
export default submitCheckout export default submitCheckout

View File

@ -5,28 +5,31 @@ import type { LoginEndpoint } from '.'
const login: LoginEndpoint['handlers']['login'] = async ({ const login: LoginEndpoint['handlers']['login'] = async ({
req, req,
res,
config: { sdkFetch, customerCookie }, config: { sdkFetch, customerCookie },
}) => { }) => {
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
const redirectUrl = getDeploymentUrl() const redirectUrl = getDeploymentUrl()
const { searchParams } = new URL(req.url)
try { try {
const loginToken = req.query?.token as string const loginToken = searchParams.get('token')
if (!loginToken) { if (!loginToken) {
res.redirect(redirectUrl) return { redirectTo: redirectUrl }
} }
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false) const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
res.setHeader(
'Set-Cookie', return {
serialize(customerCookie, jwt, { redirectTo: redirectUrl,
secure: process.env.NODE_ENV === 'production', headers: {
maxAge: 60 * 60 * 24, 'Set-Cookie': serialize(customerCookie, jwt, {
path: '/', secure: process.env.NODE_ENV === 'production',
}) maxAge: 60 * 60 * 24,
) path: '/',
res.redirect(redirectUrl) }),
},
}
} catch { } catch {
res.redirect(redirectUrl) return { redirectTo: redirectUrl }
} }
} }

View File

@ -1,5 +1,8 @@
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { decode } from 'jsonwebtoken' import {
decode,
type JwtData as CoreJwtData,
} from '@tsndr/cloudflare-worker-jwt'
import { SWRHook } from '@vercel/commerce/utils/types' import { SWRHook } from '@vercel/commerce/utils/types'
import useCustomer, { import useCustomer, {
UseCustomer, UseCustomer,
@ -7,6 +10,10 @@ import useCustomer, {
import { CUSTOMER_COOKIE, API_URL } from '../constants' import { CUSTOMER_COOKIE, API_URL } from '../constants'
import type { CustomerHook } from '@vercel/commerce/types/customer' import type { CustomerHook } from '@vercel/commerce/types/customer'
type JwtData = CoreJwtData & {
cid: string
}
export default useCustomer as UseCustomer<typeof handler> export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = { export const handler: SWRHook<CustomerHook> = {
fetchOptions: { fetchOptions: {
@ -20,7 +27,8 @@ export const handler: SWRHook<CustomerHook> = {
return null return null
} }
const decodedToken = decode(token) as { cid: string } const decodedToken = decode(token) as JwtData
const customer = await fetch<any>({ const customer = await fetch<any>({
query: options.query, query: options.query,
method: options.method, method: options.method,

View File

@ -4,8 +4,8 @@ const getFilterVariables = ({
search, search,
categoryId, categoryId,
}: { }: {
search?: string search?: string | null
categoryId?: string | number categoryId?: string | number | null
}) => { }) => {
let filterVariables: { [key: string]: any } = {} let filterVariables: { [key: string]: any } = {}
if (search) { if (search) {
@ -17,7 +17,7 @@ const getFilterVariables = ({
return filterVariables return filterVariables
} }
const getSortVariables = ({ sort }: { sort?: string }) => { const getSortVariables = ({ sort }: { sort?: string | null }) => {
let sortVariables: { [key: string]: any } = {} let sortVariables: { [key: string]: any } = {}
switch (sort) { switch (sort) {
case 'trending-desc': case 'trending-desc':

View File

@ -50,9 +50,7 @@
}, },
"dependencies": { "dependencies": {
"@vercel/commerce": "workspace:*", "@vercel/commerce": "workspace:*",
"@vercel/fetch": "^6.2.0", "lodash.debounce": "^4.0.8"
"lodash.debounce": "^4.0.8",
"node-fetch": "^2.6.7"
}, },
"peerDependencies": { "peerDependencies": {
"next": "^12", "next": "^12",

View File

@ -53,28 +53,19 @@ const buildAddToCartVariables = ({
const addItem: CartEndpoint['handlers']['addItem'] = async ({ const addItem: CartEndpoint['handlers']['addItem'] = async ({
req, req,
res, body: { item },
body: { cartId, item },
config, config,
}) => { }) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1
const productResponse = await config.fetch(getProductQuery, { const productResponse = await config.fetch(getProductQuery, {
variables: { productCode: item?.productId }, variables: { productCode: item?.productId },
}) })
const cookieHandler = new CookieHandler(config, req, res) const cookieHandler = new CookieHandler(config, req)
let accessToken = null let accessToken = null
if (!cookieHandler.getAccessToken()) { if (!cookieHandler.getAccessToken()) {
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken() let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
accessToken = anonymousShopperTokenResponse.accessToken; accessToken = anonymousShopperTokenResponse.accessToken
} else { } else {
accessToken = cookieHandler.getAccessToken() accessToken = cookieHandler.getAccessToken()
} }
@ -95,7 +86,8 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
) )
currentCart = result?.data?.currentCart currentCart = result?.data?.currentCart
} }
res.status(200).json({ data: normalizeCart(currentCart) })
return { data: normalizeCart(currentCart) }
} }
export default addItem export default addItem

View File

@ -6,17 +6,17 @@ import { getCartQuery } from '../../queries/get-cart-query'
const getCart: CartEndpoint['handlers']['getCart'] = async ({ const getCart: CartEndpoint['handlers']['getCart'] = async ({
req, req,
res,
body: { cartId },
config, config,
}) => { }) => {
let currentCart: Cart = {} let currentCart: Cart = {}
let headers
try { try {
const cookieHandler = new CookieHandler(config, req, res) const cookieHandler = new CookieHandler(config, req)
let accessToken = null let accessToken = null
if (!cookieHandler.getAccessToken()) { if (!cookieHandler.getAccessToken()) {
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken() let anonymousShopperTokenResponse =
await cookieHandler.getAnonymousToken()
const response = anonymousShopperTokenResponse.response const response = anonymousShopperTokenResponse.response
accessToken = anonymousShopperTokenResponse.accessToken accessToken = anonymousShopperTokenResponse.accessToken
cookieHandler.setAnonymousShopperCookie(response) cookieHandler.setAnonymousShopperCookie(response)
@ -30,12 +30,14 @@ const getCart: CartEndpoint['handlers']['getCart'] = async ({
{ headers: { 'x-vol-user-claims': accessToken } } { headers: { 'x-vol-user-claims': accessToken } }
) )
currentCart = result?.data?.currentCart currentCart = result?.data?.currentCart
headers = cookieHandler.headers
} catch (error) { } catch (error) {
throw error throw error
} }
res.status(200).json({
return {
data: currentCart ? normalizeCart(currentCart) : null, data: currentCart ? normalizeCart(currentCart) : null,
}) }
} }
export default getCart export default getCart

View File

@ -1,8 +1,8 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api' import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart' import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
import type { KiboCommerceAPI } from '../..' import type { KiboCommerceAPI } from '../..'
import getCart from './get-cart'; import getCart from './get-cart'
import addItem from './add-item'; import addItem from './add-item'
import updateItem from './update-item' import updateItem from './update-item'
import removeItem from './remove-item' import removeItem from './remove-item'

View File

@ -5,17 +5,10 @@ import { getCartQuery } from '../../../api/queries/get-cart-query'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req, req,
res, body: { itemId },
body: { cartId, itemId },
config, config,
}) => { }) => {
if (!itemId) { const encodedToken = req.cookies.get(config.customerCookie)
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const encodedToken = req.cookies[config.customerCookie]
const token = encodedToken const token = encodedToken
? Buffer.from(encodedToken, 'base64').toString('ascii') ? Buffer.from(encodedToken, 'base64').toString('ascii')
: null : null
@ -39,7 +32,10 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
) )
currentCart = result?.data?.currentCart currentCart = result?.data?.currentCart
} }
res.status(200).json({ data: normalizeCart(currentCart) })
return {
data: normalizeCart(currentCart),
}
} }
export default removeItem export default removeItem

View File

@ -5,17 +5,10 @@ import updateCartItemQuantityMutation from '../../../api/mutations/updateCartIte
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req, req,
res, body: { itemId, item },
body: { cartId, itemId, item },
config, config,
}) => { }) => {
if (!itemId || !item) { const encodedToken = req.cookies.get(config.cartCookie)
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const encodedToken = req.cookies[config.customerCookie]
const token = encodedToken const token = encodedToken
? Buffer.from(encodedToken, 'base64').toString('ascii') ? Buffer.from(encodedToken, 'base64').toString('ascii')
: null : null
@ -39,7 +32,8 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
) )
currentCart = result?.data?.currentCart currentCart = result?.data?.currentCart
} }
res.status(200).json({ data: normalizeCart(currentCart) })
return { data: normalizeCart(currentCart) }
} }
export default updateItem export default updateItem

View File

@ -2,16 +2,15 @@ import { Product } from '@vercel/commerce/types/product'
import { ProductsEndpoint } from '.' import { ProductsEndpoint } from '.'
import productSearchQuery from '../../../queries/product-search-query' import productSearchQuery from '../../../queries/product-search-query'
import { buildProductSearchVars } from '../../../../lib/product-search-vars' import { buildProductSearchVars } from '../../../../lib/product-search-vars'
import {normalizeProduct} from '../../../../lib/normalize' import { normalizeProduct } from '../../../../lib/normalize'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
res,
body: { search, categoryId, brandId, sort }, body: { search, categoryId, brandId, sort },
config, config,
}) => { }) => {
const pageSize = 100; const pageSize = 100
const filters = {}; const filters = {}
const startIndex = 0; const startIndex = 0
const variables = buildProductSearchVars({ const variables = buildProductSearchVars({
categoryCode: categoryId, categoryCode: categoryId,
pageSize, pageSize,
@ -20,12 +19,14 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
filters, filters,
startIndex, startIndex,
}) })
const {data} = await config.fetch(productSearchQuery, { variables }); const { data } = await config.fetch(productSearchQuery, { variables })
const found = data?.products?.items?.length > 0 ? true : false; const found = data?.products?.items?.length > 0 ? true : false
let productsResponse= data?.products?.items.map((item: any) =>normalizeProduct(item,config)); let productsResponse = data?.products?.items.map((item: any) =>
const products: Product[] = found ? productsResponse : []; normalizeProduct(item, config)
)
const products: Product[] = found ? productsResponse : []
res.status(200).json({ data: { products, found } }); return { data: { products, found } }
} }
export default getProducts export default getProducts

View File

@ -2,35 +2,32 @@ import CookieHandler from '../../../api/utils/cookie-handler'
import type { CustomerEndpoint } from '.' import type { CustomerEndpoint } from '.'
import { getCustomerAccountQuery } from '../../queries/get-customer-account-query' import { getCustomerAccountQuery } from '../../queries/get-customer-account-query'
import { normalizeCustomer } from '../../../lib/normalize' import { normalizeCustomer } from '../../../lib/normalize'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({ const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
req, async ({ req, config }) => {
res, const cookieHandler = new CookieHandler(config, req)
config, let accessToken = cookieHandler.getAccessToken()
}) => {
const cookieHandler = new CookieHandler(config, req, res)
let accessToken = cookieHandler.getAccessToken();
if (!cookieHandler.isShopperCookieAnonymous()) { if (!cookieHandler.isShopperCookieAnonymous()) {
const { data } = await config.fetch(getCustomerAccountQuery, undefined, { const { data } = await config.fetch(getCustomerAccountQuery, undefined, {
headers: { headers: {
'x-vol-user-claims': accessToken, 'x-vol-user-claims': accessToken,
}, },
})
const customer = normalizeCustomer(data?.customerAccount)
if (!customer.id) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
}) })
const customer = normalizeCustomer(data?.customerAccount)
if (!customer.id) {
throw new CommerceAPIError('Customer not found', {
status: 404,
})
}
return { data: { customer } }
} }
return res.status(200).json({ data: { customer } }) return { data: null }
} }
res.status(200).json({ data: null })
}
export default getLoggedInCustomer export default getLoggedInCustomer

View File

@ -1,66 +1,53 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { LoginEndpoint } from '.' import type { LoginEndpoint } from '.'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { loginMutation } from '../../mutations/login-mutation' import { loginMutation } from '../../mutations/login-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie'; import { prepareSetCookie } from '../../../lib/prepare-set-cookie'
import { setCookies } from '../../../lib/set-cookie'
import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date' import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date'
const invalidCredentials = /invalid credentials/i const invalidCredentials = /invalid credentials/i
const login: LoginEndpoint['handlers']['login'] = async ({ const login: LoginEndpoint['handlers']['login'] = async ({
req,
res,
body: { email, password }, body: { email, password },
config, config,
commerce,
}) => { }) => {
let response
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
try { try {
const variables = { loginInput: { username: email, password } }
const variables = { loginInput : { username: email, password }}; response = await config.fetch(loginMutation, { variables })
response = await config.fetch(loginMutation, { variables }) const { account: token } = response.data
const { account: token } = response.data;
// Set Cookie // Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays) const cookieExpirationDate = getCookieExpirationDate(
config.customerCookieMaxAgeInDays
)
const authCookie = prepareSetCookie( const authCookie = prepareSetCookie(
config.customerCookie, config.customerCookie,
JSON.stringify(token), JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {}, token.accessTokenExpiration ? { expires: cookieExpirationDate } : {}
) )
setCookies(res, [authCookie])
return { data: response, headers: { 'Set-Cookie': authCookie } }
} catch (error) { } catch (error) {
// Check if the email and password didn't match an existing account // Check if the email and password didn't match an existing account
if ( if (
error instanceof FetcherError && error instanceof FetcherError &&
invalidCredentials.test(error.message) invalidCredentials.test(error.message)
) { ) {
return res.status(401).json({ throw new CommerceAPIError(
data: null, 'Cannot find an account that matches the provided credentials',
errors: [ {
{ status: 401,
message: code: 'invalid_credentials',
'Cannot find an account that matches the provided credentials', }
code: 'invalid_credentials', )
}, } else {
], throw error
})
} }
throw error
} }
res.status(200).json({ data: response })
} }
export default login export default login

View File

@ -1,22 +1,22 @@
import type { LogoutEndpoint } from '.' import type { LogoutEndpoint } from '.'
import {prepareSetCookie} from '../../../lib/prepare-set-cookie'; import { prepareSetCookie } from '../../../lib/prepare-set-cookie'
import {setCookies} from '../../../lib/set-cookie'
const logout: LogoutEndpoint['handlers']['logout'] = async ({ const logout: LogoutEndpoint['handlers']['logout'] = async ({
res,
body: { redirectTo }, body: { redirectTo },
config, config,
}) => { }) => {
// Remove the cookie // Remove the cookie
const authCookie = prepareSetCookie(config.customerCookie,'',{ maxAge: -1, path: '/' }) const authCookie = prepareSetCookie(config.customerCookie, '', {
setCookies(res, [authCookie]) maxAge: -1,
path: '/',
})
const headers = {
'Set-Cookie': authCookie,
}
// Only allow redirects to a relative URL // Only allow redirects to a relative URL
if (redirectTo?.startsWith('/')) { return redirectTo?.startsWith('/') ? { redirectTo, headers } : { headers }
res.redirect(redirectTo)
} else {
res.status(200).json({ data: null })
}
} }
export default logout export default logout

View File

@ -1,91 +1,89 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { SignupEndpoint } from '.' import type { SignupEndpoint } from '.'
import { registerUserMutation, registerUserLoginMutation } from '../../mutations/signup-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie'; import { FetcherError } from '@vercel/commerce/utils/errors'
import { setCookies } from '../../../lib/set-cookie' import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import {
registerUserMutation,
registerUserLoginMutation,
} from '../../mutations/signup-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie'
import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date' import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date'
const invalidCredentials = /invalid credentials/i const invalidCredentials = /invalid credentials/i
const signup: SignupEndpoint['handlers']['signup'] = async ({ const signup: SignupEndpoint['handlers']['signup'] = async ({
req,
res,
body: { email, password, firstName, lastName }, body: { email, password, firstName, lastName },
config, config,
commerce,
}) => { }) => {
let response
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
try { try {
// Register user // Register user
const registerUserVariables = { const registerUserVariables = {
customerAccountInput: { customerAccountInput: {
emailAddress: email, emailAddress: email,
firstName: firstName, firstName: firstName,
lastName: lastName, lastName: lastName,
acceptsMarketing: true, acceptsMarketing: true,
id: 0 id: 0,
} },
} }
const registerUserResponse = await config.fetch(registerUserMutation, { variables: registerUserVariables}) const registerUserResponse = await config.fetch(registerUserMutation, {
const accountId = registerUserResponse.data?.account?.id; variables: registerUserVariables,
})
const accountId = registerUserResponse.data?.account?.id
// Login user // Login user
const registerUserLoginVairables = { const registerUserLoginVairables = {
accountId: accountId, accountId: accountId,
customerLoginInfoInput: { customerLoginInfoInput: {
emailAddress: email, emailAddress: email,
username: email, username: email,
password: password, password: password,
isImport: false isImport: false,
} },
} }
response = await config.fetch(registerUserLoginMutation, { variables: registerUserLoginVairables}) response = await config.fetch(registerUserLoginMutation, {
const { account: token } = response.data; variables: registerUserLoginVairables,
})
const { account: token } = response.data
// Set Cookie // Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays) const cookieExpirationDate = getCookieExpirationDate(
config.customerCookieMaxAgeInDays
)
const authCookie = prepareSetCookie( const authCookie = prepareSetCookie(
config.customerCookie, config.customerCookie,
JSON.stringify(token), JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {}, token.accessTokenExpiration ? { expires: cookieExpirationDate } : {}
) )
setCookies(res, [authCookie]) return {
data: response,
headers: {
'Set-Cookie': authCookie,
},
}
} catch (error) { } catch (error) {
// Check if the email and password didn't match an existing account // Check if the email and password didn't match an existing account
if ( if (
error instanceof FetcherError && error instanceof FetcherError &&
invalidCredentials.test(error.message) invalidCredentials.test(error.message)
) { ) {
return res.status(401).json({ throw new CommerceAPIError(
data: null, 'Cannot find an account that matches the provided credentials',
errors: [ {
{ status: 401,
message: code: 'invalid_credentials',
'Cannot find an account that matches the provided credentials', }
code: 'invalid_credentials', )
}, } else {
], throw error
})
} }
throw error
} }
res.status(200).json({ data: response })
} }
export default signup export default signup

View File

@ -1,17 +1,20 @@
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { normalizeWishlistItem } from '../../../lib/normalize' import { normalizeWishlistItem } from '../../../lib/normalize'
import { getProductQuery } from '../../../api/queries/get-product-query' import { getProductQuery } from '../../../api/queries/get-product-query'
import addItemToWishlistMutation from '../../mutations/addItemToWishlist-mutation'
import getCustomerId from '../../utils/get-customer-id'
import createWishlist from '../../mutations/create-wishlist-mutation' import createWishlist from '../../mutations/create-wishlist-mutation'
import addItemToWishlistMutation from '../../mutations/addItemToWishlist-mutation'
// Return wishlist info // Return wishlist info
const buildAddToWishlistVariables = ({ const buildAddToWishlistVariables = ({
productId, productId,
variantId, variantId,
productResponse, productResponse,
wishlist wishlist,
}: { }: {
productId: string productId: string
variantId: string variantId: string
@ -23,7 +26,7 @@ const buildAddToWishlistVariables = ({
const selectedOptions = product.variations?.find( const selectedOptions = product.variations?.find(
(v: any) => v.productCode === variantId (v: any) => v.productCode === variantId
).options ).options
const quantity=1 const quantity = 1
let options: any[] = [] let options: any[] = []
selectedOptions?.forEach((each: any) => { selectedOptions?.forEach((each: any) => {
product?.options product?.options
@ -47,53 +50,50 @@ const buildAddToWishlistVariables = ({
productCode: productId, productCode: productId,
variationProductCode: variantId ? variantId : null, variationProductCode: variantId ? variantId : null,
options, options,
}
}, },
},
} }
} }
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
res,
body: { customerToken, item }, body: { customerToken, item },
config, config,
commerce, commerce,
}) => { }) => {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null; const token = customerToken
const accessToken = token ? JSON.parse(token).accessToken : null; ? Buffer.from(customerToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
let result: { data?: any } = {} let result: { data?: any } = {}
let wishlist: any let wishlist: any
if (!item) { const customerId =
return res.status(400).json({ customerToken && (await getCustomerId({ customerToken, config }))
data: null, const wishlistName = config.defaultWishlistName
errors: [{ message: 'Missing item' }],
})
}
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
if (!customerId) { if (!customerId) {
return res.status(400).json({ throw new CommerceAPIError('Customer not found', { status: 404 })
data: null,
errors: [{ message: 'Invalid request' }],
})
} }
const wishlistResponse = await commerce.getCustomerWishlist({ const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName }, variables: { customerId, wishlistName },
config, config,
}) })
wishlist= wishlistResponse?.wishlist wishlist = wishlistResponse?.wishlist
if(Object.keys(wishlist).length === 0) { if (Object.keys(wishlist).length === 0) {
const createWishlistResponse= await config.fetch(createWishlist, {variables: { const createWishlistResponse = await config.fetch(
wishlistInput: { createWishlist,
customerAccountId: customerId, {
name: wishlistName variables: {
} wishlistInput: {
} customerAccountId: customerId,
}, {headers: { 'x-vol-user-claims': accessToken } }) name: wishlistName,
wishlist= createWishlistResponse?.data?.createWishlist },
},
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
wishlist = createWishlistResponse?.data?.createWishlist
} }
const productResponse = await config.fetch(getProductQuery, { const productResponse = await config.fetch(getProductQuery, {
@ -103,22 +103,33 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
const addItemToWishlistResponse = await config.fetch( const addItemToWishlistResponse = await config.fetch(
addItemToWishlistMutation, addItemToWishlistMutation,
{ {
variables: buildAddToWishlistVariables({ ...item, productResponse, wishlist }), variables: buildAddToWishlistVariables({
...item,
productResponse,
wishlist,
}),
}, },
{ headers: { 'x-vol-user-claims': accessToken } } { headers: { 'x-vol-user-claims': accessToken } }
) )
if(addItemToWishlistResponse?.data?.createWishlistItem){ if (addItemToWishlistResponse?.data?.createWishlistItem) {
const wishlistResponse= await commerce.getCustomerWishlist({ const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName }, variables: { customerId, wishlistName },
config, config,
}) })
wishlist= wishlistResponse?.wishlist wishlist = wishlistResponse?.wishlist
} }
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config))} }
res.status(200).json({ data: result?.data }) result = {
data: {
...wishlist,
items: wishlist?.items?.map((item: any) =>
normalizeWishlistItem(item, config)
),
},
}
return { data: result?.data }
} }
export default addItem export default addItem

View File

@ -1,35 +1,45 @@
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import { normalizeWishlistItem } from '../../../lib/normalize' import { normalizeWishlistItem } from '../../../lib/normalize'
// Return wishlist info // Return wishlist info
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
res,
body: { customerToken, includeProducts }, body: { customerToken, includeProducts },
config, config,
commerce, commerce,
}) => { }) => {
let result: { data?: any } = {} let result: { data?: any } = {}
if (customerToken) { if (customerToken) {
const customerId = customerToken && (await getCustomerId({ customerToken, config })) const customerId =
const wishlistName= config.defaultWishlistName customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName = config.defaultWishlistName
if (!customerId) { if (!customerId) {
// If the customerToken is invalid, then this request is too throw new CommerceAPIError('Wishlist not found', {
return res.status(404).json({ status: 404,
data: null, code: 'not_found',
errors: [{ message: 'Wishlist not found' }],
}) })
} }
const { wishlist } = await commerce.getCustomerWishlist({ const { wishlist } = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName }, variables: { customerId, wishlistName },
includeProducts, includeProducts,
config, config,
}) })
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config, includeProducts))} } result = {
data: {
...wishlist,
items: wishlist?.items?.map((item: any) =>
normalizeWishlistItem(item, config, includeProducts)
),
},
}
} }
res.status(200).json({ data: result?.data ?? null }) return { data: result?.data ?? null }
} }
export default getWishlist export default getWishlist

View File

@ -1,60 +1,69 @@
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { normalizeWishlistItem } from '../../../lib/normalize' import { normalizeWishlistItem } from '../../../lib/normalize'
import getCustomerId from '../../utils/get-customer-id'
import removeItemFromWishlistMutation from '../../mutations/removeItemFromWishlist-mutation' import removeItemFromWishlistMutation from '../../mutations/removeItemFromWishlist-mutation'
// Return wishlist info // Return wishlist info
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
res,
body: { customerToken, itemId }, body: { customerToken, itemId },
config, config,
commerce, commerce,
}) => { }) => {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null; const token = customerToken
const accessToken = token ? JSON.parse(token).accessToken : null; ? Buffer.from(customerToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
let result: { data?: any } = {} let result: { data?: any } = {}
let wishlist: any let wishlist: any
const customerId = customerToken && (await getCustomerId({ customerToken, config })) const customerId =
const wishlistName= config.defaultWishlistName customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName = config.defaultWishlistName
const wishlistResponse = await commerce.getCustomerWishlist({ const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName }, variables: { customerId, wishlistName },
config, config,
}) })
wishlist= wishlistResponse?.wishlist wishlist = wishlistResponse?.wishlist
if (!wishlist || !itemId) { if (!wishlist) {
return res.status(400).json({ throw new CommerceAPIError('Wishlist not found', { status: 404 })
data: null,
errors: [{ message: 'Invalid request' }],
})
} }
const removedItem = wishlist?.items?.find(
(item:any) => { const removedItem = wishlist?.items?.find((item: any) => {
return item.product.productCode === itemId; return item.product.productCode === itemId
} })
);
const removeItemFromWishlistResponse = await config.fetch( const removeItemFromWishlistResponse = await config.fetch(
removeItemFromWishlistMutation, removeItemFromWishlistMutation,
{ {
variables: { variables: {
wishlistId: wishlist?.id, wishlistId: wishlist?.id,
wishlistItemId: removedItem?.id wishlistItemId: removedItem?.id,
}, },
}, },
{ headers: { 'x-vol-user-claims': accessToken } } { headers: { 'x-vol-user-claims': accessToken } }
) )
if(removeItemFromWishlistResponse?.data?.deleteWishlistItem){ if (removeItemFromWishlistResponse?.data?.deleteWishlistItem) {
const wishlistResponse= await commerce.getCustomerWishlist({ const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName }, variables: { customerId, wishlistName },
config, config,
}) })
wishlist= wishlistResponse?.wishlist wishlist = wishlistResponse?.wishlist
}
result = {
data: {
...wishlist,
items: wishlist?.items?.map((item: any) =>
normalizeWishlistItem(item, config)
),
},
}
return {
data: result?.data,
} }
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config))} }
res.status(200).json({ data: result?.data })
} }
export default removeItem export default removeItem

View File

@ -9,16 +9,15 @@ import getCustomerWishlist from './operations/get-customer-wishlist'
import getAllProductPaths from './operations/get-all-product-paths' import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products' import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product' import getProduct from './operations/get-product'
import type { RequestInit } from '@vercel/fetch'
export interface KiboCommerceConfig extends CommerceAPIConfig { export interface KiboCommerceConfig extends CommerceAPIConfig {
apiHost?: string apiHost?: string
clientId?: string clientId?: string
sharedSecret?: string sharedSecret?: string
customerCookieMaxAgeInDays: number, customerCookieMaxAgeInDays: number
currencyCode: string, currencyCode: string
documentListName: string, documentListName: string
defaultWishlistName: string, defaultWishlistName: string
authUrl?: string authUrl?: string
} }
@ -37,7 +36,7 @@ const config: KiboCommerceConfig = {
sharedSecret: process.env.KIBO_SHARED_SECRET || '', sharedSecret: process.env.KIBO_SHARED_SECRET || '',
customerCookieMaxAgeInDays: 30, customerCookieMaxAgeInDays: 30,
currencyCode: 'USD', currencyCode: 'USD',
defaultWishlistName: 'My Wishlist' defaultWishlistName: 'My Wishlist',
} }
const operations = { const operations = {
@ -55,7 +54,7 @@ export const provider = { config, operations }
export type KiboCommerceProvider = typeof provider export type KiboCommerceProvider = typeof provider
export type KiboCommerceAPI< export type KiboCommerceAPI<
P extends KiboCommerceProvider = KiboCommerceProvider P extends KiboCommerceProvider = KiboCommerceProvider
> = CommerceAPI<P | any> > = CommerceAPI<P | any>
export function getCommerceApi<P extends KiboCommerceProvider>( export function getCommerceApi<P extends KiboCommerceProvider>(
customProvider: P = provider as any customProvider: P = provider as any

View File

@ -1,6 +1,4 @@
import type { KiboCommerceConfig } from '../index' import type { KiboCommerceConfig } from '../index'
import type { FetchOptions } from '@vercel/fetch'
import fetch from './fetch'
// This object is persisted during development // This object is persisted during development
const authCache: { kiboAuthTicket?: AppAuthTicket } = {} const authCache: { kiboAuthTicket?: AppAuthTicket } = {}
@ -41,11 +39,11 @@ export class APIAuthenticationHelper {
this._clientId = clientId this._clientId = clientId
this._sharedSecret = sharedSecret this._sharedSecret = sharedSecret
this._authUrl = authUrl this._authUrl = authUrl
if(!authTicketCache) { if (!authTicketCache) {
this._authTicketCache = new RuntimeMemCache(); this._authTicketCache = new RuntimeMemCache()
} }
} }
private _buildFetchOptions(body: any = {}): FetchOptions { private _buildFetchOptions(body: any = {}): any {
return { return {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@ -2,24 +2,25 @@ import { KiboCommerceConfig } from './../index'
import { getCookieExpirationDate } from '../../lib/get-cookie-expiration-date' import { getCookieExpirationDate } from '../../lib/get-cookie-expiration-date'
import { prepareSetCookie } from '../../lib/prepare-set-cookie' import { prepareSetCookie } from '../../lib/prepare-set-cookie'
import { setCookies } from '../../lib/set-cookie' import { setCookies } from '../../lib/set-cookie'
import { NextApiRequest } from 'next'
import getAnonymousShopperToken from './get-anonymous-shopper-token' import getAnonymousShopperToken from './get-anonymous-shopper-token'
import { NextRequest, NextResponse } from 'next/server'
const parseCookie = (cookieValue?: any) => { const parseCookie = (cookieValue?: any) => {
return cookieValue return cookieValue
? JSON.parse(Buffer.from(cookieValue, 'base64').toString('ascii')) ? JSON.parse(Buffer.from(cookieValue, 'base64').toString('ascii'))
: null : null
} }
export default class CookieHandler { export default class CookieHandler {
config: KiboCommerceConfig config: KiboCommerceConfig
request: NextApiRequest request: NextRequest
response: any headers: HeadersInit | undefined
accessToken: any accessToken: any
constructor(config: any, req: NextApiRequest, res: any) { constructor(config: any, req: NextRequest) {
this.config = config this.config = config
this.request = req this.request = req
this.response = res
const encodedToken = req.cookies[config.customerCookie] const encodedToken = req.cookies.get(config.customerCookie)
const token = parseCookie(encodedToken) const token = parseCookie(encodedToken)
this.accessToken = token ? token.accessToken : null this.accessToken = token ? token.accessToken : null
} }
@ -36,9 +37,9 @@ export default class CookieHandler {
} }
isShopperCookieAnonymous() { isShopperCookieAnonymous() {
const customerCookieKey = this.config.customerCookie const customerCookieKey = this.config.customerCookie
const shopperCookie = this.request.cookies[customerCookieKey] const shopperCookie = this.request.cookies.get(customerCookieKey)
const shopperSession = parseCookie(shopperCookie); const shopperSession = parseCookie(shopperCookie)
const isAnonymous = shopperSession?.customerAccount ? false : true const isAnonymous = shopperSession?.customerAccount ? false : true
return isAnonymous return isAnonymous
} }
setAnonymousShopperCookie(anonymousShopperTokenResponse: any) { setAnonymousShopperCookie(anonymousShopperTokenResponse: any) {
@ -53,7 +54,9 @@ export default class CookieHandler {
? { expires: cookieExpirationDate } ? { expires: cookieExpirationDate }
: {} : {}
) )
setCookies(this.response, [authCookie]) this.headers = {
'Set-Cookie': authCookie,
}
} }
getAccessToken() { getAccessToken() {
return this.accessToken return this.accessToken

View File

@ -1,43 +1,46 @@
import { FetcherError } from '@vercel/commerce/utils/errors' import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api' import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { KiboCommerceConfig } from '../index' import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
import { APIAuthenticationHelper } from './api-auth-helper'; import { APIAuthenticationHelper } from './api-auth-helper'
const fetchGraphqlApi: ( const fetchGraphqlApi: (
getConfig: () => KiboCommerceConfig getConfig: () => KiboCommerceConfig
) => GraphQLFetcher = (getConfig) => async ( ) => GraphQLFetcher =
query: string, (getConfig) =>
{ variables, preview } = {}, async (query: string, { variables, preview } = {}, headers?: HeadersInit) => {
fetchOptions const config = getConfig()
) => { const authHelper = new APIAuthenticationHelper(config)
const config = getConfig() const apiToken = await authHelper.getAccessToken()
const authHelper = new APIAuthenticationHelper(config); const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
const apiToken = await authHelper.getAccessToken(); method: 'POST',
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { headers: {
...fetchOptions, ...headers,
method: 'POST', Authorization: `Bearer ${apiToken}`,
headers: { 'Content-Type': 'application/json',
...fetchOptions?.headers, },
Authorization: `Bearer ${apiToken}`, body: JSON.stringify({
'Content-Type': 'application/json', query,
}, variables,
body: JSON.stringify({ }),
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
console.warn(`Kibo API Request Correlation ID: ${res.headers.get('x-vol-correlation')}`);
throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch KiboCommerce API' }],
status: res.status,
}) })
const json = await res.json()
if (json.errors) {
console.warn(
`Kibo API Request Correlation ID: ${res.headers.get(
'x-vol-correlation'
)}`
)
throw new FetcherError({
errors: json.errors ?? [
{ message: 'Failed to fetch KiboCommerce API' },
],
status: res.status,
})
}
return { data: json.data, res }
} }
return { data: json.data, res }
}
export default fetchGraphqlApi export default fetchGraphqlApi

View File

@ -1,19 +1,19 @@
import { FetcherError } from '@vercel/commerce/utils/errors' import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api' import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { KiboCommerceConfig } from '../index' import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher = const fetchGraphqlApi: (
getConfig: () => KiboCommerceConfig
) => GraphQLFetcher =
(getConfig) => (getConfig) =>
async (query: string, { variables, preview } = {}, fetchOptions) => { async (query: string, { variables, preview } = {}, headers?: HeadersInit) => {
const config = getConfig() const config = getConfig()
const res = await fetch(config.commerceUrl, { const res = await fetch(config.commerceUrl, {
//const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { //const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${config.apiToken}`, Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers, ...headers,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
@ -25,7 +25,9 @@ const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher =
const json = await res.json() const json = await res.json()
if (json.errors) { if (json.errors) {
throw new FetcherError({ throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch KiboCommerce API' }], errors: json.errors ?? [
{ message: 'Failed to fetch KiboCommerce API' },
],
status: res.status, status: res.status,
}) })
} }

View File

@ -8,17 +8,13 @@ async function getCustomerId({
customerToken: string customerToken: string
config: KiboCommerceConfig config: KiboCommerceConfig
}): Promise<string | undefined> { }): Promise<string | undefined> {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null; const token = customerToken
const accessToken = token ? JSON.parse(token).accessToken : null; ? Buffer.from(customerToken, 'base64').toString('ascii')
const { data } = await config.fetch( : null
getCustomerAccountQuery, const accessToken = token ? JSON.parse(token).accessToken : null
undefined, const { data } = await config.fetch(getCustomerAccountQuery, undefined, {
{ 'x-vol-user-claims': accessToken,
headers: { })
'x-vol-user-claims': accessToken,
},
}
)
return data?.customerAccount?.id return data?.customerAccount?.id
} }

View File

@ -1,3 +1,3 @@
export function setCookies(res: any, cookies: string[]): void { export function setCookies(res: any, cookies: string[]): void {
res.setHeader('Set-Cookie', cookies); res.setHeader('Set-Cookie', cookies)
} }

View File

@ -47,9 +47,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@vercel/commerce": "workspace:*", "@vercel/commerce": "workspace:*"
"@vercel/fetch": "^6.2.0",
"node-fetch": "^2.6.7"
}, },
"peerDependencies": { "peerDependencies": {
"next": "^12", "next": "^12",

View File

@ -1,17 +1,15 @@
import { FetcherError } from '@vercel/commerce/utils/errors' import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api' import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { LocalConfig } from '../index' import type { LocalConfig } from '../index'
import fetch from './fetch'
const fetchGraphqlApi: (getConfig: () => LocalConfig) => GraphQLFetcher = const fetchGraphqlApi: (getConfig: () => LocalConfig) => GraphQLFetcher =
(getConfig) => (getConfig) =>
async (query: string, { variables, preview } = {}, fetchOptions) => { async (query: string, { variables, preview } = {}, headers?: HeadersInit) => {
const config = getConfig() const config = getConfig()
const res = await fetch(config.commerceUrl, { const res = await fetch(config.commerceUrl, {
...fetchOptions,
method: 'POST', method: 'POST',
headers: { headers: {
...fetchOptions?.headers, ...headers,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({

View File

@ -48,10 +48,7 @@
}, },
"dependencies": { "dependencies": {
"@vercel/commerce": "workspace:*", "@vercel/commerce": "workspace:*",
"@vercel/fetch": "^6.2.0",
"stripe": "^8.197.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"node-fetch": "^2.6.7",
"cookie": "^0.4.1" "cookie": "^0.4.1"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -1,64 +1,63 @@
import type { CartEndpoint } from '.' import type { CartEndpoint } from '.'
import type { RawVariant } from '../../../types/product' import type { RawVariantSpec } from '../../../types/product'
import type { LineItem } from '@vercel/commerce/types/cart'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart' import { formatCart } from '../../utils/cart'
import { serialize } from 'cookie'
const addItem: CartEndpoint['handlers']['addItem'] = async ({ const addItem: CartEndpoint['handlers']['addItem'] = async ({
res, req,
body: { cartId, item }, body: { cartId, item },
config: { restBuyerFetch, cartCookie, tokenCookie }, config: { restBuyerFetch, cartCookie, tokenCookie },
}) => { }) => {
// Store token // Get token
let token let token = req.cookies.get(tokenCookie)
let headers = new Headers()
// Set the quantity if not present
if (!item.quantity) item.quantity = 1
// Create an order if it doesn't exist // Create an order if it doesn't exist
if (!cartId) { if (!cartId) {
const { ID, meta } = await restBuyerFetch( const { ID, meta } = await restBuyerFetch(
'POST', 'POST',
`/orders/Outgoing`, `/orders/Outgoing`,
{} {},
).then((response: { ID: string; meta: { token: string } }) => response) { token }
)
// Set the cart id and token
cartId = ID cartId = ID
token = meta.token
// Set the cart and token cookie headers.append(
res.setHeader('Set-Cookie', [ 'set-cookie',
serialize(tokenCookie, meta.token, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
serialize(cartCookie, cartId!, { serialize(cartCookie, cartId!, {
maxAge: 60 * 60 * 24 * 30, maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000), expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',
path: '/', path: '/',
sameSite: 'lax', sameSite: 'lax',
}), })
]) )
headers.append(
'set-cookie',
serialize(tokenCookie, meta.token.access_token, {
maxAge: meta.token.expires_in,
expires: new Date(Date.now() + meta.token.expires_in * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
})
)
} }
// Store specs let specs: RawVariantSpec[] = []
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs // If a variant is present, fetch its specs
if (item.variantId) { if (item.variantId !== 'undefined') {
specs = await restBuyerFetch( const { Specs } = await restBuyerFetch(
'GET', 'GET',
`/me/products/${item.productId}/variants/${item.variantId}`, `/me/products/${item.productId}/variants/${item.variantId}`,
null, null,
{ token } { token }
).then((res: RawVariant) => res.Specs) )
specs = Specs
} }
// Add the item to the order // Add the item to the order
@ -73,19 +72,19 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
{ token } { token }
) )
// Get cart // Get cart & line items
const [cart, lineItems] = await Promise.all([ const [cart, { Items }] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }), restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, { restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token, token,
}).then((response: { Items: LineItem[] }) => response.Items), }),
]) ])
// Format cart // Format cart
const formattedCart = formatCart(cart, lineItems) const formattedCart = formatCart(cart, Items)
// Return cart and errors // Return cart and headers
res.status(200).json({ data: formattedCart, errors: [] }) return { data: formattedCart, headers }
} }
export default addItem export default addItem

View File

@ -1,64 +1,62 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { CartEndpoint } from '.' import type { CartEndpoint } from '.'
import { serialize } from 'cookie' import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart' import { formatCart } from '../../utils/cart'
// Return current cart info // Return current cart info
const getCart: CartEndpoint['handlers']['getCart'] = async ({ const getCart: CartEndpoint['handlers']['getCart'] = async ({
req, req,
res,
body: { cartId }, body: { cartId },
config: { restBuyerFetch, cartCookie, tokenCookie }, config: { restBuyerFetch, cartCookie, tokenCookie },
}) => { }) => {
// If no cartId is provided, return data null
if (!cartId) { if (!cartId) {
return res.status(400).json({ return { data: null }
data: null,
errors: [{ message: 'Invalid request' }],
})
} }
try { try {
// Get token from cookies // Get token
const token = req.cookies[tokenCookie] const token = req.cookies.get(tokenCookie)
// Get cart // Get cart & line items
const cart = await restBuyerFetch( const [cart, { Items, meta }] = await Promise.all([
'GET', restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
`/orders/Outgoing/${cartId}`, restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
null, token,
{ token }
)
// Get line items
const lineItems = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/lineitems`,
null,
{ token }
).then((response: { Items: OrdercloudLineItem[] }) => response.Items)
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
} catch (error) {
// Reset cart and token cookie
res.setHeader('Set-Cookie', [
serialize(cartCookie, cartId, {
maxAge: -1,
path: '/',
}),
serialize(tokenCookie, cartId, {
maxAge: -1,
path: '/',
}), }),
]) ])
// Format cart
const formattedCart = formatCart(cart, Items)
// Return cart and errors
return {
data: formattedCart,
}
} catch (error) {
console.error(error)
const headers = new Headers()
headers.append(
'set-cookie',
serialize(cartCookie, '', {
maxAge: -1,
path: '/',
})
)
headers.append(
'set-cookie',
serialize(tokenCookie, '', {
maxAge: -1,
path: '/',
})
)
// Return empty cart // Return empty cart
res.status(200).json({ data: null, errors: [] }) return {
data: null,
headers,
}
} }
} }

View File

@ -1,30 +1,22 @@
import type { CartEndpoint } from '.' import type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart' import { formatCart } from '../../utils/cart'
import { OrdercloudLineItem } from '../../../types/cart' import { OrdercloudLineItem } from '../../../types/cart'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req, req,
res,
body: { cartId, itemId }, body: { cartId, itemId },
config: { restBuyerFetch, tokenCookie }, config: { restBuyerFetch, tokenCookie },
}) => { }) => {
if (!cartId || !itemId) { const token = req.cookies.get(tokenCookie)
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Remove the item to the order // Remove the item to the order
await restBuyerFetch( await restBuyerFetch(
'DELETE', 'DELETE',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`, `/orders/Outgoing/${cartId}/lineitems/${itemId}`,
null, null,
{ token } {
token,
}
) )
// Get cart // Get cart
@ -39,7 +31,7 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
const formattedCart = formatCart(cart, lineItems) const formattedCart = formatCart(cart, lineItems)
// Return cart and errors // Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] }) return { data: formattedCart }
} }
export default removeItem export default removeItem

View File

@ -6,19 +6,10 @@ import { formatCart } from '../../utils/cart'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req, req,
res,
body: { cartId, itemId, item }, body: { cartId, itemId, item },
config: { restBuyerFetch, tokenCookie }, config: { restBuyerFetch, tokenCookie },
}) => { }) => {
if (!cartId || !itemId || !item) { const token = req.cookies.get(tokenCookie)
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Store specs // Store specs
let specs: RawVariant['Specs'] = [] let specs: RawVariant['Specs'] = []
@ -27,9 +18,7 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
if (item.variantId) { if (item.variantId) {
specs = await restBuyerFetch( specs = await restBuyerFetch(
'GET', 'GET',
`/me/products/${item.productId}/variants/${item.variantId}`, `/me/products/${item.productId}/variants/${item.variantId}`
null,
{ token }
).then((res: RawVariant) => res.Specs) ).then((res: RawVariant) => res.Specs)
} }
@ -42,7 +31,9 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
Quantity: item.quantity, Quantity: item.quantity,
Specs: specs, Specs: specs,
}, },
{ token } {
token,
}
) )
// Get cart // Get cart
@ -57,7 +48,7 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
const formattedCart = formatCart(cart, lineItems) const formattedCart = formatCart(cart, lineItems)
// Return cart and errors // Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] }) return { data: formattedCart }
} }
export default updateItem export default updateItem

View File

@ -1,13 +1,14 @@
import { normalize as normalizeProduct } from '../../../../utils/product'
import { ProductsEndpoint } from '.' import { ProductsEndpoint } from '.'
import { normalize as normalizeProduct } from '../../../../utils/product'
// Get products for the product list page. Search and category filter implemented. Sort and brand filter not implemented. // Get products for the product list page. Search and category filter implemented. Sort and brand filter not implemented.
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
req, req,
res, body: { search, categoryId },
body: { search, categoryId, brandId, sort }, config: { restBuyerFetch, tokenCookie },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => { }) => {
const token = req.cookies.get(tokenCookie)
//Use a dummy base as we only care about the relative path //Use a dummy base as we only care about the relative path
const url = new URL('/me/products', 'http://a') const url = new URL('/me/products', 'http://a')
@ -18,20 +19,19 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
url.searchParams.set('categoryID', String(categoryId)) url.searchParams.set('categoryID', String(categoryId))
} }
// Get token from cookies
const token = req.cookies[tokenCookie]
var rawProducts = await restBuyerFetch( var rawProducts = await restBuyerFetch(
'GET', 'GET',
url.pathname + url.search, url.pathname + url.search,
null, null,
{ token } { token }
) ).then((response: { Items: any[] }) => response.Items)
const products = rawProducts.Items.map(normalizeProduct) return {
const found = rawProducts?.Items?.length > 0 data: {
products: rawProducts.map(normalizeProduct),
res.status(200).json({ data: { products, found } }) found: rawProducts?.length > 0,
},
}
} }
export default getProducts export default getProducts

View File

@ -2,27 +2,15 @@ import type { CheckoutEndpoint } from '.'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req, req,
res,
body: { cartId }, body: { cartId },
config: { restBuyerFetch, tokenCookie }, config: { restBuyerFetch },
}) => { }) => {
// Return an error if no item is present const token = req.cookies.get('token')
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing cookie' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Register credit card // Register credit card
const payments = await restBuyerFetch( const payments = await restBuyerFetch(
'GET', 'GET',
`/orders/Outgoing/${cartId}/payments`, `/orders/Outgoing/${cartId}/payments`
null,
{ token }
).then((response: { Items: unknown[] }) => response.Items) ).then((response: { Items: unknown[] }) => response.Items)
const address = await restBuyerFetch( const address = await restBuyerFetch(
@ -35,15 +23,15 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
) )
// Return cart and errors // Return cart and errors
res.status(200).json({
return {
data: { data: {
hasPayment: payments.length > 0, hasPayment: payments.length > 0,
hasShipping: Boolean(address), hasShipping: Boolean(address),
addressId: address, addressId: address,
cardId: payments[0]?.ID, cardId: payments[0]?.ID,
}, },
errors: [], }
})
} }
export default getCheckout export default getCheckout

View File

@ -2,31 +2,18 @@ import type { CheckoutEndpoint } from '.'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({ const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
req, req,
res,
body: { cartId }, body: { cartId },
config: { restBuyerFetch, tokenCookie }, config: { restBuyerFetch, tokenCookie },
}) => { }) => {
// Return an error if no item is present const token = req.cookies.get(tokenCookie)
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Submit order // Submit order
await restBuyerFetch( await restBuyerFetch('POST', `/orders/Outgoing/${cartId}/submit`, null, {
'POST', token,
`/orders/Outgoing/${cartId}/submit`, })
{},
{ token }
)
// Return cart and errors // Return cart and errors
res.status(200).json({ data: null, errors: [] }) return { data: null }
} }
export default submitCheckout export default submitCheckout

View File

@ -1,25 +1,11 @@
import type { CustomerAddressEndpoint } from '.' import type { CustomerAddressEndpoint } from '.'
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({ const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
res, req,
body: { item, cartId }, body: { item, cartId },
config: { restBuyerFetch }, config: { restBuyerFetch, tokenCookie },
}) => { }) => {
// Return an error if no item is present const token = req.cookies.get(tokenCookie)
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Register address // Register address
const address = await restBuyerFetch('POST', `/me/addresses`, { const address = await restBuyerFetch('POST', `/me/addresses`, {
@ -37,11 +23,16 @@ const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
}).then((response: { ID: string }) => response.ID) }).then((response: { ID: string }) => response.ID)
// Assign address to order // Assign address to order
await restBuyerFetch('PATCH', `/orders/Outgoing/${cartId}`, { await restBuyerFetch(
ShippingAddressID: address, 'PATCH',
}) `/orders/Outgoing/${cartId}`,
{
ShippingAddressID: address,
},
{ token }
)
return res.status(200).json({ data: null, errors: [] }) return { data: null }
} }
export default addItem export default addItem

View File

@ -1,9 +1,8 @@
import type { CustomerAddressEndpoint } from '.' import type { CustomerAddressEndpoint } from '.'
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({ const getAddresses: CustomerAddressEndpoint['handlers']['getAddresses'] =
res, () => {
}) => { return Promise.resolve({ data: null })
return res.status(200).json({ data: null, errors: [] }) }
}
export default getCards export default getAddresses

View File

@ -1,9 +1,7 @@
import type { CustomerAddressEndpoint } from '.' import type { CustomerAddressEndpoint } from '.'
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({ const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = () => {
res, return Promise.resolve({ data: null })
}) => {
return res.status(200).json({ data: null, errors: [] })
} }
export default removeItem export default removeItem

View File

@ -1,9 +1,7 @@
import type { CustomerAddressEndpoint } from '.' import type { CustomerAddressEndpoint } from '.'
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({ const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = () => {
res, return Promise.resolve({ data: null })
}) => {
return res.status(200).json({ data: null, errors: [] })
} }
export default updateItem export default updateItem

View File

@ -1,53 +1,47 @@
import type { CustomerCardEndpoint } from '.' import type { CustomerCardEndpoint } from '.'
import type { OredercloudCreditCard } from '../../../../types/customer/card' import type { OredercloudCreditCard } from '../../../../types/customer/card'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET as string, {
apiVersion: '2020-08-27',
})
const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({ const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
res, req,
body: { item, cartId }, body: { item, cartId },
config: { restBuyerFetch, restMiddlewareFetch }, config: { restBuyerFetch, tokenCookie },
}) => { }) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Get token // Get token
const token = await stripe.tokens const token = req.cookies.get(tokenCookie)
.create({
const [exp_month, exp_year] = item.cardExpireDate.split('/')
const stripeToken = await fetch('https://api.stripe.com/v1/tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.STRIPE_SECRET}`,
},
body: JSON.stringify({
card: { card: {
number: item.cardNumber, number: item.cardNumber,
exp_month: item.cardExpireDate.split('/')[0], exp_month,
exp_year: item.cardExpireDate.split('/')[1], exp_year,
cvc: item.cardCvc, cvc: item.cardCvc,
}, },
}) }),
.then((res: { id: string }) => res.id) })
.then((res) => res.json())
.then((res) => res.id)
// Register credit card // Register credit card
const creditCard = await restBuyerFetch('POST', `/me/creditcards`, { const creditCard = await restBuyerFetch(
Token: token, 'POST',
CardType: 'credit', `/me/creditcards`,
PartialAccountNumber: item.cardNumber.slice(-4), {
CardholderName: item.cardHolder, Token: stripeToken,
ExpirationDate: item.cardExpireDate, CardType: 'credit',
}).then((response: OredercloudCreditCard) => response.ID) PartialAccountNumber: item.cardNumber.slice(-4),
CardholderName: item.cardHolder,
ExpirationDate: item.cardExpireDate,
},
{
token,
}
).then((response: OredercloudCreditCard) => response.ID)
// Assign payment to order // Assign payment to order
const payment = await restBuyerFetch( const payment = await restBuyerFetch(
@ -56,19 +50,18 @@ const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
{ {
Type: 'CreditCard', Type: 'CreditCard',
CreditCardID: creditCard, CreditCardID: creditCard,
},
{
token,
} }
).then((response: { ID: string }) => response.ID) ).then((response: { ID: string }) => response.ID)
// Accept payment to order // Accept payment to order
await restMiddlewareFetch( await restBuyerFetch('PATCH', `/orders/All/${cartId}/payments/${payment}`, {
'PATCH', Accepted: true,
`/orders/All/${cartId}/payments/${payment}`, })
{
Accepted: true,
}
)
return res.status(200).json({ data: null, errors: [] }) return { data: null }
} }
export default addItem export default addItem

View File

@ -1,9 +1,7 @@
import type { CustomerCardEndpoint } from '.' import type { CustomerCardEndpoint } from '.'
const getCards: CustomerCardEndpoint['handlers']['getCards'] = async ({ const getCards: CustomerCardEndpoint['handlers']['getCards'] = () => {
res, return Promise.resolve({ data: null })
}) => {
return res.status(200).json({ data: null, errors: [] })
} }
export default getCards export default getCards

Some files were not shown because too many files have changed in this diff Show More