mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Move to Edge runtime
This commit is contained in:
parent
cf1878e9f0
commit
483b608e0c
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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 {}
|
||||||
|
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ async function getCustomerId({
|
|||||||
getCustomerIdQuery,
|
getCustomerIdQuery,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
headers: {
|
'Set-Cookie': `${config.customerCookie}=${customerToken}`,
|
||||||
cookie: `${config.customerCookie}=${customerToken}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 })
|
||||||
|
@ -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 }
|
||||||
|
@ -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,
|
||||||
})),
|
})),
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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> {
|
||||||
|
@ -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']>
|
||||||
}
|
}
|
||||||
|
@ -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' }],
|
||||||
}
|
}
|
||||||
|
3
packages/commerce/src/api/utils/index.ts
Normal file
3
packages/commerce/src/api/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
export const getInput = (req: NextRequest) => req.json().catch(() => ({}))
|
@ -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
|
|
||||||
}
|
|
11
packages/commerce/src/api/utils/parse-output.ts
Normal file
11
packages/commerce/src/api/utils/parse-output.ts
Normal 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
|
@ -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>>
|
||||||
|
@ -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}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
48
packages/commerce/src/api/utils/validate-method.ts
Normal file
48
packages/commerce/src/api/utils/validate-method.ts
Normal 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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
})
|
})
|
||||||
|
@ -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(),
|
||||||
|
})
|
||||||
|
@ -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(),
|
||||||
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
})
|
||||||
|
@ -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(),
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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][]
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -1 +1,3 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
export default function getCheckout(...args: any[]) {
|
||||||
|
return Promise.resolve({ data: null })
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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':
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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: {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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({
|
||||||
|
@ -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": {
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user