mirror of
https://github.com/vercel/commerce.git
synced 2025-07-26 19:51:23 +00:00
Merge branch 'main' of https://github.com/vercel/commerce into vercel-main
This commit is contained in:
8
framework/bigcommerce/.env.template
Normal file
8
framework/bigcommerce/.env.template
Normal file
@@ -0,0 +1,8 @@
|
||||
COMMERCE_PROVIDER=bigcommerce
|
||||
|
||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_URL=
|
||||
BIGCOMMERCE_STORE_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||
BIGCOMMERCE_CHANNEL_ID=
|
@@ -1,47 +1,34 @@
|
||||
# Bigcommerce Provider
|
||||
|
||||
Table of Contents
|
||||
=================
|
||||
**Demo:** https://bigcommerce.demo.vercel.store/
|
||||
|
||||
* [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks)
|
||||
* [Installation](#installation)
|
||||
* [General Usage](#general-usage)
|
||||
* [CommerceProvider](#commerceprovider)
|
||||
* [useLogin hook](#uselogin-hook)
|
||||
* [useLogout](#uselogout)
|
||||
* [useCustomer](#usecustomer)
|
||||
* [useSignup](#usesignup)
|
||||
* [usePrice](#useprice)
|
||||
* [Cart Hooks](#cart-hooks)
|
||||
* [useCart](#usecart)
|
||||
* [useAddItem](#useadditem)
|
||||
* [useUpdateItem](#useupdateitem)
|
||||
* [useRemoveItem](#useremoveitem)
|
||||
* [Wishlist Hooks](#wishlist-hooks)
|
||||
* [Product Hooks and API](#product-hooks-and-api)
|
||||
* [useSearch](#usesearch)
|
||||
* [getAllProducts](#getallproducts)
|
||||
* [getProduct](#getproduct)
|
||||
* [More](#more)
|
||||
With the deploy button below you'll be able to have a [BigCommerce](https://www.bigcommerce.com/) account and a store that works with this starter:
|
||||
|
||||
# BigCommerce Storefront Data Hooks
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-description=An%20all-in-one%20starter%20kit%20for%20high-performance%20e-commerce%20sites.&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&integration-ids=oac_MuWZiE4jtmQ2ejZQaQ7ncuDT)
|
||||
|
||||
> This project is under active development, new features and updates will be continuously added over time
|
||||
If you already have a BigCommerce account and want to use your current store, then copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
|
||||
|
||||
UI hooks and data fetching methods built from the ground up for e-commerce applications written in React, that use BigCommerce as a headless e-commerce platform. The package provides:
|
||||
|
||||
- Code splitted hooks for data fetching using [SWR](https://swr.vercel.app/), and to handle common user actions
|
||||
- Code splitted data fetching methods for initial data population and static generation of content
|
||||
- Helpers to create the API endpoints that connect to the hooks, very well suited for Next.js applications
|
||||
|
||||
## Installation
|
||||
|
||||
To install:
|
||||
|
||||
```
|
||||
yarn add storefront-data-hooks
|
||||
```bash
|
||||
cp framework/bigcommerce/.env.template .env.local
|
||||
```
|
||||
|
||||
After install, the first thing you do is: <b>set your environment variables</b> in `.env.local`
|
||||
Then, set the environment variables in `.env.local` to match the ones from your store.
|
||||
|
||||
## Contribute
|
||||
|
||||
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||
|
||||
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
<details>
|
||||
<summary>I already own a BigCommerce store. What should I do?</summary>
|
||||
<br>
|
||||
First thing you do is: <b>set your environment variables</b>
|
||||
<br>
|
||||
<br>
|
||||
.env.local
|
||||
|
||||
```sh
|
||||
BIGCOMMERCE_STOREFRONT_API_URL=<>
|
||||
@@ -49,335 +36,24 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_URL=<>
|
||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||
BIGCOMMERCE_CHANNEL_ID=<>
|
||||
```
|
||||
|
||||
## General Usage
|
||||
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
||||
|
||||
### CommerceProvider
|
||||
1. Install Vercel CLI: `npm i -g vercel`
|
||||
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
|
||||
3. Download your environment variables: `vercel env pull .env.local`
|
||||
|
||||
This component is a provider pattern component that creates commerce context for it's children. It takes config values for the locale and an optional `fetcherRef` object for data fetching.
|
||||
Next, you're free to customize the starter. More updates coming soon. Stay tuned.
|
||||
|
||||
```jsx
|
||||
...
|
||||
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
|
||||
</details>
|
||||
|
||||
const App = ({ locale = 'en-US', children }) => {
|
||||
return (
|
||||
<CommerceProvider locale={locale}>
|
||||
{children}
|
||||
</CommerceProvider>
|
||||
)
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### useLogin hook
|
||||
|
||||
Hook for bigcommerce user login functionality, returns `login` function to handle user login.
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useLogin from '@bigcommerce/storefront-data-hooks/use-login'
|
||||
|
||||
const LoginView = () => {
|
||||
const login = useLogin()
|
||||
|
||||
const handleLogin = async () => {
|
||||
await login({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin}>
|
||||
{children}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### useLogout
|
||||
|
||||
Hook to logout user.
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
|
||||
|
||||
const LogoutLink = () => {
|
||||
const logout = useLogout()
|
||||
return (
|
||||
<a onClick={() => logout()}>
|
||||
Logout
|
||||
</a>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useCustomer
|
||||
|
||||
Hook for getting logged in customer data, and fetching customer info.
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
|
||||
...
|
||||
|
||||
const Profile = () => {
|
||||
const { data } = useCustomer()
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>Hello, {data.firstName}</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useSignup
|
||||
|
||||
Hook for bigcommerce user signup, returns `signup` function to handle user signups.
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useSignup from '@bigcommerce/storefront-data-hooks/use-login'
|
||||
|
||||
const SignupView = () => {
|
||||
const signup = useSignup()
|
||||
|
||||
const handleSignup = async () => {
|
||||
await signup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSignup}>
|
||||
{children}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### usePrice
|
||||
|
||||
Helper hook to format price according to commerce locale, and return discount if available.
|
||||
|
||||
```jsx
|
||||
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
||||
...
|
||||
const { price, discount, basePrice } = usePrice(
|
||||
data && {
|
||||
amount: data.cart_amount,
|
||||
currencyCode: data.currency.code,
|
||||
}
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
## Cart Hooks
|
||||
|
||||
### useCart
|
||||
|
||||
Returns the current cart data for use
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||
|
||||
const countItem = (count: number, item: any) => count + item.quantity
|
||||
const countItems = (count: number, items: any[]) =>
|
||||
items.reduce(countItem, count)
|
||||
|
||||
const CartNumber = () => {
|
||||
const { data } = useCart()
|
||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||
|
||||
return itemsCount > 0 ? <span>{itemsCount}</span> : null
|
||||
}
|
||||
```
|
||||
|
||||
### useAddItem
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
|
||||
|
||||
const AddToCartButton = ({ productId, variantId }) => {
|
||||
const addItem = useAddItem()
|
||||
|
||||
const addToCart = async () => {
|
||||
await addItem({
|
||||
productId,
|
||||
variantId,
|
||||
})
|
||||
}
|
||||
|
||||
return <button onClick={addToCart}>Add To Cart</button>
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### useUpdateItem
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useUpdateItem from '@bigcommerce/storefront-data-hooks/cart/use-update-item'
|
||||
|
||||
const CartItem = ({ item }) => {
|
||||
const [quantity, setQuantity] = useState(item.quantity)
|
||||
const updateItem = useUpdateItem(item)
|
||||
|
||||
const updateQuantity = async (e) => {
|
||||
const val = e.target.value
|
||||
await updateItem({ quantity: val })
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
max={99}
|
||||
min={0}
|
||||
value={quantity}
|
||||
onChange={updateQuantity}
|
||||
/>
|
||||
)
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### useRemoveItem
|
||||
|
||||
Provided with a cartItemId, will remove an item from the cart:
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useRemoveItem from '@bigcommerce/storefront-data-hooks/cart/use-remove-item'
|
||||
|
||||
const RemoveButton = ({ item }) => {
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
const handleRemove = async () => {
|
||||
await removeItem({ id: item.id })
|
||||
}
|
||||
|
||||
return <button onClick={handleRemove}>Remove</button>
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
## Wishlist Hooks
|
||||
|
||||
Wishlist hooks are similar to cart hooks. See the below example for how to use `useWishlist`, `useAddItem`, and `useRemoveItem`.
|
||||
|
||||
```jsx
|
||||
import useAddItem from '@bigcommerce/storefront-data-hooks/wishlist/use-add-item'
|
||||
import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remove-item'
|
||||
import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist'
|
||||
|
||||
const WishlistButton = ({ productId, variant }) => {
|
||||
const addItem = useAddItem()
|
||||
const removeItem = useRemoveItem()
|
||||
const { data } = useWishlist()
|
||||
const { data: customer } = useCustomer()
|
||||
const itemInWishlist = data?.items?.find(
|
||||
(item) =>
|
||||
item.product_id === productId &&
|
||||
item.variant_id === variant?.node.entityId
|
||||
)
|
||||
|
||||
const handleWishlistChange = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!customer) {
|
||||
return
|
||||
}
|
||||
|
||||
if (itemInWishlist) {
|
||||
await removeItem({ id: itemInWishlist.id! })
|
||||
} else {
|
||||
await addItem({
|
||||
productId,
|
||||
variantId: variant?.node.entityId!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleWishlistChange}>
|
||||
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Product Hooks and API
|
||||
|
||||
### useSearch
|
||||
|
||||
`useSearch` handles searching the bigcommerce storefront product catalog by catalog, brand, and query string.
|
||||
|
||||
```jsx
|
||||
...
|
||||
import useSearch from '@bigcommerce/storefront-data-hooks/products/use-search'
|
||||
|
||||
const SearchPage = ({ searchString, category, brand, sortStr }) => {
|
||||
const { data } = useSearch({
|
||||
search: searchString || '',
|
||||
categoryId: category?.entityId,
|
||||
brandId: brand?.entityId,
|
||||
sort: sortStr || '',
|
||||
})
|
||||
|
||||
return (
|
||||
<Grid layout="normal">
|
||||
{data.products.map(({ node }) => (
|
||||
<ProductCard key={node.path} product={node} />
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### getAllProducts
|
||||
|
||||
API function to retrieve a product list.
|
||||
|
||||
```js
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
|
||||
|
||||
const { products } = await getAllProducts({
|
||||
variables: { field: 'featuredProducts', first: 6 },
|
||||
config,
|
||||
preview,
|
||||
})
|
||||
```
|
||||
|
||||
### getProduct
|
||||
|
||||
API product to retrieve a single product when provided with the product
|
||||
slug string.
|
||||
|
||||
```js
|
||||
import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
|
||||
import getProduct from '@bigcommerce/storefront-data-hooks/api/operations/get-product'
|
||||
|
||||
const { product } = await getProduct({
|
||||
variables: { slug },
|
||||
config,
|
||||
preview,
|
||||
})
|
||||
```
|
||||
|
||||
## More
|
||||
|
||||
Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce))
|
||||
<details>
|
||||
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
|
||||
<br>
|
||||
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
||||
<br>
|
||||
<br>
|
||||
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
||||
</details>
|
||||
|
@@ -1,116 +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'
|
||||
|
||||
type OptionSelections = {
|
||||
option_id: Number
|
||||
option_value: Number|String
|
||||
}
|
||||
|
||||
export type ItemBody = {
|
||||
productId: number
|
||||
variantId: number
|
||||
quantity?: number
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export type AddItemBody = { item: ItemBody }
|
||||
|
||||
export type UpdateItemBody = { itemId: string; item: ItemBody }
|
||||
|
||||
export type RemoveItemBody = { itemId: string }
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type Cart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
export type CartHandlers = {
|
||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
||||
updateItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<UpdateItemBody>
|
||||
>
|
||||
removeItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<RemoveItemBody>
|
||||
>
|
||||
}
|
||||
|
||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
|
||||
// TODO: a complete implementation should have schema validation for `req.body`
|
||||
const cartApi: BigcommerceApiHandler<Cart, 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 isAllowedMethod from '../utils/is-allowed-method'
|
||||
import createApiHandler, {
|
||||
BigcommerceApiHandler,
|
||||
BigcommerceHandler,
|
||||
} from '../utils/create-api-handler'
|
||||
import { BigcommerceApiError } from '../utils/errors'
|
||||
import type { ProductEdge } from '../operations/get-all-products'
|
||||
import getProducts from './handlers/get-products'
|
||||
|
||||
export type SearchProductsData = {
|
||||
products: ProductEdge[]
|
||||
found: boolean
|
||||
}
|
||||
|
||||
export type ProductsHandlers = {
|
||||
getProducts: BigcommerceHandler<
|
||||
SearchProductsData,
|
||||
{ search?: 'string'; category?: string; brand?: string; sort?: string }
|
||||
>
|
||||
}
|
||||
|
||||
const METHODS = ['GET']
|
||||
|
||||
// TODO: 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,9 +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 '.'
|
||||
|
||||
// Return current cart info
|
||||
const addItem: CartHandlers['addItem'] = async ({
|
||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
body: { cartId, item },
|
||||
config,
|
||||
@@ -26,15 +26,21 @@ const addItem: CartHandlers['addItem'] = async ({
|
||||
}),
|
||||
}
|
||||
const { data } = cartId
|
||||
? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options)
|
||||
: await config.storeApiFetch('/v3/carts', options)
|
||||
? await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}/items?include=line_items.physical_items.options`,
|
||||
options
|
||||
)
|
||||
: await config.storeApiFetch(
|
||||
'/v3/carts?include=line_items.physical_items.options',
|
||||
options
|
||||
)
|
||||
|
||||
// Create or update the cart cookie
|
||||
res.setHeader(
|
||||
'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,18 +1,22 @@
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { Cart, 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,
|
||||
}) => {
|
||||
let result: { data?: Cart } = {}
|
||||
let result: { data?: BigcommerceCart } = {}
|
||||
|
||||
if (cartId) {
|
||||
try {
|
||||
result = await config.storeApiFetch(`/v3/carts/${cartId}`)
|
||||
result = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}?include=line_items.physical_items.options`
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||
// Remove the cookie if it exists but the cart wasn't found
|
||||
@@ -23,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
|
26
framework/bigcommerce/api/endpoints/cart/index.ts
Normal file
26
framework/bigcommerce/api/endpoints/cart/index.ts
Normal file
@@ -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,8 +1,8 @@
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
// Return current cart info
|
||||
const removeItem: CartHandlers['removeItem'] = async ({
|
||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId },
|
||||
config,
|
||||
@@ -15,7 +15,7 @@ const removeItem: CartHandlers['removeItem'] = async ({
|
||||
}
|
||||
|
||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
||||
`/v3/carts/${cartId}/items/${itemId}`,
|
||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
const data = result?.data ?? null
|
||||
@@ -28,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,9 +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 '.'
|
||||
|
||||
// Return current cart info
|
||||
const updateItem: CartHandlers['updateItem'] = async ({
|
||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId, item },
|
||||
config,
|
||||
@@ -16,7 +16,7 @@ const updateItem: CartHandlers['updateItem'] = async ({
|
||||
}
|
||||
|
||||
const { data } = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}/items/${itemId}`,
|
||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
@@ -30,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,18 +1,20 @@
|
||||
import getAllProducts, { ProductEdge } from '../../operations/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',
|
||||
trending: 'total_sold',
|
||||
price: 'price',
|
||||
}
|
||||
|
||||
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')
|
||||
@@ -22,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('-')
|
||||
@@ -44,25 +46,29 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
||||
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
||||
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]: ProductEdge
|
||||
[k: string]: Product
|
||||
}>((prods, p) => {
|
||||
prods[p.node.entityId] = p
|
||||
prods[Number(p.id)] = p
|
||||
return prods
|
||||
}, {})
|
||||
const products: ProductEdge[] = found ? [] : graphqlData.products
|
||||
|
||||
const products: Product[] = found ? [] : graphqlData.products
|
||||
|
||||
// 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
|
62
framework/bigcommerce/api/endpoints/checkout/checkout.ts
Normal file
62
framework/bigcommerce/api/endpoints/checkout/checkout.ts
Normal file
@@ -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
|
18
framework/bigcommerce/api/endpoints/checkout/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/checkout/index.ts
Normal file
@@ -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,
|
18
framework/bigcommerce/api/endpoints/customer/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/customer/index.ts
Normal file
@@ -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
|
18
framework/bigcommerce/api/endpoints/login/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/login/index.ts
Normal file
@@ -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 '../../operations/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
|
18
framework/bigcommerce/api/endpoints/logout/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/logout/index.ts
Normal file
@@ -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
|
18
framework/bigcommerce/api/endpoints/signup/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/signup/index.ts
Normal file
@@ -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 '../../operations/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 '../../operations/get-customer-id'
|
||||
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 '../../operations/get-customer-id'
|
||||
import type { Wishlist } from '../../../types/wishlist'
|
||||
import type { WishlistEndpoint } from '.'
|
||||
import getCustomerId from './utils/get-customer-id'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import type { Wishlist, WishlistHandlers } from '..'
|
||||
|
||||
// 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,
|
24
framework/bigcommerce/api/endpoints/wishlist/index.ts
Normal file
24
framework/bigcommerce/api/endpoints/wishlist/index.ts
Normal file
@@ -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 '../../operations/get-customer-id'
|
||||
import getCustomerWishlist, {
|
||||
Wishlist,
|
||||
} from '../../operations/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 '..'
|
||||
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,7 +1,28 @@
|
||||
import type { RequestInit } from '@vercel/fetch'
|
||||
import type { CommerceAPIConfig } from '@commerce/api'
|
||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||
import fetchStoreApi from './utils/fetch-store-api'
|
||||
import {
|
||||
CommerceAPI,
|
||||
CommerceAPIConfig,
|
||||
getCommerceApi as commerceApi,
|
||||
} from '@commerce/api'
|
||||
import createFetchGraphqlApi from './utils/fetch-graphql-api'
|
||||
import createFetchStoreApi 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
|
||||
@@ -39,50 +60,52 @@ 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,
|
||||
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
|
||||
applyLocale: true,
|
||||
// REST API only
|
||||
storeApiUrl: STORE_API_URL,
|
||||
storeApiToken: STORE_API_TOKEN,
|
||||
storeApiClientId: STORE_API_CLIENT_ID,
|
||||
storeChannelId: STORE_CHANNEL_ID,
|
||||
storeApiFetch: fetchStoreApi,
|
||||
})
|
||||
|
||||
export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
|
||||
return config.getConfig(userConfig)
|
||||
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@@ -1,45 +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, getConfig } from '..'
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
|
||||
export type Page = definitions['page_Full']
|
||||
export default function getAllPagesOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
|
||||
config?: Partial<BigcommerceConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
export type GetAllPagesResult<
|
||||
T extends { pages: any[] } = { pages: Page[] }
|
||||
> = T
|
||||
async function getAllPages<T extends GetAllPagesOperation>(
|
||||
opts: {
|
||||
config?: Partial<BigcommerceConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getAllPages(opts?: {
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
}): Promise<GetAllPagesResult>
|
||||
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>) ?? []
|
||||
|
||||
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>) ?? []
|
||||
|
||||
const retPages = {
|
||||
pages: preview ? pages : pages.filter((p) => p.is_visible),
|
||||
return {
|
||||
pages: preview ? pages : pages.filter((p) => p.is_visible),
|
||||
}
|
||||
}
|
||||
|
||||
return retPages
|
||||
return getAllPages
|
||||
}
|
||||
|
||||
export default getAllPages
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import type {
|
||||
GetAllProductPathsQuery,
|
||||
GetAllProductPathsQueryVariables,
|
||||
} from '../../schema'
|
||||
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, getConfig } from '..'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
|
||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
||||
query getAllProductPaths($first: Int = 100) {
|
||||
@@ -20,52 +22,45 @@ export const getAllProductPathsQuery = /* GraphQL */ `
|
||||
}
|
||||
`
|
||||
|
||||
export type ProductPath = NonNullable<
|
||||
NonNullable<GetAllProductPathsQuery['site']['products']['edges']>[0]
|
||||
>
|
||||
export default function getAllProductPathsOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getAllProductPaths<
|
||||
T extends GetAllProductPathsOperation
|
||||
>(opts?: {
|
||||
variables?: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
}): Promise<T['data']>
|
||||
|
||||
export type ProductPaths = ProductPath[]
|
||||
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
|
||||
opts: {
|
||||
variables?: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
export type { GetAllProductPathsQueryVariables }
|
||||
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
|
||||
|
||||
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>),
|
||||
return {
|
||||
products: filterEdges(products as RecursiveRequired<typeof products>).map(
|
||||
({ node }) => node
|
||||
),
|
||||
}
|
||||
}
|
||||
return getAllProductPaths
|
||||
}
|
||||
|
||||
export default getAllProductPaths
|
||||
|
@@ -1,12 +1,18 @@
|
||||
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, getConfig } from '..'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
import { normalizeProduct } from '../../lib/normalize'
|
||||
|
||||
export const getAllProductsQuery = /* GraphQL */ `
|
||||
query getAllProducts(
|
||||
@@ -50,83 +56,80 @@ export type GetAllProductsResult<
|
||||
}
|
||||
> = 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<GetAllProductsResult>
|
||||
|
||||
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
|
||||
} = {}): Promise<GetAllProductsResult> {
|
||||
config = getConfig(config)
|
||||
|
||||
const locale = vars.locale || config.locale
|
||||
const variables: GetAllProductsQueryVariables = {
|
||||
...vars,
|
||||
locale,
|
||||
hasLocale: !!locale,
|
||||
function getProductsType(
|
||||
relevance?: GetAllProductsOperation['variables']['relevance']
|
||||
) {
|
||||
switch (relevance) {
|
||||
case 'featured':
|
||||
return 'featuredProducts'
|
||||
case 'best_selling':
|
||||
return 'bestSellingProducts'
|
||||
case 'newest':
|
||||
return 'newestProducts'
|
||||
default:
|
||||
return 'products'
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
export default getAllProducts
|
||||
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
|
||||
}
|
||||
|
@@ -1,87 +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 { definitions } from '../definitions/wishlist'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
||||
|
||||
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
|
||||
items?: WishlistItem[]
|
||||
}
|
||||
export default function getCustomerWishlistOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getCustomerWishlist<
|
||||
T extends GetCustomerWishlistOperation
|
||||
>(opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
includeProducts?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
export type WishlistItem = NonNullable<
|
||||
definitions['wishlist_Full']['items']
|
||||
>[0] & {
|
||||
product?: ProductEdge['node']
|
||||
}
|
||||
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
includeProducts?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
export type GetCustomerWishlistResult<
|
||||
T extends { wishlist?: any } = { wishlist?: Wishlist }
|
||||
> = T
|
||||
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)
|
||||
|
||||
export type GetCustomerWishlistVariables = {
|
||||
customerId: number
|
||||
}
|
||||
const { data = [] } = await config.storeApiFetch<
|
||||
RecursivePartial<{ data: Wishlist[] }>
|
||||
>(`/v3/wishlists?customer_id=${variables.customerId}`)
|
||||
const wishlist = data[0]
|
||||
|
||||
async function getCustomerWishlist(opts: {
|
||||
variables: GetCustomerWishlistVariables
|
||||
config?: BigcommerceConfig
|
||||
includeProducts?: boolean
|
||||
}): Promise<GetCustomerWishlistResult>
|
||||
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)
|
||||
|
||||
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[p.node.entityId] = p
|
||||
return prods
|
||||
}, {})
|
||||
// Populate the wishlist items with the graphql products
|
||||
wishlist.items.forEach((item) => {
|
||||
const product = item && productsById[item.product_id!]
|
||||
if (item && product) {
|
||||
item.product = product.node
|
||||
}
|
||||
})
|
||||
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 { wishlist: wishlist as RecursiveRequired<typeof wishlist> }
|
||||
return getCustomerWishlist
|
||||
}
|
||||
|
||||
export default getCustomerWishlist
|
||||
|
@@ -1,53 +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 { BigcommerceConfig, getConfig } from '..'
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import type { BigcommerceConfig, Provider } from '..'
|
||||
import { normalizePage } from '../../lib/normalize'
|
||||
|
||||
export type Page = definitions['page_Full']
|
||||
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']>
|
||||
|
||||
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
|
||||
async function getPage<T extends GetPageOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<BigcommerceConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
export type PageVariables = {
|
||||
id: number
|
||||
}
|
||||
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>
|
||||
|
||||
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 }
|
||||
if (preview || page?.is_visible) {
|
||||
return { page: normalizePage(page as any) }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export default getPage
|
||||
return getPage
|
||||
}
|
||||
|
@@ -1,7 +1,13 @@
|
||||
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, getConfig } from '..'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
import { normalizeProduct } from '../../lib/normalize'
|
||||
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct(
|
||||
@@ -56,63 +62,58 @@ export const getProductQuery = /* GraphQL */ `
|
||||
${productInfoFragment}
|
||||
`
|
||||
|
||||
export type ProductNode = Extract<
|
||||
GetProductQuery['site']['route']['node'],
|
||||
{ __typename: 'Product' }
|
||||
>
|
||||
// TODO: See if this type is useful for defining the Product type
|
||||
// export type ProductNode = Extract<
|
||||
// GetProductQuery['site']['route']['node'],
|
||||
// { __typename: 'Product' }
|
||||
// >
|
||||
|
||||
export type GetProductResult<
|
||||
T extends { product?: any } = { product?: ProductNode }
|
||||
> = T
|
||||
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']>
|
||||
|
||||
export type ProductVariables = { locale?: string } & (
|
||||
| { path: string; slug?: never }
|
||||
| { path?: never; slug: string }
|
||||
)
|
||||
async function getProduct<T extends GetProductOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<BigcommerceConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
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<GetProductResult> {
|
||||
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)
|
||||
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!,
|
||||
}
|
||||
return { product }
|
||||
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 {}
|
||||
return getProduct
|
||||
}
|
||||
|
||||
export default getProduct
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
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 { BigcommerceConfig, getConfig } from '..'
|
||||
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 */ `
|
||||
@@ -44,63 +49,39 @@ export const getSiteInfoQuery = /* GraphQL */ `
|
||||
${categoryTreeItemFragment}
|
||||
`
|
||||
|
||||
export type CategoriesTree = NonNullable<
|
||||
GetSiteInfoQuery['site']['categoryTree']
|
||||
>
|
||||
export default function getSiteInfoOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
|
||||
config?: Partial<BigcommerceConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
export type BrandEdge = NonNullable<
|
||||
NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0]
|
||||
>
|
||||
async function getSiteInfo<T extends GetSiteInfoOperation>(
|
||||
opts: {
|
||||
config?: Partial<BigcommerceConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
export type Brands = BrandEdge[]
|
||||
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
|
||||
|
||||
export type GetSiteInfoResult<
|
||||
T extends { categories: any[]; brands: any[] } = {
|
||||
categories: CategoriesTree
|
||||
brands: Brands
|
||||
return {
|
||||
categories: categories ?? [],
|
||||
brands: filterEdges(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>),
|
||||
}
|
||||
return getSiteInfo
|
||||
}
|
||||
|
||||
export default getSiteInfo
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import type { LoginMutation, LoginMutationVariables } from '../../schema'
|
||||
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 { BigcommerceConfig, getConfig } from '..'
|
||||
import type { BigcommerceConfig, Provider } from '..'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
mutation login($email: String!, $password: String!) {
|
||||
@@ -12,62 +17,63 @@ export const loginMutation = /* GraphQL */ `
|
||||
}
|
||||
`
|
||||
|
||||
export type LoginResult<T extends { result?: any } = { result?: string }> = T
|
||||
export default function loginOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function login<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
res: ServerResponse
|
||||
}): Promise<T['data']>
|
||||
|
||||
export type LoginVariables = LoginMutationVariables
|
||||
async function login<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
res: ServerResponse
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function login(opts: {
|
||||
variables: LoginVariables
|
||||
config?: BigcommerceConfig
|
||||
res: ServerResponse
|
||||
}): Promise<LoginResult>
|
||||
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)
|
||||
|
||||
async function login<T extends { result?: any }, V = any>(opts: {
|
||||
query: string
|
||||
variables: V
|
||||
res: ServerResponse
|
||||
config?: BigcommerceConfig
|
||||
}): Promise<LoginResult<T>>
|
||||
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')
|
||||
|
||||
async function login({
|
||||
query = loginMutation,
|
||||
variables,
|
||||
res: response,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables: LoginVariables
|
||||
res: ServerResponse
|
||||
config?: BigcommerceConfig
|
||||
}): Promise<LoginResult> {
|
||||
config = getConfig(config)
|
||||
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')
|
||||
}
|
||||
|
||||
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)!
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader(
|
||||
'Set-Cookie',
|
||||
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
||||
)
|
||||
return {
|
||||
result: data.login?.result,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: data.login?.result,
|
||||
}
|
||||
return login
|
||||
}
|
||||
|
||||
export default 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,38 +1,36 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { GraphQLFetcher } from '@commerce/api'
|
||||
import { getConfig } from '..'
|
||||
import type { BigcommerceConfig } from '../index'
|
||||
import fetch from './fetch'
|
||||
|
||||
const fetchGraphqlApi: GraphQLFetcher = async (
|
||||
query: string,
|
||||
{ variables, preview } = {},
|
||||
fetchOptions
|
||||
) => {
|
||||
// log.warn(query)
|
||||
const config = getConfig()
|
||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||
...fetchOptions,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiToken}`,
|
||||
...fetchOptions?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
if (json.errors) {
|
||||
throw new FetcherError({
|
||||
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
|
||||
status: res.status,
|
||||
const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
|
||||
(getConfig) =>
|
||||
async (query: string, { variables, preview } = {}, fetchOptions) => {
|
||||
// log.warn(query)
|
||||
const config = getConfig()
|
||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||
...fetchOptions,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiToken}`,
|
||||
...fetchOptions?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
if (json.errors) {
|
||||
throw new FetcherError({
|
||||
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
|
||||
status: res.status,
|
||||
})
|
||||
}
|
||||
|
||||
return { data: json.data, res }
|
||||
}
|
||||
|
||||
return { data: json.data, res }
|
||||
}
|
||||
|
||||
export default fetchGraphqlApi
|
||||
|
@@ -1,56 +1,56 @@
|
||||
import type { RequestInit, Response } from '@vercel/fetch'
|
||||
import { getConfig } from '..'
|
||||
import type { BigcommerceConfig } from '../index'
|
||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
||||
import fetch from './fetch'
|
||||
|
||||
export default async function fetchStoreApi<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const config = getConfig()
|
||||
let res: Response
|
||||
const fetchStoreApi =
|
||||
<T>(getConfig: () => BigcommerceConfig) =>
|
||||
async (endpoint: string, options?: RequestInit): Promise<T> => {
|
||||
const config = getConfig()
|
||||
let res: Response
|
||||
|
||||
try {
|
||||
res = await fetch(config.storeApiUrl + endpoint, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': config.storeApiToken,
|
||||
'X-Auth-Client': config.storeApiClientId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new BigcommerceNetworkError(
|
||||
`Fetch to Bigcommerce failed: ${error.message}`
|
||||
)
|
||||
try {
|
||||
res = await fetch(config.storeApiUrl + endpoint, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': config.storeApiToken,
|
||||
'X-Auth-Client': config.storeApiClientId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new BigcommerceNetworkError(
|
||||
`Fetch to Bigcommerce failed: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('Content-Type')
|
||||
const isJSON = contentType?.includes('application/json')
|
||||
|
||||
if (!res.ok) {
|
||||
const data = isJSON ? await res.json() : await getTextOrNull(res)
|
||||
const headers = getRawHeaders(res)
|
||||
const msg = `Big Commerce API error (${
|
||||
res.status
|
||||
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
|
||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
||||
}`
|
||||
|
||||
throw new BigcommerceApiError(msg, res, data)
|
||||
}
|
||||
|
||||
if (res.status !== 204 && !isJSON) {
|
||||
throw new BigcommerceApiError(
|
||||
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
||||
res
|
||||
)
|
||||
}
|
||||
|
||||
// If something was removed, the response will be empty
|
||||
return res.status === 204 ? null : await res.json()
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('Content-Type')
|
||||
const isJSON = contentType?.includes('application/json')
|
||||
|
||||
if (!res.ok) {
|
||||
const data = isJSON ? await res.json() : await getTextOrNull(res)
|
||||
const headers = getRawHeaders(res)
|
||||
const msg = `Big Commerce API error (${
|
||||
res.status
|
||||
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
|
||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
||||
}`
|
||||
|
||||
throw new BigcommerceApiError(msg, res, data)
|
||||
}
|
||||
|
||||
if (res.status !== 204 && !isJSON) {
|
||||
throw new BigcommerceApiError(
|
||||
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
||||
res
|
||||
)
|
||||
}
|
||||
|
||||
// If something was removed, the response will be empty
|
||||
return res.status === 204 ? null : await res.json()
|
||||
}
|
||||
export default fetchStoreApi
|
||||
|
||||
function getRawHeaders(res: Response) {
|
||||
const headers: { [key: string]: string } = {}
|
||||
|
@@ -1,14 +1,28 @@
|
||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||
import type { ItemBody } from '../cart'
|
||||
import type { WishlistItemBody } from '../../types/wishlist'
|
||||
import type { CartItemBody, OptionSelections } from '../../types/cart'
|
||||
|
||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
type BCWishlistItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
}
|
||||
|
||||
type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
item: WishlistItemBody
|
||||
): BCWishlistItemBody => ({
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
})
|
||||
|
||||
export const parseCartItem = (item: ItemBody) => ({
|
||||
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
||||
quantity: item.quantity,
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
option_selections: item.optionSelections
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
option_selections: item.optionSelections,
|
||||
})
|
||||
|
@@ -1,103 +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 '../operations/get-customer-wishlist'
|
||||
import getWishlist from './handlers/get-wishlist'
|
||||
import addItem from './handlers/add-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
|
||||
export type { Wishlist, WishlistItem }
|
||||
|
||||
export type ItemBody = {
|
||||
productId: number
|
||||
variantId: number
|
||||
}
|
||||
|
||||
export type AddItemBody = { item: ItemBody }
|
||||
|
||||
export type RemoveItemBody = { itemId: string }
|
||||
|
||||
export type WishlistBody = {
|
||||
customer_id: number
|
||||
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, {})
|
3
framework/bigcommerce/auth/index.ts
Normal file
3
framework/bigcommerce/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
40
framework/bigcommerce/auth/use-login.tsx
Normal file
40
framework/bigcommerce/auth/use-login.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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 { LoginHook } from '../types/login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LoginHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/login',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
26
framework/bigcommerce/auth/use-logout.tsx
Normal file
26
framework/bigcommerce/auth/use-logout.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
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<LogoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/logout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function logout() {
|
||||
const data = await fetch()
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
44
framework/bigcommerce/auth/use-signup.tsx
Normal file
44
framework/bigcommerce/auth/use-signup.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
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 { SignupHook } from '../types/signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/signup',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({
|
||||
input: { firstName, lastName, email, password },
|
||||
options,
|
||||
fetch,
|
||||
}) {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
4
framework/bigcommerce/cart/index.ts
Normal file
4
framework/bigcommerce/cart/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as useCart } from './use-cart'
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
||||
export { default as useUpdateItem } from './use-update-item'
|
@@ -1,56 +1,44 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCartAddItem from '@commerce/cart/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/cart'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||
import type { AddItemHook } from '@commerce/types/cart'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
})
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Cart, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
) => {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
const data = await fetch({
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = () => {
|
||||
return data
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
const data = await fn({ item: input })
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,13 +0,0 @@
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
import useUpdateItem from './use-update-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useCartActions() {
|
||||
const addItem = useAddItem()
|
||||
const updateItem = useUpdateItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
}
|
@@ -1,50 +1,31 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
||||
import type { Cart } from '../api/cart'
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
||||
import type { GetCartHook } from '@commerce/types/cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
}
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
export type { Cart }
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, CartInput> = (
|
||||
options,
|
||||
{ cartId },
|
||||
fetch
|
||||
) => {
|
||||
return cartId ? fetch({ ...defaultOpts, ...options }) : null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
||||
) {
|
||||
const useCart = () => {
|
||||
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||
})
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
Object.defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return Object.values(response.data?.line_items ?? {}).every(
|
||||
(items) => !items.length
|
||||
)
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useCart.extend = extendHook
|
||||
|
||||
return useCart
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,51 +1,56 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/cart'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
|
||||
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'DELETE',
|
||||
}
|
||||
export type RemoveItemFn<T = any> = T extends LineItem
|
||||
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
|
||||
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string
|
||||
}
|
||||
export type RemoveItemActionInput<T = any> = T extends LineItem
|
||||
? Partial<RemoveItemHook['actionInput']>
|
||||
: RemoveItemHook['actionInput']
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (item?: any) => {
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<RemoveItemHook>) {
|
||||
return await fetch({ ...options, body: { itemId } })
|
||||
},
|
||||
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: { item?: T } = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||
const itemId = input?.id ?? item?.id
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
const data = await fn({ itemId: input.id ?? item?.id })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
if (!itemId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
const data = await fetch({ input: { itemId } })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}
|
||||
|
||||
return useRemoveItem
|
||||
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,70 +1,84 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
||||
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
||||
import { fetcher as removeFetcher } from './use-remove-item'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
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'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
}
|
||||
export type UpdateItemActionInput<T = any> = T extends LineItem
|
||||
? Partial<UpdateItemHook['actionInput']>
|
||||
: UpdateItemHook['actionInput']
|
||||
|
||||
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
|
||||
options,
|
||||
{ itemId, item },
|
||||
fetch
|
||||
) => {
|
||||
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) {
|
||||
return removeFetcher(null, { itemId }, fetch)
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId, item },
|
||||
options,
|
||||
fetch,
|
||||
}: 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) {
|
||||
return removeItemHandler.fetcher({
|
||||
options: removeItemHandler.fetchOptions,
|
||||
input: { itemId },
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
|
||||
return await fetch({
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = (item?: any) => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
},
|
||||
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: {
|
||||
item?: T
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart() as any
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemInput) => {
|
||||
const data = await fn({
|
||||
itemId: input.id ?? item?.id,
|
||||
item: {
|
||||
productId: input.productId ?? item?.product_id,
|
||||
variantId: input.productId ?? item?.variant_id,
|
||||
quantity: input.quantity,
|
||||
debounce(async (input: UpdateItemActionInput<T>) => {
|
||||
const itemId = input.id ?? item?.id
|
||||
const productId = input.productId ?? item?.productId
|
||||
const variantId = input.productId ?? item?.variantId
|
||||
|
||||
if (!itemId || !productId || !variantId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch({
|
||||
input: {
|
||||
itemId,
|
||||
item: { productId, variantId, quantity: input.quantity },
|
||||
},
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}, cfg?.wait ?? 500),
|
||||
[fn, mutate]
|
||||
}, ctx.wait ?? 500),
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useUpdateItem.extend = extendHook
|
||||
|
||||
return useUpdateItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
6
framework/bigcommerce/commerce.config.json
Normal file
6
framework/bigcommerce/commerce.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"provider": "bigcommerce",
|
||||
"features": {
|
||||
"wishlist": true
|
||||
}
|
||||
}
|
1
framework/bigcommerce/customer/index.ts
Normal file
1
framework/bigcommerce/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
24
framework/bigcommerce/customer/use-customer.tsx
Normal file
24
framework/bigcommerce/customer/use-customer.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||
import type { CustomerHook } from '../types/customer'
|
||||
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export const handler: SWRHook<CustomerHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
41
framework/bigcommerce/fetcher.ts
Normal file
41
framework/bigcommerce/fetcher.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { Fetcher } from '@commerce/utils/types'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
|
||||
const fetcher: Fetcher = async ({
|
||||
url,
|
||||
method = 'GET',
|
||||
variables,
|
||||
body: bodyObj,
|
||||
}) => {
|
||||
const hasBody = Boolean(variables || bodyObj)
|
||||
const body = hasBody
|
||||
? JSON.stringify(variables ? { variables } : bodyObj)
|
||||
: undefined
|
||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||
const res = await fetch(url!, { method, body, headers })
|
||||
|
||||
if (res.ok) {
|
||||
const { data } = await res.json()
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getError(res)
|
||||
}
|
||||
|
||||
export default fetcher
|
@@ -1,46 +1,18 @@
|
||||
import { ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import { bigcommerceProvider } from './provider'
|
||||
import type { BigcommerceProvider } from './provider'
|
||||
|
||||
async function getText(res: Response) {
|
||||
try {
|
||||
return (await res.text()) || res.statusText
|
||||
} catch (error) {
|
||||
return res.statusText
|
||||
}
|
||||
}
|
||||
|
||||
async function getError(res: Response) {
|
||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||
const data = await res.json()
|
||||
return new FetcherError({ errors: data.errors, status: res.status })
|
||||
}
|
||||
return new FetcherError({ message: await getText(res), status: res.status })
|
||||
}
|
||||
export { bigcommerceProvider }
|
||||
export type { BigcommerceProvider }
|
||||
|
||||
export const bigcommerceConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
async fetcher({ url, method = 'GET', variables, body: bodyObj }) {
|
||||
const hasBody = Boolean(variables || bodyObj)
|
||||
const body = hasBody
|
||||
? JSON.stringify(variables ? { variables } : bodyObj)
|
||||
: undefined
|
||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
||||
const res = await fetch(url!, { method, body, headers })
|
||||
|
||||
if (res.ok) {
|
||||
const { data } = await res.json()
|
||||
return data
|
||||
}
|
||||
|
||||
throw await getError(res)
|
||||
},
|
||||
}
|
||||
|
||||
export type BigcommerceConfig = Partial<CommerceConfig>
|
||||
@@ -52,10 +24,13 @@ export type BigcommerceProps = {
|
||||
|
||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||
return (
|
||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
||||
<CoreCommerceProvider
|
||||
provider={bigcommerceProvider}
|
||||
config={{ ...bigcommerceConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||
|
5
framework/bigcommerce/lib/get-slug.ts
Normal file
5
framework/bigcommerce/lib/get-slug.ts
Normal file
@@ -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
|
13
framework/bigcommerce/lib/immutability.ts
Normal file
13
framework/bigcommerce/lib/immutability.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import update, { Context } from 'immutability-helper'
|
||||
|
||||
const c = new Context()
|
||||
|
||||
c.extend('$auto', function (value, object) {
|
||||
return object ? c.update(object, value) : c.update({}, value)
|
||||
})
|
||||
|
||||
c.extend('$autoArray', function (value, object) {
|
||||
return object ? c.update(object, value) : c.update([], value)
|
||||
})
|
||||
|
||||
export default c.update
|
136
framework/bigcommerce/lib/normalize.ts
Normal file
136
framework/bigcommerce/lib/normalize.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
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 {
|
||||
node: {
|
||||
entityId,
|
||||
values: { edges },
|
||||
...rest
|
||||
},
|
||||
} = productOption
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
values: edges?.map(({ node }: any) => node),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProduct(productNode: any): Product {
|
||||
const {
|
||||
entityId: id,
|
||||
productOptions,
|
||||
prices,
|
||||
path,
|
||||
id: _,
|
||||
options: _0,
|
||||
} = productNode
|
||||
|
||||
return update(productNode, {
|
||||
id: { $set: String(id) },
|
||||
images: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
url: urlOriginal,
|
||||
alt: altText,
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
variants: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||
id: entityId,
|
||||
options: productOptions?.edges
|
||||
? productOptions.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
$set: productOptions.edges
|
||||
? productOptions?.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
},
|
||||
brand: {
|
||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
||||
},
|
||||
slug: {
|
||||
$set: path?.replace(/^\/+|\/+$/g, ''),
|
||||
},
|
||||
price: {
|
||||
$set: {
|
||||
value: prices?.price.value,
|
||||
currencyCode: prices?.price.currencyCode,
|
||||
},
|
||||
},
|
||||
$unset: ['entityId'],
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
customerId: String(data.customer_id),
|
||||
email: data.email,
|
||||
createdAt: data.created_time,
|
||||
currency: data.currency,
|
||||
taxesIncluded: data.tax_included,
|
||||
lineItems: data.line_items.physical_items.map(normalizeLineItem),
|
||||
lineItemsSubtotalPrice: data.base_amount,
|
||||
subtotalPrice: data.base_amount + data.discount_amount,
|
||||
totalPrice: data.cart_amount,
|
||||
discounts: data.discounts?.map((discount) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLineItem(item: any): LineItem {
|
||||
return {
|
||||
id: item.id,
|
||||
variantId: String(item.variant_id),
|
||||
productId: String(item.product_id),
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
variant: {
|
||||
id: String(item.variant_id),
|
||||
sku: item.sku,
|
||||
name: item.name,
|
||||
image: {
|
||||
url: item.image_url,
|
||||
},
|
||||
requiresShipping: item.is_require_shipping,
|
||||
price: item.sale_price,
|
||||
listPrice: item.list_price,
|
||||
},
|
||||
path: item.url.split('/')[3],
|
||||
discounts: item.discounts.map((discount: any) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCategory(category: BCCategory): Category {
|
||||
return {
|
||||
id: `${category.entityId}`,
|
||||
name: category.name,
|
||||
slug: getSlug(category.path),
|
||||
path: category.path,
|
||||
}
|
||||
}
|
8
framework/bigcommerce/next.config.js
Normal file
8
framework/bigcommerce/next.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: ['cdn11.bigcommerce.com'],
|
||||
},
|
||||
}
|
2
framework/bigcommerce/product/index.ts
Normal file
2
framework/bigcommerce/product/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as usePrice } from './use-price'
|
||||
export { default as useSearch } from './use-search'
|
2
framework/bigcommerce/product/use-price.tsx
Normal file
2
framework/bigcommerce/product/use-price.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from '@commerce/product/use-price'
|
||||
export { default } from '@commerce/product/use-price'
|
50
framework/bigcommerce/product/use-search.tsx
Normal file
50
framework/bigcommerce/product/use-search.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
||||
import type { SearchProductsHook } from '../types/product'
|
||||
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number | string
|
||||
brandId?: number
|
||||
sort?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('categoryId', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brandId', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) => (input = {}) => {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceSearch from '@commerce/products/use-search'
|
||||
import type { SearchProductsData } from '../api/catalog/products'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
|
||||
options,
|
||||
{ search, categoryId, brandId, sort },
|
||||
fetch
|
||||
) => {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options?.method ?? defaultOpts.method,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
|
||||
) {
|
||||
const useSearch = (input: SearchProductsInput = {}) => {
|
||||
const response = useCommerceSearch(
|
||||
defaultOpts,
|
||||
[
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
customFetcher,
|
||||
{ revalidateOnFocus: false, ...swrOptions }
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useSearch.extend = extendHook
|
||||
|
||||
return useSearch
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
34
framework/bigcommerce/provider.ts
Normal file
34
framework/bigcommerce/provider.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { handler as useCart } from './cart/use-cart'
|
||||
import { handler as useAddItem } from './cart/use-add-item'
|
||||
import { handler as useUpdateItem } from './cart/use-update-item'
|
||||
import { handler as useRemoveItem } from './cart/use-remove-item'
|
||||
|
||||
import { handler as useWishlist } from './wishlist/use-wishlist'
|
||||
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
|
||||
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
|
||||
|
||||
import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useSearch } from './product/use-search'
|
||||
|
||||
import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogout } from './auth/use-logout'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
|
||||
import fetcher from './fetcher'
|
||||
|
||||
export const bigcommerceProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
wishlist: {
|
||||
useWishlist,
|
||||
useAddItem: useWishlistAddItem,
|
||||
useRemoveItem: useWishlistRemoveItem,
|
||||
},
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
}
|
||||
|
||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
66
framework/bigcommerce/types/cart.ts
Normal file
66
framework/bigcommerce/types/cart.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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
|
||||
export type BigcommerceCart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
created_time: string
|
||||
discounts?: { id: number; discounted_amount: number }[]
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend core cart types
|
||||
*/
|
||||
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: Core.LineItem[]
|
||||
}
|
||||
|
||||
export type OptionSelections = {
|
||||
option_id: number
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export type CartTypes = {
|
||||
cart: Cart
|
||||
item: Core.LineItem
|
||||
itemBody: CartItemBody
|
||||
}
|
||||
|
||||
export type CartHooks = Core.CartHooks<CartTypes>
|
||||
|
||||
export type GetCartHook = CartHooks['getCart']
|
||||
export type AddItemHook = CartHooks['addItem']
|
||||
export type UpdateItemHook = CartHooks['updateItem']
|
||||
export type RemoveItemHook = CartHooks['removeItem']
|
||||
|
||||
export type CartSchema = Core.CartSchema<CartTypes>
|
||||
|
||||
export type CartHandlers = Core.CartHandlers<CartTypes>
|
||||
|
||||
export type GetCartHandler = CartHandlers['getCart']
|
||||
export type AddItemHandler = CartHandlers['addItem']
|
||||
export type UpdateItemHandler = CartHandlers['updateItem']
|
||||
export type RemoveItemHandler = CartHandlers['removeItem']
|
1
framework/bigcommerce/types/checkout.ts
Normal file
1
framework/bigcommerce/types/checkout.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@commerce/types/checkout'
|
1
framework/bigcommerce/types/common.ts
Normal file
1
framework/bigcommerce/types/common.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@commerce/types/common'
|
5
framework/bigcommerce/types/customer.ts
Normal file
5
framework/bigcommerce/types/customer.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as Core from '@commerce/types/customer'
|
||||
|
||||
export * from '@commerce/types/customer'
|
||||
|
||||
export type CustomerSchema = Core.CustomerSchema
|
25
framework/bigcommerce/types/index.ts
Normal file
25
framework/bigcommerce/types/index.ts
Normal file
@@ -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,
|
||||
}
|
8
framework/bigcommerce/types/login.ts
Normal file
8
framework/bigcommerce/types/login.ts
Normal file
@@ -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
|
||||
}
|
1
framework/bigcommerce/types/logout.ts
Normal file
1
framework/bigcommerce/types/logout.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@commerce/types/logout'
|
11
framework/bigcommerce/types/page.ts
Normal file
11
framework/bigcommerce/types/page.ts
Normal file
@@ -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>
|
1
framework/bigcommerce/types/product.ts
Normal file
1
framework/bigcommerce/types/product.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@commerce/types/product'
|
1
framework/bigcommerce/types/signup.ts
Normal file
1
framework/bigcommerce/types/signup.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@commerce/types/signup'
|
19
framework/bigcommerce/types/site.ts
Normal file
19
framework/bigcommerce/types/site.ts
Normal file
@@ -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>
|
23
framework/bigcommerce/types/wishlist.ts
Normal file
23
framework/bigcommerce/types/wishlist.ts
Normal file
@@ -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>
|
@@ -1,38 +0,0 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCustomer from '@commerce/use-customer'
|
||||
import type { Customer, CustomerData } from './api/customers'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export type { Customer }
|
||||
|
||||
export const fetcher: HookFetcher<Customer | null> = async (
|
||||
options,
|
||||
_,
|
||||
fetch
|
||||
) => {
|
||||
const data = await fetch<CustomerData | null>({ ...defaultOpts, ...options })
|
||||
return data?.customer ?? null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Customer | null>
|
||||
) {
|
||||
const useCustomer = () => {
|
||||
return useCommerceCustomer(defaultOpts, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
})
|
||||
}
|
||||
|
||||
useCustomer.extend = extendHook
|
||||
|
||||
return useCustomer
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@@ -1,54 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceLogin from '@commerce/use-login'
|
||||
import type { LoginBody } from './api/customers/login'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/login',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type LoginInput = LoginBody
|
||||
|
||||
export const fetcher: HookFetcher<null, LoginBody> = (
|
||||
options,
|
||||
{ email, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogin = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login(input: LoginInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogin.extend = extendHook
|
||||
|
||||
return useLogin
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@@ -1,38 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import useCommerceLogout from '@commerce/use-logout'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/logout',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogout = () => {
|
||||
const { mutate } = useCustomer()
|
||||
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login() {
|
||||
const data = await fn(null)
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogout.extend = extendHook
|
||||
|
||||
return useLogout
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@@ -1,2 +0,0 @@
|
||||
export * from '@commerce/use-price'
|
||||
export { default } from '@commerce/use-price'
|
@@ -1,54 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceSignup from '@commerce/use-signup'
|
||||
import type { SignupBody } from './api/customers/signup'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/signup',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type SignupInput = SignupBody
|
||||
|
||||
export const fetcher: HookFetcher<null, SignupBody> = (
|
||||
options,
|
||||
{ firstName, lastName, email, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useSignup = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function signup(input: SignupInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useSignup.extend = extendHook
|
||||
|
||||
return useSignup
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
3
framework/bigcommerce/wishlist/index.ts
Normal file
3
framework/bigcommerce/wishlist/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useWishlist } from './use-wishlist'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
@@ -1,39 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
|
||||
import type { AddItemHook } from '../types/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
) => {
|
||||
// TODO: add validations before doing the fetch
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = (opts?: UseWishlistOptions) => {
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/wishlist',
|
||||
method: 'POST',
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||
const { revalidate } = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
async function addItem(item) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
@@ -41,17 +26,12 @@ export function extendHook(customFetcher: typeof fetcher) {
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({ item: input })
|
||||
// TODO: add validations before doing the fetch
|
||||
const data = await fetch({ input: { item } })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn, revalidate, customer]
|
||||
[fetch, revalidate, customer]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,43 +1,26 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useRemoveItem, {
|
||||
UseRemoveItem,
|
||||
} from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemHook } from '../types/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'DELETE',
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
||||
export const handler: MutationHook<RemoveItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/wishlist',
|
||||
method: 'DELETE',
|
||||
},
|
||||
useHook: ({ fetch }) => ({ wishlist } = {}) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const { revalidate } = useWishlist(wishlist)
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
async function removeItem(input) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
@@ -45,17 +28,11 @@ export function extendHook(customFetcher: typeof fetcher) {
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({ itemId: String(input.id) })
|
||||
const data = await fetch({ input: { itemId: String(input.id) } })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn, revalidate, customer]
|
||||
[fetch, revalidate, customer]
|
||||
)
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
|
||||
return useRemoveItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useWishlistActions() {
|
||||
const addItem = useAddItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, removeItem }
|
||||
}
|
@@ -1,76 +1,53 @@
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
|
||||
import type { Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||
import type { GetWishlistHook } from '../types/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
}
|
||||
export default useWishlist as UseWishlist<typeof handler>
|
||||
|
||||
export type { Wishlist }
|
||||
export const handler: SWRHook<GetWishlistHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||
if (!customerId) return null
|
||||
|
||||
export interface UseWishlistOptions {
|
||||
includeProducts?: boolean
|
||||
}
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
export interface UseWishlistInput extends UseWishlistOptions {
|
||||
customerId?: number
|
||||
}
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = (
|
||||
options,
|
||||
{ customerId, includeProducts },
|
||||
fetch
|
||||
) => {
|
||||
if (!customerId) return null
|
||||
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
||||
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options?.method ?? defaultOpts.method,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
|
||||
) {
|
||||
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useCommerceWishlist(
|
||||
defaultOpts,
|
||||
[
|
||||
const response = useData({
|
||||
input: [
|
||||
['customerId', customer?.entityId],
|
||||
['includeProducts', includeProducts],
|
||||
['includeProducts', input?.includeProducts],
|
||||
],
|
||||
customFetcher,
|
||||
{
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
}
|
||||
)
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
Object.defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
...input?.swrOptions,
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useWishlist.extend = extendHook
|
||||
|
||||
return useWishlist
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
334
framework/commerce/README.md
Normal file
334
framework/commerce/README.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Commerce Framework
|
||||
|
||||
- [Commerce Framework](#commerce-framework)
|
||||
- [Commerce Hooks](#commerce-hooks)
|
||||
- [CommerceProvider](#commerceprovider)
|
||||
- [Authentication Hooks](#authentication-hooks)
|
||||
- [useSignup](#usesignup)
|
||||
- [useLogin](#uselogin)
|
||||
- [useLogout](#uselogout)
|
||||
- [Customer Hooks](#customer-hooks)
|
||||
- [useCustomer](#usecustomer)
|
||||
- [Product Hooks](#product-hooks)
|
||||
- [usePrice](#useprice)
|
||||
- [useSearch](#usesearch)
|
||||
- [Cart Hooks](#cart-hooks)
|
||||
- [useCart](#usecart)
|
||||
- [useAddItem](#useadditem)
|
||||
- [useUpdateItem](#useupdateitem)
|
||||
- [useRemoveItem](#useremoveitem)
|
||||
- [Wishlist Hooks](#wishlist-hooks)
|
||||
- [Commerce API](#commerce-api)
|
||||
- [More](#more)
|
||||
|
||||
The commerce framework ships multiple hooks and a Node.js API, both using an underlying headless e-commerce platform, which we call commerce providers.
|
||||
|
||||
The core features are:
|
||||
|
||||
- Code splitted hooks for data fetching using [SWR](https://swr.vercel.app/), and to handle common user actions
|
||||
- A Node.js API for initial data population, static generation of content and for creating the API endpoints that connect to the hooks, if required.
|
||||
|
||||
> 👩🔬 If you would like to contribute a new provider, check the docs for [Adding a new Commerce Provider](./new-provider.md).
|
||||
|
||||
> 🚧 The core commerce framework is under active development, new features and updates will be continuously added over time. Breaking changes are expected while we finish the API.
|
||||
|
||||
## Commerce Hooks
|
||||
|
||||
A commerce hook is a [React hook](https://reactjs.org/docs/hooks-intro.html) that's connected to a commerce provider. They focus on user actions and data fetching of data that wasn't statically generated.
|
||||
|
||||
Data fetching hooks use [SWR](https://swr.vercel.app/) underneath and you're welcome to use any of its [return values](https://swr.vercel.app/docs/options#return-values) and [options](https://swr.vercel.app/docs/options#options). For example, using the `useCustomer` hook:
|
||||
|
||||
```jsx
|
||||
const { data, isLoading, error } = useCustomer({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### CommerceProvider
|
||||
|
||||
This component adds the provider config and handlers to the context of your React tree for it's children. You can optionally pass the `locale` to it:
|
||||
|
||||
```jsx
|
||||
import { CommerceProvider } from '@framework'
|
||||
|
||||
const App = ({ locale = 'en-US', children }) => {
|
||||
return <CommerceProvider locale={locale}>{children}</CommerceProvider>
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication Hooks
|
||||
|
||||
### useSignup
|
||||
|
||||
Returns a _signup_ function that can be used to sign up the current visitor:
|
||||
|
||||
```jsx
|
||||
import useSignup from '@framework/auth/use-signup'
|
||||
|
||||
const SignupView = () => {
|
||||
const signup = useSignup()
|
||||
|
||||
const handleSignup = async () => {
|
||||
await signup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
return <form onSubmit={handleSignup}>{children}</form>
|
||||
}
|
||||
```
|
||||
|
||||
### useLogin
|
||||
|
||||
Returns a _login_ function that can be used to sign in the current visitor into an existing customer:
|
||||
|
||||
```jsx
|
||||
import useLogin from '@framework/auth/use-login'
|
||||
|
||||
const LoginView = () => {
|
||||
const login = useLogin()
|
||||
const handleLogin = async () => {
|
||||
await login({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
return <form onSubmit={handleLogin}>{children}</form>
|
||||
}
|
||||
```
|
||||
|
||||
### useLogout
|
||||
|
||||
Returns a _logout_ function that signs out the current customer when called.
|
||||
|
||||
```jsx
|
||||
import useLogout from '@framework/auth/use-logout'
|
||||
|
||||
const LogoutButton = () => {
|
||||
const logout = useLogout()
|
||||
return (
|
||||
<button type="button" onClick={() => logout()}>
|
||||
Logout
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Customer Hooks
|
||||
|
||||
### useCustomer
|
||||
|
||||
Fetches and returns the data of the signed in customer:
|
||||
|
||||
```jsx
|
||||
import useCustomer from '@framework/customer/use-customer'
|
||||
|
||||
const Profile = () => {
|
||||
const { data, isLoading, error } = useCustomer()
|
||||
|
||||
if (isLoading) return <p>Loading...</p>
|
||||
if (error) return <p>{error.message}</p>
|
||||
if (!data) return null
|
||||
|
||||
return <div>Hello, {data.firstName}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Product Hooks
|
||||
|
||||
### usePrice
|
||||
|
||||
Helper hook to format price according to the commerce locale and currency code. It also handles discounts:
|
||||
|
||||
```jsx
|
||||
import useCart from '@framework/cart/use-cart'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
|
||||
// ...
|
||||
const { data } = useCart()
|
||||
const { price, discount, basePrice } = usePrice(
|
||||
data && {
|
||||
amount: data.subtotalPrice,
|
||||
currencyCode: data.currency.code,
|
||||
// If `baseAmount` is used, a discount will be calculated
|
||||
// baseAmount: number,
|
||||
}
|
||||
)
|
||||
// ...
|
||||
```
|
||||
|
||||
### useSearch
|
||||
|
||||
Fetches and returns the products that match a set of filters:
|
||||
|
||||
```jsx
|
||||
import useSearch from '@framework/product/use-search'
|
||||
|
||||
const SearchPage = ({ searchString, category, brand, sortStr }) => {
|
||||
const { data } = useSearch({
|
||||
search: searchString || '',
|
||||
categoryId: category?.entityId,
|
||||
brandId: brand?.entityId,
|
||||
sort: sortStr,
|
||||
})
|
||||
|
||||
return (
|
||||
<Grid layout="normal">
|
||||
{data.products.map((product) => (
|
||||
<ProductCard key={product.path} product={product} />
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Cart Hooks
|
||||
|
||||
### useCart
|
||||
|
||||
Fetches and returns the data of the current cart:
|
||||
|
||||
```jsx
|
||||
import useCart from '@framework/cart/use-cart'
|
||||
|
||||
const CartTotal = () => {
|
||||
const { data, isLoading, isEmpty, error } = useCart()
|
||||
|
||||
if (isLoading) return <p>Loading...</p>
|
||||
if (error) return <p>{error.message}</p>
|
||||
if (isEmpty) return <p>The cart is empty</p>
|
||||
|
||||
return <p>The cart total is {data.totalPrice}</p>
|
||||
}
|
||||
```
|
||||
|
||||
### useAddItem
|
||||
|
||||
Returns a function that adds a new item to the cart when called, if this is the first item it will create the cart:
|
||||
|
||||
```jsx
|
||||
import { useAddItem } from '@framework/cart'
|
||||
|
||||
const AddToCartButton = ({ productId, variantId }) => {
|
||||
const addItem = useAddItem()
|
||||
|
||||
const addToCart = async () => {
|
||||
await addItem({
|
||||
productId,
|
||||
variantId,
|
||||
})
|
||||
}
|
||||
|
||||
return <button onClick={addToCart}>Add To Cart</button>
|
||||
}
|
||||
```
|
||||
|
||||
### useUpdateItem
|
||||
|
||||
Returns a function that updates a current item in the cart when called, usually the quantity.
|
||||
|
||||
```jsx
|
||||
import { useUpdateItem } from '@framework/cart'
|
||||
|
||||
const CartItemQuantity = ({ item }) => {
|
||||
const [quantity, setQuantity] = useState(item.quantity)
|
||||
const updateItem = useUpdateItem({ item })
|
||||
|
||||
const updateQuantity = async (e) => {
|
||||
const val = e.target.value
|
||||
|
||||
setQuantity(val)
|
||||
await updateItem({ quantity: val })
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
max={99}
|
||||
min={0}
|
||||
value={quantity}
|
||||
onChange={updateQuantity}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
If the `quantity` is lower than 1 the item will be removed from the cart.
|
||||
|
||||
### useRemoveItem
|
||||
|
||||
Returns a function that removes an item in the cart when called:
|
||||
|
||||
```jsx
|
||||
import { useRemoveItem } from '@framework/cart'
|
||||
|
||||
const RemoveButton = ({ item }) => {
|
||||
const removeItem = useRemoveItem()
|
||||
const handleRemove = async () => {
|
||||
await removeItem(item)
|
||||
}
|
||||
|
||||
return <button onClick={handleRemove}>Remove</button>
|
||||
}
|
||||
```
|
||||
|
||||
## Wishlist Hooks
|
||||
|
||||
Wishlist hooks work just like [cart hooks](#cart-hooks). Feel free to check how those work first.
|
||||
|
||||
The example below shows how to use the `useWishlist`, `useAddItem` and `useRemoveItem` hooks:
|
||||
|
||||
```jsx
|
||||
import { useWishlist, useAddItem, useRemoveItem } from '@framework/wishlist'
|
||||
|
||||
const WishlistButton = ({ productId, variant }) => {
|
||||
const addItem = useAddItem()
|
||||
const removeItem = useRemoveItem()
|
||||
const { data, isLoading, isEmpty, error } = useWishlist()
|
||||
|
||||
if (isLoading) return <p>Loading...</p>
|
||||
if (error) return <p>{error.message}</p>
|
||||
if (isEmpty) return <p>The wihslist is empty</p>
|
||||
|
||||
const { data: customer } = useCustomer()
|
||||
const itemInWishlist = data?.items?.find(
|
||||
(item) => item.product_id === productId && item.variant_id === variant.id
|
||||
)
|
||||
|
||||
const handleWishlistChange = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!customer) return
|
||||
|
||||
if (itemInWishlist) {
|
||||
await removeItem({ id: itemInWishlist.id })
|
||||
} else {
|
||||
await addItem({
|
||||
productId,
|
||||
variantId: variant.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleWishlistChange}>
|
||||
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Commerce API
|
||||
|
||||
While commerce hooks focus on client side data fetching and interactions, the commerce API focuses on static content generation for pages and API endpoints in a Node.js context.
|
||||
|
||||
> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released.
|
||||
|
||||
## More
|
||||
|
||||
Feel free to read through the source for more usage, and check the commerce vercel demo and commerce repo for usage examples: ([demo.vercel.store](https://demo.vercel.store/)) ([repo](https://github.com/vercel/commerce))
|
62
framework/commerce/api/endpoints/cart.ts
Normal file
62
framework/commerce/api/endpoints/cart.ts
Normal file
@@ -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
|
31
framework/commerce/api/endpoints/catalog/products.ts
Normal file
31
framework/commerce/api/endpoints/catalog/products.ts
Normal file
@@ -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
|
35
framework/commerce/api/endpoints/checkout.ts
Normal file
35
framework/commerce/api/endpoints/checkout.ts
Normal file
@@ -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
|
35
framework/commerce/api/endpoints/customer.ts
Normal file
35
framework/commerce/api/endpoints/customer.ts
Normal file
@@ -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
|
35
framework/commerce/api/endpoints/login.ts
Normal file
35
framework/commerce/api/endpoints/login.ts
Normal file
@@ -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
|
37
framework/commerce/api/endpoints/logout.ts
Normal file
37
framework/commerce/api/endpoints/logout.ts
Normal file
@@ -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
|
38
framework/commerce/api/endpoints/signup.ts
Normal file
38
framework/commerce/api/endpoints/signup.ts
Normal file
@@ -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
|
58
framework/commerce/api/endpoints/wishlist.ts
Normal file
58
framework/commerce/api/endpoints/wishlist.ts
Normal file
@@ -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
Reference in New Issue
Block a user