1
0
mirror of https://github.com/vercel/commerce.git synced 2025-07-22 12:24:18 +00:00

Merge branch 'master' of https://github.com/vercel/commerce into custom-checkout

This commit is contained in:
Bel Curcio
2021-06-03 12:37:47 -03:00
329 changed files with 5858 additions and 3515 deletions
components
cart
CartItem
common
Footer
HomeAllProductsGrid
Layout
Navbar
UserNav
product
wishlist
WishlistButton
WishlistCard
framework
bigcommerce
commerce
shopify
swell
vendure
lib
next.config.jspackage.json
pages
tsconfig.json

@@ -5,7 +5,7 @@ import Link from 'next/link'
import s from './CartItem.module.css'
import { Trash, Plus, Minus, Cross } from '@components/icons'
import { useUI } from '@components/ui/context'
import type { LineItem } from '@framework/types'
import type { LineItem } from '@commerce/types/cart'
import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item'
@@ -37,7 +37,7 @@ const CartItem = ({
const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState(item.quantity)
const [quantity, setQuantity] = useState<number | ''>(item.quantity)
const [removing, setRemoving] = useState(false)
const updateQuantity = async (val: number) => {
@@ -45,9 +45,10 @@ const CartItem = ({
}
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
if (Number.isInteger(val) && val >= 0) {
setQuantity(Number(e.target.value))
const val = !e.target.value ? '' : Number(e.target.value)
if (!val || (Number.isInteger(val) && val >= 0)) {
setQuantity(val)
}
}

@@ -2,7 +2,7 @@ import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { Page } from '@framework/common/get-all-pages'
import type { Page } from '@commerce/types/page'
import getSlug from '@lib/get-slug'
import { Github, Vercel } from '@components/icons'
import { Logo, Container } from '@components/ui'

@@ -1,6 +1,6 @@
import { FC } from 'react'
import Link from 'next/link'
import type { Product } from '@commerce/types'
import type { Product } from '@commerce/types/product'
import { Grid } from '@components/ui'
import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css'

@@ -1,11 +1,13 @@
import cn from 'classnames'
import dynamic from 'next/dynamic'
import s from './Layout.module.css'
import { useRouter } from 'next/router'
import React, { FC } from 'react'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import cn from 'classnames'
import type { Page } from '@commerce/types/page'
import type { Category } from '@commerce/types/site'
import { CommerceProvider } from '@framework'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { useUI } from '@components/ui/context'
import { Navbar, Footer } from '@components/common'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import CartSidebarView from '@components/cart/CartSidebarView'
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
@@ -13,8 +15,7 @@ import PaymentMethodView from '@components/checkout/PaymentMethodView'
import ShippingView from '@components/checkout/ShippingView'
import LoginView from '@components/auth/LoginView'
import { CommerceProvider } from '@framework'
import type { Page } from '@framework/common/get-all-pages'
import s from './Layout.module.css'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
@@ -44,13 +45,13 @@ const FeatureBar = dynamic(
interface Props {
pageProps: {
pages?: Page[]
commerceFeatures: Record<string, boolean>
categories: Category[]
}
}
const Layout: FC<Props> = ({
children,
pageProps: { commerceFeatures, ...pageProps },
pageProps: { categories = [], ...pageProps },
}) => {
const {
displaySidebar,
@@ -62,10 +63,16 @@ const Layout: FC<Props> = ({
} = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter()
const navBarlinks = categories.slice(0, 2).map((c) => ({
label: c.name,
href: `/search/${c.slug}`,
}))
return (
<CommerceProvider locale={locale}>
<div className={cn(s.root)}>
<Navbar />
<Navbar links={navBarlinks} />
<main className="fit">{children}</main>
<Footer pages={pageProps.pages} />

@@ -5,7 +5,15 @@ import { Searchbar, UserNav } from '@components/common'
import NavbarRoot from './NavbarRoot'
import s from './Navbar.module.css'
const Navbar: FC = () => (
interface Link {
href: string
label: string
}
interface NavbarProps {
links?: Link[]
}
const Navbar: FC<NavbarProps> = ({ links }) => (
<NavbarRoot>
<Container>
<div className={s.nav}>
@@ -19,15 +27,11 @@ const Navbar: FC = () => (
<Link href="/search">
<a className={s.link}>All</a>
</Link>
<Link href="/search?q=clothes">
<a className={s.link}>Clothes</a>
</Link>
<Link href="/search?q=accessories">
<a className={s.link}>Accessories</a>
</Link>
<Link href="/search?q=shoes">
<a className={s.link}>Shoes</a>
</Link>
{links?.map((l) => (
<Link href={l.href} key={l.href}>
<a className={s.link}>{l.label}</a>
</Link>
))}
</nav>
</div>

@@ -1,7 +1,7 @@
import { FC } from 'react'
import Link from 'next/link'
import cn from 'classnames'
import type { LineItem } from '@framework/types'
import type { LineItem } from '@commerce/types/cart'
import useCart from '@framework/cart/use-cart'
import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common'

@@ -1,7 +1,7 @@
import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import type { Product } from '@commerce/types'
import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton'
@@ -122,7 +122,7 @@ const ProductCard: FC<Props> = ({
<Image
alt={product.name || 'Product Image'}
className={s.productImage}
src={product.images[0].url || placeholderImg}
src={product.images[0]?.url || placeholderImg}
height={540}
width={540}
quality="85"

@@ -3,7 +3,7 @@ import Image from 'next/image'
import { NextSeo } from 'next-seo'
import s from './ProductView.module.css'
import { FC, useEffect, useState } from 'react'
import type { Product } from '@commerce/types'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import { getVariant, SelectedOptions } from '../helpers'
import { Swatch, ProductSlider } from '@components/product'
@@ -22,6 +22,8 @@ interface Props {
}
const ProductView: FC<Props> = ({ product, relatedProducts }) => {
// TODO: fix this missing argument issue
/* @ts-ignore */
const addItem = useAddItem()
const { price } = usePrice({
amount: product.price.value,
@@ -158,8 +160,11 @@ const ProductView: FC<Props> = ({ product, relatedProducts }) => {
className={s.button}
onClick={addToCart}
loading={loading}
disabled={variant?.availableForSale === false}
>
Add to Cart
{variant?.availableForSale === false
? 'Not Available'
: 'Add To Cart'}
</Button>
</div>

@@ -44,14 +44,15 @@ const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
className={swatchClassName}
style={color ? { backgroundColor: color } : {}}
aria-label="Variant Swatch"
{...(label && color && { title: label })}
{...props}
>
{variant === 'color' && active && (
{color && active && (
<span>
<Check />
</span>
)}
{variant !== 'color' ? label : null}
{!color ? label : null}
</Button>
)
}

@@ -1,4 +1,4 @@
import type { Product } from '@commerce/types'
import type { Product } from '@commerce/types/product'
export type SelectedOptions = Record<string, string | null>
export function getVariant(product: Product, opts: SelectedOptions) {

@@ -6,8 +6,8 @@ import useAddItem from '@framework/wishlist/use-add-item'
import useCustomer from '@framework/customer/use-customer'
import useWishlist from '@framework/wishlist/use-wishlist'
import useRemoveItem from '@framework/wishlist/use-remove-item'
import type { Product, ProductVariant } from '@commerce/types'
import s from './WishlistButton.module.css'
import type { Product, ProductVariant } from '@commerce/types/product'
type Props = {
productId: Product['id']

@@ -7,7 +7,7 @@ import { Trash } from '@components/icons'
import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
import type { Product } from '@commerce/types'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import useAddItem from '@framework/cart/use-add-item'
import useRemoveItem from '@framework/wishlist/use-remove-item'
@@ -16,16 +16,21 @@ interface Props {
product: Product
}
const placeholderImg = '/product-img-placeholder.svg'
const WishlistCard: FC<Props> = ({ product }) => {
const { price } = usePrice({
amount: product.prices?.price?.value,
baseAmount: product.prices?.retailPrice?.value,
currencyCode: product.prices?.price?.currencyCode!,
amount: product.price?.value,
baseAmount: product.price?.retailPrice,
currencyCode: product.price?.currencyCode!,
})
// @ts-ignore Wishlist is not always enabled
const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
const [loading, setLoading] = useState(false)
const [removing, setRemoving] = useState(false)
// TODO: fix this missing argument issue
/* @ts-ignore */
const addItem = useAddItem()
const { openSidebar } = useUI()
@@ -58,10 +63,10 @@ const WishlistCard: FC<Props> = ({ product }) => {
<div className={cn(s.root, { 'opacity-75 pointer-events-none': removing })}>
<div className={`col-span-3 ${s.productBg}`}>
<Image
src={product.images[0].url}
src={product.images[0]?.url || placeholderImg}
width={400}
height={400}
alt={product.images[0].alt || 'Product Image'}
alt={product.images[0]?.alt || 'Product Image'}
/>
</div>

@@ -1,78 +0,0 @@
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getCart from './handlers/get-cart'
import addItem from './handlers/add-item'
import updateItem from './handlers/update-item'
import removeItem from './handlers/remove-item'
import type {
BigcommerceCart,
GetCartHandlerBody,
AddCartItemHandlerBody,
UpdateCartItemHandlerBody,
RemoveCartItemHandlerBody,
} from '../../types'
export type CartHandlers = {
getCart: BigcommerceHandler<BigcommerceCart, GetCartHandlerBody>
addItem: BigcommerceHandler<BigcommerceCart, AddCartItemHandlerBody>
updateItem: BigcommerceHandler<BigcommerceCart, UpdateCartItemHandlerBody>
removeItem: BigcommerceHandler<BigcommerceCart, RemoveCartItemHandlerBody>
}
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
// Return current cart info
if (req.method === 'GET') {
const body = { cartId }
return await handlers['getCart']({ req, res, config, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ req, res, config, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ req, res, config, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { ...req.body, cartId }
return await handlers['removeItem']({ req, res, config, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export const handlers = { getCart, addItem, updateItem, removeItem }
export default createApiHandler(cartApi, handlers, {})

@@ -1,48 +0,0 @@
import type { Product } from '@commerce/types'
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getProducts from './handlers/get-products'
export type SearchProductsData = {
products: Product[]
found: boolean
}
export type ProductsHandlers = {
getProducts: BigcommerceHandler<
SearchProductsData,
{ search?: string; category?: string; brand?: string; sort?: string }
>
}
const METHODS = ['GET']
// TODO(lf): a complete implementation should have schema validation for `req.body`
const productsApi: BigcommerceApiHandler<
SearchProductsData,
ProductsHandlers
> = async (req, res, config, handlers) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = req.query
return await handlers['getProducts']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export const handlers = { getProducts }
export default createApiHandler(productsApi, handlers, {})

@@ -1,77 +0,0 @@
import isAllowedMethod from './utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
} from './utils/create-api-handler'
import { BigcommerceApiError } from './utils/errors'
const METHODS = ['GET']
const fullCheckout = true
// TODO: a complete implementation should have schema validation for `req.body`
const checkoutApi: BigcommerceApiHandler<any> = async (req, res, config) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
if (!cartId) {
res.redirect('/cart')
return
}
const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/redirect_urls`,
{
method: 'POST',
}
)
if (fullCheckout) {
res.redirect(data.checkout_url)
return
}
// TODO: make the embedded checkout work too!
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout</title>
<script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
<script>
window.onload = function() {
checkoutKitLoader.load('checkout-sdk').then(function (service) {
service.embedCheckout({
containerId: 'checkout',
url: '${data.embedded_checkout_url}'
});
});
}
</script>
</head>
<body>
<div id="checkout"></div>
</body>
</html>
`
res.status(200)
res.setHeader('Content-Type', 'text/html')
res.write(html)
res.end()
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default createApiHandler(checkoutApi, {}, {})

@@ -1,46 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import getLoggedInCustomer, {
Customer,
} from './handlers/get-logged-in-customer'
export type { Customer }
export type CustomerData = {
customer: Customer
}
export type CustomersHandlers = {
getLoggedInCustomer: BigcommerceHandler<CustomerData>
}
const METHODS = ['GET']
const customersApi: BigcommerceApiHandler<
CustomerData,
CustomersHandlers
> = async (req, res, config, handlers) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = null
return await handlers['getLoggedInCustomer']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const handlers = { getLoggedInCustomer }
export default createApiHandler(customersApi, handlers, {})

@@ -1,45 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import login from './handlers/login'
export type LoginBody = {
email: string
password: string
}
export type LoginHandlers = {
login: BigcommerceHandler<null, Partial<LoginBody>>
}
const METHODS = ['POST']
const loginApi: BigcommerceApiHandler<null, LoginHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = req.body ?? {}
return await handlers['login']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const handlers = { login }
export default createApiHandler(loginApi, handlers, {})

@@ -1,42 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import logout from './handlers/logout'
export type LogoutHandlers = {
logout: BigcommerceHandler<null, { redirectTo?: string }>
}
const METHODS = ['GET']
const logoutApi: BigcommerceApiHandler<null, LogoutHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
return await handlers['logout']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const handlers = { logout }
export default createApiHandler(logoutApi, handlers, {})

@@ -1,50 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import signup from './handlers/signup'
export type SignupBody = {
firstName: string
lastName: string
email: string
password: string
}
export type SignupHandlers = {
signup: BigcommerceHandler<null, { cartId?: string } & Partial<SignupBody>>
}
const METHODS = ['POST']
const signupApi: BigcommerceApiHandler<null, SignupHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId }
return await handlers['signup']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const handlers = { signup }
export default createApiHandler(signupApi, handlers, {})

@@ -1,8 +1,9 @@
import { normalizeCart } from '../../../lib/normalize'
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
import type { CartEndpoint } from '.'
const addItem: CartHandlers['addItem'] = async ({
const addItem: CartEndpoint['handlers']['addItem'] = async ({
res,
body: { cartId, item },
config,
@@ -39,7 +40,7 @@ const addItem: CartHandlers['addItem'] = async ({
'Set-Cookie',
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
)
res.status(200).json({ data })
res.status(200).json({ data: normalizeCart(data) })
}
export default addItem

@@ -1,10 +1,11 @@
import type { BigcommerceCart } from '../../../types'
import { normalizeCart } from '../../../lib/normalize'
import { BigcommerceApiError } from '../../utils/errors'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '../'
import type { BigcommerceCart } from '../../../types/cart'
import type { CartEndpoint } from '.'
// Return current cart info
const getCart: CartHandlers['getCart'] = async ({
const getCart: CartEndpoint['handlers']['getCart'] = async ({
res,
body: { cartId },
config,
@@ -26,7 +27,9 @@ const getCart: CartHandlers['getCart'] = async ({
}
}
res.status(200).json({ data: result.data ?? null })
res.status(200).json({
data: result.data ? normalizeCart(result.data) : null,
})
}
export default getCart

@@ -0,0 +1,26 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import cartEndpoint from '@commerce/api/endpoints/cart'
import type { CartSchema } from '../../../types/cart'
import type { BigcommerceAPI } from '../..'
import getCart from './get-cart'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CartAPI = GetAPISchema<BigcommerceAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint']
export const handlers: CartEndpoint['handlers'] = {
getCart,
addItem,
updateItem,
removeItem,
}
const cartApi = createEndpoint<CartAPI>({
handler: cartEndpoint,
handlers,
})
export default cartApi

@@ -1,7 +1,8 @@
import { normalizeCart } from '../../../lib/normalize'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
import type { CartEndpoint } from '.'
const removeItem: CartHandlers['removeItem'] = async ({
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
res,
body: { cartId, itemId },
config,
@@ -27,7 +28,7 @@ const removeItem: CartHandlers['removeItem'] = async ({
: // Remove the cart cookie if the cart was removed (empty items)
getCartCookie(config.cartCookie)
)
res.status(200).json({ data })
res.status(200).json({ data: data && normalizeCart(data) })
}
export default removeItem

@@ -1,8 +1,9 @@
import { normalizeCart } from '../../../lib/normalize'
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
import type { CartEndpoint } from '.'
const updateItem: CartHandlers['updateItem'] = async ({
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
res,
body: { cartId, itemId, item },
config,
@@ -29,7 +30,7 @@ const updateItem: CartHandlers['updateItem'] = async ({
'Set-Cookie',
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
)
res.status(200).json({ data })
res.status(200).json({ data: normalizeCart(data) })
}
export default updateItem

@@ -1,6 +1,5 @@
import { Product } from '@commerce/types'
import getAllProducts, { ProductEdge } from '../../../product/get-all-products'
import type { ProductsHandlers } from '../products'
import { Product } from '@commerce/types/product'
import { ProductsEndpoint } from '.'
const SORT: { [key: string]: string | undefined } = {
latest: 'id',
@@ -11,10 +10,11 @@ const SORT: { [key: string]: string | undefined } = {
const LIMIT = 12
// Return current cart info
const getProducts: ProductsHandlers['getProducts'] = async ({
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
res,
body: { search, category, brand, sort },
body: { search, categoryId, brandId, sort },
config,
commerce,
}) => {
// Use a dummy base as we only care about the relative path
const url = new URL('/v3/catalog/products', 'http://a')
@@ -24,11 +24,11 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
if (search) url.searchParams.set('keyword', search)
if (category && Number.isInteger(Number(category)))
url.searchParams.set('categories:in', category)
if (categoryId && Number.isInteger(Number(categoryId)))
url.searchParams.set('categories:in', String(categoryId))
if (brand && Number.isInteger(Number(brand)))
url.searchParams.set('brand_id', brand)
if (brandId && Number.isInteger(Number(brandId)))
url.searchParams.set('brand_id', String(brandId))
if (sort) {
const [_sort, direction] = sort.split('-')
@@ -47,18 +47,18 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
url.pathname + url.search
)
const entityIds = data.map((p) => p.id)
const found = entityIds.length > 0
const ids = data.map((p) => String(p.id))
const found = ids.length > 0
// We want the GraphQL version of each product
const graphqlData = await getAllProducts({
variables: { first: LIMIT, entityIds },
const graphqlData = await commerce.getAllProducts({
variables: { first: LIMIT, ids },
config,
})
// Put the products in an object that we can use to get them by id
const productsById = graphqlData.products.reduce<{
[k: number]: Product
[k: string]: Product
}>((prods, p) => {
prods[Number(p.id)] = p
return prods
@@ -68,7 +68,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
// Populate the products array with the graphql products, in the order
// assigned by the list of entity ids
entityIds.forEach((id) => {
ids.forEach((id) => {
const product = productsById[id]
if (product) products.push(product)
})

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import productsEndpoint from '@commerce/api/endpoints/catalog/products'
import type { ProductsSchema } from '../../../../types/product'
import type { BigcommerceAPI } from '../../..'
import getProducts from './get-products'
export type ProductsAPI = GetAPISchema<BigcommerceAPI, ProductsSchema>
export type ProductsEndpoint = ProductsAPI['endpoint']
export const handlers: ProductsEndpoint['handlers'] = { getProducts }
const productsApi = createEndpoint<ProductsAPI>({
handler: productsEndpoint,
handlers,
})
export default productsApi

@@ -0,0 +1,62 @@
import type { CheckoutEndpoint } from '.'
const fullCheckout = true
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
req,
res,
config,
}) => {
const { cookies } = req
const cartId = cookies[config.cartCookie]
if (!cartId) {
res.redirect('/cart')
return
}
const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/redirect_urls`,
{
method: 'POST',
}
)
if (fullCheckout) {
res.redirect(data.checkout_url)
return
}
// TODO: make the embedded checkout work too!
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout</title>
<script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
<script>
window.onload = function() {
checkoutKitLoader.load('checkout-sdk').then(function (service) {
service.embedCheckout({
containerId: 'checkout',
url: '${data.embedded_checkout_url}'
});
});
}
</script>
</head>
<body>
<div id="checkout"></div>
</body>
</html>
`
res.status(200)
res.setHeader('Content-Type', 'text/html')
res.write(html)
res.end()
}
export default checkout

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
import type { CheckoutSchema } from '../../../types/checkout'
import type { BigcommerceAPI } from '../..'
import checkout from './checkout'
export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

@@ -1,5 +1,5 @@
import type { GetLoggedInCustomerQuery } from '../../../schema'
import type { CustomersHandlers } from '..'
import type { CustomerEndpoint } from '.'
export const getLoggedInCustomerQuery = /* GraphQL */ `
query getLoggedInCustomer {
@@ -24,7 +24,7 @@ export const getLoggedInCustomerQuery = /* GraphQL */ `
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
req,
res,
config,

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerEndpoint from '@commerce/api/endpoints/customer'
import type { CustomerSchema } from '../../../types/customer'
import type { BigcommerceAPI } from '../..'
import getLoggedInCustomer from './get-logged-in-customer'
export type CustomerAPI = GetAPISchema<BigcommerceAPI, CustomerSchema>
export type CustomerEndpoint = CustomerAPI['endpoint']
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
const customerApi = createEndpoint<CustomerAPI>({
handler: customerEndpoint,
handlers,
})
export default customerApi

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import loginEndpoint from '@commerce/api/endpoints/login'
import type { LoginSchema } from '../../../types/login'
import type { BigcommerceAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<BigcommerceAPI, LoginSchema>
export type LoginEndpoint = LoginAPI['endpoint']
export const handlers: LoginEndpoint['handlers'] = { login }
const loginApi = createEndpoint<LoginAPI>({
handler: loginEndpoint,
handlers,
})
export default loginApi

@@ -1,13 +1,13 @@
import { FetcherError } from '@commerce/utils/errors'
import login from '../../../auth/login'
import type { LoginHandlers } from '../login'
import type { LoginEndpoint } from '.'
const invalidCredentials = /invalid credentials/i
const loginHandler: LoginHandlers['login'] = async ({
const login: LoginEndpoint['handlers']['login'] = async ({
res,
body: { email, password },
config,
commerce,
}) => {
// TODO: Add proper validations with something like Ajv
if (!(email && password)) {
@@ -21,7 +21,7 @@ const loginHandler: LoginHandlers['login'] = async ({
// and numeric characters.
try {
await login({ variables: { email, password }, config, res })
await commerce.login({ variables: { email, password }, config, res })
} catch (error) {
// Check if the email and password didn't match an existing account
if (
@@ -46,4 +46,4 @@ const loginHandler: LoginHandlers['login'] = async ({
res.status(200).json({ data: null })
}
export default loginHandler
export default login

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import logoutEndpoint from '@commerce/api/endpoints/logout'
import type { LogoutSchema } from '../../../types/logout'
import type { BigcommerceAPI } from '../..'
import logout from './logout'
export type LogoutAPI = GetAPISchema<BigcommerceAPI, LogoutSchema>
export type LogoutEndpoint = LogoutAPI['endpoint']
export const handlers: LogoutEndpoint['handlers'] = { logout }
const logoutApi = createEndpoint<LogoutAPI>({
handler: logoutEndpoint,
handlers,
})
export default logoutApi

@@ -1,7 +1,7 @@
import { serialize } from 'cookie'
import { LogoutHandlers } from '../logout'
import type { LogoutEndpoint } from '.'
const logoutHandler: LogoutHandlers['logout'] = async ({
const logout: LogoutEndpoint['handlers']['logout'] = async ({
res,
body: { redirectTo },
config,
@@ -20,4 +20,4 @@ const logoutHandler: LogoutHandlers['logout'] = async ({
}
}
export default logoutHandler
export default logout

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import signupEndpoint from '@commerce/api/endpoints/signup'
import type { SignupSchema } from '../../../types/signup'
import type { BigcommerceAPI } from '../..'
import signup from './signup'
export type SignupAPI = GetAPISchema<BigcommerceAPI, SignupSchema>
export type SignupEndpoint = SignupAPI['endpoint']
export const handlers: SignupEndpoint['handlers'] = { signup }
const singupApi = createEndpoint<SignupAPI>({
handler: signupEndpoint,
handlers,
})
export default singupApi

@@ -1,11 +1,11 @@
import { BigcommerceApiError } from '../../utils/errors'
import login from '../../../auth/login'
import { SignupHandlers } from '../signup'
import type { SignupEndpoint } from '.'
const signup: SignupHandlers['signup'] = async ({
const signup: SignupEndpoint['handlers']['signup'] = async ({
res,
body: { firstName, lastName, email, password },
config,
commerce,
}) => {
// TODO: Add proper validations with something like Ajv
if (!(firstName && lastName && email && password)) {
@@ -54,7 +54,7 @@ const signup: SignupHandlers['signup'] = async ({
}
// Login the customer right after creating it
await login({ variables: { email, password }, res, config })
await commerce.login({ variables: { email, password }, res, config })
res.status(200).json({ data: null })
}

@@ -1,13 +1,14 @@
import type { WishlistHandlers } from '..'
import getCustomerId from '../../../customer/get-customer-id'
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import { parseWishlistItem } from '../../utils/parse-item'
import getCustomerId from './utils/get-customer-id'
import type { WishlistEndpoint } from '.'
// Returns the wishlist of the signed customer
const addItem: WishlistHandlers['addItem'] = async ({
// Return wishlist info
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
res,
body: { customerToken, item },
config,
commerce,
}) => {
if (!item) {
return res.status(400).json({
@@ -26,7 +27,7 @@ const addItem: WishlistHandlers['addItem'] = async ({
})
}
const { wishlist } = await getCustomerWishlist({
const { wishlist } = await commerce.getCustomerWishlist({
variables: { customerId },
config,
})

@@ -1,12 +1,14 @@
import getCustomerId from '../../../customer/get-customer-id'
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
import type { Wishlist, WishlistHandlers } from '..'
import type { Wishlist } from '../../../types/wishlist'
import type { WishlistEndpoint } from '.'
import getCustomerId from './utils/get-customer-id'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
// Return wishlist info
const getWishlist: WishlistHandlers['getWishlist'] = async ({
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
res,
body: { customerToken, includeProducts },
config,
commerce,
}) => {
let result: { data?: Wishlist } = {}
@@ -22,7 +24,7 @@ const getWishlist: WishlistHandlers['getWishlist'] = async ({
})
}
const { wishlist } = await getCustomerWishlist({
const { wishlist } = await commerce.getCustomerWishlist({
variables: { customerId },
includeProducts,
config,

@@ -0,0 +1,24 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import wishlistEndpoint from '@commerce/api/endpoints/wishlist'
import type { WishlistSchema } from '../../../types/wishlist'
import type { BigcommerceAPI } from '../..'
import getWishlist from './get-wishlist'
import addItem from './add-item'
import removeItem from './remove-item'
export type WishlistAPI = GetAPISchema<BigcommerceAPI, WishlistSchema>
export type WishlistEndpoint = WishlistAPI['endpoint']
export const handlers: WishlistEndpoint['handlers'] = {
getWishlist,
addItem,
removeItem,
}
const wishlistApi = createEndpoint<WishlistAPI>({
handler: wishlistEndpoint,
handlers,
})
export default wishlistApi

@@ -1,20 +1,20 @@
import getCustomerId from '../../../customer/get-customer-id'
import getCustomerWishlist, {
Wishlist,
} from '../../../customer/get-customer-wishlist'
import type { WishlistHandlers } from '..'
import type { Wishlist } from '../../../types/wishlist'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import getCustomerId from './utils/get-customer-id'
import type { WishlistEndpoint } from '.'
// Return current wishlist info
const removeItem: WishlistHandlers['removeItem'] = async ({
// Return wishlist info
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
res,
body: { customerToken, itemId },
config,
commerce,
}) => {
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
const { wishlist } =
(customerId &&
(await getCustomerWishlist({
(await commerce.getCustomerWishlist({
variables: { customerId },
config,
}))) ||

@@ -1,5 +1,5 @@
import { GetCustomerIdQuery } from '../schema'
import { BigcommerceConfig, getConfig } from '../api'
import type { GetCustomerIdQuery } from '../../../../schema'
import type { BigcommerceConfig } from '../../..'
export const getCustomerIdQuery = /* GraphQL */ `
query getCustomerId {
@@ -14,10 +14,8 @@ async function getCustomerId({
config,
}: {
customerToken: string
config?: BigcommerceConfig
}): Promise<number | undefined> {
config = getConfig(config)
config: BigcommerceConfig
}): Promise<string | undefined> {
const { data } = await config.fetch<GetCustomerIdQuery>(
getCustomerIdQuery,
undefined,
@@ -28,7 +26,7 @@ async function getCustomerId({
}
)
return data?.customer?.entityId
return String(data?.customer?.entityId)
}
export default getCustomerId

@@ -1,8 +1,29 @@
import type { RequestInit } from '@vercel/fetch'
import type { CommerceAPIConfig } from '@commerce/api'
import {
CommerceAPI,
CommerceAPIConfig,
getCommerceApi as commerceApi,
} from '@commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import fetchStoreApi from './utils/fetch-store-api'
import type { CartAPI } from './endpoints/cart'
import type { CustomerAPI } from './endpoints/customer'
import type { LoginAPI } from './endpoints/login'
import type { LogoutAPI } from './endpoints/logout'
import type { SignupAPI } from './endpoints/signup'
import type { ProductsAPI } from './endpoints/catalog/products'
import type { WishlistAPI } from './endpoints/wishlist'
import login from './operations/login'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getCustomerWishlist from './operations/get-customer-wishlist'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
export interface BigcommerceConfig extends CommerceAPIConfig {
// Indicates if the returned metadata with translations should be applied to the
// data or returned as it is
@@ -39,34 +60,12 @@ if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) {
)
}
export class Config {
private config: BigcommerceConfig
constructor(config: Omit<BigcommerceConfig, 'customerCookie'>) {
this.config = {
...config,
// The customerCookie is not customizable for now, BC sets the cookie and it's
// not important to rename it
customerCookie: 'SHOP_TOKEN',
}
}
getConfig(userConfig: Partial<BigcommerceConfig> = {}) {
return Object.entries(userConfig).reduce<BigcommerceConfig>(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.config }
)
}
setConfig(newConfig: Partial<BigcommerceConfig>) {
Object.assign(this.config, newConfig)
}
}
const ONE_DAY = 60 * 60 * 24
const config = new Config({
const config: BigcommerceConfig = {
commerceUrl: API_URL,
apiToken: API_TOKEN,
customerCookie: 'SHOP_TOKEN',
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
cartCookieMaxAge: ONE_DAY * 30,
fetch: fetchGraphqlApi,
@@ -77,12 +76,36 @@ const config = new Config({
storeApiClientId: STORE_API_CLIENT_ID,
storeChannelId: STORE_CHANNEL_ID,
storeApiFetch: fetchStoreApi,
})
export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
return config.getConfig(userConfig)
}
export function setConfig(newConfig: Partial<BigcommerceConfig>) {
return config.setConfig(newConfig)
const operations = {
login,
getAllPages,
getPage,
getSiteInfo,
getCustomerWishlist,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type APIs =
| CartAPI
| CustomerAPI
| LoginAPI
| LogoutAPI
| SignupAPI
| ProductsAPI
| WishlistAPI
export type BigcommerceAPI<P extends Provider = Provider> = CommerceAPI<P>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): BigcommerceAPI<P> {
return commerceApi(customProvider)
}

@@ -0,0 +1,46 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type { Page, GetAllPagesOperation } from '../../types/page'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, Provider } from '..'
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<BigcommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<BigcommerceConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const cfg = commerce.getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const { data } = await cfg.storeApiFetch<
RecursivePartial<{ data: Page[] }>
>('/v3/content/pages')
const pages = (data as RecursiveRequired<typeof data>) ?? []
return {
pages: preview ? pages : pages.filter((p) => p.is_visible),
}
}
return getAllPages
}

@@ -0,0 +1,66 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type { GetAllProductPathsQuery } from '../../schema'
import type { GetAllProductPathsOperation } from '../../types/product'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { BigcommerceConfig, Provider } from '..'
export const getAllProductPathsQuery = /* GraphQL */ `
query getAllProductPaths($first: Int = 100) {
site {
products(first: $first) {
edges {
node {
path
}
}
}
}
}
`
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: BigcommerceConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: BigcommerceConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query = getAllProductPathsQuery,
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: BigcommerceConfig
} = {}): Promise<T['data']> {
config = commerce.getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<
RecursivePartial<GetAllProductPathsQuery>
>(query, { variables })
const products = data.site?.products?.edges
return {
products: filterEdges(products as RecursiveRequired<typeof products>).map(
({ node }) => node
),
}
}
return getAllProductPaths
}

@@ -0,0 +1,135 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type {
GetAllProductsQuery,
GetAllProductsQueryVariables,
} from '../../schema'
import type { GetAllProductsOperation } from '../../types/product'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import setProductLocaleMeta from '../utils/set-product-locale-meta'
import { productConnectionFragment } from '../fragments/product'
import { BigcommerceConfig, Provider } from '..'
import { normalizeProduct } from '../../lib/normalize'
export const getAllProductsQuery = /* GraphQL */ `
query getAllProducts(
$hasLocale: Boolean = false
$locale: String = "null"
$entityIds: [Int!]
$first: Int = 10
$products: Boolean = false
$featuredProducts: Boolean = false
$bestSellingProducts: Boolean = false
$newestProducts: Boolean = false
) {
site {
products(first: $first, entityIds: $entityIds) @include(if: $products) {
...productConnnection
}
featuredProducts(first: $first) @include(if: $featuredProducts) {
...productConnnection
}
bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
...productConnnection
}
newestProducts(first: $first) @include(if: $newestProducts) {
...productConnnection
}
}
}
${productConnectionFragment}
`
export type ProductEdge = NonNullable<
NonNullable<GetAllProductsQuery['site']['products']['edges']>[0]
>
export type ProductNode = ProductEdge['node']
export type GetAllProductsResult<
T extends Record<keyof GetAllProductsResult, any[]> = {
products: ProductEdge[]
}
> = T
function getProductsType(
relevance?: GetAllProductsOperation['variables']['relevance']
) {
switch (relevance) {
case 'featured':
return 'featuredProducts'
case 'best_selling':
return 'bestSellingProducts'
case 'newest':
return 'newestProducts'
default:
return 'products'
}
}
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
variables?: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>({
query = getAllProductsQuery,
variables: vars = {},
config: cfg,
}: {
query?: string
variables?: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
const { locale } = config
const field = getProductsType(vars.relevance)
const variables: GetAllProductsQueryVariables = {
locale,
hasLocale: !!locale,
}
variables[field] = true
if (vars.first) variables.first = vars.first
if (vars.ids) variables.entityIds = vars.ids.map((id) => Number(id))
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
query,
{ variables }
)
const edges = data.site?.[field]?.edges
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
if (locale && config.applyLocale) {
products.forEach((product: RecursivePartial<ProductEdge>) => {
if (product.node) setProductLocaleMeta(product.node)
})
}
return {
products: products.map(({ node }) => normalizeProduct(node as any)),
}
}
return getAllProducts
}

@@ -0,0 +1,81 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type {
GetCustomerWishlistOperation,
Wishlist,
} from '../../types/wishlist'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, Provider } from '..'
import getAllProducts, { ProductEdge } from './get-all-products'
export default function getCustomerWishlistOperation({
commerce,
}: OperationContext<Provider>) {
async function getCustomerWishlist<
T extends GetCustomerWishlistOperation
>(opts: {
variables: T['variables']
config?: BigcommerceConfig
includeProducts?: boolean
}): Promise<T['data']>
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>(
opts: {
variables: T['variables']
config?: BigcommerceConfig
includeProducts?: boolean
} & OperationOptions
): Promise<T['data']>
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>({
config,
variables,
includeProducts,
}: {
url?: string
variables: T['variables']
config?: BigcommerceConfig
includeProducts?: boolean
}): Promise<T['data']> {
config = commerce.getConfig(config)
const { data = [] } = await config.storeApiFetch<
RecursivePartial<{ data: Wishlist[] }>
>(`/v3/wishlists?customer_id=${variables.customerId}`)
const wishlist = data[0]
if (includeProducts && wishlist?.items?.length) {
const ids = wishlist.items
?.map((item) => (item?.product_id ? String(item?.product_id) : null))
.filter((id): id is string => !!id)
if (ids?.length) {
const graphqlData = await commerce.getAllProducts({
variables: { first: 100, ids },
config,
})
// Put the products in an object that we can use to get them by id
const productsById = graphqlData.products.reduce<{
[k: number]: ProductEdge
}>((prods, p) => {
prods[Number(p.id)] = p as any
return prods
}, {})
// Populate the wishlist items with the graphql products
wishlist.items.forEach((item) => {
const product = item && productsById[item.product_id!]
if (item && product) {
// @ts-ignore Fix this type when the wishlist type is properly defined
item.product = product
}
})
}
}
return { wishlist: wishlist as RecursiveRequired<typeof wishlist> }
}
return getCustomerWishlist
}

@@ -0,0 +1,54 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type { GetPageOperation, Page } from '../../types/page'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import type { BigcommerceConfig, Provider } from '..'
import { normalizePage } from '../../lib/normalize'
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage<T extends GetPageOperation>(opts: {
variables: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getPage<T extends GetPageOperation>(
opts: {
variables: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getPage<T extends GetPageOperation>({
url,
variables,
config,
preview,
}: {
url?: string
variables: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']> {
const cfg = commerce.getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const { data } = await cfg.storeApiFetch<
RecursivePartial<{ data: Page[] }>
>(url || `/v3/content/pages?id=${variables.id}&include=body`)
const firstPage = data?.[0]
const page = firstPage as RecursiveRequired<typeof firstPage>
if (preview || page?.is_visible) {
return { page: normalizePage(page as any) }
}
return {}
}
return getPage
}

@@ -0,0 +1,119 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type { GetProductOperation } from '../../types/product'
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
import setProductLocaleMeta from '../utils/set-product-locale-meta'
import { productInfoFragment } from '../fragments/product'
import { BigcommerceConfig, Provider } from '..'
import { normalizeProduct } from '../../lib/normalize'
export const getProductQuery = /* GraphQL */ `
query getProduct(
$hasLocale: Boolean = false
$locale: String = "null"
$path: String!
) {
site {
route(path: $path) {
node {
__typename
... on Product {
...productInfo
variants {
edges {
node {
entityId
defaultImage {
urlOriginal
altText
isDefault
}
prices {
...productPrices
}
inventory {
aggregated {
availableToSell
warningLevel
}
isInStock
}
productOptions {
edges {
node {
__typename
entityId
displayName
...multipleChoiceOption
}
}
}
}
}
}
}
}
}
}
}
${productInfoFragment}
`
// TODO: See if this type is useful for defining the Product type
// export type ProductNode = Extract<
// GetProductQuery['site']['route']['node'],
// { __typename: 'Product' }
// >
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>(opts: {
variables: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getProduct<T extends GetProductOperation>(
opts: {
variables: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getProduct<T extends GetProductOperation>({
query = getProductQuery,
variables: { slug, ...vars },
config: cfg,
}: {
query?: string
variables: T['variables']
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
const { locale } = config
const variables: GetProductQueryVariables = {
locale,
hasLocale: !!locale,
path: slug ? `/${slug}/` : vars.path!,
}
const { data } = await config.fetch<GetProductQuery>(query, { variables })
const product = data.site?.route?.node
if (product?.__typename === 'Product') {
if (locale && config.applyLocale) {
setProductLocaleMeta(product)
}
return { product: normalizeProduct(product as any) }
}
return {}
}
return getProduct
}

@@ -0,0 +1,87 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type { GetSiteInfoOperation } from '../../types/site'
import type { GetSiteInfoQuery } from '../../schema'
import filterEdges from '../utils/filter-edges'
import type { BigcommerceConfig, Provider } from '..'
import { categoryTreeItemFragment } from '../fragments/category-tree'
import { normalizeCategory } from '../../lib/normalize'
// Get 3 levels of categories
export const getSiteInfoQuery = /* GraphQL */ `
query getSiteInfo {
site {
categoryTree {
...categoryTreeItem
children {
...categoryTreeItem
children {
...categoryTreeItem
}
}
}
brands {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
entityId
name
defaultImage {
urlOriginal
altText
}
pageTitle
metaDesc
metaKeywords
searchKeywords
path
}
}
}
}
}
${categoryTreeItemFragment}
`
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
config?: Partial<BigcommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>(
opts: {
config?: Partial<BigcommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>({
query = getSiteInfoQuery,
config,
}: {
query?: string
config?: Partial<BigcommerceConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const cfg = commerce.getConfig(config)
const { data } = await cfg.fetch<GetSiteInfoQuery>(query)
const categories = data.site.categoryTree.map(normalizeCategory)
const brands = data.site?.brands?.edges
return {
categories: categories ?? [],
brands: filterEdges(brands),
}
}
return getSiteInfo
}

@@ -0,0 +1,79 @@
import type { ServerResponse } from 'http'
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type { LoginOperation } from '../../types/login'
import type { LoginMutation } from '../../schema'
import type { RecursivePartial } from '../utils/types'
import concatHeader from '../utils/concat-cookie'
import type { BigcommerceConfig, Provider } from '..'
export const loginMutation = /* GraphQL */ `
mutation login($email: String!, $password: String!) {
login(email: $email, password: $password) {
result
}
}
`
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends LoginOperation>(opts: {
variables: T['variables']
config?: BigcommerceConfig
res: ServerResponse
}): Promise<T['data']>
async function login<T extends LoginOperation>(
opts: {
variables: T['variables']
config?: BigcommerceConfig
res: ServerResponse
} & OperationOptions
): Promise<T['data']>
async function login<T extends LoginOperation>({
query = loginMutation,
variables,
res: response,
config,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: BigcommerceConfig
}): Promise<T['data']> {
config = commerce.getConfig(config)
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
query,
{ variables }
)
// Bigcommerce returns a Set-Cookie header with the auth cookie
let cookie = res.headers.get('Set-Cookie')
if (cookie && typeof cookie === 'string') {
// In development, don't set a secure cookie or the browser will ignore it
if (process.env.NODE_ENV !== 'production') {
cookie = cookie.replace('; Secure', '')
// SameSite=none can't be set unless the cookie is Secure
// bc seems to sometimes send back SameSite=None rather than none so make
// this case insensitive
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
}
response.setHeader(
'Set-Cookie',
concatHeader(response.getHeader('Set-Cookie'), cookie)!
)
}
return {
result: data.login?.result,
}
}
return login
}

@@ -1,58 +0,0 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { BigcommerceConfig, getConfig } from '..'
export type BigcommerceApiHandler<
T = any,
H extends BigcommerceHandlers = {},
Options extends {} = {}
> = (
req: NextApiRequest,
res: NextApiResponse<BigcommerceApiResponse<T>>,
config: BigcommerceConfig,
handlers: H,
// Custom configs that may be used by a particular handler
options: Options
) => void | Promise<void>
export type BigcommerceHandler<T = any, Body = null> = (options: {
req: NextApiRequest
res: NextApiResponse<BigcommerceApiResponse<T>>
config: BigcommerceConfig
body: Body
}) => void | Promise<void>
export type BigcommerceHandlers<T = any> = {
[k: string]: BigcommerceHandler<T, any>
}
export type BigcommerceApiResponse<T> = {
data: T | null
errors?: { message: string; code?: string }[]
}
export default function createApiHandler<
T = any,
H extends BigcommerceHandlers = {},
Options extends {} = {}
>(
handler: BigcommerceApiHandler<T, H, Options>,
handlers: H,
defaultOptions: Options
) {
return function getApiHandler({
config,
operations,
options,
}: {
config?: BigcommerceConfig
operations?: Partial<H>
options?: Options extends {} ? Partial<Options> : never
} = {}): NextApiHandler {
const ops = { ...operations, ...handlers }
const opts = { ...defaultOptions, ...options }
return function apiHandler(req, res) {
return handler(req, res, getConfig(config), ops, opts)
}
}
}

@@ -1,6 +1,6 @@
import { FetcherError } from '@commerce/utils/errors'
import type { GraphQLFetcher } from '@commerce/api'
import { getConfig } from '..'
import { provider } from '..'
import fetch from './fetch'
const fetchGraphqlApi: GraphQLFetcher = async (
@@ -9,7 +9,7 @@ const fetchGraphqlApi: GraphQLFetcher = async (
fetchOptions
) => {
// log.warn(query)
const config = getConfig()
const { config } = provider
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',

@@ -1,5 +1,5 @@
import type { RequestInit, Response } from '@vercel/fetch'
import { getConfig } from '..'
import { provider } from '..'
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
import fetch from './fetch'
@@ -7,7 +7,7 @@ export default async function fetchStoreApi<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const config = getConfig()
const { config } = provider
let res: Response
try {

@@ -1,28 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export default function isAllowedMethod(
req: NextApiRequest,
res: NextApiResponse,
allowedMethods: string[]
) {
const methods = allowedMethods.includes('OPTIONS')
? allowedMethods
: [...allowedMethods, 'OPTIONS']
if (!req.method || !methods.includes(req.method)) {
res.status(405)
res.setHeader('Allow', methods.join(', '))
res.end()
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
}

@@ -1,5 +1,5 @@
import type { ItemBody as WishlistItemBody } from '../wishlist'
import type { CartItemBody, OptionSelections } from '../../types'
import type { WishlistItemBody } from '../../types/wishlist'
import type { CartItemBody, OptionSelections } from '../../types/cart'
type BCWishlistItemBody = {
product_id: number

@@ -1,4 +1,4 @@
import type { ProductNode } from '../../product/get-all-products'
import type { ProductNode } from '../operations/get-all-products'
import type { RecursivePartial } from './types'
export default function setProductLocaleMeta(

@@ -1,104 +0,0 @@
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import type {
Wishlist,
WishlistItem,
} from '../../customer/get-customer-wishlist'
import getWishlist from './handlers/get-wishlist'
import addItem from './handlers/add-item'
import removeItem from './handlers/remove-item'
import type { Product, ProductVariant, Customer } from '@commerce/types'
export type { Wishlist, WishlistItem }
export type ItemBody = {
productId: Product['id']
variantId: ProductVariant['id']
}
export type AddItemBody = { item: ItemBody }
export type RemoveItemBody = { itemId: Product['id'] }
export type WishlistBody = {
customer_id: Customer['entityId']
is_public: number
name: string
items: any[]
}
export type AddWishlistBody = { wishlist: WishlistBody }
export type WishlistHandlers = {
getWishlist: BigcommerceHandler<
Wishlist,
{ customerToken?: string; includeProducts?: boolean }
>
addItem: BigcommerceHandler<
Wishlist,
{ customerToken?: string } & Partial<AddItemBody>
>
removeItem: BigcommerceHandler<
Wishlist,
{ customerToken?: string } & Partial<RemoveItemBody>
>
}
const METHODS = ['GET', 'POST', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const customerToken = cookies[config.customerCookie]
try {
// Return current wishlist info
if (req.method === 'GET') {
const body = {
customerToken,
includeProducts: req.query.products === '1',
}
return await handlers['getWishlist']({ req, res, config, body })
}
// Add an item to the wishlist
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return await handlers['addItem']({ req, res, config, body })
}
// Remove an item from the wishlist
if (req.method === 'DELETE') {
const body = { ...req.body, customerToken }
return await handlers['removeItem']({ req, res, config, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export const handlers = {
getWishlist,
addItem,
removeItem,
}
export default createApiHandler(wishlistApi, handlers, {})

@@ -1,73 +0,0 @@
import type { ServerResponse } from 'http'
import type { LoginMutation, LoginMutationVariables } from '../schema'
import type { RecursivePartial } from '../api/utils/types'
import concatHeader from '../api/utils/concat-cookie'
import { BigcommerceConfig, getConfig } from '../api'
export const loginMutation = /* GraphQL */ `
mutation login($email: String!, $password: String!) {
login(email: $email, password: $password) {
result
}
}
`
export type LoginResult<T extends { result?: any } = { result?: string }> = T
export type LoginVariables = LoginMutationVariables
async function login(opts: {
variables: LoginVariables
config?: BigcommerceConfig
res: ServerResponse
}): Promise<LoginResult>
async function login<T extends { result?: any }, V = any>(opts: {
query: string
variables: V
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult<T>>
async function login({
query = loginMutation,
variables,
res: response,
config,
}: {
query?: string
variables: LoginVariables
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult> {
config = getConfig(config)
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
query,
{ variables }
)
// Bigcommerce returns a Set-Cookie header with the auth cookie
let cookie = res.headers.get('Set-Cookie')
if (cookie && typeof cookie === 'string') {
// In development, don't set a secure cookie or the browser will ignore it
if (process.env.NODE_ENV !== 'production') {
cookie = cookie.replace('; Secure', '')
// SameSite=none can't be set unless the cookie is Secure
// bc seems to sometimes send back SameSite=None rather than none so make
// this case insensitive
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
}
response.setHeader(
'Set-Cookie',
concatHeader(response.getHeader('Set-Cookie'), cookie)!
)
}
return {
result: data.login?.result,
}
}
export default login

@@ -2,14 +2,14 @@ import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import type { LoginBody } from '../api/customers/login'
import type { LoginHook } from '../types/login'
import useCustomer from '../customer/use-customer'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<null, {}, LoginBody> = {
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
url: '/api/bigcommerce/customers/login',
url: '/api/login',
method: 'POST',
},
async fetcher({ input: { email, password }, options, fetch }) {

@@ -1,13 +1,14 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import type { LogoutHook } from '../types/logout'
import useCustomer from '../customer/use-customer'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<null> = {
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
url: '/api/bigcommerce/customers/logout',
url: '/api/logout',
method: 'GET',
},
useHook: ({ fetch }) => () => {

@@ -2,14 +2,14 @@ import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import type { SignupBody } from '../api/customers/signup'
import type { SignupHook } from '../types/signup'
import useCustomer from '../customer/use-customer'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<null, {}, SignupBody, SignupBody> = {
export const handler: MutationHook<SignupHook> = {
fetchOptions: {
url: '/api/bigcommerce/customers/signup',
url: '/api/signup',
method: 'POST',
},
async fetcher({

@@ -2,20 +2,14 @@ import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import { normalizeCart } from '../lib/normalize'
import type {
Cart,
BigcommerceCart,
CartItemBody,
AddCartItemBody,
} from '../types'
import type { AddItemHook } from '@commerce/types/cart'
import useCart from './use-cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<Cart, {}, CartItemBody> = {
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/bigcommerce/cart',
url: '/api/cart',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
@@ -28,12 +22,12 @@ export const handler: MutationHook<Cart, {}, CartItemBody> = {
})
}
const data = await fetch<BigcommerceCart, AddCartItemBody>({
const data = await fetch({
...options,
body: { item },
})
return normalizeCart(data)
return data
},
useHook: ({ fetch }) => () => {
const { mutate } = useCart()

@@ -1,25 +1,15 @@
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
import { normalizeCart } from '../lib/normalize'
import type { Cart } from '../types'
import useCart, { UseCart } from '@commerce/cart/use-cart'
import type { GetCartHook } from '@commerce/types/cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<
Cart | null,
{},
FetchCartInput,
{ isEmpty?: boolean }
> = {
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
url: '/api/bigcommerce/cart',
url: '/api/cart',
method: 'GET',
},
async fetcher({ input: { cartId }, options, fetch }) {
const data = cartId ? await fetch(options) : null
return data && normalizeCart(data)
},
useHook: ({ useData }) => (input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },

@@ -4,48 +4,33 @@ import type {
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, {
RemoveItemInput as RemoveItemInputBase,
UseRemoveItem,
} from '@commerce/cart/use-remove-item'
import { normalizeCart } from '../lib/normalize'
import type {
RemoveCartItemBody,
Cart,
BigcommerceCart,
LineItem,
} from '../types'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
: (input: RemoveItemInput<T>) => Promise<Cart | null>
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemInput<T = any> = T extends LineItem
? Partial<RemoveItemInputBase>
: RemoveItemInputBase
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/bigcommerce/cart',
url: '/api/cart',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveCartItemBody>) {
const data = await fetch<BigcommerceCart>({
...options,
body: { itemId },
})
return normalizeCart(data)
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({
fetch,
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => <
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => <
T extends LineItem | undefined = undefined
>(
ctx: { item?: T } = {}

@@ -5,36 +5,27 @@ import type {
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, {
UpdateItemInput as UpdateItemInputBase,
UseUpdateItem,
} from '@commerce/cart/use-update-item'
import { normalizeCart } from '../lib/normalize'
import type {
UpdateCartItemBody,
Cart,
BigcommerceCart,
LineItem,
} from '../types'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
import type { LineItem, UpdateItemHook } from '@commerce/types/cart'
import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart'
export type UpdateItemInput<T = any> = T extends LineItem
? Partial<UpdateItemInputBase<LineItem>>
: UpdateItemInputBase<LineItem>
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/bigcommerce/cart',
url: '/api/cart',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateCartItemBody>) {
}: HookFetcherContext<UpdateItemHook>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
@@ -50,16 +41,12 @@ export const handler = {
})
}
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
return await fetch({
...options,
body: { itemId, item },
})
return normalizeCart(data)
},
useHook: ({
fetch,
}: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) => <
T extends LineItem | undefined = undefined
>(
ctx: {
@@ -71,7 +58,7 @@ export const handler = {
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemInput<T>) => {
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId

@@ -1,43 +0,0 @@
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
import { BigcommerceConfig, getConfig } from '../api'
import { definitions } from '../api/definitions/store-content'
export type Page = definitions['page_Full']
export type GetAllPagesResult<
T extends { pages: any[] } = { pages: Page[] }
> = T
async function getAllPages(opts?: {
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetAllPagesResult>
async function getAllPages<T extends { pages: any[] }>(opts: {
url: string
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetAllPagesResult<T>>
async function getAllPages({
config,
preview,
}: {
url?: string
config?: BigcommerceConfig
preview?: boolean
} = {}): Promise<GetAllPagesResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const { data } = await config.storeApiFetch<
RecursivePartial<{ data: Page[] }>
>('/v3/content/pages')
const pages = (data as RecursiveRequired<typeof data>) ?? []
return {
pages: preview ? pages : pages.filter((p) => p.is_visible),
}
}
export default getAllPages

@@ -1,53 +0,0 @@
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
import { BigcommerceConfig, getConfig } from '../api'
import { definitions } from '../api/definitions/store-content'
export type Page = definitions['page_Full']
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: number
}
async function getPage(opts: {
url?: string
variables: PageVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetPageResult>
async function getPage<T extends { page?: any }, V = any>(opts: {
url: string
variables: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetPageResult<T>>
async function getPage({
url,
variables,
config,
preview,
}: {
url?: string
variables: PageVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetPageResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const { data } = await config.storeApiFetch<
RecursivePartial<{ data: Page[] }>
>(url || `/v3/content/pages?id=${variables.id}&include=body`)
const firstPage = data?.[0]
const page = firstPage as RecursiveRequired<typeof firstPage>
if (preview || page?.is_visible) {
return { page }
}
return {}
}
export default getPage

@@ -1,106 +0,0 @@
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema'
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
import filterEdges from '../api/utils/filter-edges'
import { BigcommerceConfig, getConfig } from '../api'
import { categoryTreeItemFragment } from '../api/fragments/category-tree'
// Get 3 levels of categories
export const getSiteInfoQuery = /* GraphQL */ `
query getSiteInfo {
site {
categoryTree {
...categoryTreeItem
children {
...categoryTreeItem
children {
...categoryTreeItem
}
}
}
brands {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
entityId
name
defaultImage {
urlOriginal
altText
}
pageTitle
metaDesc
metaKeywords
searchKeywords
path
}
}
}
}
}
${categoryTreeItemFragment}
`
export type CategoriesTree = NonNullable<
GetSiteInfoQuery['site']['categoryTree']
>
export type BrandEdge = NonNullable<
NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0]
>
export type Brands = BrandEdge[]
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: CategoriesTree
brands: Brands
}
> = T
async function getSiteInfo(opts?: {
variables?: GetSiteInfoQueryVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetSiteInfoResult>
async function getSiteInfo<
T extends { categories: any[]; brands: any[] },
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetSiteInfoResult<T>>
async function getSiteInfo({
query = getSiteInfoQuery,
variables,
config,
}: {
query?: string
variables?: GetSiteInfoQueryVariables
config?: BigcommerceConfig
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<RecursivePartial<GetSiteInfoQuery>>(
query,
{ variables }
)
const categories = data.site?.categoryTree
const brands = data.site?.brands?.edges
return {
categories: (categories as RecursiveRequired<typeof categories>) ?? [],
brands: filterEdges(brands as RecursiveRequired<typeof brands>),
}
}
export default getSiteInfo

@@ -1,88 +0,0 @@
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
import { definitions } from '../api/definitions/wishlist'
import { BigcommerceConfig, getConfig } from '../api'
import getAllProducts, { ProductEdge } from '../product/get-all-products'
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
items?: WishlistItem[]
}
export type WishlistItem = NonNullable<
definitions['wishlist_Full']['items']
>[0] & {
product?: ProductEdge['node']
}
export type GetCustomerWishlistResult<
T extends { wishlist?: any } = { wishlist?: Wishlist }
> = T
export type GetCustomerWishlistVariables = {
customerId: number
}
async function getCustomerWishlist(opts: {
variables: GetCustomerWishlistVariables
config?: BigcommerceConfig
includeProducts?: boolean
}): Promise<GetCustomerWishlistResult>
async function getCustomerWishlist<
T extends { wishlist?: any },
V = any
>(opts: {
url: string
variables: V
config?: BigcommerceConfig
includeProducts?: boolean
}): Promise<GetCustomerWishlistResult<T>>
async function getCustomerWishlist({
config,
variables,
includeProducts,
}: {
url?: string
variables: GetCustomerWishlistVariables
config?: BigcommerceConfig
includeProducts?: boolean
}): Promise<GetCustomerWishlistResult> {
config = getConfig(config)
const { data = [] } = await config.storeApiFetch<
RecursivePartial<{ data: Wishlist[] }>
>(`/v3/wishlists?customer_id=${variables.customerId}`)
const wishlist = data[0]
if (includeProducts && wishlist?.items?.length) {
const entityIds = wishlist.items
?.map((item) => item?.product_id)
.filter((id): id is number => !!id)
if (entityIds?.length) {
const graphqlData = await getAllProducts({
variables: { first: 100, entityIds },
config,
})
// Put the products in an object that we can use to get them by id
const productsById = graphqlData.products.reduce<{
[k: number]: ProductEdge
}>((prods, p) => {
prods[Number(p.id)] = p as any
return prods
}, {})
// Populate the wishlist items with the graphql products
wishlist.items.forEach((item) => {
const product = item && productsById[item.product_id!]
if (item && product) {
// @ts-ignore Fix this type when the wishlist type is properly defined
item.product = product
}
})
}
}
return { wishlist: wishlist as RecursiveRequired<typeof wishlist> }
}
export default getCustomerWishlist

@@ -1,16 +1,16 @@
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { Customer, CustomerData } from '../api/customers'
import type { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<Customer | null> = {
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
url: '/api/bigcommerce/customers',
url: '/api/customer',
method: 'GET',
},
async fetcher({ options, fetch }) {
const data = await fetch<CustomerData | null>(options)
const data = await fetch(options)
return data?.customer ?? null
},
useHook: ({ useData }) => (input) => {

@@ -0,0 +1,5 @@
// Remove trailing and leading slash, usually included in nodes
// returned by the BigCommerce API
const getSlug = (path: string) => path.replace(/^\/|\/$/g, '')
export default getSlug

@@ -1,6 +1,10 @@
import type { Product } from '@commerce/types'
import type { Cart, BigcommerceCart, LineItem } from '../types'
import type { Product } from '../types/product'
import type { Cart, BigcommerceCart, LineItem } from '../types/cart'
import type { Page } from '../types/page'
import type { BCCategory, Category } from '../types/site'
import { definitions } from '../api/definitions/store-content'
import update from './immutability'
import getSlug from './get-slug'
function normalizeProductOption(productOption: any) {
const {
@@ -69,6 +73,16 @@ export function normalizeProduct(productNode: any): Product {
})
}
export function normalizePage(page: definitions['page_Full']): Page {
return {
id: String(page.id),
name: page.name,
is_visible: page.is_visible,
sort_order: page.sort_order,
body: page.body,
}
}
export function normalizeCart(data: BigcommerceCart): Cart {
return {
id: data.id,
@@ -111,3 +125,12 @@ function normalizeLineItem(item: any): LineItem {
})),
}
}
export function normalizeCategory(category: BCCategory): Category {
return {
id: `${category.entityId}`,
name: category.name,
slug: getSlug(category.path),
path: category.path,
}
}

@@ -1,71 +0,0 @@
import type {
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables,
} from '../schema'
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
import filterEdges from '../api/utils/filter-edges'
import { BigcommerceConfig, getConfig } from '../api'
export const getAllProductPathsQuery = /* GraphQL */ `
query getAllProductPaths($first: Int = 100) {
site {
products(first: $first) {
edges {
node {
path
}
}
}
}
}
`
export type ProductPath = NonNullable<
NonNullable<GetAllProductPathsQuery['site']['products']['edges']>[0]
>
export type ProductPaths = ProductPath[]
export type { GetAllProductPathsQueryVariables }
export type GetAllProductPathsResult<
T extends { products: any[] } = { products: ProductPaths }
> = T
async function getAllProductPaths(opts?: {
variables?: GetAllProductPathsQueryVariables
config?: BigcommerceConfig
}): Promise<GetAllProductPathsResult>
async function getAllProductPaths<
T extends { products: any[] },
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
}): Promise<GetAllProductPathsResult<T>>
async function getAllProductPaths({
query = getAllProductPathsQuery,
variables,
config,
}: {
query?: string
variables?: GetAllProductPathsQueryVariables
config?: BigcommerceConfig
} = {}): Promise<GetAllProductPathsResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<
RecursivePartial<GetAllProductPathsQuery>
>(query, { variables })
const products = data.site?.products?.edges
return {
products: filterEdges(products as RecursiveRequired<typeof products>),
}
}
export default getAllProductPaths

@@ -1,135 +0,0 @@
import type {
GetAllProductsQuery,
GetAllProductsQueryVariables,
} from '../schema'
import type { Product } from '@commerce/types'
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
import filterEdges from '../api/utils/filter-edges'
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
import { productConnectionFragment } from '../api/fragments/product'
import { BigcommerceConfig, getConfig } from '../api'
import { normalizeProduct } from '../lib/normalize'
export const getAllProductsQuery = /* GraphQL */ `
query getAllProducts(
$hasLocale: Boolean = false
$locale: String = "null"
$entityIds: [Int!]
$first: Int = 10
$products: Boolean = false
$featuredProducts: Boolean = false
$bestSellingProducts: Boolean = false
$newestProducts: Boolean = false
) {
site {
products(first: $first, entityIds: $entityIds) @include(if: $products) {
...productConnnection
}
featuredProducts(first: $first) @include(if: $featuredProducts) {
...productConnnection
}
bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
...productConnnection
}
newestProducts(first: $first) @include(if: $newestProducts) {
...productConnnection
}
}
}
${productConnectionFragment}
`
export type ProductEdge = NonNullable<
NonNullable<GetAllProductsQuery['site']['products']['edges']>[0]
>
export type ProductNode = ProductEdge['node']
export type GetAllProductsResult<
T extends Record<keyof GetAllProductsResult, any[]> = {
products: ProductEdge[]
}
> = T
const FIELDS = [
'products',
'featuredProducts',
'bestSellingProducts',
'newestProducts',
]
export type ProductTypes =
| 'products'
| 'featuredProducts'
| 'bestSellingProducts'
| 'newestProducts'
export type ProductVariables = { field?: ProductTypes } & Omit<
GetAllProductsQueryVariables,
ProductTypes | 'hasLocale'
>
async function getAllProducts(opts?: {
variables?: ProductVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<{ products: Product[] }>
async function getAllProducts<
T extends Record<keyof GetAllProductsResult, any[]>,
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetAllProductsResult<T>>
async function getAllProducts({
query = getAllProductsQuery,
variables: { field = 'products', ...vars } = {},
config,
}: {
query?: string
variables?: ProductVariables
config?: BigcommerceConfig
preview?: boolean
// TODO: fix the product type here
} = {}): Promise<{ products: Product[] | any[] }> {
config = getConfig(config)
const locale = vars.locale || config.locale
const variables: GetAllProductsQueryVariables = {
...vars,
locale,
hasLocale: !!locale,
}
if (!FIELDS.includes(field)) {
throw new Error(
`The field variable has to match one of ${FIELDS.join(', ')}`
)
}
variables[field] = true
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
query,
{ variables }
)
const edges = data.site?.[field]?.edges
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
if (locale && config.applyLocale) {
products.forEach((product: RecursivePartial<ProductEdge>) => {
if (product.node) setProductLocaleMeta(product.node)
})
}
return { products: products.map(({ node }) => normalizeProduct(node as any)) }
}
export default getAllProducts

@@ -1,121 +0,0 @@
import type { GetProductQuery, GetProductQueryVariables } from '../schema'
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
import { productInfoFragment } from '../api/fragments/product'
import { BigcommerceConfig, getConfig } from '../api'
import { normalizeProduct } from '../lib/normalize'
import type { Product } from '@commerce/types'
export const getProductQuery = /* GraphQL */ `
query getProduct(
$hasLocale: Boolean = false
$locale: String = "null"
$path: String!
) {
site {
route(path: $path) {
node {
__typename
... on Product {
...productInfo
variants {
edges {
node {
entityId
defaultImage {
urlOriginal
altText
isDefault
}
prices {
...productPrices
}
inventory {
aggregated {
availableToSell
warningLevel
}
isInStock
}
productOptions {
edges {
node {
__typename
entityId
displayName
...multipleChoiceOption
}
}
}
}
}
}
}
}
}
}
}
${productInfoFragment}
`
export type ProductNode = Extract<
GetProductQuery['site']['route']['node'],
{ __typename: 'Product' }
>
export type GetProductResult<
T extends { product?: any } = { product?: ProductNode }
> = T
export type ProductVariables = { locale?: string } & (
| { path: string; slug?: never }
| { path?: never; slug: string }
)
async function getProduct(opts: {
variables: ProductVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetProductResult>
async function getProduct<T extends { product?: any }, V = any>(opts: {
query: string
variables: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetProductResult<T>>
async function getProduct({
query = getProductQuery,
variables: { slug, ...vars },
config,
}: {
query?: string
variables: ProductVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<Product | {} | any> {
config = getConfig(config)
const locale = vars.locale || config.locale
const variables: GetProductQueryVariables = {
...vars,
locale,
hasLocale: !!locale,
path: slug ? `/${slug}/` : vars.path!,
}
const { data } = await config.fetch<GetProductQuery>(query, { variables })
const product = data.site?.route?.node
if (product?.__typename === 'Product') {
if (locale && config.applyLocale) {
setProductLocaleMeta(product)
}
return { product: normalizeProduct(product as any) }
}
return {}
}
export default getProduct

@@ -1,4 +1,2 @@
export { default as usePrice } from './use-price'
export { default as useSearch } from './use-search'
export { default as getProduct } from './get-product'
export { default as getAllProducts } from './get-all-products'

@@ -1,23 +1,20 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
import type { SearchProductsData } from '../api/catalog/products'
import type { SearchProductsHook } from '../types/product'
export default useSearch as UseSearch<typeof handler>
export type SearchProductsInput = {
search?: string
categoryId?: number
categoryId?: number | string
brandId?: number
sort?: string
locale?: string
}
export const handler: SWRHook<
SearchProductsData,
SearchProductsInput,
SearchProductsInput
> = {
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
url: '/api/bigcommerce/catalog/products',
url: '/api/catalog/products',
method: 'GET',
},
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
@@ -26,9 +23,9 @@ export const handler: SWRHook<
if (search) url.searchParams.set('search', search)
if (Number.isInteger(categoryId))
url.searchParams.set('category', String(categoryId))
url.searchParams.set('categoryId', String(categoryId))
if (Number.isInteger(brandId))
url.searchParams.set('brand', String(brandId))
url.searchParams.set('brandId', String(brandId))
if (sort) url.searchParams.set('sort', sort)
return fetch({

@@ -1,4 +1,6 @@
import * as Core from '@commerce/types'
import * as Core from '@commerce/types/cart'
export * from '@commerce/types/cart'
// TODO: this type should match:
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
@@ -23,16 +25,14 @@ export type BigcommerceCart = {
// TODO: add missing fields
}
export type Cart = Core.Cart & {
lineItems: LineItem[]
}
export type LineItem = Core.LineItem
/**
* Cart mutations
* Extend core cart types
*/
export type Cart = Core.Cart & {
lineItems: Core.LineItem[]
}
export type OptionSelections = {
option_id: number
option_value: number | string
@@ -43,16 +43,24 @@ export type CartItemBody = Core.CartItemBody & {
optionSelections?: OptionSelections
}
export type GetCartHandlerBody = Core.GetCartHandlerBody
export type CartTypes = {
cart: Cart
item: Core.LineItem
itemBody: CartItemBody
}
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
export type CartHooks = Core.CartHooks<CartTypes>
export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody<CartItemBody>
export type GetCartHook = CartHooks['getCart']
export type AddItemHook = CartHooks['addItem']
export type UpdateItemHook = CartHooks['updateItem']
export type RemoveItemHook = CartHooks['removeItem']
export type UpdateCartItemBody = Core.UpdateCartItemBody<CartItemBody>
export type CartSchema = Core.CartSchema<CartTypes>
export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody<CartItemBody>
export type CartHandlers = Core.CartHandlers<CartTypes>
export type RemoveCartItemBody = Core.RemoveCartItemBody
export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody
export type GetCartHandler = CartHandlers['getCart']
export type AddItemHandler = CartHandlers['addItem']
export type UpdateItemHandler = CartHandlers['updateItem']
export type RemoveItemHandler = CartHandlers['removeItem']

@@ -0,0 +1 @@
export * from '@commerce/types/checkout'

@@ -0,0 +1 @@
export * from '@commerce/types/common'

@@ -0,0 +1,5 @@
import * as Core from '@commerce/types/customer'
export * from '@commerce/types/customer'
export type CustomerSchema = Core.CustomerSchema

@@ -0,0 +1,25 @@
import * as Cart from './cart'
import * as Checkout from './checkout'
import * as Common from './common'
import * as Customer from './customer'
import * as Login from './login'
import * as Logout from './logout'
import * as Page from './page'
import * as Product from './product'
import * as Signup from './signup'
import * as Site from './site'
import * as Wishlist from './wishlist'
export type {
Cart,
Checkout,
Common,
Customer,
Login,
Logout,
Page,
Product,
Signup,
Site,
Wishlist,
}

@@ -0,0 +1,8 @@
import * as Core from '@commerce/types/login'
import type { LoginMutationVariables } from '../schema'
export * from '@commerce/types/login'
export type LoginOperation = Core.LoginOperation & {
variables: LoginMutationVariables
}

@@ -0,0 +1 @@
export * from '@commerce/types/logout'

@@ -0,0 +1,11 @@
import * as Core from '@commerce/types/page'
export * from '@commerce/types/page'
export type Page = Core.Page
export type PageTypes = {
page: Page
}
export type GetAllPagesOperation = Core.GetAllPagesOperation<PageTypes>
export type GetPageOperation = Core.GetPageOperation<PageTypes>

@@ -0,0 +1 @@
export * from '@commerce/types/product'

@@ -0,0 +1 @@
export * from '@commerce/types/signup'

@@ -0,0 +1,19 @@
import * as Core from '@commerce/types/site'
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema'
export * from '@commerce/types/site'
export type BCCategory = NonNullable<
GetSiteInfoQuery['site']['categoryTree']
>[0]
export type Brand = NonNullable<
NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0]
>
export type SiteTypes = {
category: Core.Category
brand: Brand
}
export type GetSiteInfoOperation = Core.GetSiteInfoOperation<SiteTypes>

@@ -0,0 +1,23 @@
import * as Core from '@commerce/types/wishlist'
import { definitions } from '../api/definitions/wishlist'
import type { ProductEdge } from '../api/operations/get-all-products'
export * from '@commerce/types/wishlist'
export type WishlistItem = NonNullable<
definitions['wishlist_Full']['items']
>[0] & {
product?: ProductEdge['node']
}
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
items?: WishlistItem[]
}
export type WishlistTypes = {
wishlist: Wishlist
itemBody: Core.WishlistItemBody
}
export type WishlistSchema = Core.WishlistSchema<WishlistTypes>
export type GetCustomerWishlistOperation = Core.GetCustomerWishlistOperation<WishlistTypes>

@@ -2,15 +2,15 @@ import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
import type { ItemBody, AddItemBody } from '../api/wishlist'
import type { AddItemHook } from '../types/wishlist'
import useCustomer from '../customer/use-customer'
import useWishlist from './use-wishlist'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any, {}, ItemBody, AddItemBody> = {
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/bigcommerce/wishlist',
url: '/api/wishlist',
method: 'POST',
},
useHook: ({ fetch }) => () => {

@@ -2,23 +2,17 @@ import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useRemoveItem, {
RemoveItemInput,
UseRemoveItem,
} from '@commerce/wishlist/use-remove-item'
import type { RemoveItemBody, Wishlist } from '../api/wishlist'
import type { RemoveItemHook } from '../types/wishlist'
import useCustomer from '../customer/use-customer'
import useWishlist, { UseWishlistInput } from './use-wishlist'
import useWishlist from './use-wishlist'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<
Wishlist | null,
{ wishlist?: UseWishlistInput },
RemoveItemInput,
RemoveItemBody
> = {
export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: {
url: '/api/bigcommerce/wishlist',
url: '/api/wishlist',
method: 'DELETE',
},
useHook: ({ fetch }) => ({ wishlist } = {}) => {

@@ -1,21 +1,14 @@
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
import type { Wishlist } from '../api/wishlist'
import type { GetWishlistHook } from '../types/wishlist'
import useCustomer from '../customer/use-customer'
export type UseWishlistInput = { includeProducts?: boolean }
export default useWishlist as UseWishlist<typeof handler>
export const handler: SWRHook<
Wishlist | null,
UseWishlistInput,
{ customerId?: number } & UseWishlistInput,
{ isEmpty?: boolean }
> = {
export const handler: SWRHook<GetWishlistHook> = {
fetchOptions: {
url: '/api/bigcommerce/wishlist',
url: '/api/wishlist',
method: 'GET',
},
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {

@@ -0,0 +1,62 @@
import type { CartSchema } from '../../types/cart'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const cartEndpoint: GetAPISchema<
any,
CartSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getCart'],
POST: handlers['addItem'],
PUT: handlers['updateItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
// Return current cart info
if (req.method === 'GET') {
const body = { cartId }
return await handlers['getCart']({ ...ctx, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { ...req.body, cartId }
return await handlers['removeItem']({ ...ctx, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default cartEndpoint

@@ -0,0 +1,31 @@
import type { ProductsSchema } from '../../../types/product'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
import type { GetAPISchema } from '../..'
const productsEndpoint: GetAPISchema<
any,
ProductsSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) {
return
}
try {
const body = req.query
return await handlers['getProducts']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default productsEndpoint

@@ -0,0 +1,35 @@
import type { CheckoutSchema } from '../../types/checkout'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const checkoutEndpoint: GetAPISchema<
any,
CheckoutSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['checkout'],
})
) {
return
}
try {
const body = null
return await handlers['checkout']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default checkoutEndpoint

@@ -0,0 +1,35 @@
import type { CustomerSchema } from '../../types/customer'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const customerEndpoint: GetAPISchema<
any,
CustomerSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getLoggedInCustomer'],
})
) {
return
}
try {
const body = null
return await handlers['getLoggedInCustomer']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default customerEndpoint

@@ -0,0 +1,35 @@
import type { LoginSchema } from '../../types/login'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const loginEndpoint: GetAPISchema<
any,
LoginSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
POST: handlers['login'],
})
) {
return
}
try {
const body = req.body ?? {}
return await handlers['login']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default loginEndpoint

@@ -0,0 +1,37 @@
import type { LogoutSchema } from '../../types/logout'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const logoutEndpoint: GetAPISchema<
any,
LogoutSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['logout'],
})
) {
return
}
try {
const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
return await handlers['logout']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default logoutEndpoint

@@ -0,0 +1,38 @@
import type { SignupSchema } from '../../types/signup'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const signupEndpoint: GetAPISchema<
any,
SignupSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
POST: handlers['signup'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId }
return await handlers['signup']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default signupEndpoint

@@ -0,0 +1,58 @@
import type { WishlistSchema } from '../../types/wishlist'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const wishlistEndpoint: GetAPISchema<
any,
WishlistSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getWishlist'],
POST: handlers['addItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
const { cookies } = req
const customerToken = cookies[config.customerCookie]
try {
// Return current wishlist info
if (req.method === 'GET') {
const body = {
customerToken,
includeProducts: req.query.products === '1',
}
return await handlers['getWishlist']({ ...ctx, body })
}
// Add an item to the wishlist
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return await handlers['addItem']({ ...ctx, body })
}
// Remove an item from the wishlist
if (req.method === 'DELETE') {
const body = { ...req.body, customerToken }
return await handlers['removeItem']({ ...ctx, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default wishlistEndpoint

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