├── README.md ├── example ├── account │ └── use-customer.tsx ├── api │ ├── cart │ │ ├── handlers │ │ │ ├── add-item.ts │ │ │ ├── get-cart.ts │ │ │ ├── remove-item.ts │ │ │ └── update-item.ts │ │ └── index.ts │ ├── catalog │ │ ├── handlers │ │ │ └── get-products.ts │ │ └── products.ts │ ├── checkout.ts │ ├── customers │ │ ├── handlers │ │ │ ├── get-logged-in-customer.ts │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ └── signup.ts │ │ ├── index.ts │ │ ├── login.ts │ │ ├── logout.ts │ │ └── signup.ts │ ├── definitions │ │ ├── catalog.ts │ │ ├── store-content.ts │ │ └── wishlist.ts │ ├── fragments │ │ ├── category-tree.ts │ │ └── product.ts │ ├── index.ts │ ├── operations │ │ ├── get-all-pages.ts │ │ ├── get-all-product-paths.ts │ │ ├── get-all-products.ts │ │ ├── get-customer-id.ts │ │ ├── get-customer-wishlist.ts │ │ ├── get-page.ts │ │ ├── get-product.ts │ │ ├── get-site-info.ts │ │ └── login.ts │ ├── utils │ │ ├── concat-cookie.ts │ │ ├── create-api-handler.ts │ │ ├── errors.ts │ │ ├── fetch-graphql-api.ts │ │ ├── fetch-store-api.ts │ │ ├── fetch.ts │ │ ├── filter-edges.ts │ │ ├── get-cart-cookie.ts │ │ ├── is-allowed-method.ts │ │ ├── parse-item.ts │ │ ├── set-product-locale-meta.ts │ │ └── types.ts │ └── wishlist │ │ ├── handlers │ │ ├── add-item.ts │ │ ├── get-wishlist.ts │ │ └── remove-item.ts │ │ └── index.ts ├── auth │ ├── use-login.tsx │ ├── use-logout.tsx │ └── use-signup.tsx ├── cart │ ├── use-add-item.tsx │ ├── use-cart-actions.tsx │ ├── use-cart.tsx │ ├── use-remove-item.tsx │ └── use-update-item.tsx ├── index.tsx ├── products │ ├── use-price.tsx │ └── use-search.tsx ├── schema.d.ts ├── schema.graphql ├── scripts │ └── generate-definitions.js ├── src │ ├── api │ │ └── index.ts │ ├── cart │ │ ├── use-add-item.tsx │ │ ├── use-cart-actions.tsx │ │ ├── use-cart.tsx │ │ ├── use-remove-item.tsx │ │ └── use-update-item.tsx │ ├── index.tsx │ ├── products │ │ └── use-search.tsx │ ├── use-customer.tsx │ ├── use-login.tsx │ ├── use-logout.tsx │ ├── use-price.tsx │ ├── use-signup.tsx │ ├── utils │ │ ├── errors.ts │ │ ├── types.ts │ │ ├── use-action.tsx │ │ └── use-data.tsx │ └── wishlist │ │ ├── use-add-item.tsx │ │ ├── use-remove-item.tsx │ │ └── use-wishlist.tsx └── wishlist │ ├── use-add-item.tsx │ ├── use-remove-item.tsx │ ├── use-wishlist-actions.tsx │ └── use-wishlist.tsx ├── package.json ├── src └── commerce │ ├── api │ └── index.ts │ ├── cart │ ├── use-add-item.tsx │ ├── use-cart-actions.tsx │ ├── use-cart.tsx │ ├── use-remove-item.tsx │ └── use-update-item.tsx │ ├── index.tsx │ ├── products │ └── use-search.tsx │ ├── use-customer.tsx │ ├── use-login.tsx │ ├── use-logout.tsx │ ├── use-price.tsx │ ├── use-signup.tsx │ ├── utils │ ├── errors.ts │ ├── types.ts │ ├── use-action.tsx │ └── use-data.tsx │ └── wishlist │ ├── use-add-item.tsx │ ├── use-remove-item.tsx │ └── use-wishlist.tsx └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Commerce Framework 2 | 3 | This repository serves as a model to build your own hooks for [Next.js Commerce](https://nextjs.org/commerce). We encourage contributors and teams to build their own hooks under a separate package with the corresponding configuration. An example of using this repository as a seed are the hooks we built with [BigCommerce](https://github.com/bigcommerce/storefront-data-hooks). 4 | 5 | ## Folder Structure 6 | 7 | The root folder contains an `index.tsx` file with the CommerceProvider to set up the right commerce context. Additional components needed to run the framework need to be there. 8 | 9 | `src` contains all the handlers used by the whole framework. The rest of the folders in the root file are named by functionality, e.g cart, auth, wishlist and are the API of the framework. Therefore, most of the work should be done in the `src` folder. Otherwise, it could affect the usage of the framework altering its functionality and possibly adding unexpected output. 10 | 11 | ## Commerce API 12 | 13 | ### CommerceProvider 14 | 15 | The main configuration provider that creates the Commerce context. Used by all Commerce hooks. Every provider may define its own defaults. 16 | 17 | ```jsx 18 | import { CommerceProvider } from "@commerce-framework"; 19 | 20 | const App = ({ locale = "en-US", children }) => ( 21 | {children} 22 | ); 23 | ``` 24 | 25 | - `locale:` Locale to use for i18n. This is **required**. 26 | - `cartCookie:` Name of the cookie that saves the cart id. This is **optional**. 27 | - `fetcher:` Global fetcher function that will be used for every API call to a GraphQL or REST endpoint in the browser, it's in charge of error management on network errors. Usage of this config is **optional**. 28 | 29 | > **CommerceProvider** should not re-render unless a prop changes, and the only prop that's expected to change in most apps is `locale`. 30 | 31 | ### useCommerce() 32 | 33 | Returns the configs (including defaults) that are defined in the nearest `CommerceProvider`. 34 | 35 | ```jsx 36 | import { useCommerce } from "@commerce-framework/hooks"; 37 | 38 | const commerce = useCommerce(); 39 | ``` 40 | 41 | May be useful if you want to create another `CommerceProvider` and extend its options. 42 | 43 | ### useCustomer 44 | 45 | Returns the logged-in customer and its data. 46 | 47 | ```jsx 48 | import { useCustomer } from "@commerce-framework/hooks"; 49 | 50 | const customer = useCustomer(); 51 | const { data, error, loading } = customer; 52 | 53 | if (error) return

There was an error: {error.message}

; 54 | if (loading) return

Loading...

; 55 | 56 | return ; 57 | ``` 58 | 59 | The customer is revalidated by operations executed by `useLogin`, `useSignup` and `useLogout`. 60 | 61 | ### useLogin Hook 62 | 63 | Returns a function that allows a visitor to login into its customer account. 64 | 65 | ```jsx 66 | import { useLogin } from "@commerce-framework/hooks"; 67 | 68 | const LoginView = () => { 69 | const login = useLogin(); 70 | 71 | const handleLogin = async () => { 72 | await login({ 73 | email, 74 | password, 75 | }); 76 | }; 77 | 78 | return
{children}
; 79 | }; 80 | ``` 81 | 82 | ### useLogout 83 | 84 | Returns a function that allows the current customer to log out. 85 | 86 | ```jsx 87 | import { useLogout } from "@commerce-framework/hooks"; 88 | 89 | const LogoutLink = () => { 90 | const logout = useLogout(); 91 | return logout()}>Logout; 92 | }; 93 | ``` 94 | 95 | ### useSignup 96 | 97 | Returns a function that allows a visitor to sign up into the store. 98 | The signup operation returns `null`. 99 | 100 | ```jsx 101 | import { useSignup } from "@commerce-framework/hooks"; 102 | 103 | const SignupView = () => { 104 | const signup = useSignup(); 105 | 106 | const handleSignup = async () => { 107 | await signup({ 108 | email, 109 | firstName, 110 | lastName, 111 | password, 112 | }); 113 | }; 114 | 115 | return
{children}
; 116 | }; 117 | ``` 118 | 119 | ### usePrice 120 | 121 | Formats an amount--usually the price of a product-- into an internationalized string. It uses the current locale. 122 | 123 | ```jsx 124 | import { usePrice } from "@commerce-framework/hooks"; 125 | 126 | const { price, basePrice, discount } = usePrice({ 127 | amount: 100, 128 | baseAmount: 50, 129 | currencyCode: "USD", 130 | }); 131 | ``` 132 | 133 | `usePrice` receives an object with: 134 | 135 | - `amount:` A valid number, usually the sale price of a product. This is **required**. 136 | - `baseAmount:` A valid number, usually the listed price of a product. If it's higher than `amount` then the product is on sale and `usePrice` will return the discounted percentage. This is **optional**. 137 | - `currencyCode:` The currency code to use. This is **required**. 138 | 139 | `usePrice` returns an object with: 140 | 141 | - `price:` The formatted price of a product. 142 | - `basePrice:` The formatted base price of the product. Only present if `baseAmount` is set. 143 | - `discount:` The discounted percentage. Only present if `baseAmount` is set. 144 | 145 | ## Cart Hooks 146 | 147 | ### useCart 148 | 149 | Returns the current cart data. 150 | 151 | ```jsx 152 | import { useCart } from "@commerce-framework/hooks"; 153 | 154 | const { data, error, isEmpty, loading } = useCart(); 155 | ``` 156 | 157 | ### useAddItem 158 | 159 | Returns a function that when called adds a new item to the current cart. 160 | The `addItem` operation returns the updated cart. 161 | 162 | ```jsx 163 | import { useAddItem } from "@commerce-framework/hooks"; 164 | 165 | const AddToCartButton = ({ productId, variantId }) => { 166 | const addItem = useAddItem(); 167 | 168 | const addToCart = async () => { 169 | await addItem({ 170 | productId, 171 | variantId, 172 | }); 173 | }; 174 | 175 | return ; 176 | }; 177 | ``` 178 | 179 | ### useUpdateItem 180 | 181 | Returns a function to update an item of the current cart. 182 | 183 | ```jsx 184 | const updateItem = useUpdateItem(); 185 | 186 | await updateItem({ 187 | id, 188 | productId, 189 | variantId, 190 | quantity, 191 | }); 192 | 193 | // You can optionally send the item to the hook: 194 | const updateItem = useUpdateItem(item); 195 | 196 | // And now only the quantity is required 197 | await updateItem({ 198 | quantity, 199 | }); 200 | ``` 201 | 202 | ### useRemoveItem 203 | 204 | Returns a function that when called removes an item from the current cart. 205 | 206 | ```jsx 207 | const removeItem = useRemoveItem(); 208 | 209 | await removeItem({ 210 | id, 211 | }); 212 | ``` 213 | 214 | The `removeItem` operation returns the updated cart. 215 | 216 | The cart is deleted if the last remaining item is removed. 217 | 218 | ## Wishlist Hooks 219 | 220 | ### useWishlist 221 | 222 | Returns the current wishlist of the logged in user. 223 | 224 | ```jsx 225 | import { useWishlist } from "@commerce-framework/hooks"; 226 | 227 | const { data } = useWishlist(); 228 | ``` 229 | 230 | ## WIP 231 | 232 | - getItem 233 | - getAllItems 234 | - useSearch 235 | 236 | ## Handling errors and loading states 237 | 238 | Data fetching looks like `useCart` and `useCustomer` return props to handle errors and loading states. For example, using the `useCommerce` hook: 239 | 240 | ```jsx 241 | const customer = useCustomer(); 242 | const { data, error, loading } = customer; 243 | 244 | if (error) return

There was an error: {error.message}

; 245 | if (loading) return

Loading...

; 246 | 247 | return ; 248 | ``` 249 | 250 | And using the `useCart` hook: 251 | 252 | ```jsx 253 | const cart = useCart(); 254 | const { data, error, loading, isEmpty } = cart; 255 | 256 | if (error) return

There was an error: {error.message}

; 257 | if (loading) return

Loading...

; 258 | if (isEmpty) return

The cart is currently empty

; 259 | 260 | return ; 261 | ``` 262 | 263 | Hooks that execute user actions are async operations, you can track the loading state and errors using state hooks and try...catch. For example: 264 | 265 | ```jsx 266 | function LoginButton({ data }) { 267 | const login = useLogin(); 268 | 269 | const [loading, setLoading] = useState(false); 270 | const [errorMsg, setErrorMsg] = useState(false); 271 | const handleClick = async () => { 272 | // We're about to run the action, so set the state to loading 273 | setLoading(true); 274 | // Reset the error message if it failed before 275 | setErrorMsg(""); 276 | 277 | try { 278 | await login({ 279 | email: data.email, 280 | password: data.password, 281 | }); 282 | } catch (error) { 283 | // Handle error codes specific for the `login` action 284 | if (error.code === "invalid_credentials") { 285 | setErrorMsg( 286 | "Cannot find an account that matches the provided credentials" 287 | ); 288 | } else { 289 | setErrorMsg(error.message); 290 | } 291 | } finally { 292 | setLoading(false); 293 | } 294 | }; 295 | 296 | return ( 297 |
298 | 301 | {errorMsg &&

{errorMsg}

} 302 |
303 | ); 304 | } 305 | ``` 306 | 307 | Error codes depend on the hook that is being used. In this case `invalid_credentials` is used by `useLogin`. 308 | 309 | All commerce related errors are an instance of `CommerceError`. 310 | 311 | ## Extending UI Hooks 312 | 313 | All hooks that involve a fetch operation can be extended with a custom fetcher function. For example, if we wanted to extend the `useCart` hook: 314 | 315 | ```jsx 316 | import useCartHook, { fetcher } from "commerce-lib/cart/use-cart"; 317 | 318 | const useCart = useCartHook.extend( 319 | (options, input, fetch) => { 320 | // Do something different 321 | return fetcher(options, input, fetch); 322 | }, 323 | { 324 | // Optionally change the default SWR options 325 | revalidateOnFocus: true, 326 | } 327 | ); 328 | 329 | // Then in your component, using your new hook works in the same way 330 | const { data, error, isEmpty, updating } = useCart(); 331 | ``` 332 | 333 | The `extend` method is available for all hooks with fetch operations, it receives a fetcher function as the first parameter and SWR options as the second parameter if the hook uses SWR. `extend` returns a new hook that works exactly as the hook it's extending from. 334 | 335 | The fetcher function receives the following arguments: 336 | 337 | - `options:` This is an object that may have a `query` (if the hook is using GraphQL), a `url`, and a `method`. 338 | - `input:` An object with the data that's required to execute the operation. 339 | - `fetch:` The fetch function that is set by the `CommerceProvider`. 340 | 341 | 342 | -------------------------------------------------------------------------------- /example /account/use-customer.tsx: -------------------------------------------------------------------------------- 1 | import type { HookFetcher } from '../src/utils/types' 2 | import type { SwrOptions } from '../src/utils/use-data' 3 | import useCommerceCustomer from '../src/use-customer' 4 | import type { Customer, CustomerData } from '../api/customers' 5 | 6 | const defaultOpts = { 7 | url: '/api/bigcommerce/customers', 8 | method: 'GET', 9 | } 10 | 11 | export type { Customer } 12 | 13 | export const fetcher: HookFetcher = async ( 14 | options, 15 | _, 16 | fetch 17 | ) => { 18 | const data = await fetch({ ...defaultOpts, ...options }) 19 | return data?.customer ?? null 20 | } 21 | 22 | export function extendHook( 23 | customFetcher: typeof fetcher, 24 | swrOptions?: SwrOptions 25 | ) { 26 | const useCustomer = () => { 27 | return useCommerceCustomer(defaultOpts, [], customFetcher, { 28 | revalidateOnFocus: false, 29 | ...swrOptions, 30 | }) 31 | } 32 | 33 | useCustomer.extend = extendHook 34 | 35 | return useCustomer 36 | } 37 | 38 | export default extendHook(fetcher) 39 | -------------------------------------------------------------------------------- /example /api/cart/handlers/add-item.ts: -------------------------------------------------------------------------------- 1 | import { parseCartItem } from '../../utils/parse-item' 2 | import getCartCookie from '../../utils/get-cart-cookie' 3 | import type { CartHandlers } from '..' 4 | 5 | // Return current cart info 6 | const addItem: CartHandlers['addItem'] = async ({ 7 | res, 8 | body: { cartId, item }, 9 | config, 10 | }) => { 11 | if (!item) { 12 | return res.status(400).json({ 13 | data: null, 14 | errors: [{ message: 'Missing item' }], 15 | }) 16 | } 17 | if (!item.quantity) item.quantity = 1 18 | 19 | const options = { 20 | method: 'POST', 21 | body: JSON.stringify({ 22 | line_items: [parseCartItem(item)], 23 | ...(!cartId && config.storeChannelId 24 | ? { channel_id: config.storeChannelId } 25 | : {}), 26 | }), 27 | } 28 | const { data } = cartId 29 | ? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options) 30 | : await config.storeApiFetch('/v3/carts', options) 31 | 32 | // Create or update the cart cookie 33 | res.setHeader( 34 | 'Set-Cookie', 35 | getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge) 36 | ) 37 | res.status(200).json({ data }) 38 | } 39 | 40 | export default addItem 41 | -------------------------------------------------------------------------------- /example /api/cart/handlers/get-cart.ts: -------------------------------------------------------------------------------- 1 | import { BigcommerceApiError } from '../../utils/errors' 2 | import getCartCookie from '../../utils/get-cart-cookie' 3 | import type { Cart, CartHandlers } from '..' 4 | 5 | // Return current cart info 6 | const getCart: CartHandlers['getCart'] = async ({ 7 | res, 8 | body: { cartId }, 9 | config, 10 | }) => { 11 | let result: { data?: Cart } = {} 12 | 13 | if (cartId) { 14 | try { 15 | result = await config.storeApiFetch(`/v3/carts/${cartId}`) 16 | } catch (error) { 17 | if (error instanceof BigcommerceApiError && error.status === 404) { 18 | // Remove the cookie if it exists but the cart wasn't found 19 | res.setHeader('Set-Cookie', getCartCookie(config.cartCookie)) 20 | } else { 21 | throw error 22 | } 23 | } 24 | } 25 | 26 | res.status(200).json({ data: result.data ?? null }) 27 | } 28 | 29 | export default getCart 30 | -------------------------------------------------------------------------------- /example /api/cart/handlers/remove-item.ts: -------------------------------------------------------------------------------- 1 | import getCartCookie from '../../utils/get-cart-cookie' 2 | import type { CartHandlers } from '..' 3 | 4 | // Return current cart info 5 | const removeItem: CartHandlers['removeItem'] = async ({ 6 | res, 7 | body: { cartId, itemId }, 8 | config, 9 | }) => { 10 | if (!cartId || !itemId) { 11 | return res.status(400).json({ 12 | data: null, 13 | errors: [{ message: 'Invalid request' }], 14 | }) 15 | } 16 | 17 | const result = await config.storeApiFetch<{ data: any } | null>( 18 | `/v3/carts/${cartId}/items/${itemId}`, 19 | { method: 'DELETE' } 20 | ) 21 | const data = result?.data ?? null 22 | 23 | res.setHeader( 24 | 'Set-Cookie', 25 | data 26 | ? // Update the cart cookie 27 | getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) 28 | : // Remove the cart cookie if the cart was removed (empty items) 29 | getCartCookie(config.cartCookie) 30 | ) 31 | res.status(200).json({ data }) 32 | } 33 | 34 | export default removeItem 35 | -------------------------------------------------------------------------------- /example /api/cart/handlers/update-item.ts: -------------------------------------------------------------------------------- 1 | import { parseCartItem } from '../../utils/parse-item' 2 | import getCartCookie from '../../utils/get-cart-cookie' 3 | import type { CartHandlers } from '..' 4 | 5 | // Return current cart info 6 | const updateItem: CartHandlers['updateItem'] = async ({ 7 | res, 8 | body: { cartId, itemId, item }, 9 | config, 10 | }) => { 11 | if (!cartId || !itemId || !item) { 12 | return res.status(400).json({ 13 | data: null, 14 | errors: [{ message: 'Invalid request' }], 15 | }) 16 | } 17 | 18 | const { data } = await config.storeApiFetch( 19 | `/v3/carts/${cartId}/items/${itemId}`, 20 | { 21 | method: 'PUT', 22 | body: JSON.stringify({ 23 | line_item: parseCartItem(item), 24 | }), 25 | } 26 | ) 27 | 28 | // Update the cart cookie 29 | res.setHeader( 30 | 'Set-Cookie', 31 | getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge) 32 | ) 33 | res.status(200).json({ data }) 34 | } 35 | 36 | export default updateItem 37 | -------------------------------------------------------------------------------- /example /api/cart/index.ts: -------------------------------------------------------------------------------- 1 | import isAllowedMethod from '../utils/is-allowed-method' 2 | import createApiHandler, { 3 | BigcommerceApiHandler, 4 | BigcommerceHandler, 5 | } from '../utils/create-api-handler' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import getCart from './handlers/get-cart' 8 | import addItem from './handlers/add-item' 9 | import updateItem from './handlers/update-item' 10 | import removeItem from './handlers/remove-item' 11 | 12 | export type ItemBody = { 13 | productId: number 14 | variantId: number 15 | quantity?: number 16 | } 17 | 18 | export type AddItemBody = { item: ItemBody } 19 | 20 | export type UpdateItemBody = { itemId: string; item: ItemBody } 21 | 22 | export type RemoveItemBody = { itemId: string } 23 | 24 | // TODO: this type should match: 25 | // https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses 26 | export type Cart = { 27 | id: string 28 | parent_id?: string 29 | customer_id: number 30 | email: string 31 | currency: { code: string } 32 | tax_included: boolean 33 | base_amount: number 34 | discount_amount: number 35 | cart_amount: number 36 | line_items: { 37 | custom_items: any[] 38 | digital_items: any[] 39 | gift_certificates: any[] 40 | physical_items: any[] 41 | } 42 | // TODO: add missing fields 43 | } 44 | 45 | export type CartHandlers = { 46 | getCart: BigcommerceHandler 47 | addItem: BigcommerceHandler> 48 | updateItem: BigcommerceHandler< 49 | Cart, 50 | { cartId?: string } & Partial 51 | > 52 | removeItem: BigcommerceHandler< 53 | Cart, 54 | { cartId?: string } & Partial 55 | > 56 | } 57 | 58 | const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] 59 | 60 | // TODO: a complete implementation should have schema validation for `req.body` 61 | const cartApi: BigcommerceApiHandler = async ( 62 | req, 63 | res, 64 | config, 65 | handlers 66 | ) => { 67 | if (!isAllowedMethod(req, res, METHODS)) return 68 | 69 | const { cookies } = req 70 | const cartId = cookies[config.cartCookie] 71 | 72 | try { 73 | // Return current cart info 74 | if (req.method === 'GET') { 75 | const body = { cartId } 76 | return await handlers['getCart']({ req, res, config, body }) 77 | } 78 | 79 | // Create or add an item to the cart 80 | if (req.method === 'POST') { 81 | const body = { ...req.body, cartId } 82 | return await handlers['addItem']({ req, res, config, body }) 83 | } 84 | 85 | // Update item in cart 86 | if (req.method === 'PUT') { 87 | const body = { ...req.body, cartId } 88 | return await handlers['updateItem']({ req, res, config, body }) 89 | } 90 | 91 | // Remove an item from the cart 92 | if (req.method === 'DELETE') { 93 | const body = { ...req.body, cartId } 94 | return await handlers['removeItem']({ req, res, config, body }) 95 | } 96 | } catch (error) { 97 | console.error(error) 98 | 99 | const message = 100 | error instanceof BigcommerceApiError 101 | ? 'An unexpected error ocurred with the Bigcommerce API' 102 | : 'An unexpected error ocurred' 103 | 104 | res.status(500).json({ data: null, errors: [{ message }] }) 105 | } 106 | } 107 | 108 | export const handlers = { getCart, addItem, updateItem, removeItem } 109 | 110 | export default createApiHandler(cartApi, handlers, {}) 111 | -------------------------------------------------------------------------------- /example /api/catalog/handlers/get-products.ts: -------------------------------------------------------------------------------- 1 | import getAllProducts, { ProductEdge } from '../../operations/get-all-products' 2 | import type { ProductsHandlers } from '../products' 3 | 4 | const SORT: { [key: string]: string | undefined } = { 5 | latest: 'id', 6 | trending: 'total_sold', 7 | price: 'price', 8 | } 9 | const LIMIT = 12 10 | 11 | // Return current cart info 12 | const getProducts: ProductsHandlers['getProducts'] = async ({ 13 | res, 14 | body: { search, category, brand, sort }, 15 | config, 16 | }) => { 17 | // Use a dummy base as we only care about the relative path 18 | const url = new URL('/v3/catalog/products', 'http://a') 19 | 20 | url.searchParams.set('is_visible', 'true') 21 | url.searchParams.set('limit', String(LIMIT)) 22 | 23 | if (search) url.searchParams.set('keyword', search) 24 | 25 | if (category && Number.isInteger(Number(category))) 26 | url.searchParams.set('categories:in', category) 27 | 28 | if (brand && Number.isInteger(Number(brand))) 29 | url.searchParams.set('brand_id', brand) 30 | 31 | if (sort) { 32 | const [_sort, direction] = sort.split('-') 33 | const sortValue = SORT[_sort] 34 | 35 | if (sortValue && direction) { 36 | url.searchParams.set('sort', sortValue) 37 | url.searchParams.set('direction', direction) 38 | } 39 | } 40 | 41 | // We only want the id of each product 42 | url.searchParams.set('include_fields', 'id') 43 | 44 | const { data } = await config.storeApiFetch<{ data: { id: number }[] }>( 45 | url.pathname + url.search 46 | ) 47 | const entityIds = data.map((p) => p.id) 48 | const found = entityIds.length > 0 49 | // We want the GraphQL version of each product 50 | const graphqlData = await getAllProducts({ 51 | variables: { first: LIMIT, entityIds }, 52 | config, 53 | }) 54 | // Put the products in an object that we can use to get them by id 55 | const productsById = graphqlData.products.reduce<{ 56 | [k: number]: ProductEdge 57 | }>((prods, p) => { 58 | prods[p.node.entityId] = p 59 | return prods 60 | }, {}) 61 | const products: ProductEdge[] = found ? [] : graphqlData.products 62 | 63 | // Populate the products array with the graphql products, in the order 64 | // assigned by the list of entity ids 65 | entityIds.forEach((id) => { 66 | const product = productsById[id] 67 | if (product) products.push(product) 68 | }) 69 | 70 | res.status(200).json({ data: { products, found } }) 71 | } 72 | 73 | export default getProducts 74 | -------------------------------------------------------------------------------- /example /api/catalog/products.ts: -------------------------------------------------------------------------------- 1 | import isAllowedMethod from '../utils/is-allowed-method' 2 | import createApiHandler, { 3 | BigcommerceApiHandler, 4 | BigcommerceHandler, 5 | } from '../utils/create-api-handler' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import type { ProductEdge } from '../operations/get-all-products' 8 | import getProducts from './handlers/get-products' 9 | 10 | export type SearchProductsData = { 11 | products: ProductEdge[] 12 | found: boolean 13 | } 14 | 15 | export type ProductsHandlers = { 16 | getProducts: BigcommerceHandler< 17 | SearchProductsData, 18 | { search?: 'string'; category?: string; brand?: string; sort?: string } 19 | > 20 | } 21 | 22 | const METHODS = ['GET'] 23 | 24 | // TODO: a complete implementation should have schema validation for `req.body` 25 | const productsApi: BigcommerceApiHandler< 26 | SearchProductsData, 27 | ProductsHandlers 28 | > = async (req, res, config, handlers) => { 29 | if (!isAllowedMethod(req, res, METHODS)) return 30 | 31 | try { 32 | const body = req.query 33 | return await handlers['getProducts']({ req, res, config, body }) 34 | } catch (error) { 35 | console.error(error) 36 | 37 | const message = 38 | error instanceof BigcommerceApiError 39 | ? 'An unexpected error ocurred with the Bigcommerce API' 40 | : 'An unexpected error ocurred' 41 | 42 | res.status(500).json({ data: null, errors: [{ message }] }) 43 | } 44 | } 45 | 46 | export const handlers = { getProducts } 47 | 48 | export default createApiHandler(productsApi, handlers, {}) 49 | -------------------------------------------------------------------------------- /example /api/checkout.ts: -------------------------------------------------------------------------------- 1 | import isAllowedMethod from './utils/is-allowed-method' 2 | import createApiHandler, { 3 | BigcommerceApiHandler, 4 | } from './utils/create-api-handler' 5 | import { BigcommerceApiError } from './utils/errors' 6 | 7 | const METHODS = ['GET'] 8 | const fullCheckout = true 9 | 10 | // TODO: a complete implementation should have schema validation for `req.body` 11 | const checkoutApi: BigcommerceApiHandler = async (req, res, config) => { 12 | if (!isAllowedMethod(req, res, METHODS)) return 13 | 14 | const { cookies } = req 15 | const cartId = cookies[config.cartCookie] 16 | 17 | try { 18 | if (!cartId) { 19 | res.redirect('/cart') 20 | return 21 | } 22 | 23 | const { data } = await config.storeApiFetch( 24 | `/v3/carts/${cartId}/redirect_urls`, 25 | { 26 | method: 'POST', 27 | } 28 | ) 29 | 30 | if (fullCheckout) { 31 | res.redirect(data.checkout_url) 32 | return 33 | } 34 | 35 | // TODO: make the embedded checkout work too! 36 | const html = ` 37 | 38 | 39 | 40 | 41 | 42 | Checkout 43 | 44 | 54 | 55 | 56 |
57 | 58 | 59 | ` 60 | 61 | res.status(200) 62 | res.setHeader('Content-Type', 'text/html') 63 | res.write(html) 64 | res.end() 65 | } catch (error) { 66 | console.error(error) 67 | 68 | const message = 69 | error instanceof BigcommerceApiError 70 | ? 'An unexpected error ocurred with the Bigcommerce API' 71 | : 'An unexpected error ocurred' 72 | 73 | res.status(500).json({ data: null, errors: [{ message }] }) 74 | } 75 | } 76 | 77 | export default createApiHandler(checkoutApi, {}, {}) 78 | -------------------------------------------------------------------------------- /example /api/customers/handlers/get-logged-in-customer.ts: -------------------------------------------------------------------------------- 1 | import type { GetLoggedInCustomerQuery } from '../../../schema' 2 | import type { CustomersHandlers } from '..' 3 | 4 | export const getLoggedInCustomerQuery = /* GraphQL */ ` 5 | query getLoggedInCustomer { 6 | customer { 7 | entityId 8 | firstName 9 | lastName 10 | email 11 | company 12 | customerGroupId 13 | notes 14 | phone 15 | addressCount 16 | attributeCount 17 | storeCredit { 18 | value 19 | currencyCode 20 | } 21 | } 22 | } 23 | ` 24 | 25 | export type Customer = NonNullable 26 | 27 | const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({ 28 | req, 29 | res, 30 | config, 31 | }) => { 32 | const token = req.cookies[config.customerCookie] 33 | 34 | if (token) { 35 | const { data } = await config.fetch( 36 | getLoggedInCustomerQuery, 37 | undefined, 38 | { 39 | headers: { 40 | cookie: `${config.customerCookie}=${token}`, 41 | }, 42 | } 43 | ) 44 | const { customer } = data 45 | 46 | if (!customer) { 47 | return res.status(400).json({ 48 | data: null, 49 | errors: [{ message: 'Customer not found', code: 'not_found' }], 50 | }) 51 | } 52 | 53 | return res.status(200).json({ data: { customer } }) 54 | } 55 | 56 | res.status(200).json({ data: null }) 57 | } 58 | 59 | export default getLoggedInCustomer 60 | -------------------------------------------------------------------------------- /example /api/customers/handlers/login.ts: -------------------------------------------------------------------------------- 1 | import { FetcherError } from '../../../src/utils/errors' 2 | import login from '../../operations/login' 3 | import type { LoginHandlers } from '../login' 4 | 5 | const invalidCredentials = /invalid credentials/i 6 | 7 | const loginHandler: LoginHandlers['login'] = async ({ 8 | res, 9 | body: { email, password }, 10 | config, 11 | }) => { 12 | // TODO: Add proper validations with something like Ajv 13 | if (!(email && password)) { 14 | return res.status(400).json({ 15 | data: null, 16 | errors: [{ message: 'Invalid request' }], 17 | }) 18 | } 19 | // TODO: validate the password and email 20 | // Passwords must be at least 7 characters and contain both alphabetic 21 | // and numeric characters. 22 | 23 | try { 24 | await login({ variables: { email, password }, config, res }) 25 | } catch (error) { 26 | // Check if the email and password didn't match an existing account 27 | if ( 28 | error instanceof FetcherError && 29 | invalidCredentials.test(error.message) 30 | ) { 31 | return res.status(401).json({ 32 | data: null, 33 | errors: [ 34 | { 35 | message: 36 | 'Cannot find an account that matches the provided credentials', 37 | code: 'invalid_credentials', 38 | }, 39 | ], 40 | }) 41 | } 42 | 43 | throw error 44 | } 45 | 46 | res.status(200).json({ data: null }) 47 | } 48 | 49 | export default loginHandler 50 | -------------------------------------------------------------------------------- /example /api/customers/handlers/logout.ts: -------------------------------------------------------------------------------- 1 | import { serialize } from 'cookie' 2 | import { LogoutHandlers } from '../logout' 3 | 4 | const logoutHandler: LogoutHandlers['logout'] = async ({ 5 | res, 6 | body: { redirectTo }, 7 | config, 8 | }) => { 9 | // Remove the cookie 10 | res.setHeader( 11 | 'Set-Cookie', 12 | serialize(config.customerCookie, '', { maxAge: -1, path: '/' }) 13 | ) 14 | 15 | // Only allow redirects to a relative URL 16 | if (redirectTo?.startsWith('/')) { 17 | res.redirect(redirectTo) 18 | } else { 19 | res.status(200).json({ data: null }) 20 | } 21 | } 22 | 23 | export default logoutHandler 24 | -------------------------------------------------------------------------------- /example /api/customers/handlers/signup.ts: -------------------------------------------------------------------------------- 1 | import { BigcommerceApiError } from '../../utils/errors' 2 | import login from '../../operations/login' 3 | import { SignupHandlers } from '../signup' 4 | 5 | const signup: SignupHandlers['signup'] = async ({ 6 | res, 7 | body: { firstName, lastName, email, password }, 8 | config, 9 | }) => { 10 | // TODO: Add proper validations with something like Ajv 11 | if (!(firstName && lastName && email && password)) { 12 | return res.status(400).json({ 13 | data: null, 14 | errors: [{ message: 'Invalid request' }], 15 | }) 16 | } 17 | // TODO: validate the password and email 18 | // Passwords must be at least 7 characters and contain both alphabetic 19 | // and numeric characters. 20 | 21 | try { 22 | await config.storeApiFetch('/v3/customers', { 23 | method: 'POST', 24 | body: JSON.stringify([ 25 | { 26 | first_name: firstName, 27 | last_name: lastName, 28 | email, 29 | authentication: { 30 | new_password: password, 31 | }, 32 | }, 33 | ]), 34 | }) 35 | } catch (error) { 36 | if (error instanceof BigcommerceApiError && error.status === 422) { 37 | const hasEmailError = '0.email' in error.data?.errors 38 | 39 | // If there's an error with the email, it most likely means it's duplicated 40 | if (hasEmailError) { 41 | return res.status(400).json({ 42 | data: null, 43 | errors: [ 44 | { 45 | message: 'The email is already in use', 46 | code: 'duplicated_email', 47 | }, 48 | ], 49 | }) 50 | } 51 | } 52 | 53 | throw error 54 | } 55 | 56 | // Login the customer right after creating it 57 | await login({ variables: { email, password }, res, config }) 58 | 59 | res.status(200).json({ data: null }) 60 | } 61 | 62 | export default signup 63 | -------------------------------------------------------------------------------- /example /api/customers/index.ts: -------------------------------------------------------------------------------- 1 | import createApiHandler, { 2 | BigcommerceApiHandler, 3 | BigcommerceHandler, 4 | } from '../utils/create-api-handler' 5 | import isAllowedMethod from '../utils/is-allowed-method' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import getLoggedInCustomer, { 8 | Customer, 9 | } from './handlers/get-logged-in-customer' 10 | 11 | export type { Customer } 12 | 13 | export type CustomerData = { 14 | customer: Customer 15 | } 16 | 17 | export type CustomersHandlers = { 18 | getLoggedInCustomer: BigcommerceHandler 19 | } 20 | 21 | const METHODS = ['GET'] 22 | 23 | const customersApi: BigcommerceApiHandler< 24 | CustomerData, 25 | CustomersHandlers 26 | > = async (req, res, config, handlers) => { 27 | if (!isAllowedMethod(req, res, METHODS)) return 28 | 29 | try { 30 | const body = null 31 | return await handlers['getLoggedInCustomer']({ req, res, config, body }) 32 | } catch (error) { 33 | console.error(error) 34 | 35 | const message = 36 | error instanceof BigcommerceApiError 37 | ? 'An unexpected error ocurred with the Bigcommerce API' 38 | : 'An unexpected error ocurred' 39 | 40 | res.status(500).json({ data: null, errors: [{ message }] }) 41 | } 42 | } 43 | 44 | const handlers = { getLoggedInCustomer } 45 | 46 | export default createApiHandler(customersApi, handlers, {}) 47 | -------------------------------------------------------------------------------- /example /api/customers/login.ts: -------------------------------------------------------------------------------- 1 | import createApiHandler, { 2 | BigcommerceApiHandler, 3 | BigcommerceHandler, 4 | } from '../utils/create-api-handler' 5 | import isAllowedMethod from '../utils/is-allowed-method' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import login from './handlers/login' 8 | 9 | export type LoginBody = { 10 | email: string 11 | password: string 12 | } 13 | 14 | export type LoginHandlers = { 15 | login: BigcommerceHandler> 16 | } 17 | 18 | const METHODS = ['POST'] 19 | 20 | const loginApi: BigcommerceApiHandler = async ( 21 | req, 22 | res, 23 | config, 24 | handlers 25 | ) => { 26 | if (!isAllowedMethod(req, res, METHODS)) return 27 | 28 | try { 29 | const body = req.body ?? {} 30 | return await handlers['login']({ req, res, config, body }) 31 | } catch (error) { 32 | console.error(error) 33 | 34 | const message = 35 | error instanceof BigcommerceApiError 36 | ? 'An unexpected error ocurred with the Bigcommerce API' 37 | : 'An unexpected error ocurred' 38 | 39 | res.status(500).json({ data: null, errors: [{ message }] }) 40 | } 41 | } 42 | 43 | const handlers = { login } 44 | 45 | export default createApiHandler(loginApi, handlers, {}) 46 | -------------------------------------------------------------------------------- /example /api/customers/logout.ts: -------------------------------------------------------------------------------- 1 | import createApiHandler, { 2 | BigcommerceApiHandler, 3 | BigcommerceHandler, 4 | } from '../utils/create-api-handler' 5 | import isAllowedMethod from '../utils/is-allowed-method' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import logout from './handlers/logout' 8 | 9 | export type LogoutHandlers = { 10 | logout: BigcommerceHandler 11 | } 12 | 13 | const METHODS = ['GET'] 14 | 15 | const logoutApi: BigcommerceApiHandler = async ( 16 | req, 17 | res, 18 | config, 19 | handlers 20 | ) => { 21 | if (!isAllowedMethod(req, res, METHODS)) return 22 | 23 | try { 24 | const redirectTo = req.query.redirect_to 25 | const body = typeof redirectTo === 'string' ? { redirectTo } : {} 26 | 27 | return await handlers['logout']({ req, res, config, body }) 28 | } catch (error) { 29 | console.error(error) 30 | 31 | const message = 32 | error instanceof BigcommerceApiError 33 | ? 'An unexpected error ocurred with the Bigcommerce API' 34 | : 'An unexpected error ocurred' 35 | 36 | res.status(500).json({ data: null, errors: [{ message }] }) 37 | } 38 | } 39 | 40 | const handlers = { logout } 41 | 42 | export default createApiHandler(logoutApi, handlers, {}) 43 | -------------------------------------------------------------------------------- /example /api/customers/signup.ts: -------------------------------------------------------------------------------- 1 | import createApiHandler, { 2 | BigcommerceApiHandler, 3 | BigcommerceHandler, 4 | } from '../utils/create-api-handler' 5 | import isAllowedMethod from '../utils/is-allowed-method' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import signup from './handlers/signup' 8 | 9 | export type SignupBody = { 10 | firstName: string 11 | lastName: string 12 | email: string 13 | password: string 14 | } 15 | 16 | export type SignupHandlers = { 17 | signup: BigcommerceHandler> 18 | } 19 | 20 | const METHODS = ['POST'] 21 | 22 | const signupApi: BigcommerceApiHandler = async ( 23 | req, 24 | res, 25 | config, 26 | handlers 27 | ) => { 28 | if (!isAllowedMethod(req, res, METHODS)) return 29 | 30 | const { cookies } = req 31 | const cartId = cookies[config.cartCookie] 32 | 33 | try { 34 | const body = { ...req.body, cartId } 35 | return await handlers['signup']({ req, res, config, body }) 36 | } catch (error) { 37 | console.error(error) 38 | 39 | const message = 40 | error instanceof BigcommerceApiError 41 | ? 'An unexpected error ocurred with the Bigcommerce API' 42 | : 'An unexpected error ocurred' 43 | 44 | res.status(500).json({ data: null, errors: [{ message }] }) 45 | } 46 | } 47 | 48 | const handlers = { signup } 49 | 50 | export default createApiHandler(signupApi, handlers, {}) 51 | -------------------------------------------------------------------------------- /example /api/definitions/store-content.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by swagger-to-ts. 3 | * Do not make direct changes to the file. 4 | */ 5 | 6 | export interface definitions { 7 | blogPost_Full: { 8 | /** 9 | * ID of this blog post. (READ-ONLY) 10 | */ 11 | id?: number 12 | } & definitions['blogPost_Base'] 13 | addresses: { 14 | /** 15 | * Full URL of where the resource is located. 16 | */ 17 | url?: string 18 | /** 19 | * Resource being accessed. 20 | */ 21 | resource?: string 22 | } 23 | formField: { 24 | /** 25 | * Name of the form field 26 | */ 27 | name?: string 28 | /** 29 | * Value of the form field 30 | */ 31 | value?: string 32 | } 33 | page_Full: { 34 | /** 35 | * ID of the page. 36 | */ 37 | id?: number 38 | } & definitions['page_Base'] 39 | redirect: { 40 | /** 41 | * Numeric ID of the redirect. 42 | */ 43 | id?: number 44 | /** 45 | * The path from which to redirect. 46 | */ 47 | path: string 48 | forward: definitions['forward'] 49 | /** 50 | * URL of the redirect. READ-ONLY 51 | */ 52 | url?: string 53 | } 54 | forward: { 55 | /** 56 | * The type of redirect. If it is a `manual` redirect then type will always be manual. Dynamic redirects will have the type of the page. Such as product or category. 57 | */ 58 | type?: string 59 | /** 60 | * Reference of the redirect. Dynamic redirects will have the category or product number. Manual redirects will have the url that is being directed to. 61 | */ 62 | ref?: number 63 | } 64 | customer_Full: { 65 | /** 66 | * Unique numeric ID of this customer. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request. 67 | */ 68 | id?: number 69 | /** 70 | * Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation. 71 | */ 72 | _authentication?: { 73 | force_reset?: string 74 | password?: string 75 | password_confirmation?: string 76 | } 77 | /** 78 | * The name of the company for which the customer works. 79 | */ 80 | company?: string 81 | /** 82 | * First name of the customer. 83 | */ 84 | first_name: string 85 | /** 86 | * Last name of the customer. 87 | */ 88 | last_name: string 89 | /** 90 | * Email address of the customer. 91 | */ 92 | email: string 93 | /** 94 | * Phone number of the customer. 95 | */ 96 | phone?: string 97 | /** 98 | * Date on which the customer registered from the storefront or was created in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request. 99 | */ 100 | date_created?: string 101 | /** 102 | * Date on which the customer updated their details in the storefront or was updated in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request. 103 | */ 104 | date_modified?: string 105 | /** 106 | * The amount of credit the customer has. (Float, Float as String, Integer) 107 | */ 108 | store_credit?: string 109 | /** 110 | * The customer’s IP address when they signed up. 111 | */ 112 | registration_ip_address?: string 113 | /** 114 | * The group to which the customer belongs. 115 | */ 116 | customer_group_id?: number 117 | /** 118 | * Store-owner notes on the customer. 119 | */ 120 | notes?: string 121 | /** 122 | * Used to identify customers who fall into special sales-tax categories – in particular, those who are fully or partially exempt from paying sales tax. Can be blank, or can contain a single AvaTax code. (The codes are case-sensitive.) Stores that subscribe to BigCommerce’s Avalara Premium integration will use this code to determine how/whether to apply sales tax. Does not affect sales-tax calculations for stores that do not subscribe to Avalara Premium. 123 | */ 124 | tax_exempt_category?: string 125 | /** 126 | * Records whether the customer would like to receive marketing content from this store. READ-ONLY.This is a READ-ONLY field; do not set or modify its value in a POST or PUT request. 127 | */ 128 | accepts_marketing?: boolean 129 | addresses?: definitions['addresses'] 130 | /** 131 | * Array of custom fields. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request. 132 | */ 133 | form_fields?: definitions['formField'][] 134 | /** 135 | * Force a password change on next login. 136 | */ 137 | reset_pass_on_login?: boolean 138 | } 139 | categoryAccessLevel: { 140 | /** 141 | * + `all` - Customers can access all categories 142 | * + `specific` - Customers can access a specific list of categories 143 | * + `none` - Customers are prevented from viewing any of the categories in this group. 144 | */ 145 | type?: 'all' | 'specific' | 'none' 146 | /** 147 | * Is an array of category IDs and should be supplied only if `type` is specific. 148 | */ 149 | categories?: string[] 150 | } 151 | timeZone: { 152 | /** 153 | * a string identifying the time zone, in the format: /. 154 | */ 155 | name?: string 156 | /** 157 | * a negative or positive number, identifying the offset from UTC/GMT, in seconds, during winter/standard time. 158 | */ 159 | raw_offset?: number 160 | /** 161 | * "-/+" offset from UTC/GMT, in seconds, during summer/daylight saving time. 162 | */ 163 | dst_offset?: number 164 | /** 165 | * a boolean indicating whether this time zone observes daylight saving time. 166 | */ 167 | dst_correction?: boolean 168 | date_format?: definitions['dateFormat'] 169 | } 170 | count_Response: { count?: number } 171 | dateFormat: { 172 | /** 173 | * string that defines dates’ display format, in the pattern: M jS Y 174 | */ 175 | display?: string 176 | /** 177 | * string that defines the CSV export format for orders, customers, and products, in the pattern: M jS Y 178 | */ 179 | export?: string 180 | /** 181 | * string that defines dates’ extended-display format, in the pattern: M jS Y @ g:i A. 182 | */ 183 | extended_display?: string 184 | } 185 | blogTags: { tag?: string; post_ids?: number[] }[] 186 | blogPost_Base: { 187 | /** 188 | * Title of this blog post. 189 | */ 190 | title: string 191 | /** 192 | * URL for the public blog post. 193 | */ 194 | url?: string 195 | /** 196 | * URL to preview the blog post. (READ-ONLY) 197 | */ 198 | preview_url?: string 199 | /** 200 | * Text body of the blog post. 201 | */ 202 | body: string 203 | /** 204 | * Tags to characterize the blog post. 205 | */ 206 | tags?: string[] 207 | /** 208 | * Summary of the blog post. (READ-ONLY) 209 | */ 210 | summary?: string 211 | /** 212 | * Whether the blog post is published. 213 | */ 214 | is_published?: boolean 215 | published_date?: definitions['publishedDate'] 216 | /** 217 | * Published date in `ISO 8601` format. 218 | */ 219 | published_date_iso8601?: string 220 | /** 221 | * Description text for this blog post’s `` element. 222 | */ 223 | meta_description?: string 224 | /** 225 | * Keywords for this blog post’s `` element. 226 | */ 227 | meta_keywords?: string 228 | /** 229 | * Name of the blog post’s author. 230 | */ 231 | author?: string 232 | /** 233 | * Local path to a thumbnail uploaded to `product_images/` via [WebDav](https://support.bigcommerce.com/s/article/File-Access-WebDAV). 234 | */ 235 | thumbnail_path?: string 236 | } 237 | publishedDate: { timezone_type?: string; date?: string; timezone?: string } 238 | /** 239 | * Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation. 240 | */ 241 | authentication: { 242 | force_reset?: string 243 | password?: string 244 | password_confirmation?: string 245 | } 246 | customer_Base: { [key: string]: any } 247 | page_Base: { 248 | /** 249 | * ID of any parent Web page. 250 | */ 251 | parent_id?: number 252 | /** 253 | * `page`: free-text page 254 | * `link`: link to another web address 255 | * `rss_feed`: syndicated content from an RSS feed 256 | * `contact_form`: When the store's contact form is used. 257 | */ 258 | type: 'page' | 'rss_feed' | 'contact_form' | 'raw' | 'link' 259 | /** 260 | * Where the page’s type is a contact form: object whose members are the fields enabled (in the control panel) for storefront display. Possible members are:`fullname`: full name of the customer submitting the form; `phone`: customer’s phone number, as submitted on the form; `companyname`: customer’s submitted company name; `orderno`: customer’s submitted order number; `rma`: customer’s submitted RMA (Return Merchandise Authorization) number. 261 | */ 262 | contact_fields?: string 263 | /** 264 | * Where the page’s type is a contact form: email address that receives messages sent via the form. 265 | */ 266 | email?: string 267 | /** 268 | * Page name, as displayed on the storefront. 269 | */ 270 | name: string 271 | /** 272 | * Relative URL on the storefront for this page. 273 | */ 274 | url?: string 275 | /** 276 | * Description contained within this page’s `` element. 277 | */ 278 | meta_description?: string 279 | /** 280 | * HTML or variable that populates this page’s `` element, in default/desktop view. Required in POST if page type is `raw`. 281 | */ 282 | body: string 283 | /** 284 | * HTML to use for this page's body when viewed in the mobile template (deprecated). 285 | */ 286 | mobile_body?: string 287 | /** 288 | * If true, this page has a mobile version. 289 | */ 290 | has_mobile_version?: boolean 291 | /** 292 | * If true, this page appears in the storefront’s navigation menu. 293 | */ 294 | is_visible?: boolean 295 | /** 296 | * If true, this page is the storefront’s home page. 297 | */ 298 | is_homepage?: boolean 299 | /** 300 | * Text specified for this page’s `` element. (If empty, the value of the name property is used.) 301 | */ 302 | meta_title?: string 303 | /** 304 | * Layout template for this page. This field is writable only for stores with a Blueprint theme applied. 305 | */ 306 | layout_file?: string 307 | /** 308 | * Order in which this page should display on the storefront. (Lower integers specify earlier display.) 309 | */ 310 | sort_order?: number 311 | /** 312 | * Comma-separated list of keywords that shoppers can use to locate this page when searching the store. 313 | */ 314 | search_keywords?: string 315 | /** 316 | * Comma-separated list of SEO-relevant keywords to include in the page’s `<meta/>` element. 317 | */ 318 | meta_keywords?: string 319 | /** 320 | * If page type is `rss_feed` the n this field is visisble. Required in POST required for `rss page` type. 321 | */ 322 | feed: string 323 | /** 324 | * If page type is `link` this field is returned. Required in POST to create a `link` page. 325 | */ 326 | link: string 327 | content_type?: 'application/json' | 'text/javascript' | 'text/html' 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /example /api/definitions/wishlist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by swagger-to-ts. 3 | * Do not make direct changes to the file. 4 | */ 5 | 6 | export interface definitions { 7 | wishlist_Post: { 8 | /** 9 | * The customer id. 10 | */ 11 | customer_id: number 12 | /** 13 | * Whether the wishlist is available to the public. 14 | */ 15 | is_public?: boolean 16 | /** 17 | * The title of the wishlist. 18 | */ 19 | name?: string 20 | /** 21 | * Array of Wishlist items. 22 | */ 23 | items?: { 24 | /** 25 | * The ID of the product. 26 | */ 27 | product_id?: number 28 | /** 29 | * The variant ID of the product. 30 | */ 31 | variant_id?: number 32 | }[] 33 | } 34 | wishlist_Put: { 35 | /** 36 | * The customer id. 37 | */ 38 | customer_id: number 39 | /** 40 | * Whether the wishlist is available to the public. 41 | */ 42 | is_public?: boolean 43 | /** 44 | * The title of the wishlist. 45 | */ 46 | name?: string 47 | /** 48 | * Array of Wishlist items. 49 | */ 50 | items?: { 51 | /** 52 | * The ID of the item 53 | */ 54 | id?: number 55 | /** 56 | * The ID of the product. 57 | */ 58 | product_id?: number 59 | /** 60 | * The variant ID of the item. 61 | */ 62 | variant_id?: number 63 | }[] 64 | } 65 | wishlist_Full: { 66 | /** 67 | * Wishlist ID, provided after creating a wishlist with a POST. 68 | */ 69 | id?: number 70 | /** 71 | * The ID the customer to which the wishlist belongs. 72 | */ 73 | customer_id?: number 74 | /** 75 | * The Wishlist's name. 76 | */ 77 | name?: string 78 | /** 79 | * Whether the Wishlist is available to the public. 80 | */ 81 | is_public?: boolean 82 | /** 83 | * The token of the Wishlist. This is created internally within BigCommerce. The Wishlist ID is to be used for external apps. Read-Only 84 | */ 85 | token?: string 86 | /** 87 | * Array of Wishlist items 88 | */ 89 | items?: definitions['wishlistItem_Full'][] 90 | } 91 | wishlistItem_Full: { 92 | /** 93 | * The ID of the item 94 | */ 95 | id?: number 96 | /** 97 | * The ID of the product. 98 | */ 99 | product_id?: number 100 | /** 101 | * The variant ID of the item. 102 | */ 103 | variant_id?: number 104 | } 105 | wishlistItem_Post: { 106 | /** 107 | * The ID of the product. 108 | */ 109 | product_id?: number 110 | /** 111 | * The variant ID of the product. 112 | */ 113 | variant_id?: number 114 | } 115 | /** 116 | * Data about the response, including pagination and collection totals. 117 | */ 118 | pagination: { 119 | /** 120 | * Total number of items in the result set. 121 | */ 122 | total?: number 123 | /** 124 | * Total number of items in the collection response. 125 | */ 126 | count?: number 127 | /** 128 | * The amount of items returned in the collection per page, controlled by the limit parameter. 129 | */ 130 | per_page?: number 131 | /** 132 | * The page you are currently on within the collection. 133 | */ 134 | current_page?: number 135 | /** 136 | * The total number of pages in the collection. 137 | */ 138 | total_pages?: number 139 | } 140 | error: { status?: number; title?: string; type?: string } 141 | metaCollection: { pagination?: definitions['pagination'] } 142 | } 143 | -------------------------------------------------------------------------------- /example /api/fragments/category-tree.ts: -------------------------------------------------------------------------------- 1 | export const categoryTreeItemFragment = /* GraphQL */ ` 2 | fragment categoryTreeItem on CategoryTreeItem { 3 | entityId 4 | name 5 | path 6 | description 7 | productCount 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /example /api/fragments/product.ts: -------------------------------------------------------------------------------- 1 | export const productPrices = /* GraphQL */ ` 2 | fragment productPrices on Prices { 3 | price { 4 | value 5 | currencyCode 6 | } 7 | salePrice { 8 | value 9 | currencyCode 10 | } 11 | retailPrice { 12 | value 13 | currencyCode 14 | } 15 | } 16 | ` 17 | 18 | export const swatchOptionFragment = /* GraphQL */ ` 19 | fragment swatchOption on SwatchOptionValue { 20 | isDefault 21 | hexColors 22 | } 23 | ` 24 | 25 | export const multipleChoiceOptionFragment = /* GraphQL */ ` 26 | fragment multipleChoiceOption on MultipleChoiceOption { 27 | values { 28 | edges { 29 | node { 30 | label 31 | ...swatchOption 32 | } 33 | } 34 | } 35 | } 36 | 37 | ${swatchOptionFragment} 38 | ` 39 | 40 | export const productInfoFragment = /* GraphQL */ ` 41 | fragment productInfo on Product { 42 | entityId 43 | name 44 | path 45 | brand { 46 | entityId 47 | } 48 | description 49 | prices { 50 | ...productPrices 51 | } 52 | images { 53 | edges { 54 | node { 55 | urlOriginal 56 | altText 57 | isDefault 58 | } 59 | } 60 | } 61 | variants { 62 | edges { 63 | node { 64 | entityId 65 | defaultImage { 66 | urlOriginal 67 | altText 68 | isDefault 69 | } 70 | } 71 | } 72 | } 73 | productOptions { 74 | edges { 75 | node { 76 | __typename 77 | entityId 78 | displayName 79 | ...multipleChoiceOption 80 | } 81 | } 82 | } 83 | localeMeta: metafields(namespace: $locale, keys: ["name", "description"]) 84 | @include(if: $hasLocale) { 85 | edges { 86 | node { 87 | key 88 | value 89 | } 90 | } 91 | } 92 | } 93 | 94 | ${productPrices} 95 | ${multipleChoiceOptionFragment} 96 | ` 97 | 98 | export const productConnectionFragment = /* GraphQL */ ` 99 | fragment productConnnection on ProductConnection { 100 | pageInfo { 101 | startCursor 102 | endCursor 103 | } 104 | edges { 105 | cursor 106 | node { 107 | ...productInfo 108 | } 109 | } 110 | } 111 | 112 | ${productInfoFragment} 113 | ` 114 | -------------------------------------------------------------------------------- /example /api/index.ts: -------------------------------------------------------------------------------- 1 | import type { RequestInit } from '@vercel/fetch' 2 | import type { CommerceAPIConfig } from '../src/api' 3 | import fetchGraphqlApi from './utils/fetch-graphql-api' 4 | import fetchStoreApi from './utils/fetch-store-api' 5 | 6 | export interface BigcommerceConfig extends CommerceAPIConfig { 7 | // Indicates if the returned metadata with translations should be applied to the 8 | // data or returned as it is 9 | applyLocale?: boolean 10 | storeApiUrl: string 11 | storeApiToken: string 12 | storeApiClientId: string 13 | storeChannelId?: string 14 | storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T> 15 | } 16 | 17 | const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL 18 | const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN 19 | const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL 20 | const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN 21 | const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID 22 | const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID 23 | 24 | if (!API_URL) { 25 | throw new Error( 26 | `The environment variable BIGCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store` 27 | ) 28 | } 29 | 30 | if (!API_TOKEN) { 31 | throw new Error( 32 | `The environment variable BIGCOMMERCE_STOREFRONT_API_TOKEN is missing and it's required to access your store` 33 | ) 34 | } 35 | 36 | if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) { 37 | throw new Error( 38 | `The environment variables BIGCOMMERCE_STORE_API_URL, BIGCOMMERCE_STORE_API_TOKEN, BIGCOMMERCE_STORE_API_CLIENT_ID have to be set in order to access the REST API of your store` 39 | ) 40 | } 41 | 42 | export class Config { 43 | private config: BigcommerceConfig 44 | 45 | constructor(config: Omit<BigcommerceConfig, 'customerCookie'>) { 46 | this.config = { 47 | ...config, 48 | // The customerCookie is not customizable for now, BC sets the cookie and it's 49 | // not important to rename it 50 | customerCookie: 'SHOP_TOKEN', 51 | } 52 | } 53 | 54 | getConfig(userConfig: Partial<BigcommerceConfig> = {}) { 55 | return Object.entries(userConfig).reduce<BigcommerceConfig>( 56 | (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), 57 | { ...this.config } 58 | ) 59 | } 60 | 61 | setConfig(newConfig: Partial<BigcommerceConfig>) { 62 | Object.assign(this.config, newConfig) 63 | } 64 | } 65 | 66 | const ONE_DAY = 60 * 60 * 24 67 | const config = new Config({ 68 | commerceUrl: API_URL, 69 | apiToken: API_TOKEN, 70 | cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId', 71 | cartCookieMaxAge: ONE_DAY * 30, 72 | fetch: fetchGraphqlApi, 73 | applyLocale: true, 74 | // REST API only 75 | storeApiUrl: STORE_API_URL, 76 | storeApiToken: STORE_API_TOKEN, 77 | storeApiClientId: STORE_API_CLIENT_ID, 78 | storeChannelId: STORE_CHANNEL_ID, 79 | storeApiFetch: fetchStoreApi, 80 | }) 81 | 82 | export function getConfig(userConfig?: Partial<BigcommerceConfig>) { 83 | return config.getConfig(userConfig) 84 | } 85 | 86 | export function setConfig(newConfig: Partial<BigcommerceConfig>) { 87 | return config.setConfig(newConfig) 88 | } 89 | -------------------------------------------------------------------------------- /example /api/operations/get-all-pages.ts: -------------------------------------------------------------------------------- 1 | import type { RecursivePartial, RecursiveRequired } from '../utils/types' 2 | import { BigcommerceConfig, getConfig } from '..' 3 | import { definitions } from '../definitions/store-content' 4 | 5 | export type Page = definitions['page_Full'] 6 | 7 | export type GetAllPagesResult< 8 | T extends { pages: any[] } = { pages: Page[] } 9 | > = T 10 | 11 | async function getAllPages(opts?: { 12 | config?: BigcommerceConfig 13 | preview?: boolean 14 | }): Promise<GetAllPagesResult> 15 | 16 | async function getAllPages<T extends { pages: any[] }>(opts: { 17 | url: string 18 | config?: BigcommerceConfig 19 | preview?: boolean 20 | }): Promise<GetAllPagesResult<T>> 21 | 22 | async function getAllPages({ 23 | config, 24 | preview, 25 | }: { 26 | url?: string 27 | config?: BigcommerceConfig 28 | preview?: boolean 29 | } = {}): Promise<GetAllPagesResult> { 30 | config = getConfig(config) 31 | // RecursivePartial forces the method to check for every prop in the data, which is 32 | // required in case there's a custom `url` 33 | const { data } = await config.storeApiFetch< 34 | RecursivePartial<{ data: Page[] }> 35 | >('/v3/content/pages') 36 | const pages = (data as RecursiveRequired<typeof data>) ?? [] 37 | 38 | return { 39 | pages: preview ? pages : pages.filter((p) => p.is_visible), 40 | } 41 | } 42 | 43 | export default getAllPages 44 | -------------------------------------------------------------------------------- /example /api/operations/get-all-product-paths.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetAllProductPathsQuery, 3 | GetAllProductPathsQueryVariables, 4 | } from '../../schema' 5 | import type { RecursivePartial, RecursiveRequired } from '../utils/types' 6 | import filterEdges from '../utils/filter-edges' 7 | import { BigcommerceConfig, getConfig } from '..' 8 | 9 | export const getAllProductPathsQuery = /* GraphQL */ ` 10 | query getAllProductPaths($first: Int = 100) { 11 | site { 12 | products(first: $first) { 13 | edges { 14 | node { 15 | path 16 | } 17 | } 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export type ProductPath = NonNullable< 24 | NonNullable<GetAllProductPathsQuery['site']['products']['edges']>[0] 25 | > 26 | 27 | export type ProductPaths = ProductPath[] 28 | 29 | export type { GetAllProductPathsQueryVariables } 30 | 31 | export type GetAllProductPathsResult< 32 | T extends { products: any[] } = { products: ProductPaths } 33 | > = T 34 | 35 | async function getAllProductPaths(opts?: { 36 | variables?: GetAllProductPathsQueryVariables 37 | config?: BigcommerceConfig 38 | }): Promise<GetAllProductPathsResult> 39 | 40 | async function getAllProductPaths< 41 | T extends { products: any[] }, 42 | V = any 43 | >(opts: { 44 | query: string 45 | variables?: V 46 | config?: BigcommerceConfig 47 | }): Promise<GetAllProductPathsResult<T>> 48 | 49 | async function getAllProductPaths({ 50 | query = getAllProductPathsQuery, 51 | variables, 52 | config, 53 | }: { 54 | query?: string 55 | variables?: GetAllProductPathsQueryVariables 56 | config?: BigcommerceConfig 57 | } = {}): Promise<GetAllProductPathsResult> { 58 | config = getConfig(config) 59 | // RecursivePartial forces the method to check for every prop in the data, which is 60 | // required in case there's a custom `query` 61 | const { data } = await config.fetch< 62 | RecursivePartial<GetAllProductPathsQuery> 63 | >(query, { variables }) 64 | const products = data.site?.products?.edges 65 | 66 | return { 67 | products: filterEdges(products as RecursiveRequired<typeof products>), 68 | } 69 | } 70 | 71 | export default getAllProductPaths 72 | -------------------------------------------------------------------------------- /example /api/operations/get-all-products.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetAllProductsQuery, 3 | GetAllProductsQueryVariables, 4 | } from '../../schema' 5 | import type { RecursivePartial, RecursiveRequired } from '../utils/types' 6 | import filterEdges from '../utils/filter-edges' 7 | import setProductLocaleMeta from '../utils/set-product-locale-meta' 8 | import { productConnectionFragment } from '../fragments/product' 9 | import { BigcommerceConfig, getConfig } from '..' 10 | 11 | export const getAllProductsQuery = /* GraphQL */ ` 12 | query getAllProducts( 13 | $hasLocale: Boolean = false 14 | $locale: String = "null" 15 | $entityIds: [Int!] 16 | $first: Int = 10 17 | $products: Boolean = false 18 | $featuredProducts: Boolean = false 19 | $bestSellingProducts: Boolean = false 20 | $newestProducts: Boolean = false 21 | ) { 22 | site { 23 | products(first: $first, entityIds: $entityIds) @include(if: $products) { 24 | ...productConnnection 25 | } 26 | featuredProducts(first: $first) @include(if: $featuredProducts) { 27 | ...productConnnection 28 | } 29 | bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { 30 | ...productConnnection 31 | } 32 | newestProducts(first: $first) @include(if: $newestProducts) { 33 | ...productConnnection 34 | } 35 | } 36 | } 37 | 38 | ${productConnectionFragment} 39 | ` 40 | 41 | export type ProductEdge = NonNullable< 42 | NonNullable<GetAllProductsQuery['site']['products']['edges']>[0] 43 | > 44 | 45 | export type ProductNode = ProductEdge['node'] 46 | 47 | export type GetAllProductsResult< 48 | T extends Record<keyof GetAllProductsResult, any[]> = { 49 | products: ProductEdge[] 50 | } 51 | > = T 52 | 53 | const FIELDS = [ 54 | 'products', 55 | 'featuredProducts', 56 | 'bestSellingProducts', 57 | 'newestProducts', 58 | ] 59 | 60 | export type ProductTypes = 61 | | 'products' 62 | | 'featuredProducts' 63 | | 'bestSellingProducts' 64 | | 'newestProducts' 65 | 66 | export type ProductVariables = { field?: ProductTypes } & Omit< 67 | GetAllProductsQueryVariables, 68 | ProductTypes | 'hasLocale' 69 | > 70 | 71 | async function getAllProducts(opts?: { 72 | variables?: ProductVariables 73 | config?: BigcommerceConfig 74 | preview?: boolean 75 | }): Promise<GetAllProductsResult> 76 | 77 | async function getAllProducts< 78 | T extends Record<keyof GetAllProductsResult, any[]>, 79 | V = any 80 | >(opts: { 81 | query: string 82 | variables?: V 83 | config?: BigcommerceConfig 84 | preview?: boolean 85 | }): Promise<GetAllProductsResult<T>> 86 | 87 | async function getAllProducts({ 88 | query = getAllProductsQuery, 89 | variables: { field = 'products', ...vars } = {}, 90 | config, 91 | }: { 92 | query?: string 93 | variables?: ProductVariables 94 | config?: BigcommerceConfig 95 | preview?: boolean 96 | } = {}): Promise<GetAllProductsResult> { 97 | config = getConfig(config) 98 | 99 | const locale = vars.locale || config.locale 100 | const variables: GetAllProductsQueryVariables = { 101 | ...vars, 102 | locale, 103 | hasLocale: !!locale, 104 | } 105 | 106 | if (!FIELDS.includes(field)) { 107 | throw new Error( 108 | `The field variable has to match one of ${FIELDS.join(', ')}` 109 | ) 110 | } 111 | 112 | variables[field] = true 113 | 114 | // RecursivePartial forces the method to check for every prop in the data, which is 115 | // required in case there's a custom `query` 116 | const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>( 117 | query, 118 | { variables } 119 | ) 120 | const edges = data.site?.[field]?.edges 121 | const products = filterEdges(edges as RecursiveRequired<typeof edges>) 122 | 123 | if (locale && config.applyLocale) { 124 | products.forEach((product: RecursivePartial<ProductEdge>) => { 125 | if (product.node) setProductLocaleMeta(product.node) 126 | }) 127 | } 128 | 129 | return { products } 130 | } 131 | 132 | export default getAllProducts 133 | -------------------------------------------------------------------------------- /example /api/operations/get-customer-id.ts: -------------------------------------------------------------------------------- 1 | import { GetCustomerIdQuery } from '../../schema' 2 | import { BigcommerceConfig, getConfig } from '..' 3 | 4 | export const getCustomerIdQuery = /* GraphQL */ ` 5 | query getCustomerId { 6 | customer { 7 | entityId 8 | } 9 | } 10 | ` 11 | 12 | async function getCustomerId({ 13 | customerToken, 14 | config, 15 | }: { 16 | customerToken: string 17 | config?: BigcommerceConfig 18 | }): Promise<number | undefined> { 19 | config = getConfig(config) 20 | 21 | const { data } = await config.fetch<GetCustomerIdQuery>( 22 | getCustomerIdQuery, 23 | undefined, 24 | { 25 | headers: { 26 | cookie: `${config.customerCookie}=${customerToken}`, 27 | }, 28 | } 29 | ) 30 | 31 | return data?.customer?.entityId 32 | } 33 | 34 | export default getCustomerId 35 | -------------------------------------------------------------------------------- /example /api/operations/get-customer-wishlist.ts: -------------------------------------------------------------------------------- 1 | import type { RecursivePartial, RecursiveRequired } from '../utils/types' 2 | import { definitions } from '../definitions/wishlist' 3 | import { BigcommerceConfig, getConfig } from '..' 4 | import getAllProducts, { ProductEdge } from './get-all-products' 5 | 6 | export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & { 7 | items?: WishlistItem[] 8 | } 9 | 10 | export type WishlistItem = NonNullable< 11 | definitions['wishlist_Full']['items'] 12 | >[0] & { 13 | product?: ProductEdge['node'] 14 | } 15 | 16 | export type GetCustomerWishlistResult< 17 | T extends { wishlist?: any } = { wishlist?: Wishlist } 18 | > = T 19 | 20 | export type GetCustomerWishlistVariables = { 21 | customerId: number 22 | } 23 | 24 | async function getCustomerWishlist(opts: { 25 | variables: GetCustomerWishlistVariables 26 | config?: BigcommerceConfig 27 | includeProducts?: boolean 28 | }): Promise<GetCustomerWishlistResult> 29 | 30 | async function getCustomerWishlist< 31 | T extends { wishlist?: any }, 32 | V = any 33 | >(opts: { 34 | url: string 35 | variables: V 36 | config?: BigcommerceConfig 37 | includeProducts?: boolean 38 | }): Promise<GetCustomerWishlistResult<T>> 39 | 40 | async function getCustomerWishlist({ 41 | config, 42 | variables, 43 | includeProducts, 44 | }: { 45 | url?: string 46 | variables: GetCustomerWishlistVariables 47 | config?: BigcommerceConfig 48 | includeProducts?: boolean 49 | }): Promise<GetCustomerWishlistResult> { 50 | config = getConfig(config) 51 | 52 | const { data = [] } = await config.storeApiFetch< 53 | RecursivePartial<{ data: Wishlist[] }> 54 | >(`/v3/wishlists?customer_id=${variables.customerId}`) 55 | const wishlist = data[0] 56 | 57 | if (includeProducts && wishlist?.items?.length) { 58 | const entityIds = wishlist.items 59 | ?.map((item) => item?.product_id) 60 | .filter((id): id is number => !!id) 61 | 62 | if (entityIds?.length) { 63 | const graphqlData = await getAllProducts({ 64 | variables: { first: 100, entityIds }, 65 | config, 66 | }) 67 | // Put the products in an object that we can use to get them by id 68 | const productsById = graphqlData.products.reduce<{ 69 | [k: number]: ProductEdge 70 | }>((prods, p) => { 71 | prods[p.node.entityId] = p 72 | return prods 73 | }, {}) 74 | // Populate the wishlist items with the graphql products 75 | wishlist.items.forEach((item) => { 76 | const product = item && productsById[item.product_id!] 77 | if (item && product) { 78 | item.product = product.node 79 | } 80 | }) 81 | } 82 | } 83 | 84 | return { wishlist: wishlist as RecursiveRequired<typeof wishlist> } 85 | } 86 | 87 | export default getCustomerWishlist 88 | -------------------------------------------------------------------------------- /example /api/operations/get-page.ts: -------------------------------------------------------------------------------- 1 | import type { RecursivePartial, RecursiveRequired } from '../utils/types' 2 | import { BigcommerceConfig, getConfig } from '..' 3 | import { definitions } from '../definitions/store-content' 4 | 5 | export type Page = definitions['page_Full'] 6 | 7 | export type GetPageResult<T extends { page?: any } = { page?: Page }> = T 8 | 9 | export type PageVariables = { 10 | id: number 11 | } 12 | 13 | async function getPage(opts: { 14 | url?: string 15 | variables: PageVariables 16 | config?: BigcommerceConfig 17 | preview?: boolean 18 | }): Promise<GetPageResult> 19 | 20 | async function getPage<T extends { page?: any }, V = any>(opts: { 21 | url: string 22 | variables: V 23 | config?: BigcommerceConfig 24 | preview?: boolean 25 | }): Promise<GetPageResult<T>> 26 | 27 | async function getPage({ 28 | url, 29 | variables, 30 | config, 31 | preview, 32 | }: { 33 | url?: string 34 | variables: PageVariables 35 | config?: BigcommerceConfig 36 | preview?: boolean 37 | }): Promise<GetPageResult> { 38 | config = getConfig(config) 39 | // RecursivePartial forces the method to check for every prop in the data, which is 40 | // required in case there's a custom `url` 41 | const { data } = await config.storeApiFetch<RecursivePartial<{ data: Page[] }>>( 42 | url || `/v3/content/pages?id=${variables.id}&include=body` 43 | ) 44 | const firstPage = data?.[0] 45 | const page = firstPage as RecursiveRequired<typeof firstPage> 46 | 47 | if (preview || page?.is_visible) { 48 | return { page } 49 | } 50 | return {} 51 | } 52 | 53 | export default getPage 54 | -------------------------------------------------------------------------------- /example /api/operations/get-product.ts: -------------------------------------------------------------------------------- 1 | import type { GetProductQuery, GetProductQueryVariables } from '../../schema' 2 | import setProductLocaleMeta from '../utils/set-product-locale-meta' 3 | import { productInfoFragment } from '../fragments/product' 4 | import { BigcommerceConfig, getConfig } from '..' 5 | 6 | export const getProductQuery = /* GraphQL */ ` 7 | query getProduct( 8 | $hasLocale: Boolean = false 9 | $locale: String = "null" 10 | $path: String! 11 | ) { 12 | site { 13 | route(path: $path) { 14 | node { 15 | __typename 16 | ... on Product { 17 | ...productInfo 18 | variants { 19 | edges { 20 | node { 21 | entityId 22 | defaultImage { 23 | urlOriginal 24 | altText 25 | isDefault 26 | } 27 | prices { 28 | ...productPrices 29 | } 30 | inventory { 31 | aggregated { 32 | availableToSell 33 | warningLevel 34 | } 35 | isInStock 36 | } 37 | productOptions { 38 | edges { 39 | node { 40 | __typename 41 | entityId 42 | displayName 43 | ...multipleChoiceOption 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | ${productInfoFragment} 57 | ` 58 | 59 | export type ProductNode = Extract< 60 | GetProductQuery['site']['route']['node'], 61 | { __typename: 'Product' } 62 | > 63 | 64 | export type GetProductResult< 65 | T extends { product?: any } = { product?: ProductNode } 66 | > = T 67 | 68 | export type ProductVariables = { locale?: string } & ( 69 | | { path: string; slug?: never } 70 | | { path?: never; slug: string } 71 | ) 72 | 73 | async function getProduct(opts: { 74 | variables: ProductVariables 75 | config?: BigcommerceConfig 76 | preview?: boolean 77 | }): Promise<GetProductResult> 78 | 79 | async function getProduct<T extends { product?: any }, V = any>(opts: { 80 | query: string 81 | variables: V 82 | config?: BigcommerceConfig 83 | preview?: boolean 84 | }): Promise<GetProductResult<T>> 85 | 86 | async function getProduct({ 87 | query = getProductQuery, 88 | variables: { slug, ...vars }, 89 | config, 90 | }: { 91 | query?: string 92 | variables: ProductVariables 93 | config?: BigcommerceConfig 94 | preview?: boolean 95 | }): Promise<GetProductResult> { 96 | config = getConfig(config) 97 | 98 | const locale = vars.locale || config.locale 99 | const variables: GetProductQueryVariables = { 100 | ...vars, 101 | locale, 102 | hasLocale: !!locale, 103 | path: slug ? `/${slug}/` : vars.path!, 104 | } 105 | const { data } = await config.fetch<GetProductQuery>(query, { variables }) 106 | const product = data.site?.route?.node 107 | 108 | if (product?.__typename === 'Product') { 109 | if (locale && config.applyLocale) { 110 | setProductLocaleMeta(product) 111 | } 112 | return { product } 113 | } 114 | 115 | return {} 116 | } 117 | 118 | export default getProduct 119 | -------------------------------------------------------------------------------- /example /api/operations/get-site-info.ts: -------------------------------------------------------------------------------- 1 | import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema' 2 | import type { RecursivePartial, RecursiveRequired } from '../utils/types' 3 | import filterEdges from '../utils/filter-edges' 4 | import { BigcommerceConfig, getConfig } from '..' 5 | import { categoryTreeItemFragment } from '../fragments/category-tree' 6 | 7 | // Get 3 levels of categories 8 | export const getSiteInfoQuery = /* GraphQL */ ` 9 | query getSiteInfo { 10 | site { 11 | categoryTree { 12 | ...categoryTreeItem 13 | children { 14 | ...categoryTreeItem 15 | children { 16 | ...categoryTreeItem 17 | } 18 | } 19 | } 20 | brands { 21 | pageInfo { 22 | startCursor 23 | endCursor 24 | } 25 | edges { 26 | cursor 27 | node { 28 | entityId 29 | name 30 | defaultImage { 31 | urlOriginal 32 | altText 33 | } 34 | pageTitle 35 | metaDesc 36 | metaKeywords 37 | searchKeywords 38 | path 39 | } 40 | } 41 | } 42 | } 43 | } 44 | ${categoryTreeItemFragment} 45 | ` 46 | 47 | export type CategoriesTree = NonNullable< 48 | GetSiteInfoQuery['site']['categoryTree'] 49 | > 50 | 51 | export type BrandEdge = NonNullable< 52 | NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0] 53 | > 54 | 55 | export type Brands = BrandEdge[] 56 | 57 | export type GetSiteInfoResult< 58 | T extends { categories: any[]; brands: any[] } = { 59 | categories: CategoriesTree 60 | brands: Brands 61 | } 62 | > = T 63 | 64 | async function getSiteInfo(opts?: { 65 | variables?: GetSiteInfoQueryVariables 66 | config?: BigcommerceConfig 67 | preview?: boolean 68 | }): Promise<GetSiteInfoResult> 69 | 70 | async function getSiteInfo< 71 | T extends { categories: any[]; brands: any[] }, 72 | V = any 73 | >(opts: { 74 | query: string 75 | variables?: V 76 | config?: BigcommerceConfig 77 | preview?: boolean 78 | }): Promise<GetSiteInfoResult<T>> 79 | 80 | async function getSiteInfo({ 81 | query = getSiteInfoQuery, 82 | variables, 83 | config, 84 | }: { 85 | query?: string 86 | variables?: GetSiteInfoQueryVariables 87 | config?: BigcommerceConfig 88 | preview?: boolean 89 | } = {}): Promise<GetSiteInfoResult> { 90 | config = getConfig(config) 91 | // RecursivePartial forces the method to check for every prop in the data, which is 92 | // required in case there's a custom `query` 93 | const { data } = await config.fetch<RecursivePartial<GetSiteInfoQuery>>( 94 | query, 95 | { variables } 96 | ) 97 | const categories = data.site?.categoryTree 98 | const brands = data.site?.brands?.edges 99 | 100 | return { 101 | categories: (categories as RecursiveRequired<typeof categories>) ?? [], 102 | brands: filterEdges(brands as RecursiveRequired<typeof brands>), 103 | } 104 | } 105 | 106 | export default getSiteInfo 107 | -------------------------------------------------------------------------------- /example /api/operations/login.ts: -------------------------------------------------------------------------------- 1 | import type { ServerResponse } from 'http' 2 | import type { LoginMutation, LoginMutationVariables } from '../../schema' 3 | import type { RecursivePartial } from '../utils/types' 4 | import concatHeader from '../utils/concat-cookie' 5 | import { BigcommerceConfig, getConfig } from '..' 6 | 7 | export const loginMutation = /* GraphQL */ ` 8 | mutation login($email: String!, $password: String!) { 9 | login(email: $email, password: $password) { 10 | result 11 | } 12 | } 13 | ` 14 | 15 | export type LoginResult<T extends { result?: any } = { result?: string }> = T 16 | 17 | export type LoginVariables = LoginMutationVariables 18 | 19 | async function login(opts: { 20 | variables: LoginVariables 21 | config?: BigcommerceConfig 22 | res: ServerResponse 23 | }): Promise<LoginResult> 24 | 25 | async function login<T extends { result?: any }, V = any>(opts: { 26 | query: string 27 | variables: V 28 | res: ServerResponse 29 | config?: BigcommerceConfig 30 | }): Promise<LoginResult<T>> 31 | 32 | async function login({ 33 | query = loginMutation, 34 | variables, 35 | res: response, 36 | config, 37 | }: { 38 | query?: string 39 | variables: LoginVariables 40 | res: ServerResponse 41 | config?: BigcommerceConfig 42 | }): Promise<LoginResult> { 43 | config = getConfig(config) 44 | 45 | const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>( 46 | query, 47 | { variables } 48 | ) 49 | // Bigcommerce returns a Set-Cookie header with the auth cookie 50 | let cookie = res.headers.get('Set-Cookie') 51 | 52 | if (cookie && typeof cookie === 'string') { 53 | // In development, don't set a secure cookie or the browser will ignore it 54 | if (process.env.NODE_ENV !== 'production') { 55 | cookie = cookie.replace('; Secure', '') 56 | // SameSite=none can't be set unless the cookie is Secure 57 | cookie = cookie.replace('; SameSite=none', '; SameSite=lax') 58 | } 59 | 60 | response.setHeader( 61 | 'Set-Cookie', 62 | concatHeader(response.getHeader('Set-Cookie'), cookie)! 63 | ) 64 | } 65 | 66 | return { 67 | result: data.login?.result, 68 | } 69 | } 70 | 71 | export default login 72 | -------------------------------------------------------------------------------- /example /api/utils/concat-cookie.ts: -------------------------------------------------------------------------------- 1 | type Header = string | number | string[] | undefined 2 | 3 | export default function concatHeader(prev: Header, val: Header) { 4 | if (!val) return prev 5 | if (!prev) return val 6 | 7 | if (Array.isArray(prev)) return prev.concat(String(val)) 8 | 9 | prev = String(prev) 10 | 11 | if (Array.isArray(val)) return [prev].concat(val) 12 | 13 | return [prev, String(val)] 14 | } 15 | -------------------------------------------------------------------------------- /example /api/utils/create-api-handler.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' 2 | import { BigcommerceConfig, getConfig } from '..' 3 | 4 | export type BigcommerceApiHandler< 5 | T = any, 6 | H extends BigcommerceHandlers = {}, 7 | Options extends {} = {} 8 | > = ( 9 | req: NextApiRequest, 10 | res: NextApiResponse<BigcommerceApiResponse<T>>, 11 | config: BigcommerceConfig, 12 | handlers: H, 13 | // Custom configs that may be used by a particular handler 14 | options: Options 15 | ) => void | Promise<void> 16 | 17 | export type BigcommerceHandler<T = any, Body = null> = (options: { 18 | req: NextApiRequest 19 | res: NextApiResponse<BigcommerceApiResponse<T>> 20 | config: BigcommerceConfig 21 | body: Body 22 | }) => void | Promise<void> 23 | 24 | export type BigcommerceHandlers<T = any> = { 25 | [k: string]: BigcommerceHandler<T, any> 26 | } 27 | 28 | export type BigcommerceApiResponse<T> = { 29 | data: T | null 30 | errors?: { message: string; code?: string }[] 31 | } 32 | 33 | export default function createApiHandler< 34 | T = any, 35 | H extends BigcommerceHandlers = {}, 36 | Options extends {} = {} 37 | >( 38 | handler: BigcommerceApiHandler<T, H, Options>, 39 | handlers: H, 40 | defaultOptions: Options 41 | ) { 42 | return function getApiHandler({ 43 | config, 44 | operations, 45 | options, 46 | }: { 47 | config?: BigcommerceConfig 48 | operations?: Partial<H> 49 | options?: Options extends {} ? Partial<Options> : never 50 | } = {}): NextApiHandler { 51 | const ops = { ...operations, ...handlers } 52 | const opts = { ...defaultOptions, ...options } 53 | 54 | return function apiHandler(req, res) { 55 | return handler(req, res, getConfig(config), ops, opts) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example /api/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from '@vercel/fetch' 2 | 3 | // Used for GraphQL errors 4 | export class BigcommerceGraphQLError extends Error {} 5 | 6 | export class BigcommerceApiError extends Error { 7 | status: number 8 | res: Response 9 | data: any 10 | 11 | constructor(msg: string, res: Response, data?: any) { 12 | super(msg) 13 | this.name = 'BigcommerceApiError' 14 | this.status = res.status 15 | this.res = res 16 | this.data = data 17 | } 18 | } 19 | 20 | export class BigcommerceNetworkError extends Error { 21 | constructor(msg: string) { 22 | super(msg) 23 | this.name = 'BigcommerceNetworkError' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example /api/utils/fetch-graphql-api.ts: -------------------------------------------------------------------------------- 1 | import { FetcherError } from '../../src/utils/errors' 2 | import type { GraphQLFetcher } from '../../src/api' 3 | import { getConfig } from '..' 4 | import fetch from './fetch' 5 | 6 | const fetchGraphqlApi: GraphQLFetcher = async ( 7 | query: string, 8 | { variables, preview } = {}, 9 | fetchOptions 10 | ) => { 11 | // log.warn(query) 12 | const config = getConfig() 13 | const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { 14 | ...fetchOptions, 15 | method: 'POST', 16 | headers: { 17 | Authorization: `Bearer ${config.apiToken}`, 18 | ...fetchOptions?.headers, 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify({ 22 | query, 23 | variables, 24 | }), 25 | }) 26 | 27 | const json = await res.json() 28 | if (json.errors) { 29 | throw new FetcherError({ 30 | errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }], 31 | status: res.status, 32 | }) 33 | } 34 | 35 | return { data: json.data, res } 36 | } 37 | 38 | export default fetchGraphqlApi 39 | -------------------------------------------------------------------------------- /example /api/utils/fetch-store-api.ts: -------------------------------------------------------------------------------- 1 | import type { RequestInit, Response } from '@vercel/fetch' 2 | import { getConfig } from '..' 3 | import { BigcommerceApiError, BigcommerceNetworkError } from './errors' 4 | import fetch from './fetch' 5 | 6 | export default async function fetchStoreApi<T>( 7 | endpoint: string, 8 | options?: RequestInit 9 | ): Promise<T> { 10 | const config = getConfig() 11 | let res: Response 12 | 13 | try { 14 | res = await fetch(config.storeApiUrl + endpoint, { 15 | ...options, 16 | headers: { 17 | ...options?.headers, 18 | 'Content-Type': 'application/json', 19 | 'X-Auth-Token': config.storeApiToken, 20 | 'X-Auth-Client': config.storeApiClientId, 21 | }, 22 | }) 23 | } catch (error) { 24 | throw new BigcommerceNetworkError( 25 | `Fetch to Bigcommerce failed: ${error.message}` 26 | ) 27 | } 28 | 29 | const contentType = res.headers.get('Content-Type') 30 | const isJSON = contentType?.includes('application/json') 31 | 32 | if (!res.ok) { 33 | const data = isJSON ? await res.json() : await getTextOrNull(res) 34 | const headers = getRawHeaders(res) 35 | const msg = `Big Commerce API error (${ 36 | res.status 37 | }) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${ 38 | typeof data === 'string' ? data : JSON.stringify(data, null, 2) 39 | }` 40 | 41 | throw new BigcommerceApiError(msg, res, data) 42 | } 43 | 44 | if (res.status !== 204 && !isJSON) { 45 | throw new BigcommerceApiError( 46 | `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`, 47 | res 48 | ) 49 | } 50 | 51 | // If something was removed, the response will be empty 52 | return res.status === 204 ? null : await res.json() 53 | } 54 | 55 | function getRawHeaders(res: Response) { 56 | const headers: { [key: string]: string } = {} 57 | 58 | res.headers.forEach((value, key) => { 59 | headers[key] = value 60 | }) 61 | 62 | return headers 63 | } 64 | 65 | function getTextOrNull(res: Response) { 66 | try { 67 | return res.text() 68 | } catch (err) { 69 | return null 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /example /api/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import zeitFetch from '@vercel/fetch' 2 | 3 | export default zeitFetch() 4 | -------------------------------------------------------------------------------- /example /api/utils/filter-edges.ts: -------------------------------------------------------------------------------- 1 | export default function filterEdges<T>( 2 | edges: (T | null | undefined)[] | null | undefined 3 | ) { 4 | return edges?.filter((edge): edge is T => !!edge) ?? [] 5 | } 6 | -------------------------------------------------------------------------------- /example /api/utils/get-cart-cookie.ts: -------------------------------------------------------------------------------- 1 | import { serialize, CookieSerializeOptions } from 'cookie' 2 | 3 | export default function getCartCookie( 4 | name: string, 5 | cartId?: string, 6 | maxAge?: number 7 | ) { 8 | const options: CookieSerializeOptions = 9 | cartId && maxAge 10 | ? { 11 | maxAge, 12 | expires: new Date(Date.now() + maxAge * 1000), 13 | secure: process.env.NODE_ENV === 'production', 14 | path: '/', 15 | sameSite: 'lax', 16 | } 17 | : { maxAge: -1, path: '/' } // Removes the cookie 18 | 19 | return serialize(name, cartId || '', options) 20 | } 21 | -------------------------------------------------------------------------------- /example /api/utils/is-allowed-method.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | export default function isAllowedMethod( 4 | req: NextApiRequest, 5 | res: NextApiResponse, 6 | allowedMethods: string[] 7 | ) { 8 | const methods = allowedMethods.includes('OPTIONS') 9 | ? allowedMethods 10 | : [...allowedMethods, 'OPTIONS'] 11 | 12 | if (!req.method || !methods.includes(req.method)) { 13 | res.status(405) 14 | res.setHeader('Allow', methods.join(', ')) 15 | res.end() 16 | return false 17 | } 18 | 19 | if (req.method === 'OPTIONS') { 20 | res.status(200) 21 | res.setHeader('Allow', methods.join(', ')) 22 | res.setHeader('Content-Length', '0') 23 | res.end() 24 | return false 25 | } 26 | 27 | return true 28 | } 29 | -------------------------------------------------------------------------------- /example /api/utils/parse-item.ts: -------------------------------------------------------------------------------- 1 | import type { ItemBody as WishlistItemBody } from '../wishlist' 2 | import type { ItemBody } from '../cart' 3 | 4 | export const parseWishlistItem = (item: WishlistItemBody) => ({ 5 | product_id: item.productId, 6 | variant_id: item.variantId, 7 | }) 8 | 9 | export const parseCartItem = (item: ItemBody) => ({ 10 | quantity: item.quantity, 11 | product_id: item.productId, 12 | variant_id: item.variantId, 13 | }) 14 | -------------------------------------------------------------------------------- /example /api/utils/set-product-locale-meta.ts: -------------------------------------------------------------------------------- 1 | import type { ProductNode } from '../operations/get-all-products' 2 | import type { RecursivePartial } from './types' 3 | 4 | export default function setProductLocaleMeta( 5 | node: RecursivePartial<ProductNode> 6 | ) { 7 | if (node.localeMeta?.edges) { 8 | node.localeMeta.edges = node.localeMeta.edges.filter((edge) => { 9 | const { key, value } = edge?.node ?? {} 10 | if (key && key in node) { 11 | ;(node as any)[key] = value 12 | return false 13 | } 14 | return true 15 | }) 16 | 17 | if (!node.localeMeta.edges.length) { 18 | delete node.localeMeta 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example /api/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type RecursivePartial<T> = { 2 | [P in keyof T]?: RecursivePartial<T[P]> 3 | } 4 | 5 | export type RecursiveRequired<T> = { 6 | [P in keyof T]-?: RecursiveRequired<T[P]> 7 | } 8 | -------------------------------------------------------------------------------- /example /api/wishlist/handlers/add-item.ts: -------------------------------------------------------------------------------- 1 | import type { WishlistHandlers } from '..' 2 | import getCustomerId from '../../operations/get-customer-id' 3 | import getCustomerWishlist from '../../operations/get-customer-wishlist' 4 | import { parseWishlistItem } from '../../utils/parse-item' 5 | 6 | // Returns the wishlist of the signed customer 7 | const addItem: WishlistHandlers['addItem'] = async ({ 8 | res, 9 | body: { customerToken, item }, 10 | config, 11 | }) => { 12 | if (!item) { 13 | return res.status(400).json({ 14 | data: null, 15 | errors: [{ message: 'Missing item' }], 16 | }) 17 | } 18 | 19 | const customerId = 20 | customerToken && (await getCustomerId({ customerToken, config })) 21 | 22 | if (!customerId) { 23 | return res.status(400).json({ 24 | data: null, 25 | errors: [{ message: 'Invalid request' }], 26 | }) 27 | } 28 | 29 | const { wishlist } = await getCustomerWishlist({ 30 | variables: { customerId }, 31 | config, 32 | }) 33 | const options = { 34 | method: 'POST', 35 | body: JSON.stringify( 36 | wishlist 37 | ? { 38 | items: [parseWishlistItem(item)], 39 | } 40 | : { 41 | name: 'Wishlist', 42 | customer_id: customerId, 43 | items: [parseWishlistItem(item)], 44 | is_public: false, 45 | } 46 | ), 47 | } 48 | 49 | const { data } = wishlist 50 | ? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options) 51 | : await config.storeApiFetch('/v3/wishlists', options) 52 | 53 | res.status(200).json({ data }) 54 | } 55 | 56 | export default addItem 57 | -------------------------------------------------------------------------------- /example /api/wishlist/handlers/get-wishlist.ts: -------------------------------------------------------------------------------- 1 | import getCustomerId from '../../operations/get-customer-id' 2 | import getCustomerWishlist from '../../operations/get-customer-wishlist' 3 | import type { Wishlist, WishlistHandlers } from '..' 4 | 5 | // Return wishlist info 6 | const getWishlist: WishlistHandlers['getWishlist'] = async ({ 7 | res, 8 | body: { customerToken, includeProducts }, 9 | config, 10 | }) => { 11 | let result: { data?: Wishlist } = {} 12 | 13 | if (customerToken) { 14 | const customerId = 15 | customerToken && (await getCustomerId({ customerToken, config })) 16 | 17 | if (!customerId) { 18 | // If the customerToken is invalid, then this request is too 19 | return res.status(404).json({ 20 | data: null, 21 | errors: [{ message: 'Wishlist not found' }], 22 | }) 23 | } 24 | 25 | const { wishlist } = await getCustomerWishlist({ 26 | variables: { customerId }, 27 | includeProducts, 28 | config, 29 | }) 30 | 31 | result = { data: wishlist } 32 | } 33 | 34 | res.status(200).json({ data: result.data ?? null }) 35 | } 36 | 37 | export default getWishlist 38 | -------------------------------------------------------------------------------- /example /api/wishlist/handlers/remove-item.ts: -------------------------------------------------------------------------------- 1 | import getCustomerId from '../../operations/get-customer-id' 2 | import getCustomerWishlist, { 3 | Wishlist, 4 | } from '../../operations/get-customer-wishlist' 5 | import type { WishlistHandlers } from '..' 6 | 7 | // Return current wishlist info 8 | const removeItem: WishlistHandlers['removeItem'] = async ({ 9 | res, 10 | body: { customerToken, itemId }, 11 | config, 12 | }) => { 13 | const customerId = 14 | customerToken && (await getCustomerId({ customerToken, config })) 15 | const { wishlist } = 16 | (customerId && 17 | (await getCustomerWishlist({ 18 | variables: { customerId }, 19 | config, 20 | }))) || 21 | {} 22 | 23 | if (!wishlist || !itemId) { 24 | return res.status(400).json({ 25 | data: null, 26 | errors: [{ message: 'Invalid request' }], 27 | }) 28 | } 29 | 30 | const result = await config.storeApiFetch<{ data: Wishlist } | null>( 31 | `/v3/wishlists/${wishlist.id}/items/${itemId}`, 32 | { method: 'DELETE' } 33 | ) 34 | const data = result?.data ?? null 35 | 36 | res.status(200).json({ data }) 37 | } 38 | 39 | export default removeItem 40 | -------------------------------------------------------------------------------- /example /api/wishlist/index.ts: -------------------------------------------------------------------------------- 1 | import isAllowedMethod from '../utils/is-allowed-method' 2 | import createApiHandler, { 3 | BigcommerceApiHandler, 4 | BigcommerceHandler, 5 | } from '../utils/create-api-handler' 6 | import { BigcommerceApiError } from '../utils/errors' 7 | import type { 8 | Wishlist, 9 | WishlistItem, 10 | } from '../operations/get-customer-wishlist' 11 | import getWishlist from './handlers/get-wishlist' 12 | import addItem from './handlers/add-item' 13 | import removeItem from './handlers/remove-item' 14 | 15 | export type { Wishlist, WishlistItem } 16 | 17 | export type ItemBody = { 18 | productId: number 19 | variantId: number 20 | } 21 | 22 | export type AddItemBody = { item: ItemBody } 23 | 24 | export type RemoveItemBody = { itemId: string } 25 | 26 | export type WishlistBody = { 27 | customer_id: number 28 | is_public: number 29 | name: string 30 | items: any[] 31 | } 32 | 33 | export type AddWishlistBody = { wishlist: WishlistBody } 34 | 35 | export type WishlistHandlers = { 36 | getWishlist: BigcommerceHandler< 37 | Wishlist, 38 | { customerToken?: string; includeProducts?: boolean } 39 | > 40 | addItem: BigcommerceHandler< 41 | Wishlist, 42 | { customerToken?: string } & Partial<AddItemBody> 43 | > 44 | removeItem: BigcommerceHandler< 45 | Wishlist, 46 | { customerToken?: string } & Partial<RemoveItemBody> 47 | > 48 | } 49 | 50 | const METHODS = ['GET', 'POST', 'DELETE'] 51 | 52 | // TODO: a complete implementation should have schema validation for `req.body` 53 | const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async ( 54 | req, 55 | res, 56 | config, 57 | handlers 58 | ) => { 59 | if (!isAllowedMethod(req, res, METHODS)) return 60 | 61 | const { cookies } = req 62 | const customerToken = cookies[config.customerCookie] 63 | 64 | try { 65 | // Return current wishlist info 66 | if (req.method === 'GET') { 67 | const body = { 68 | customerToken, 69 | includeProducts: req.query.products === '1', 70 | } 71 | return await handlers['getWishlist']({ req, res, config, body }) 72 | } 73 | 74 | // Add an item to the wishlist 75 | if (req.method === 'POST') { 76 | const body = { ...req.body, customerToken } 77 | return await handlers['addItem']({ req, res, config, body }) 78 | } 79 | 80 | // Remove an item from the wishlist 81 | if (req.method === 'DELETE') { 82 | const body = { ...req.body, customerToken } 83 | return await handlers['removeItem']({ req, res, config, body }) 84 | } 85 | } catch (error) { 86 | console.error(error) 87 | 88 | const message = 89 | error instanceof BigcommerceApiError 90 | ? 'An unexpected error ocurred with the Bigcommerce API' 91 | : 'An unexpected error ocurred' 92 | 93 | res.status(500).json({ data: null, errors: [{ message }] }) 94 | } 95 | } 96 | 97 | export const handlers = { 98 | getWishlist, 99 | addItem, 100 | removeItem, 101 | } 102 | 103 | export default createApiHandler(wishlistApi, handlers, {}) 104 | -------------------------------------------------------------------------------- /example /auth/use-login.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { HookFetcher } from '../src/utils/types' 3 | import { CommerceError } from '../src/utils/errors' 4 | import useCommerceLogin from '../src/use-login' 5 | import type { LoginBody } from '../api/customers/login' 6 | import useCustomer from '../account/use-customer' 7 | 8 | const defaultOpts = { 9 | url: '/api/bigcommerce/customers/login', 10 | method: 'POST', 11 | } 12 | 13 | export type LoginInput = LoginBody 14 | 15 | export const fetcher: HookFetcher<null, LoginBody> = ( 16 | options, 17 | { email, password }, 18 | fetch 19 | ) => { 20 | if (!(email && password)) { 21 | throw new CommerceError({ 22 | message: 23 | 'A first name, last name, email and password are required to login', 24 | }) 25 | } 26 | 27 | return fetch({ 28 | ...defaultOpts, 29 | ...options, 30 | body: { email, password }, 31 | }) 32 | } 33 | 34 | export function extendHook(customFetcher: typeof fetcher) { 35 | const useLogin = () => { 36 | const { revalidate } = useCustomer() 37 | const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher) 38 | 39 | return useCallback( 40 | async function login(input: LoginInput) { 41 | const data = await fn(input) 42 | await revalidate() 43 | return data 44 | }, 45 | [fn] 46 | ) 47 | } 48 | 49 | useLogin.extend = extendHook 50 | 51 | return useLogin 52 | } 53 | 54 | export default extendHook(fetcher) 55 | -------------------------------------------------------------------------------- /example /auth/use-logout.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { HookFetcher } from '../src/utils/types' 3 | import useCommerceLogout from '../src/use-logout' 4 | import useCustomer from '../account/use-customer' 5 | 6 | const defaultOpts = { 7 | url: '/api/bigcommerce/customers/logout', 8 | method: 'GET', 9 | } 10 | 11 | export const fetcher: HookFetcher<null> = (options, _, fetch) => { 12 | return fetch({ 13 | ...defaultOpts, 14 | ...options, 15 | }) 16 | } 17 | 18 | export function extendHook(customFetcher: typeof fetcher) { 19 | const useLogout = () => { 20 | const { mutate } = useCustomer() 21 | const fn = useCommerceLogout<null>(defaultOpts, customFetcher) 22 | 23 | return useCallback( 24 | async function login() { 25 | const data = await fn(null) 26 | await mutate(null, false) 27 | return data 28 | }, 29 | [fn] 30 | ) 31 | } 32 | 33 | useLogout.extend = extendHook 34 | 35 | return useLogout 36 | } 37 | 38 | export default extendHook(fetcher) 39 | -------------------------------------------------------------------------------- /example /auth/use-signup.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { HookFetcher } from '../src/utils/types' 3 | import { CommerceError } from '../src/utils/errors' 4 | import useCommerceSignup from '../src/use-signup' 5 | import type { SignupBody } from '../api/customers/signup' 6 | import useCustomer from '../account/use-customer' 7 | 8 | const defaultOpts = { 9 | url: '/api/bigcommerce/customers/signup', 10 | method: 'POST', 11 | } 12 | 13 | export type SignupInput = SignupBody 14 | 15 | export const fetcher: HookFetcher<null, SignupBody> = ( 16 | options, 17 | { firstName, lastName, email, password }, 18 | fetch 19 | ) => { 20 | if (!(firstName && lastName && email && password)) { 21 | throw new CommerceError({ 22 | message: 23 | 'A first name, last name, email and password are required to signup', 24 | }) 25 | } 26 | 27 | return fetch({ 28 | ...defaultOpts, 29 | ...options, 30 | body: { firstName, lastName, email, password }, 31 | }) 32 | } 33 | 34 | export function extendHook(customFetcher: typeof fetcher) { 35 | const useSignup = () => { 36 | const { revalidate } = useCustomer() 37 | const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher) 38 | 39 | return useCallback( 40 | async function signup(input: SignupInput) { 41 | const data = await fn(input) 42 | await revalidate() 43 | return data 44 | }, 45 | [fn] 46 | ) 47 | } 48 | 49 | useSignup.extend = extendHook 50 | 51 | return useSignup 52 | } 53 | 54 | export default extendHook(fetcher) 55 | -------------------------------------------------------------------------------- /example /cart/use-add-item.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { HookFetcher } from '../src/utils/types' 3 | import { CommerceError } from '../src/utils/errors' 4 | import useCartAddItem from '../src/cart/use-add-item' 5 | import type { ItemBody, AddItemBody } from '../api/cart' 6 | import useCart, { Cart } from './use-cart' 7 | 8 | const defaultOpts = { 9 | url: '/api/bigcommerce/cart', 10 | method: 'POST', 11 | } 12 | 13 | export type AddItemInput = ItemBody 14 | 15 | export const fetcher: HookFetcher<Cart, AddItemBody> = ( 16 | options, 17 | { item }, 18 | fetch 19 | ) => { 20 | if ( 21 | item.quantity && 22 | (!Number.isInteger(item.quantity) || item.quantity! < 1) 23 | ) { 24 | throw new CommerceError({ 25 | message: 'The item quantity has to be a valid integer greater than 0', 26 | }) 27 | } 28 | 29 | return fetch({ 30 | ...defaultOpts, 31 | ...options, 32 | body: { item }, 33 | }) 34 | } 35 | 36 | export function extendHook(customFetcher: typeof fetcher) { 37 | const useAddItem = () => { 38 | const { mutate } = useCart() 39 | const fn = useCartAddItem(defaultOpts, customFetcher) 40 | 41 | return useCallback( 42 | async function addItem(input: AddItemInput) { 43 | const data = await fn({ item: input }) 44 | await mutate(data, false) 45 | return data 46 | }, 47 | [fn, mutate] 48 | ) 49 | } 50 | 51 | useAddItem.extend = extendHook 52 | 53 | return useAddItem 54 | } 55 | 56 | export default extendHook(fetcher) 57 | -------------------------------------------------------------------------------- /example /cart/use-cart-actions.tsx: -------------------------------------------------------------------------------- 1 | import useAddItem from './use-add-item' 2 | import useRemoveItem from './use-remove-item' 3 | import useUpdateItem from './use-update-item' 4 | 5 | // This hook is probably not going to be used, but it's here 6 | // to show how a commerce should be structuring it 7 | export default function useCartActions() { 8 | const addItem = useAddItem() 9 | const updateItem = useUpdateItem() 10 | const removeItem = useRemoveItem() 11 | 12 | return { addItem, updateItem, removeItem } 13 | } 14 | -------------------------------------------------------------------------------- /example /cart/use-cart.tsx: -------------------------------------------------------------------------------- 1 | import type { HookFetcher } from '../src/utils/types' 2 | import type { SwrOptions } from '../src/utils/use-data' 3 | import useCommerceCart, { CartInput } from '../src/cart/use-cart' 4 | import type { Cart } from '../api/cart' 5 | 6 | const defaultOpts = { 7 | url: '/api/bigcommerce/cart', 8 | method: 'GET', 9 | } 10 | 11 | export type { Cart } 12 | 13 | export const fetcher: HookFetcher<Cart | null, CartInput> = ( 14 | options, 15 | { cartId }, 16 | fetch 17 | ) => { 18 | return cartId ? fetch({ ...defaultOpts, ...options }) : null 19 | } 20 | 21 | export function extendHook( 22 | customFetcher: typeof fetcher, 23 | swrOptions?: SwrOptions<Cart | null, CartInput> 24 | ) { 25 | const useCart = () => { 26 | const response = useCommerceCart(defaultOpts, [], customFetcher, { 27 | revalidateOnFocus: false, 28 | ...swrOptions, 29 | }) 30 | 31 | // Uses a getter to only calculate the prop when required 32 | // response.data is also a getter and it's better to not trigger it early 33 | Object.defineProperty(response, 'isEmpty', { 34 | get() { 35 | return Object.values(response.data?.line_items ?? {}).every( 36 | (items) => !items.length 37 | ) 38 | }, 39 | set: (x) => x, 40 | }) 41 | 42 | return response 43 | } 44 | 45 | useCart.extend = extendHook 46 | 47 | return useCart 48 | } 49 | 50 | export default extendHook(fetcher) 51 | -------------------------------------------------------------------------------- /example /cart/use-remove-item.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { HookFetcher } from '../src/utils/types' 3 | import useCartRemoveItem from '../src/cart/use-remove-item' 4 | import type { RemoveItemBody } from '../api/cart' 5 | import useCart, { Cart } from './use-cart' 6 | 7 | const defaultOpts = { 8 | url: '/api/bigcommerce/cart', 9 | method: 'DELETE', 10 | } 11 | 12 | export type RemoveItemInput = { 13 | id: string 14 | } 15 | 16 | export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = ( 17 | options, 18 | { itemId }, 19 | fetch 20 | ) => { 21 | return fetch({ 22 | ...defaultOpts, 23 | ...options, 24 | body: { itemId }, 25 | }) 26 | } 27 | 28 | export function extendHook(customFetcher: typeof fetcher) { 29 | const useRemoveItem = (item?: any) => { 30 | const { mutate } = useCart() 31 | const fn = useCartRemoveItem<Cart | null, RemoveItemBody>( 32 | defaultOpts, 33 | customFetcher 34 | ) 35 | 36 | return useCallback( 37 | async function removeItem(input: RemoveItemInput) { 38 | const data = await fn({ itemId: input.id ?? item?.id }) 39 | await mutate(data, false) 40 | return data 41 | }, 42 | [fn, mutate] 43 | ) 44 | } 45 | 46 | useRemoveItem.extend = extendHook 47 | 48 | return useRemoveItem 49 | } 50 | 51 | export default extendHook(fetcher) 52 | -------------------------------------------------------------------------------- /example /cart/use-update-item.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import debounce from 'lodash.debounce' 3 | import type { HookFetcher } from '../src/utils/types' 4 | import { CommerceError } from '../src/utils/errors' 5 | import useCartUpdateItem from '../src/cart/use-update-item' 6 | import type { ItemBody, UpdateItemBody } from '../api/cart' 7 | import { fetcher as removeFetcher } from './use-remove-item' 8 | import useCart, { Cart } from './use-cart' 9 | 10 | const defaultOpts = { 11 | url: '/api/bigcommerce/cart', 12 | method: 'PUT', 13 | } 14 | 15 | export type UpdateItemInput = Partial<{ id: string } & ItemBody> 16 | 17 | export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = ( 18 | options, 19 | { itemId, item }, 20 | fetch 21 | ) => { 22 | if (Number.isInteger(item.quantity)) { 23 | // Also allow the update hook to remove an item if the quantity is lower than 1 24 | if (item.quantity! < 1) { 25 | return removeFetcher(null, { itemId }, fetch) 26 | } 27 | } else if (item.quantity) { 28 | throw new CommerceError({ 29 | message: 'The item quantity has to be a valid integer', 30 | }) 31 | } 32 | 33 | return fetch({ 34 | ...defaultOpts, 35 | ...options, 36 | body: { itemId, item }, 37 | }) 38 | } 39 | 40 | function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { 41 | const useUpdateItem = (item?: any) => { 42 | const { mutate } = useCart() 43 | const fn = useCartUpdateItem<Cart | null, UpdateItemBody>( 44 | defaultOpts, 45 | customFetcher 46 | ) 47 | 48 | return useCallback( 49 | debounce(async (input: UpdateItemInput) => { 50 | const data = await fn({ 51 | itemId: input.id ?? item?.id, 52 | item: { 53 | productId: input.productId ?? item?.product_id, 54 | variantId: input.productId ?? item?.variant_id, 55 | quantity: input.quantity, 56 | }, 57 | }) 58 | await mutate(data, false) 59 | return data 60 | }, cfg?.wait ?? 500), 61 | [fn, mutate] 62 | ) 63 | } 64 | 65 | useUpdateItem.extend = extendHook 66 | 67 | return useUpdateItem 68 | } 69 | 70 | export default extendHook(fetcher) 71 | -------------------------------------------------------------------------------- /example /index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import * as React from 'react' 3 | import { 4 | CommerceConfig, 5 | CommerceProvider as CoreCommerceProvider, 6 | useCommerce as useCoreCommerce, 7 | } from './src' 8 | import { FetcherError } from './src/utils/errors' 9 | 10 | async function getText(res: Response) { 11 | try { 12 | return (await res.text()) || res.statusText 13 | } catch (error) { 14 | return res.statusText 15 | } 16 | } 17 | 18 | async function getError(res: Response) { 19 | if (res.headers.get('Content-Type')?.includes('application/json')) { 20 | const data = await res.json() 21 | return new FetcherError({ errors: data.errors, status: res.status }) 22 | } 23 | return new FetcherError({ message: await getText(res), status: res.status }) 24 | } 25 | 26 | export const bigcommerceConfig: CommerceConfig = { 27 | locale: 'en-us', 28 | cartCookie: 'bc_cartId', 29 | async fetcher({ url, method = 'GET', variables, body: bodyObj }) { 30 | const hasBody = Boolean(variables || bodyObj) 31 | const body = hasBody 32 | ? JSON.stringify(variables ? { variables } : bodyObj) 33 | : undefined 34 | const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined 35 | const res = await fetch(url!, { method, body, headers }) 36 | 37 | if (res.ok) { 38 | const { data } = await res.json() 39 | return data 40 | } 41 | 42 | throw await getError(res) 43 | }, 44 | } 45 | 46 | export type BigcommerceConfig = Partial<CommerceConfig> 47 | 48 | export type BigcommerceProps = { 49 | children?: ReactNode 50 | locale: string 51 | } & BigcommerceConfig 52 | 53 | export function CommerceProvider({ children, ...config }: BigcommerceProps) { 54 | return ( 55 | <CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}> 56 | {children} 57 | </CoreCommerceProvider> 58 | ) 59 | } 60 | 61 | export const useCommerce = () => useCoreCommerce() 62 | -------------------------------------------------------------------------------- /example /products/use-price.tsx: -------------------------------------------------------------------------------- 1 | export * from '../src/use-price' 2 | export { default } from '../src/use-price' 3 | -------------------------------------------------------------------------------- /example /products/use-search.tsx: -------------------------------------------------------------------------------- 1 | import type { HookFetcher } from '../src/utils/types' 2 | import type { SwrOptions } from '../src/utils/use-data' 3 | import useCommerceSearch from '../src/products/use-search' 4 | import type { SearchProductsData } from '../api/catalog/products' 5 | 6 | const defaultOpts = { 7 | url: '/api/bigcommerce/catalog/products', 8 | method: 'GET', 9 | } 10 | 11 | export type SearchProductsInput = { 12 | search?: string 13 | categoryId?: number 14 | brandId?: number 15 | sort?: string 16 | } 17 | 18 | export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = ( 19 | options, 20 | { search, categoryId, brandId, sort }, 21 | fetch 22 | ) => { 23 | // Use a dummy base as we only care about the relative path 24 | const url = new URL(options?.url ?? defaultOpts.url, 'http://a') 25 | 26 | if (search) url.searchParams.set('search', search) 27 | if (Number.isInteger(categoryId)) 28 | url.searchParams.set('category', String(categoryId)) 29 | if (Number.isInteger(categoryId)) 30 | url.searchParams.set('brand', String(brandId)) 31 | if (sort) url.searchParams.set('sort', sort) 32 | 33 | return fetch({ 34 | url: url.pathname + url.search, 35 | method: options?.method ?? defaultOpts.method, 36 | }) 37 | } 38 | 39 | export function extendHook( 40 | customFetcher: typeof fetcher, 41 | swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput> 42 | ) { 43 | const useSearch = (input: SearchProductsInput = {}) => { 44 | const response = useCommerceSearch( 45 | defaultOpts, 46 | [ 47 | ['search', input.search], 48 | ['categoryId', input.categoryId], 49 | ['brandId', input.brandId], 50 | ['sort', input.sort], 51 | ], 52 | customFetcher, 53 | { revalidateOnFocus: false, ...swrOptions } 54 | ) 55 | 56 | return response 57 | } 58 | 59 | useSearch.extend = extendHook 60 | 61 | return useSearch 62 | } 63 | 64 | export default extendHook(fetcher) 65 | -------------------------------------------------------------------------------- /example /schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | Login result 3 | """ 4 | type LoginResult { 5 | """ 6 | The result of a login 7 | """ 8 | result: String! 9 | } 10 | 11 | """ 12 | Logout result 13 | """ 14 | type LogoutResult { 15 | """ 16 | The result of a logout 17 | """ 18 | result: String! 19 | } 20 | 21 | type Mutation { 22 | login(email: String!, password: String!): LoginResult! 23 | logout: LogoutResult! 24 | } 25 | 26 | """ 27 | Aggregated 28 | """ 29 | type Aggregated { 30 | """ 31 | Number of available products in stock. This can be 'null' if inventory is not set orif the store's Inventory Settings disable displaying stock levels on the storefront. 32 | """ 33 | availableToSell: Long! 34 | 35 | """ 36 | Indicates a threshold low-stock level. This can be 'null' if the inventory warning level is not set or if the store's Inventory Settings disable displaying stock levels on the storefront. 37 | """ 38 | warningLevel: Int! 39 | } 40 | 41 | """ 42 | Aggregated Product Inventory 43 | """ 44 | type AggregatedInventory { 45 | """ 46 | Number of available products in stock. This can be 'null' if inventory is not set orif the store's Inventory Settings disable displaying stock levels on the storefront. 47 | """ 48 | availableToSell: Int! 49 | 50 | """ 51 | Indicates a threshold low-stock level. This can be 'null' if the inventory warning level is not set or if the store's Inventory Settings disable displaying stock levels on the storefront. 52 | """ 53 | warningLevel: Int! 54 | } 55 | 56 | """ 57 | Brand 58 | """ 59 | type Brand implements Node { 60 | """ 61 | The ID of an object 62 | """ 63 | id: ID! 64 | 65 | """ 66 | Id of the brand. 67 | """ 68 | entityId: Int! 69 | 70 | """ 71 | Name of the brand. 72 | """ 73 | name: String! 74 | 75 | """ 76 | Default image for brand. 77 | """ 78 | defaultImage: Image 79 | 80 | """ 81 | Page title for the brand. 82 | """ 83 | pageTitle: String! 84 | 85 | """ 86 | Meta description for the brand. 87 | """ 88 | metaDesc: String! 89 | 90 | """ 91 | Meta keywords for the brand. 92 | """ 93 | metaKeywords: [String!]! 94 | 95 | """ 96 | Search keywords for the brand. 97 | """ 98 | searchKeywords: [String!]! 99 | 100 | """ 101 | Path for the brand page. 102 | """ 103 | path: String! 104 | products( 105 | before: String 106 | after: String 107 | first: Int 108 | last: Int 109 | ): ProductConnection! 110 | 111 | """ 112 | Metafield data related to a brand. 113 | """ 114 | metafields( 115 | namespace: String! 116 | keys: [String!] = [] 117 | before: String 118 | after: String 119 | first: Int 120 | last: Int 121 | ): MetafieldConnection! 122 | } 123 | 124 | """ 125 | A connection to a list of items. 126 | """ 127 | type BrandConnection { 128 | """ 129 | Information to aid in pagination. 130 | """ 131 | pageInfo: PageInfo! 132 | 133 | """ 134 | A list of edges. 135 | """ 136 | edges: [BrandEdge] 137 | } 138 | 139 | """ 140 | An edge in a connection. 141 | """ 142 | type BrandEdge { 143 | """ 144 | The item at the end of the edge. 145 | """ 146 | node: Brand! 147 | 148 | """ 149 | A cursor for use in pagination. 150 | """ 151 | cursor: String! 152 | } 153 | 154 | """ 155 | Breadcrumb 156 | """ 157 | type Breadcrumb { 158 | """ 159 | Category id. 160 | """ 161 | entityId: Int! 162 | 163 | """ 164 | Name of the category. 165 | """ 166 | name: String! 167 | } 168 | 169 | """ 170 | A connection to a list of items. 171 | """ 172 | type BreadcrumbConnection { 173 | """ 174 | Information to aid in pagination. 175 | """ 176 | pageInfo: PageInfo! 177 | 178 | """ 179 | A list of edges. 180 | """ 181 | edges: [BreadcrumbEdge] 182 | } 183 | 184 | """ 185 | An edge in a connection. 186 | """ 187 | type BreadcrumbEdge { 188 | """ 189 | The item at the end of the edge. 190 | """ 191 | node: Breadcrumb! 192 | 193 | """ 194 | A cursor for use in pagination. 195 | """ 196 | cursor: String! 197 | } 198 | 199 | """ 200 | Bulk pricing tier that sets a fixed price for the product or variant. 201 | """ 202 | type BulkPricingFixedPriceDiscount implements BulkPricingTier { 203 | """ 204 | This price will override the current product price. 205 | """ 206 | price: BigDecimal! 207 | 208 | """ 209 | Minimum item quantity that applies to this bulk pricing tier. 210 | """ 211 | minimumQuantity: Int! 212 | 213 | """ 214 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound. 215 | """ 216 | maximumQuantity: Int 217 | } 218 | 219 | """ 220 | Bulk pricing tier that reduces the price of the product or variant by a percentage. 221 | """ 222 | type BulkPricingPercentageDiscount implements BulkPricingTier { 223 | """ 224 | The percentage that will be removed from the product price. 225 | """ 226 | percentOff: BigDecimal! 227 | 228 | """ 229 | Minimum item quantity that applies to this bulk pricing tier. 230 | """ 231 | minimumQuantity: Int! 232 | 233 | """ 234 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound. 235 | """ 236 | maximumQuantity: Int 237 | } 238 | 239 | """ 240 | Bulk pricing tier that will subtract an amount from the price of the product or variant. 241 | """ 242 | type BulkPricingRelativePriceDiscount implements BulkPricingTier { 243 | """ 244 | The price of the product/variant will be reduced by this priceAdjustment. 245 | """ 246 | priceAdjustment: BigDecimal! 247 | 248 | """ 249 | Minimum item quantity that applies to this bulk pricing tier. 250 | """ 251 | minimumQuantity: Int! 252 | 253 | """ 254 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound. 255 | """ 256 | maximumQuantity: Int 257 | } 258 | 259 | """ 260 | A set of bulk pricing tiers that define price discounts which apply when purchasing specified quantities of a product or variant. 261 | """ 262 | interface BulkPricingTier { 263 | """ 264 | Minimum item quantity that applies to this bulk pricing tier. 265 | """ 266 | minimumQuantity: Int! 267 | 268 | """ 269 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound. 270 | """ 271 | maximumQuantity: Int 272 | } 273 | 274 | """ 275 | Product Option 276 | """ 277 | interface CatalogProductOption { 278 | """ 279 | Unique ID for the option. 280 | """ 281 | entityId: Int! 282 | 283 | """ 284 | Display name for the option. 285 | """ 286 | displayName: String! 287 | 288 | """ 289 | One of the option values is required to be selected for the checkout. 290 | """ 291 | isRequired: Boolean! 292 | } 293 | 294 | """ 295 | Product Option Value 296 | """ 297 | interface CatalogProductOptionValue { 298 | """ 299 | Unique ID for the option value. 300 | """ 301 | entityId: Int! 302 | 303 | """ 304 | Label for the option value. 305 | """ 306 | label: String! 307 | 308 | """ 309 | Indicates whether this value is the chosen default selected value. 310 | """ 311 | isDefault: Boolean! 312 | } 313 | 314 | """ 315 | Category 316 | """ 317 | type Category implements Node { 318 | """ 319 | The ID of an object 320 | """ 321 | id: ID! 322 | 323 | """ 324 | Unique ID for the category. 325 | """ 326 | entityId: Int! 327 | 328 | """ 329 | Category name. 330 | """ 331 | name: String! 332 | 333 | """ 334 | Category path. 335 | """ 336 | path: String! 337 | 338 | """ 339 | Default image for the category. 340 | """ 341 | defaultImage: Image 342 | 343 | """ 344 | Category description. 345 | """ 346 | description: String! 347 | 348 | """ 349 | Category breadcrumbs. 350 | """ 351 | breadcrumbs( 352 | depth: Int! 353 | before: String 354 | after: String 355 | first: Int 356 | last: Int 357 | ): BreadcrumbConnection! 358 | products( 359 | before: String 360 | after: String 361 | first: Int 362 | last: Int 363 | ): ProductConnection! 364 | 365 | """ 366 | Metafield data related to a category. 367 | """ 368 | metafields( 369 | namespace: String! 370 | keys: [String!] = [] 371 | before: String 372 | after: String 373 | first: Int 374 | last: Int 375 | ): MetafieldConnection! 376 | } 377 | 378 | """ 379 | A connection to a list of items. 380 | """ 381 | type CategoryConnection { 382 | """ 383 | Information to aid in pagination. 384 | """ 385 | pageInfo: PageInfo! 386 | 387 | """ 388 | A list of edges. 389 | """ 390 | edges: [CategoryEdge] 391 | } 392 | 393 | """ 394 | An edge in a connection. 395 | """ 396 | type CategoryEdge { 397 | """ 398 | The item at the end of the edge. 399 | """ 400 | node: Category! 401 | 402 | """ 403 | A cursor for use in pagination. 404 | """ 405 | cursor: String! 406 | } 407 | 408 | """ 409 | An item in a tree of categories. 410 | """ 411 | type CategoryTreeItem { 412 | """ 413 | The id category. 414 | """ 415 | entityId: Int! 416 | 417 | """ 418 | The name of category. 419 | """ 420 | name: String! 421 | 422 | """ 423 | Path assigned to this category 424 | """ 425 | path: String! 426 | 427 | """ 428 | The description of this category. 429 | """ 430 | description: String! 431 | 432 | """ 433 | The number of products in this category. 434 | """ 435 | productCount: Int! 436 | 437 | """ 438 | Subcategories of this category 439 | """ 440 | children: [CategoryTreeItem!]! 441 | } 442 | 443 | """ 444 | A simple yes/no question represented by a checkbox. 445 | """ 446 | type CheckboxOption implements CatalogProductOption { 447 | """ 448 | Indicates the default checked status. 449 | """ 450 | checkedByDefault: Boolean! 451 | 452 | """ 453 | Unique ID for the option. 454 | """ 455 | entityId: Int! 456 | 457 | """ 458 | Display name for the option. 459 | """ 460 | displayName: String! 461 | 462 | """ 463 | One of the option values is required to be selected for the checkout. 464 | """ 465 | isRequired: Boolean! 466 | } 467 | 468 | """ 469 | Contact field 470 | """ 471 | type ContactField { 472 | """ 473 | Store address line. 474 | """ 475 | address: String! 476 | 477 | """ 478 | Store country. 479 | """ 480 | country: String! 481 | 482 | """ 483 | Store address type. 484 | """ 485 | addressType: String! 486 | 487 | """ 488 | Store email. 489 | """ 490 | email: String! 491 | 492 | """ 493 | Store phone number. 494 | """ 495 | phone: String! 496 | } 497 | 498 | """ 499 | Custom field 500 | """ 501 | type CustomField { 502 | """ 503 | Custom field id. 504 | """ 505 | entityId: Int! 506 | 507 | """ 508 | Name of the custom field. 509 | """ 510 | name: String! 511 | 512 | """ 513 | Value of the custom field. 514 | """ 515 | value: String! 516 | } 517 | 518 | """ 519 | A connection to a list of items. 520 | """ 521 | type CustomFieldConnection { 522 | """ 523 | Information to aid in pagination. 524 | """ 525 | pageInfo: PageInfo! 526 | 527 | """ 528 | A list of edges. 529 | """ 530 | edges: [CustomFieldEdge] 531 | } 532 | 533 | """ 534 | An edge in a connection. 535 | """ 536 | type CustomFieldEdge { 537 | """ 538 | The item at the end of the edge. 539 | """ 540 | node: CustomField! 541 | 542 | """ 543 | A cursor for use in pagination. 544 | """ 545 | cursor: String! 546 | } 547 | 548 | """ 549 | A customer that shops on a store 550 | """ 551 | type Customer { 552 | """ 553 | The ID of the customer. 554 | """ 555 | entityId: Int! 556 | 557 | """ 558 | The company name of the customer. 559 | """ 560 | company: String! 561 | 562 | """ 563 | The customer group id of the customer. 564 | """ 565 | customerGroupId: Int! 566 | 567 | """ 568 | The email address of the customer. 569 | """ 570 | email: String! 571 | 572 | """ 573 | The first name of the customer. 574 | """ 575 | firstName: String! 576 | 577 | """ 578 | The last name of the customer. 579 | """ 580 | lastName: String! 581 | 582 | """ 583 | The notes of the customer. 584 | """ 585 | notes: String! 586 | 587 | """ 588 | The phone number of the customer. 589 | """ 590 | phone: String! 591 | 592 | """ 593 | The tax exempt category of the customer. 594 | """ 595 | taxExemptCategory: String! 596 | 597 | """ 598 | Customer addresses count. 599 | """ 600 | addressCount: Int! 601 | 602 | """ 603 | Customer attributes count. 604 | """ 605 | attributeCount: Int! 606 | 607 | """ 608 | Customer store credit. 609 | """ 610 | storeCredit: [Money!]! 611 | 612 | """ 613 | Customer attributes. 614 | """ 615 | attributes: CustomerAttributes! 616 | } 617 | 618 | """ 619 | A custom, store-specific attribute for a customer 620 | """ 621 | type CustomerAttribute { 622 | """ 623 | The ID of the custom customer attribute 624 | """ 625 | entityId: Int! 626 | 627 | """ 628 | The value of the custom customer attribute 629 | """ 630 | value: String 631 | 632 | """ 633 | The name of the custom customer attribute 634 | """ 635 | name: String! 636 | } 637 | 638 | """ 639 | Custom, store-specific customer attributes 640 | """ 641 | type CustomerAttributes { 642 | attribute( 643 | """ 644 | The ID of the customer attribute 645 | """ 646 | entityId: Int! 647 | ): CustomerAttribute! 648 | } 649 | 650 | """ 651 | A calendar for allowing selection of a date. 652 | """ 653 | type DateFieldOption implements CatalogProductOption { 654 | """ 655 | Unique ID for the option. 656 | """ 657 | entityId: Int! 658 | 659 | """ 660 | Display name for the option. 661 | """ 662 | displayName: String! 663 | 664 | """ 665 | One of the option values is required to be selected for the checkout. 666 | """ 667 | isRequired: Boolean! 668 | } 669 | 670 | scalar DateTime 671 | 672 | """ 673 | Date Time Extended 674 | """ 675 | type DateTimeExtended { 676 | """ 677 | ISO-8601 formatted date in UTC 678 | """ 679 | utc: DateTime! 680 | } 681 | 682 | """ 683 | Display field 684 | """ 685 | type DisplayField { 686 | """ 687 | Short date format. 688 | """ 689 | shortDateFormat: String! 690 | 691 | """ 692 | Extended date format. 693 | """ 694 | extendedDateFormat: String! 695 | } 696 | 697 | """ 698 | A form allowing selection and uploading of a file from the user's local computer. 699 | """ 700 | type FileUploadFieldOption implements CatalogProductOption { 701 | """ 702 | Unique ID for the option. 703 | """ 704 | entityId: Int! 705 | 706 | """ 707 | Display name for the option. 708 | """ 709 | displayName: String! 710 | 711 | """ 712 | One of the option values is required to be selected for the checkout. 713 | """ 714 | isRequired: Boolean! 715 | } 716 | 717 | """ 718 | Image 719 | """ 720 | type Image { 721 | """ 722 | Absolute path to image using store CDN. 723 | """ 724 | url(width: Int!, height: Int): String! 725 | 726 | """ 727 | Absolute path to original image using store CDN. 728 | """ 729 | urlOriginal: String! 730 | 731 | """ 732 | Text description of an image that can be used for SEO and/or accessibility purposes. 733 | """ 734 | altText: String! 735 | 736 | """ 737 | Indicates whether this is the primary image. 738 | """ 739 | isDefault: Boolean! 740 | } 741 | 742 | """ 743 | A connection to a list of items. 744 | """ 745 | type ImageConnection { 746 | """ 747 | Information to aid in pagination. 748 | """ 749 | pageInfo: PageInfo! 750 | 751 | """ 752 | A list of edges. 753 | """ 754 | edges: [ImageEdge] 755 | } 756 | 757 | """ 758 | An edge in a connection. 759 | """ 760 | type ImageEdge { 761 | """ 762 | The item at the end of the edge. 763 | """ 764 | node: Image! 765 | 766 | """ 767 | A cursor for use in pagination. 768 | """ 769 | cursor: String! 770 | } 771 | 772 | """ 773 | An inventory 774 | """ 775 | type Inventory { 776 | """ 777 | Locations 778 | """ 779 | locations( 780 | entityIds: [Int!] 781 | codes: [String!] 782 | typeIds: [String!] 783 | before: String 784 | after: String 785 | first: Int 786 | last: Int 787 | ): LocationConnection! 788 | } 789 | 790 | """ 791 | Inventory By Locations 792 | """ 793 | type InventoryByLocations { 794 | """ 795 | Location id. 796 | """ 797 | locationEntityId: Long! 798 | 799 | """ 800 | Number of available products in stock. 801 | """ 802 | availableToSell: Long! 803 | 804 | """ 805 | Indicates a threshold low-stock level. 806 | """ 807 | warningLevel: Int! 808 | 809 | """ 810 | Indicates whether this product is in stock. 811 | """ 812 | isInStock: Boolean! 813 | } 814 | 815 | """ 816 | A connection to a list of items. 817 | """ 818 | type LocationConnection { 819 | """ 820 | Information to aid in pagination. 821 | """ 822 | pageInfo: PageInfo! 823 | 824 | """ 825 | A list of edges. 826 | """ 827 | edges: [LocationEdge] 828 | } 829 | 830 | """ 831 | An edge in a connection. 832 | """ 833 | type LocationEdge { 834 | """ 835 | The item at the end of the edge. 836 | """ 837 | node: InventoryByLocations! 838 | 839 | """ 840 | A cursor for use in pagination. 841 | """ 842 | cursor: String! 843 | } 844 | 845 | """ 846 | Logo field 847 | """ 848 | type LogoField { 849 | """ 850 | Logo title. 851 | """ 852 | title: String! 853 | 854 | """ 855 | Store logo image. 856 | """ 857 | image: Image! 858 | } 859 | 860 | """ 861 | Measurement 862 | """ 863 | type Measurement { 864 | """ 865 | Unformatted weight measurement value. 866 | """ 867 | value: Float! 868 | 869 | """ 870 | Unit of measurement. 871 | """ 872 | unit: String! 873 | } 874 | 875 | """ 876 | A connection to a list of items. 877 | """ 878 | type MetafieldConnection { 879 | """ 880 | Information to aid in pagination. 881 | """ 882 | pageInfo: PageInfo! 883 | 884 | """ 885 | A list of edges. 886 | """ 887 | edges: [MetafieldEdge] 888 | } 889 | 890 | """ 891 | An edge in a connection. 892 | """ 893 | type MetafieldEdge { 894 | """ 895 | The item at the end of the edge. 896 | """ 897 | node: Metafields! 898 | 899 | """ 900 | A cursor for use in pagination. 901 | """ 902 | cursor: String! 903 | } 904 | 905 | """ 906 | Key/Value pairs of data attached tied to a resource entity (product, brand, category, etc.) 907 | """ 908 | type Metafields { 909 | """ 910 | The ID of an object 911 | """ 912 | id: ID! 913 | 914 | """ 915 | The ID of the metafield when referencing via our backend API. 916 | """ 917 | entityId: Int! 918 | 919 | """ 920 | A label for identifying a metafield data value. 921 | """ 922 | key: String! 923 | 924 | """ 925 | A metafield value. 926 | """ 927 | value: String! 928 | } 929 | 930 | """ 931 | A money object - includes currency code and a money amount 932 | """ 933 | type Money { 934 | """ 935 | Currency code of the current money. 936 | """ 937 | currencyCode: String! 938 | 939 | """ 940 | The amount of money. 941 | """ 942 | value: BigDecimal! 943 | } 944 | 945 | """ 946 | A min and max pair of money objects 947 | """ 948 | type MoneyRange { 949 | """ 950 | Minimum money object. 951 | """ 952 | min: Money! 953 | 954 | """ 955 | Maximum money object. 956 | """ 957 | max: Money! 958 | } 959 | 960 | """ 961 | A multi-line text input field, aka a text box. 962 | """ 963 | type MultiLineTextFieldOption implements CatalogProductOption { 964 | """ 965 | Unique ID for the option. 966 | """ 967 | entityId: Int! 968 | 969 | """ 970 | Display name for the option. 971 | """ 972 | displayName: String! 973 | 974 | """ 975 | One of the option values is required to be selected for the checkout. 976 | """ 977 | isRequired: Boolean! 978 | } 979 | 980 | """ 981 | An option type that has a fixed list of values. 982 | """ 983 | type MultipleChoiceOption implements CatalogProductOption { 984 | """ 985 | The chosen display style for this multiple choice option. 986 | """ 987 | displayStyle: String! 988 | 989 | """ 990 | List of option values. 991 | """ 992 | values( 993 | before: String 994 | after: String 995 | first: Int 996 | last: Int 997 | ): ProductOptionValueConnection! 998 | 999 | """ 1000 | Unique ID for the option. 1001 | """ 1002 | entityId: Int! 1003 | 1004 | """ 1005 | Display name for the option. 1006 | """ 1007 | displayName: String! 1008 | 1009 | """ 1010 | One of the option values is required to be selected for the checkout. 1011 | """ 1012 | isRequired: Boolean! 1013 | } 1014 | 1015 | """ 1016 | A simple multiple choice value comprised of an id and a label. 1017 | """ 1018 | type MultipleChoiceOptionValue implements CatalogProductOptionValue { 1019 | """ 1020 | Unique ID for the option value. 1021 | """ 1022 | entityId: Int! 1023 | 1024 | """ 1025 | Label for the option value. 1026 | """ 1027 | label: String! 1028 | 1029 | """ 1030 | Indicates whether this value is the chosen default selected value. 1031 | """ 1032 | isDefault: Boolean! 1033 | } 1034 | 1035 | """ 1036 | An object with an ID 1037 | """ 1038 | interface Node { 1039 | """ 1040 | The id of the object. 1041 | """ 1042 | id: ID! 1043 | } 1044 | 1045 | """ 1046 | A single line text input field that only accepts numbers. 1047 | """ 1048 | type NumberFieldOption implements CatalogProductOption { 1049 | """ 1050 | Unique ID for the option. 1051 | """ 1052 | entityId: Int! 1053 | 1054 | """ 1055 | Display name for the option. 1056 | """ 1057 | displayName: String! 1058 | 1059 | """ 1060 | One of the option values is required to be selected for the checkout. 1061 | """ 1062 | isRequired: Boolean! 1063 | } 1064 | 1065 | """ 1066 | A connection to a list of items. 1067 | """ 1068 | type OptionConnection { 1069 | """ 1070 | Information to aid in pagination. 1071 | """ 1072 | pageInfo: PageInfo! 1073 | 1074 | """ 1075 | A list of edges. 1076 | """ 1077 | edges: [OptionEdge] 1078 | } 1079 | 1080 | """ 1081 | An edge in a connection. 1082 | """ 1083 | type OptionEdge { 1084 | """ 1085 | The item at the end of the edge. 1086 | """ 1087 | node: ProductOption! 1088 | 1089 | """ 1090 | A cursor for use in pagination. 1091 | """ 1092 | cursor: String! 1093 | } 1094 | 1095 | """ 1096 | A connection to a list of items. 1097 | """ 1098 | type OptionValueConnection { 1099 | """ 1100 | Information to aid in pagination. 1101 | """ 1102 | pageInfo: PageInfo! 1103 | 1104 | """ 1105 | A list of edges. 1106 | """ 1107 | edges: [OptionValueEdge] 1108 | } 1109 | 1110 | """ 1111 | An edge in a connection. 1112 | """ 1113 | type OptionValueEdge { 1114 | """ 1115 | The item at the end of the edge. 1116 | """ 1117 | node: ProductOptionValue! 1118 | 1119 | """ 1120 | A cursor for use in pagination. 1121 | """ 1122 | cursor: String! 1123 | } 1124 | 1125 | input OptionValueId { 1126 | optionEntityId: Int! 1127 | valueEntityId: Int! 1128 | } 1129 | 1130 | """ 1131 | Information about pagination in a connection. 1132 | """ 1133 | type PageInfo { 1134 | """ 1135 | When paginating forwards, are there more items? 1136 | """ 1137 | hasNextPage: Boolean! 1138 | 1139 | """ 1140 | When paginating backwards, are there more items? 1141 | """ 1142 | hasPreviousPage: Boolean! 1143 | 1144 | """ 1145 | When paginating backwards, the cursor to continue. 1146 | """ 1147 | startCursor: String 1148 | 1149 | """ 1150 | When paginating forwards, the cursor to continue. 1151 | """ 1152 | endCursor: String 1153 | } 1154 | 1155 | """ 1156 | The min and max range of prices that apply to this product. 1157 | """ 1158 | type PriceRanges { 1159 | """ 1160 | Product price min/max range. 1161 | """ 1162 | priceRange: MoneyRange! 1163 | 1164 | """ 1165 | Product retail price min/max range. 1166 | """ 1167 | retailPriceRange: MoneyRange 1168 | } 1169 | 1170 | """ 1171 | The various prices that can be set on a product. 1172 | """ 1173 | type Prices { 1174 | """ 1175 | Calculated price of the product. 1176 | """ 1177 | price: Money! 1178 | 1179 | """ 1180 | Sale price of the product. 1181 | """ 1182 | salePrice: Money 1183 | 1184 | """ 1185 | Original price of the product. 1186 | """ 1187 | basePrice: Money 1188 | 1189 | """ 1190 | Retail price of the product. 1191 | """ 1192 | retailPrice: Money 1193 | 1194 | """ 1195 | Minimum advertised price of the product. 1196 | """ 1197 | mapPrice: Money 1198 | 1199 | """ 1200 | Product price min/max range. 1201 | """ 1202 | priceRange: MoneyRange! 1203 | 1204 | """ 1205 | Product retail price min/max range. 1206 | """ 1207 | retailPriceRange: MoneyRange 1208 | 1209 | """ 1210 | The difference between the retail price (MSRP) and the current price, which can be presented to the shopper as their savings. 1211 | """ 1212 | saved: Money 1213 | 1214 | """ 1215 | List of bulk pricing tiers applicable to a product or variant. 1216 | """ 1217 | bulkPricing: [BulkPricingTier!]! 1218 | } 1219 | 1220 | """ 1221 | Product 1222 | """ 1223 | type Product implements Node { 1224 | """ 1225 | The ID of an object 1226 | """ 1227 | id: ID! 1228 | 1229 | """ 1230 | Id of the product. 1231 | """ 1232 | entityId: Int! 1233 | 1234 | """ 1235 | Default product variant when no options are selected. 1236 | """ 1237 | sku: String! 1238 | 1239 | """ 1240 | Relative URL path to product page. 1241 | """ 1242 | path: String! 1243 | 1244 | """ 1245 | Name of the product. 1246 | """ 1247 | name: String! 1248 | 1249 | """ 1250 | Description of the product. 1251 | """ 1252 | description: String! 1253 | 1254 | """ 1255 | Description of the product in plain text. 1256 | """ 1257 | plainTextDescription(characterLimit: Int = 120): String! 1258 | 1259 | """ 1260 | Warranty information of the product. 1261 | """ 1262 | warranty: String! 1263 | 1264 | """ 1265 | Minimum purchasable quantity for this product in a single order. 1266 | """ 1267 | minPurchaseQuantity: Int 1268 | 1269 | """ 1270 | Maximum purchasable quantity for this product in a single order. 1271 | """ 1272 | maxPurchaseQuantity: Int 1273 | 1274 | """ 1275 | Absolute URL path for adding a product to cart. 1276 | """ 1277 | addToCartUrl: String! 1278 | 1279 | """ 1280 | Absolute URL path for adding a product to customer's wishlist. 1281 | """ 1282 | addToWishlistUrl: String! 1283 | 1284 | """ 1285 | Prices object determined by supplied product ID, variant ID, and selected option IDs. 1286 | """ 1287 | prices(includeTax: Boolean = false, currencyCode: currencyCode): Prices 1288 | 1289 | """ 1290 | The minimum and maximum price of this product based on variant pricing and/or modifier price rules. 1291 | """ 1292 | priceRanges(includeTax: Boolean = false): PriceRanges 1293 | @deprecated(reason: "Use priceRanges inside prices node instead.") 1294 | 1295 | """ 1296 | Weight of the product. 1297 | """ 1298 | weight: Measurement 1299 | 1300 | """ 1301 | Height of the product. 1302 | """ 1303 | height: Measurement 1304 | 1305 | """ 1306 | Width of the product. 1307 | """ 1308 | width: Measurement 1309 | 1310 | """ 1311 | Depth of the product. 1312 | """ 1313 | depth: Measurement 1314 | 1315 | """ 1316 | Product options. 1317 | """ 1318 | options( 1319 | before: String 1320 | after: String 1321 | first: Int 1322 | last: Int 1323 | ): OptionConnection! 1324 | 1325 | """ 1326 | Product options. 1327 | """ 1328 | productOptions( 1329 | before: String 1330 | after: String 1331 | first: Int 1332 | last: Int 1333 | ): ProductOptionConnection! 1334 | 1335 | """ 1336 | Summary of the product reviews, includes the total number of reviews submitted and summation of the ratings on the reviews (ratings range from 0-5 per review). 1337 | """ 1338 | reviewSummary: Reviews! 1339 | 1340 | """ 1341 | Type of product, ex: physical, digital 1342 | """ 1343 | type: String! 1344 | 1345 | """ 1346 | The availability state of the product. 1347 | """ 1348 | availability: String! 1349 | @deprecated(reason: "Use status inside availabilityV2 instead.") 1350 | 1351 | """ 1352 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'. 1353 | """ 1354 | availabilityDescription: String! 1355 | @deprecated(reason: "Use description inside availabilityV2 instead.") 1356 | 1357 | """ 1358 | The availability state of the product. 1359 | """ 1360 | availabilityV2: ProductAvailability! 1361 | 1362 | """ 1363 | List of categories associated with the product. 1364 | """ 1365 | categories( 1366 | before: String 1367 | after: String 1368 | first: Int 1369 | last: Int 1370 | ): CategoryConnection! 1371 | 1372 | """ 1373 | Brand associated with the product. 1374 | """ 1375 | brand: Brand 1376 | 1377 | """ 1378 | Variants associated with the product. 1379 | """ 1380 | variants( 1381 | before: String 1382 | after: String 1383 | first: Int 1384 | last: Int 1385 | entityIds: [Int!] = [] 1386 | optionValueIds: [OptionValueId!] = [] 1387 | ): VariantConnection! 1388 | 1389 | """ 1390 | Custom fields of the product. 1391 | """ 1392 | customFields( 1393 | names: [String!] = [] 1394 | before: String 1395 | after: String 1396 | first: Int 1397 | last: Int 1398 | ): CustomFieldConnection! 1399 | 1400 | """ 1401 | A list of the images for a product. 1402 | """ 1403 | images(before: String, after: String, first: Int, last: Int): ImageConnection! 1404 | 1405 | """ 1406 | Default image for a product. 1407 | """ 1408 | defaultImage: Image 1409 | 1410 | """ 1411 | Related products for this product. 1412 | """ 1413 | relatedProducts( 1414 | before: String 1415 | after: String 1416 | first: Int 1417 | last: Int 1418 | ): RelatedProductsConnection! 1419 | 1420 | """ 1421 | Inventory information of the product. 1422 | """ 1423 | inventory: ProductInventory! 1424 | 1425 | """ 1426 | Metafield data related to a product. 1427 | """ 1428 | metafields( 1429 | namespace: String! 1430 | keys: [String!] = [] 1431 | before: String 1432 | after: String 1433 | first: Int 1434 | last: Int 1435 | ): MetafieldConnection! 1436 | 1437 | """ 1438 | Product creation date 1439 | """ 1440 | createdAt: DateTimeExtended! 1441 | @deprecated(reason: "Alpha version. Do not use in production.") 1442 | } 1443 | 1444 | """ 1445 | Product availability 1446 | """ 1447 | interface ProductAvailability { 1448 | """ 1449 | The availability state of the product. 1450 | """ 1451 | status: ProductAvailabilityStatus! 1452 | 1453 | """ 1454 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'. 1455 | """ 1456 | description: String! 1457 | } 1458 | 1459 | """ 1460 | Product availability status 1461 | """ 1462 | enum ProductAvailabilityStatus { 1463 | Available 1464 | Preorder 1465 | Unavailable 1466 | } 1467 | 1468 | """ 1469 | Available Product 1470 | """ 1471 | type ProductAvailable implements ProductAvailability { 1472 | """ 1473 | The availability state of the product. 1474 | """ 1475 | status: ProductAvailabilityStatus! 1476 | 1477 | """ 1478 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'. 1479 | """ 1480 | description: String! 1481 | } 1482 | 1483 | """ 1484 | A connection to a list of items. 1485 | """ 1486 | type ProductConnection { 1487 | """ 1488 | Information to aid in pagination. 1489 | """ 1490 | pageInfo: PageInfo! 1491 | 1492 | """ 1493 | A list of edges. 1494 | """ 1495 | edges: [ProductEdge] 1496 | } 1497 | 1498 | """ 1499 | An edge in a connection. 1500 | """ 1501 | type ProductEdge { 1502 | """ 1503 | The item at the end of the edge. 1504 | """ 1505 | node: Product! 1506 | 1507 | """ 1508 | A cursor for use in pagination. 1509 | """ 1510 | cursor: String! 1511 | } 1512 | 1513 | """ 1514 | Product Inventory Information 1515 | """ 1516 | type ProductInventory { 1517 | """ 1518 | Indicates whether this product is in stock. 1519 | """ 1520 | isInStock: Boolean! 1521 | 1522 | """ 1523 | Indicates whether this product's inventory is being tracked on variant level. If true, you may wish to check the variants node to understand the true inventory of each individual variant, rather than relying on this product-level aggregate to understand how many items may be added to cart. 1524 | """ 1525 | hasVariantInventory: Boolean! 1526 | 1527 | """ 1528 | Aggregated product inventory information. This data may not be available if not set or if the store's Inventory Settings have disabled displaying stock levels on the storefront. 1529 | """ 1530 | aggregated: AggregatedInventory 1531 | } 1532 | 1533 | """ 1534 | Product Option 1535 | """ 1536 | type ProductOption { 1537 | """ 1538 | Unique ID for the option. 1539 | """ 1540 | entityId: Int! 1541 | 1542 | """ 1543 | Display name for the option. 1544 | """ 1545 | displayName: String! 1546 | 1547 | """ 1548 | One of the option values is required to be selected for the checkout. 1549 | """ 1550 | isRequired: Boolean! 1551 | 1552 | """ 1553 | Option values. 1554 | """ 1555 | values( 1556 | before: String 1557 | after: String 1558 | first: Int 1559 | last: Int 1560 | ): OptionValueConnection! 1561 | } 1562 | 1563 | """ 1564 | A connection to a list of items. 1565 | """ 1566 | type ProductOptionConnection { 1567 | """ 1568 | Information to aid in pagination. 1569 | """ 1570 | pageInfo: PageInfo! 1571 | 1572 | """ 1573 | A list of edges. 1574 | """ 1575 | edges: [ProductOptionEdge] 1576 | } 1577 | 1578 | """ 1579 | An edge in a connection. 1580 | """ 1581 | type ProductOptionEdge { 1582 | """ 1583 | The item at the end of the edge. 1584 | """ 1585 | node: CatalogProductOption! 1586 | 1587 | """ 1588 | A cursor for use in pagination. 1589 | """ 1590 | cursor: String! 1591 | } 1592 | 1593 | """ 1594 | Product Option Value 1595 | """ 1596 | type ProductOptionValue { 1597 | """ 1598 | Unique ID for the option value. 1599 | """ 1600 | entityId: Int! 1601 | 1602 | """ 1603 | Label for the option value. 1604 | """ 1605 | label: String! 1606 | } 1607 | 1608 | """ 1609 | A connection to a list of items. 1610 | """ 1611 | type ProductOptionValueConnection { 1612 | """ 1613 | Information to aid in pagination. 1614 | """ 1615 | pageInfo: PageInfo! 1616 | 1617 | """ 1618 | A list of edges. 1619 | """ 1620 | edges: [ProductOptionValueEdge] 1621 | } 1622 | 1623 | """ 1624 | An edge in a connection. 1625 | """ 1626 | type ProductOptionValueEdge { 1627 | """ 1628 | The item at the end of the edge. 1629 | """ 1630 | node: CatalogProductOptionValue! 1631 | 1632 | """ 1633 | A cursor for use in pagination. 1634 | """ 1635 | cursor: String! 1636 | } 1637 | 1638 | """ 1639 | A Product PickList Value - a product to be mapped to the base product if selected. 1640 | """ 1641 | type ProductPickListOptionValue implements CatalogProductOptionValue { 1642 | """ 1643 | The ID of the product associated with this option value. 1644 | """ 1645 | productId: Int! 1646 | 1647 | """ 1648 | Unique ID for the option value. 1649 | """ 1650 | entityId: Int! 1651 | 1652 | """ 1653 | Label for the option value. 1654 | """ 1655 | label: String! 1656 | 1657 | """ 1658 | Indicates whether this value is the chosen default selected value. 1659 | """ 1660 | isDefault: Boolean! 1661 | } 1662 | 1663 | """ 1664 | PreOrder Product 1665 | """ 1666 | type ProductPreOrder implements ProductAvailability { 1667 | """ 1668 | The message to be shown in the store when a product is put into the pre-order availability state, e.g. "Expected release date is %%DATE%%" 1669 | """ 1670 | message: String 1671 | 1672 | """ 1673 | Product release date 1674 | """ 1675 | willBeReleasedAt: DateTimeExtended 1676 | 1677 | """ 1678 | The availability state of the product. 1679 | """ 1680 | status: ProductAvailabilityStatus! 1681 | 1682 | """ 1683 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'. 1684 | """ 1685 | description: String! 1686 | } 1687 | 1688 | """ 1689 | Unavailable Product 1690 | """ 1691 | type ProductUnavailable implements ProductAvailability { 1692 | """ 1693 | The message to be shown in the store when "Call for pricing" is enabled for this product, e.g. "Contact us at 555-5555" 1694 | """ 1695 | message: String 1696 | 1697 | """ 1698 | The availability state of the product. 1699 | """ 1700 | status: ProductAvailabilityStatus! 1701 | 1702 | """ 1703 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'. 1704 | """ 1705 | description: String! 1706 | } 1707 | 1708 | type Query { 1709 | site: Site! 1710 | 1711 | """ 1712 | The currently logged in customer. 1713 | """ 1714 | customer: Customer 1715 | 1716 | """ 1717 | Fetches an object given its ID 1718 | """ 1719 | node( 1720 | """ 1721 | The ID of an object 1722 | """ 1723 | id: ID! 1724 | ): Node 1725 | inventory: Inventory! 1726 | @deprecated(reason: "Alpha version. Do not use in production.") 1727 | } 1728 | 1729 | """ 1730 | A connection to a list of items. 1731 | """ 1732 | type RelatedProductsConnection { 1733 | """ 1734 | Information to aid in pagination. 1735 | """ 1736 | pageInfo: PageInfo! 1737 | 1738 | """ 1739 | A list of edges. 1740 | """ 1741 | edges: [RelatedProductsEdge] 1742 | } 1743 | 1744 | """ 1745 | An edge in a connection. 1746 | """ 1747 | type RelatedProductsEdge { 1748 | """ 1749 | The item at the end of the edge. 1750 | """ 1751 | node: Product! 1752 | 1753 | """ 1754 | A cursor for use in pagination. 1755 | """ 1756 | cursor: String! 1757 | } 1758 | 1759 | """ 1760 | Review Rating Summary 1761 | """ 1762 | type Reviews { 1763 | """ 1764 | Total number of reviews on product. 1765 | """ 1766 | numberOfReviews: Int! 1767 | 1768 | """ 1769 | Summation of rating scores from each review. 1770 | """ 1771 | summationOfRatings: Int! 1772 | } 1773 | 1774 | """ 1775 | route 1776 | """ 1777 | type Route { 1778 | """ 1779 | node 1780 | """ 1781 | node: Node 1782 | } 1783 | 1784 | """ 1785 | Store settings information from the control panel. 1786 | """ 1787 | type Settings { 1788 | """ 1789 | The name of the store. 1790 | """ 1791 | storeName: String! 1792 | 1793 | """ 1794 | The hash of the store. 1795 | """ 1796 | storeHash: String! 1797 | 1798 | """ 1799 | The current store status. 1800 | """ 1801 | status: StorefrontStatusType! 1802 | 1803 | """ 1804 | Logo information for the store. 1805 | """ 1806 | logo: LogoField! 1807 | 1808 | """ 1809 | Contact information for the store. 1810 | """ 1811 | contact: ContactField 1812 | 1813 | """ 1814 | Store urls. 1815 | """ 1816 | url: UrlField! 1817 | 1818 | """ 1819 | Store display format information. 1820 | """ 1821 | display: DisplayField! 1822 | 1823 | """ 1824 | Channel ID. 1825 | """ 1826 | channelId: Long! 1827 | } 1828 | 1829 | """ 1830 | A site 1831 | """ 1832 | type Site { 1833 | categoryTree: [CategoryTreeItem!]! 1834 | 1835 | """ 1836 | Details of the brand. 1837 | """ 1838 | brands( 1839 | before: String 1840 | after: String 1841 | first: Int 1842 | last: Int 1843 | productEntityIds: [Int!] = [] 1844 | ): BrandConnection! 1845 | 1846 | """ 1847 | Details of the products. 1848 | """ 1849 | products( 1850 | before: String 1851 | after: String 1852 | first: Int 1853 | last: Int 1854 | ids: [ID!] = [] 1855 | entityIds: [Int!] = [] 1856 | ): ProductConnection! 1857 | 1858 | """ 1859 | Details of the newest products. 1860 | """ 1861 | newestProducts( 1862 | before: String 1863 | after: String 1864 | first: Int 1865 | last: Int 1866 | ): ProductConnection! 1867 | 1868 | """ 1869 | Details of the best selling products. 1870 | """ 1871 | bestSellingProducts( 1872 | before: String 1873 | after: String 1874 | first: Int 1875 | last: Int 1876 | ): ProductConnection! 1877 | 1878 | """ 1879 | Details of the featured products. 1880 | """ 1881 | featuredProducts( 1882 | before: String 1883 | after: String 1884 | first: Int 1885 | last: Int 1886 | ): ProductConnection! 1887 | 1888 | """ 1889 | A single product object with variant pricing overlay capabilities. 1890 | """ 1891 | product( 1892 | id: ID 1893 | entityId: Int 1894 | variantEntityId: Int 1895 | optionValueIds: [OptionValueId!] = [] 1896 | sku: String 1897 | ): Product 1898 | 1899 | """ 1900 | Route for a node 1901 | """ 1902 | route(path: String!): Route! 1903 | 1904 | """ 1905 | Store settings. 1906 | """ 1907 | settings: Settings 1908 | } 1909 | 1910 | """ 1911 | Storefront Mode 1912 | """ 1913 | enum StorefrontStatusType { 1914 | LAUNCHED 1915 | MAINTENANCE 1916 | PRE_LAUNCH 1917 | HIBERNATION 1918 | } 1919 | 1920 | """ 1921 | A swatch option value - swatch values can be associated with a list of hexidecimal colors or an image. 1922 | """ 1923 | type SwatchOptionValue implements CatalogProductOptionValue { 1924 | """ 1925 | List of up to 3 hex encoded colors to associate with a swatch value. 1926 | """ 1927 | hexColors: [String!]! 1928 | 1929 | """ 1930 | Absolute path of a swatch texture image. 1931 | """ 1932 | imageUrl(width: Int!, height: Int): String 1933 | 1934 | """ 1935 | Unique ID for the option value. 1936 | """ 1937 | entityId: Int! 1938 | 1939 | """ 1940 | Label for the option value. 1941 | """ 1942 | label: String! 1943 | 1944 | """ 1945 | Indicates whether this value is the chosen default selected value. 1946 | """ 1947 | isDefault: Boolean! 1948 | } 1949 | 1950 | """ 1951 | A single line text input field. 1952 | """ 1953 | type TextFieldOption implements CatalogProductOption { 1954 | """ 1955 | Unique ID for the option. 1956 | """ 1957 | entityId: Int! 1958 | 1959 | """ 1960 | Display name for the option. 1961 | """ 1962 | displayName: String! 1963 | 1964 | """ 1965 | One of the option values is required to be selected for the checkout. 1966 | """ 1967 | isRequired: Boolean! 1968 | } 1969 | 1970 | """ 1971 | Url field 1972 | """ 1973 | type UrlField { 1974 | """ 1975 | Store url. 1976 | """ 1977 | vanityUrl: String! 1978 | 1979 | """ 1980 | CDN url to fetch assets. 1981 | """ 1982 | cdnUrl: String! 1983 | } 1984 | 1985 | """ 1986 | Variant 1987 | """ 1988 | type Variant implements Node { 1989 | """ 1990 | The ID of an object 1991 | """ 1992 | id: ID! 1993 | 1994 | """ 1995 | Id of the variant. 1996 | """ 1997 | entityId: Int! 1998 | 1999 | """ 2000 | Sku of the variant. 2001 | """ 2002 | sku: String! 2003 | 2004 | """ 2005 | The variant's weight. If a weight was not explicitly specified on the variant, this will be the product's weight. 2006 | """ 2007 | weight: Measurement 2008 | 2009 | """ 2010 | The variant's height. If a height was not explicitly specified on the variant, this will be the product's height. 2011 | """ 2012 | height: Measurement 2013 | 2014 | """ 2015 | The variant's width. If a width was not explicitly specified on the variant, this will be the product's width. 2016 | """ 2017 | width: Measurement 2018 | 2019 | """ 2020 | The variant's depth. If a depth was not explicitly specified on the variant, this will be the product's depth. 2021 | """ 2022 | depth: Measurement 2023 | 2024 | """ 2025 | The options which define a variant. 2026 | """ 2027 | options( 2028 | before: String 2029 | after: String 2030 | first: Int 2031 | last: Int 2032 | ): OptionConnection! 2033 | 2034 | """ 2035 | Product options that compose this variant. 2036 | """ 2037 | productOptions( 2038 | before: String 2039 | after: String 2040 | first: Int 2041 | last: Int 2042 | ): ProductOptionConnection! 2043 | 2044 | """ 2045 | Default image for a variant. 2046 | """ 2047 | defaultImage: Image 2048 | 2049 | """ 2050 | Variant prices 2051 | """ 2052 | prices(includeTax: Boolean = false, currencyCode: currencyCode): Prices 2053 | 2054 | """ 2055 | Variant inventory 2056 | """ 2057 | inventory: VariantInventory 2058 | 2059 | """ 2060 | Metafield data related to a variant. 2061 | """ 2062 | metafields( 2063 | namespace: String! 2064 | keys: [String!] = [] 2065 | before: String 2066 | after: String 2067 | first: Int 2068 | last: Int 2069 | ): MetafieldConnection! 2070 | } 2071 | 2072 | """ 2073 | A connection to a list of items. 2074 | """ 2075 | type VariantConnection { 2076 | """ 2077 | Information to aid in pagination. 2078 | """ 2079 | pageInfo: PageInfo! 2080 | 2081 | """ 2082 | A list of edges. 2083 | """ 2084 | edges: [VariantEdge] 2085 | } 2086 | 2087 | """ 2088 | An edge in a connection. 2089 | """ 2090 | type VariantEdge { 2091 | """ 2092 | The item at the end of the edge. 2093 | """ 2094 | node: Variant! 2095 | 2096 | """ 2097 | A cursor for use in pagination. 2098 | """ 2099 | cursor: String! 2100 | } 2101 | 2102 | """ 2103 | Variant Inventory 2104 | """ 2105 | type VariantInventory { 2106 | """ 2107 | Aggregated product variant inventory information. This data may not be available if not set or if the store's Inventory Settings have disabled displaying stock levels on the storefront. 2108 | """ 2109 | aggregated: Aggregated 2110 | 2111 | """ 2112 | Indicates whether this product is in stock. 2113 | """ 2114 | isInStock: Boolean! 2115 | 2116 | """ 2117 | Inventory by locations. 2118 | """ 2119 | byLocation( 2120 | locationEntityIds: [Int!] = [] 2121 | before: String 2122 | after: String 2123 | first: Int 2124 | last: Int 2125 | ): LocationConnection 2126 | } 2127 | 2128 | """ 2129 | Please select a currency 2130 | """ 2131 | enum currencyCode { 2132 | ADP 2133 | AED 2134 | AFA 2135 | AFN 2136 | ALK 2137 | ALL 2138 | AMD 2139 | ANG 2140 | AOA 2141 | AOK 2142 | AON 2143 | AOR 2144 | ARA 2145 | ARL 2146 | ARM 2147 | ARP 2148 | ARS 2149 | ATS 2150 | AUD 2151 | AWG 2152 | AZM 2153 | AZN 2154 | BAD 2155 | BAM 2156 | BAN 2157 | BBD 2158 | BDT 2159 | BEC 2160 | BEF 2161 | BEL 2162 | BGL 2163 | BGM 2164 | BGN 2165 | BGO 2166 | BHD 2167 | BIF 2168 | BMD 2169 | BND 2170 | BOB 2171 | BOL 2172 | BOP 2173 | BOV 2174 | BRB 2175 | BRC 2176 | BRE 2177 | BRL 2178 | BRN 2179 | BRR 2180 | BRZ 2181 | BSD 2182 | BTN 2183 | BUK 2184 | BWP 2185 | BYB 2186 | BYR 2187 | BZD 2188 | CAD 2189 | CDF 2190 | CHE 2191 | CHF 2192 | CHW 2193 | CLE 2194 | CLF 2195 | CLP 2196 | CNX 2197 | CNY 2198 | COP 2199 | COU 2200 | CRC 2201 | CSD 2202 | CSK 2203 | CVE 2204 | CYP 2205 | CZK 2206 | DDM 2207 | DEM 2208 | DJF 2209 | DKK 2210 | DOP 2211 | DZD 2212 | ECS 2213 | ECV 2214 | EEK 2215 | EGP 2216 | ERN 2217 | ESA 2218 | ESB 2219 | ESP 2220 | ETB 2221 | EUR 2222 | FIM 2223 | FJD 2224 | FKP 2225 | FRF 2226 | GBP 2227 | GEK 2228 | GEL 2229 | GHC 2230 | GHS 2231 | GIP 2232 | GMD 2233 | GNF 2234 | GNS 2235 | GQE 2236 | GRD 2237 | GTQ 2238 | GWE 2239 | GWP 2240 | GYD 2241 | HKD 2242 | HNL 2243 | HRD 2244 | HRK 2245 | HTG 2246 | HUF 2247 | IDR 2248 | IEP 2249 | ILP 2250 | ILR 2251 | ILS 2252 | INR 2253 | IQD 2254 | ISJ 2255 | ISK 2256 | ITL 2257 | JMD 2258 | JOD 2259 | JPY 2260 | KES 2261 | KGS 2262 | KHR 2263 | KMF 2264 | KRH 2265 | KRO 2266 | KRW 2267 | KWD 2268 | KYD 2269 | KZT 2270 | LAK 2271 | LBP 2272 | LKR 2273 | LRD 2274 | LSL 2275 | LTL 2276 | LTT 2277 | LUC 2278 | LUF 2279 | LUL 2280 | LVL 2281 | LVR 2282 | LYD 2283 | MAD 2284 | MAF 2285 | MCF 2286 | MDC 2287 | MDL 2288 | MGA 2289 | MGF 2290 | MKD 2291 | MKN 2292 | MLF 2293 | MMK 2294 | MNT 2295 | MOP 2296 | MRO 2297 | MTL 2298 | MTP 2299 | MUR 2300 | MVP 2301 | MVR 2302 | MWK 2303 | MXN 2304 | MXP 2305 | MXV 2306 | MYR 2307 | MZE 2308 | MZM 2309 | MZN 2310 | NAD 2311 | NGN 2312 | NIC 2313 | NIO 2314 | NLG 2315 | NOK 2316 | NPR 2317 | NZD 2318 | OMR 2319 | PAB 2320 | PEI 2321 | PEN 2322 | PES 2323 | PGK 2324 | PHP 2325 | PKR 2326 | PLN 2327 | PLZ 2328 | PTE 2329 | PYG 2330 | QAR 2331 | RHD 2332 | ROL 2333 | RON 2334 | RSD 2335 | RUB 2336 | RUR 2337 | RWF 2338 | SAR 2339 | SBD 2340 | SCR 2341 | SDD 2342 | SDG 2343 | SDP 2344 | SEK 2345 | SGD 2346 | SHP 2347 | SIT 2348 | SKK 2349 | SLL 2350 | SOS 2351 | SRD 2352 | SRG 2353 | SSP 2354 | STD 2355 | SUR 2356 | SVC 2357 | SYP 2358 | SZL 2359 | THB 2360 | TJR 2361 | TJS 2362 | TMM 2363 | TMT 2364 | TND 2365 | TOP 2366 | TPE 2367 | TRL 2368 | TRY 2369 | TTD 2370 | TWD 2371 | TZS 2372 | UAH 2373 | UAK 2374 | UGS 2375 | UGX 2376 | USD 2377 | USN 2378 | USS 2379 | UYI 2380 | UYP 2381 | UYU 2382 | UZS 2383 | VEB 2384 | VEF 2385 | VND 2386 | VNN 2387 | VUV 2388 | WST 2389 | XAF 2390 | XCD 2391 | XEU 2392 | XFO 2393 | XFU 2394 | XOF 2395 | XPF 2396 | XRE 2397 | YDD 2398 | YER 2399 | YUD 2400 | YUM 2401 | YUN 2402 | YUR 2403 | ZAL 2404 | ZAR 2405 | ZMK 2406 | ZMW 2407 | ZRN 2408 | ZRZ 2409 | ZWD 2410 | ZWL 2411 | ZWR 2412 | } 2413 | 2414 | """ 2415 | The `BigDecimal` scalar type represents signed fractional values with arbitrary precision. 2416 | """ 2417 | scalar BigDecimal 2418 | 2419 | """ 2420 | The `Long` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1. 2421 | """ 2422 | scalar Long 2423 | -------------------------------------------------------------------------------- /example /scripts/generate-definitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates definitions for REST API endpoints that are being 3 | * used by ../api using https://github.com/drwpow/swagger-to-ts 4 | */ 5 | const { readFileSync, promises } = require('fs') 6 | const path = require('path') 7 | const fetch = require('node-fetch') 8 | const swaggerToTS = require('@manifoldco/swagger-to-ts').default 9 | 10 | async function getSchema(filename) { 11 | const url = `https://next-api.stoplight.io/projects/8433/files/${filename}` 12 | const res = await fetch(url) 13 | 14 | if (!res.ok) { 15 | throw new Error(`Request failed with ${res.status}: ${res.statusText}`) 16 | } 17 | 18 | return res.json() 19 | } 20 | 21 | const schemas = Object.entries({ 22 | '../api/definitions/catalog.ts': 23 | 'BigCommerce_Catalog_API.oas2.yml?ref=version%2F20.930', 24 | '../api/definitions/store-content.ts': 25 | 'BigCommerce_Store_Content_API.oas2.yml?ref=version%2F20.930', 26 | '../api/definitions/wishlist.ts': 27 | 'BigCommerce_Wishlist_API.oas2.yml?ref=version%2F20.930', 28 | // swagger-to-ts is not working for the schema of the cart API 29 | // '../api/definitions/cart.ts': 30 | // 'BigCommerce_Server_to_Server_Cart_API.oas2.yml', 31 | }) 32 | 33 | async function writeDefinitions() { 34 | const ops = schemas.map(async ([dest, filename]) => { 35 | const destination = path.join(__dirname, dest) 36 | const schema = await getSchema(filename) 37 | const definition = swaggerToTS(schema.content, { 38 | prettierConfig: 'package.json', 39 | }) 40 | 41 | await promises.writeFile(destination, definition) 42 | 43 | console.log(`✔️ Added definitions for: ${dest}`) 44 | }) 45 | 46 | await Promise.all(ops) 47 | } 48 | 49 | writeDefinitions() 50 | -------------------------------------------------------------------------------- /example /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { RequestInit, Response } from '@vercel/fetch' 2 | 3 | export interface CommerceAPIConfig { 4 | locale?: string 5 | commerceUrl: string 6 | apiToken: string 7 | cartCookie: string 8 | cartCookieMaxAge: number 9 | customerCookie: string 10 | fetch<Data = any, Variables = any>( 11 | query: string, 12 | queryData?: CommerceAPIFetchOptions<Variables>, 13 | fetchOptions?: RequestInit 14 | ): Promise<GraphQLFetcherResult<Data>> 15 | } 16 | 17 | export type GraphQLFetcher< 18 | Data extends GraphQLFetcherResult = GraphQLFetcherResult, 19 | Variables = any 20 | > = ( 21 | query: string, 22 | queryData?: CommerceAPIFetchOptions<Variables>, 23 | fetchOptions?: RequestInit 24 | ) => Promise<Data> 25 | 26 | export interface GraphQLFetcherResult<Data = any> { 27 | data: Data 28 | res: Response 29 | } 30 | 31 | export interface CommerceAPIFetchOptions<Variables> { 32 | variables?: Variables 33 | preview?: boolean 34 | } 35 | 36 | // TODO: define interfaces for all the available operations and API endpoints 37 | -------------------------------------------------------------------------------- /example /src/cart/use-add-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useAddItem = useAction 4 | 5 | export default useAddItem 6 | -------------------------------------------------------------------------------- /example /src/cart/use-cart-actions.tsx: -------------------------------------------------------------------------------- 1 | import type { HookFetcher, HookFetcherOptions } from '../utils/types' 2 | import useAddItem from './use-add-item' 3 | import useRemoveItem from './use-remove-item' 4 | import useUpdateItem from './use-update-item' 5 | 6 | // This hook is probably not going to be used, but it's here 7 | // to show how a commerce should be structuring it 8 | export default function useCartActions<T, Input>( 9 | options: HookFetcherOptions, 10 | fetcher: HookFetcher<T, Input> 11 | ) { 12 | const addItem = useAddItem<T, Input>(options, fetcher) 13 | const updateItem = useUpdateItem<T, Input>(options, fetcher) 14 | const removeItem = useRemoveItem<T, Input>(options, fetcher) 15 | 16 | return { addItem, updateItem, removeItem } 17 | } 18 | -------------------------------------------------------------------------------- /example /src/cart/use-cart.tsx: -------------------------------------------------------------------------------- 1 | import type { responseInterface } from 'swr' 2 | import Cookies from 'js-cookie' 3 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' 4 | import useData, { SwrOptions } from '../utils/use-data' 5 | import { useCommerce } from '..' 6 | 7 | export type CartResponse<Result> = responseInterface<Result, Error> & { 8 | isEmpty: boolean 9 | } 10 | 11 | export type CartInput = { 12 | cartId: string | undefined 13 | } 14 | 15 | export default function useCart<Result>( 16 | options: HookFetcherOptions, 17 | input: HookInput, 18 | fetcherFn: HookFetcher<Result, CartInput>, 19 | swrOptions?: SwrOptions<Result, CartInput> 20 | ) { 21 | const { cartCookie } = useCommerce() 22 | 23 | const fetcher: typeof fetcherFn = (options, input, fetch) => { 24 | input.cartId = Cookies.get(cartCookie) 25 | return fetcherFn(options, input, fetch) 26 | } 27 | 28 | const response = useData(options, input, fetcher, swrOptions) 29 | 30 | return Object.assign(response, { isEmpty: true }) as CartResponse<Result> 31 | } 32 | -------------------------------------------------------------------------------- /example /src/cart/use-remove-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useRemoveItem = useAction 4 | 5 | export default useRemoveItem 6 | -------------------------------------------------------------------------------- /example /src/cart/use-update-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useUpdateItem = useAction 4 | 5 | export default useUpdateItem 6 | -------------------------------------------------------------------------------- /example /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ReactNode, 3 | MutableRefObject, 4 | createContext, 5 | useContext, 6 | useMemo, 7 | useRef, 8 | } from 'react' 9 | import * as React from 'react' 10 | import { Fetcher } from './utils/types' 11 | 12 | const Commerce = createContext<CommerceContextValue | {}>({}) 13 | 14 | export type CommerceProps = { 15 | children?: ReactNode 16 | config: CommerceConfig 17 | } 18 | 19 | export type CommerceConfig = { fetcher: Fetcher<any> } & Omit< 20 | CommerceContextValue, 21 | 'fetcherRef' 22 | > 23 | 24 | export type CommerceContextValue = { 25 | fetcherRef: MutableRefObject<Fetcher<any>> 26 | locale: string 27 | cartCookie: string 28 | } 29 | 30 | export function CommerceProvider({ children, config }: CommerceProps) { 31 | if (!config) { 32 | throw new Error('CommerceProvider requires a valid config object') 33 | } 34 | 35 | const fetcherRef = useRef(config.fetcher) 36 | // Because the config is an object, if the parent re-renders this provider 37 | // will re-render every consumer unless we memoize the config 38 | const cfg = useMemo( 39 | () => ({ 40 | fetcherRef, 41 | locale: config.locale, 42 | cartCookie: config.cartCookie, 43 | }), 44 | [config.locale, config.cartCookie] 45 | ) 46 | 47 | return <Commerce.Provider value={cfg}>{children}</Commerce.Provider> 48 | } 49 | 50 | export function useCommerce<T extends CommerceContextValue>() { 51 | return useContext(Commerce) as T 52 | } 53 | -------------------------------------------------------------------------------- /example /src/products/use-search.tsx: -------------------------------------------------------------------------------- 1 | import useData from '../utils/use-data' 2 | 3 | const useSearch = useData 4 | 5 | export default useSearch 6 | -------------------------------------------------------------------------------- /example /src/use-customer.tsx: -------------------------------------------------------------------------------- 1 | import useData from './utils/use-data' 2 | 3 | const useCustomer = useData 4 | 5 | export default useCustomer 6 | -------------------------------------------------------------------------------- /example /src/use-login.tsx: -------------------------------------------------------------------------------- 1 | import useAction from './utils/use-action' 2 | 3 | const useLogin = useAction 4 | 5 | export default useLogin 6 | -------------------------------------------------------------------------------- /example /src/use-logout.tsx: -------------------------------------------------------------------------------- 1 | import useAction from './utils/use-action' 2 | 3 | const useLogout = useAction 4 | 5 | export default useLogout 6 | -------------------------------------------------------------------------------- /example /src/use-price.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useCommerce } from '.' 3 | 4 | export function formatPrice({ 5 | amount, 6 | currencyCode, 7 | locale, 8 | }: { 9 | amount: number 10 | currencyCode: string 11 | locale: string 12 | }) { 13 | const formatCurrency = new Intl.NumberFormat(locale, { 14 | style: 'currency', 15 | currency: currencyCode, 16 | }) 17 | 18 | return formatCurrency.format(amount) 19 | } 20 | 21 | export function formatVariantPrice({ 22 | amount, 23 | baseAmount, 24 | currencyCode, 25 | locale, 26 | }: { 27 | baseAmount: number 28 | amount: number 29 | currencyCode: string 30 | locale: string 31 | }) { 32 | const hasDiscount = baseAmount > amount 33 | const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' }) 34 | const discount = hasDiscount 35 | ? formatDiscount.format((baseAmount - amount) / baseAmount) 36 | : null 37 | 38 | const price = formatPrice({ amount, currencyCode, locale }) 39 | const basePrice = hasDiscount 40 | ? formatPrice({ amount: baseAmount, currencyCode, locale }) 41 | : null 42 | 43 | return { price, basePrice, discount } 44 | } 45 | 46 | export default function usePrice( 47 | data?: { 48 | amount: number 49 | baseAmount?: number 50 | currencyCode: string 51 | } | null 52 | ) { 53 | const { amount, baseAmount, currencyCode } = data ?? {} 54 | const { locale } = useCommerce() 55 | const value = useMemo(() => { 56 | if (typeof amount !== 'number' || !currencyCode) return '' 57 | 58 | return baseAmount 59 | ? formatVariantPrice({ amount, baseAmount, currencyCode, locale }) 60 | : formatPrice({ amount, currencyCode, locale }) 61 | }, [amount, baseAmount, currencyCode]) 62 | 63 | return typeof value === 'string' ? { price: value } : value 64 | } 65 | -------------------------------------------------------------------------------- /example /src/use-signup.tsx: -------------------------------------------------------------------------------- 1 | import useAction from './utils/use-action' 2 | 3 | const useSignup = useAction 4 | 5 | export default useSignup 6 | -------------------------------------------------------------------------------- /example /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export type ErrorData = { 2 | message: string 3 | code?: string 4 | } 5 | 6 | export type ErrorProps = { 7 | code?: string 8 | } & ( 9 | | { message: string; errors?: never } 10 | | { message?: never; errors: ErrorData[] } 11 | ) 12 | 13 | export class CommerceError extends Error { 14 | code?: string 15 | errors: ErrorData[] 16 | 17 | constructor({ message, code, errors }: ErrorProps) { 18 | const error: ErrorData = message 19 | ? { message, ...(code ? { code } : {}) } 20 | : errors![0] 21 | 22 | super(error.message) 23 | this.errors = message ? [error] : errors! 24 | 25 | if (error.code) this.code = error.code 26 | } 27 | } 28 | 29 | export class FetcherError extends CommerceError { 30 | status: number 31 | 32 | constructor( 33 | options: { 34 | status: number 35 | } & ErrorProps 36 | ) { 37 | super(options) 38 | this.status = options.status 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | // Core fetcher added by CommerceProvider 2 | export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T> 3 | 4 | export type FetcherOptions = { 5 | url?: string 6 | query?: string 7 | method?: string 8 | variables?: any 9 | body?: any 10 | } 11 | 12 | export type HookFetcher<Result, Input = null> = ( 13 | options: HookFetcherOptions | null, 14 | input: Input, 15 | fetch: <T = Result>(options: FetcherOptions) => Promise<T> 16 | ) => Result | Promise<Result> 17 | 18 | export type HookFetcherOptions = { 19 | query?: string 20 | url?: string 21 | method?: string 22 | } 23 | 24 | export type HookInput = [string, string | number | boolean | undefined][] 25 | -------------------------------------------------------------------------------- /example /src/utils/use-action.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { HookFetcher, HookFetcherOptions } from './types' 3 | import { useCommerce } from '..' 4 | 5 | export default function useAction<T, Input = null>( 6 | options: HookFetcherOptions, 7 | fetcher: HookFetcher<T, Input> 8 | ) { 9 | const { fetcherRef } = useCommerce() 10 | 11 | return useCallback( 12 | (input: Input) => fetcher(options, input, fetcherRef.current), 13 | [fetcher] 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /example /src/utils/use-data.tsx: -------------------------------------------------------------------------------- 1 | import useSWR, { ConfigInterface, responseInterface } from 'swr' 2 | import type { HookInput, HookFetcher, HookFetcherOptions } from './types' 3 | import { CommerceError } from './errors' 4 | import { useCommerce } from '..' 5 | 6 | export type SwrOptions<Result, Input = null> = ConfigInterface< 7 | Result, 8 | CommerceError, 9 | HookFetcher<Result, Input> 10 | > 11 | 12 | export type UseData = <Result = any, Input = null>( 13 | options: HookFetcherOptions | (() => HookFetcherOptions | null), 14 | input: HookInput, 15 | fetcherFn: HookFetcher<Result, Input>, 16 | swrOptions?: SwrOptions<Result, Input> 17 | ) => responseInterface<Result, CommerceError> 18 | 19 | const useData: UseData = (options, input, fetcherFn, swrOptions) => { 20 | const { fetcherRef } = useCommerce() 21 | const fetcher = async ( 22 | url?: string, 23 | query?: string, 24 | method?: string, 25 | ...args: any[] 26 | ) => { 27 | try { 28 | return await fetcherFn( 29 | { url, query, method }, 30 | // Transform the input array into an object 31 | args.reduce((obj, val, i) => { 32 | obj[input[i][0]!] = val 33 | return obj 34 | }, {}), 35 | fetcherRef.current 36 | ) 37 | } catch (error) { 38 | // SWR will not log errors, but any error that's not an instance 39 | // of CommerceError is not welcomed by this hook 40 | if (!(error instanceof CommerceError)) { 41 | console.error(error) 42 | } 43 | throw error 44 | } 45 | } 46 | const response = useSWR( 47 | () => { 48 | const opts = typeof options === 'function' ? options() : options 49 | return opts 50 | ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])] 51 | : null 52 | }, 53 | fetcher, 54 | swrOptions 55 | ) 56 | 57 | return response 58 | } 59 | 60 | export default useData 61 | -------------------------------------------------------------------------------- /example /src/wishlist/use-add-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useAddItem = useAction 4 | 5 | export default useAddItem 6 | -------------------------------------------------------------------------------- /example /src/wishlist/use-remove-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useRemoveItem = useAction 4 | 5 | export default useRemoveItem 6 | -------------------------------------------------------------------------------- /example /src/wishlist/use-wishlist.tsx: -------------------------------------------------------------------------------- 1 | import type { responseInterface } from 'swr' 2 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' 3 | import useData, { SwrOptions } from '../utils/use-data' 4 | 5 | export type WishlistResponse<Result> = responseInterface<Result, Error> & { 6 | isEmpty: boolean 7 | } 8 | 9 | export default function useWishlist<Result, Input = null>( 10 | options: HookFetcherOptions, 11 | input: HookInput, 12 | fetcherFn: HookFetcher<Result, Input>, 13 | swrOptions?: SwrOptions<Result, Input> 14 | ) { 15 | const response = useData(options, input, fetcherFn, swrOptions) 16 | return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result> 17 | } 18 | -------------------------------------------------------------------------------- /example /wishlist/use-add-item.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { HookFetcher } from '../src/utils/types' 3 | import { CommerceError } from '../src/utils/errors' 4 | import useWishlistAddItem from '../src/wishlist/use-add-item' 5 | import type { ItemBody, AddItemBody } from '../api/wishlist' 6 | import useCustomer from '../account/use-customer' 7 | import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist' 8 | 9 | const defaultOpts = { 10 | url: '/api/bigcommerce/wishlist', 11 | method: 'POST', 12 | } 13 | 14 | export type AddItemInput = ItemBody 15 | 16 | export const fetcher: HookFetcher<Wishlist, AddItemBody> = ( 17 | options, 18 | { item }, 19 | fetch 20 | ) => { 21 | // TODO: add validations before doing the fetch 22 | return fetch({ 23 | ...defaultOpts, 24 | ...options, 25 | body: { item }, 26 | }) 27 | } 28 | 29 | export function extendHook(customFetcher: typeof fetcher) { 30 | const useAddItem = (opts?: UseWishlistOptions) => { 31 | const { data: customer } = useCustomer() 32 | const { revalidate } = useWishlist(opts) 33 | const fn = useWishlistAddItem(defaultOpts, customFetcher) 34 | 35 | return useCallback( 36 | async function addItem(input: AddItemInput) { 37 | if (!customer) { 38 | // A signed customer is required in order to have a wishlist 39 | throw new CommerceError({ 40 | message: 'Signed customer not found', 41 | }) 42 | } 43 | 44 | const data = await fn({ item: input }) 45 | await revalidate() 46 | return data 47 | }, 48 | [fn, revalidate, customer] 49 | ) 50 | } 51 | 52 | useAddItem.extend = extendHook 53 | 54 | return useAddItem 55 | } 56 | 57 | export default extendHook(fetcher) 58 | -------------------------------------------------------------------------------- /example /wishlist/use-remove-item.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { HookFetcher } from '../src/utils/types' 3 | import { CommerceError } from '../src/utils/errors' 4 | import useWishlistRemoveItem from '../src/wishlist/use-remove-item' 5 | import type { RemoveItemBody } from '../api/wishlist' 6 | import useCustomer from '../account/use-customer' 7 | import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist' 8 | 9 | const defaultOpts = { 10 | url: '/api/bigcommerce/wishlist', 11 | method: 'DELETE', 12 | } 13 | 14 | export type RemoveItemInput = { 15 | id: string | number 16 | } 17 | 18 | export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = ( 19 | options, 20 | { itemId }, 21 | fetch 22 | ) => { 23 | return fetch({ 24 | ...defaultOpts, 25 | ...options, 26 | body: { itemId }, 27 | }) 28 | } 29 | 30 | export function extendHook(customFetcher: typeof fetcher) { 31 | const useRemoveItem = (opts?: UseWishlistOptions) => { 32 | const { data: customer } = useCustomer() 33 | const { revalidate } = useWishlist(opts) 34 | const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>( 35 | defaultOpts, 36 | customFetcher 37 | ) 38 | 39 | return useCallback( 40 | async function removeItem(input: RemoveItemInput) { 41 | if (!customer) { 42 | // A signed customer is required in order to have a wishlist 43 | throw new CommerceError({ 44 | message: 'Signed customer not found', 45 | }) 46 | } 47 | 48 | const data = await fn({ itemId: String(input.id) }) 49 | await revalidate() 50 | return data 51 | }, 52 | [fn, revalidate, customer] 53 | ) 54 | } 55 | 56 | useRemoveItem.extend = extendHook 57 | 58 | return useRemoveItem 59 | } 60 | 61 | export default extendHook(fetcher) 62 | -------------------------------------------------------------------------------- /example /wishlist/use-wishlist-actions.tsx: -------------------------------------------------------------------------------- 1 | import useAddItem from './use-add-item' 2 | import useRemoveItem from './use-remove-item' 3 | 4 | // This hook is probably not going to be used, but it's here 5 | // to show how a commerce should be structuring it 6 | export default function useWishlistActions() { 7 | const addItem = useAddItem() 8 | const removeItem = useRemoveItem() 9 | 10 | return { addItem, removeItem } 11 | } 12 | -------------------------------------------------------------------------------- /example /wishlist/use-wishlist.tsx: -------------------------------------------------------------------------------- 1 | import { HookFetcher } from '../src/utils/types' 2 | import { SwrOptions } from '../src/utils/use-data' 3 | import useCommerceWishlist from '../src/wishlist/use-wishlist' 4 | import type { Wishlist } from '../api/wishlist' 5 | import useCustomer from '../account/use-customer' 6 | 7 | const defaultOpts = { 8 | url: '/api/bigcommerce/wishlist', 9 | method: 'GET', 10 | } 11 | 12 | export type { Wishlist } 13 | 14 | export interface UseWishlistOptions { 15 | includeProducts?: boolean 16 | } 17 | 18 | export interface UseWishlistInput extends UseWishlistOptions { 19 | customerId?: number 20 | } 21 | 22 | export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = ( 23 | options, 24 | { customerId, includeProducts }, 25 | fetch 26 | ) => { 27 | if (!customerId) return null 28 | 29 | // Use a dummy base as we only care about the relative path 30 | const url = new URL(options?.url ?? defaultOpts.url, 'http://a') 31 | 32 | if (includeProducts) url.searchParams.set('products', '1') 33 | 34 | return fetch({ 35 | url: url.pathname + url.search, 36 | method: options?.method ?? defaultOpts.method, 37 | }) 38 | } 39 | 40 | export function extendHook( 41 | customFetcher: typeof fetcher, 42 | swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput> 43 | ) { 44 | const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => { 45 | const { data: customer } = useCustomer() 46 | const response = useCommerceWishlist( 47 | defaultOpts, 48 | [ 49 | ['customerId', customer?.entityId], 50 | ['includeProducts', includeProducts], 51 | ], 52 | customFetcher, 53 | { 54 | revalidateOnFocus: false, 55 | ...swrOptions, 56 | } 57 | ) 58 | 59 | // Uses a getter to only calculate the prop when required 60 | // response.data is also a getter and it's better to not trigger it early 61 | Object.defineProperty(response, 'isEmpty', { 62 | get() { 63 | return (response.data?.items?.length || 0) <= 0 64 | }, 65 | set: (x) => x, 66 | }) 67 | 68 | return response 69 | } 70 | 71 | useWishlist.extend = extendHook 72 | 73 | return useWishlist 74 | } 75 | 76 | export default extendHook(fetcher) 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commerce-framework", 3 | "version": "1.0.0", 4 | "description": "Next.js Commerce Framework", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vercel/commerce-framework.git" 12 | }, 13 | "keywords": [ 14 | "nextjs", 15 | "commerce" 16 | ], 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/vercel/commerce-framework/issues" 21 | }, 22 | "homepage": "https://github.com/vercel/commerce-framework#readme", 23 | "dependencies": { 24 | "@types/lodash.debounce": "^4.0.6", 25 | "@vercel/fetch": "^6.1.0", 26 | "js-cookie": "^2.2.1", 27 | "swr": "^0.3.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commerce/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { RequestInit, Response } from "@vercel/fetch"; 2 | 3 | export interface CommerceAPIConfig { 4 | locale?: string; 5 | commerceUrl: string; 6 | apiToken: string; 7 | cartCookie: string; 8 | cartCookieMaxAge: number; 9 | customerCookie: string; 10 | fetch<Data = any, Variables = any>( 11 | query: string, 12 | queryData?: CommerceAPIFetchOptions<Variables>, 13 | fetchOptions?: RequestInit 14 | ): Promise<GraphQLFetcherResult<Data>>; 15 | } 16 | 17 | export type GraphQLFetcher< 18 | Data extends GraphQLFetcherResult = GraphQLFetcherResult, 19 | Variables = any 20 | > = ( 21 | query: string, 22 | queryData?: CommerceAPIFetchOptions<Variables>, 23 | fetchOptions?: RequestInit 24 | ) => Promise<Data>; 25 | 26 | export interface GraphQLFetcherResult<Data = any> { 27 | data: Data; 28 | res: Response; 29 | } 30 | 31 | export interface CommerceAPIFetchOptions<Variables> { 32 | variables?: Variables; 33 | preview?: boolean; 34 | } 35 | -------------------------------------------------------------------------------- /src/commerce/cart/use-add-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useAddItem = useAction 4 | 5 | export default useAddItem 6 | -------------------------------------------------------------------------------- /src/commerce/cart/use-cart-actions.tsx: -------------------------------------------------------------------------------- 1 | import type { HookFetcher, HookFetcherOptions } from '../utils/types' 2 | import useAddItem from './use-add-item' 3 | import useRemoveItem from './use-remove-item' 4 | import useUpdateItem from './use-update-item' 5 | 6 | // This hook is probably not going to be used, but it's here 7 | // to show how a commerce should be structuring it 8 | export default function useCartActions<T, Input>( 9 | options: HookFetcherOptions, 10 | fetcher: HookFetcher<T, Input> 11 | ) { 12 | const addItem = useAddItem<T, Input>(options, fetcher) 13 | const updateItem = useUpdateItem<T, Input>(options, fetcher) 14 | const removeItem = useRemoveItem<T, Input>(options, fetcher) 15 | 16 | return { addItem, updateItem, removeItem } 17 | } 18 | -------------------------------------------------------------------------------- /src/commerce/cart/use-cart.tsx: -------------------------------------------------------------------------------- 1 | import type { responseInterface } from 'swr' 2 | import Cookies from 'js-cookie' 3 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' 4 | import useData, { SwrOptions } from '../utils/use-data' 5 | import { useCommerce } from '..' 6 | 7 | export type CartResponse<Result> = responseInterface<Result, Error> & { 8 | isEmpty: boolean 9 | } 10 | 11 | export type CartInput = { 12 | cartId: string | undefined 13 | } 14 | 15 | export default function useCart<Result>( 16 | options: HookFetcherOptions, 17 | input: HookInput, 18 | fetcherFn: HookFetcher<Result, CartInput>, 19 | swrOptions?: SwrOptions<Result, CartInput> 20 | ) { 21 | const { cartCookie } = useCommerce() 22 | 23 | const fetcher: typeof fetcherFn = (options, input, fetch) => { 24 | input.cartId = Cookies.get(cartCookie) 25 | return fetcherFn(options, input, fetch) 26 | } 27 | 28 | const response = useData(options, input, fetcher, swrOptions) 29 | 30 | return Object.assign(response, { isEmpty: true }) as CartResponse<Result> 31 | } 32 | -------------------------------------------------------------------------------- /src/commerce/cart/use-remove-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useRemoveItem = useAction 4 | 5 | export default useRemoveItem 6 | -------------------------------------------------------------------------------- /src/commerce/cart/use-update-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useUpdateItem = useAction 4 | 5 | export default useUpdateItem 6 | -------------------------------------------------------------------------------- /src/commerce/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ReactNode, 3 | MutableRefObject, 4 | createContext, 5 | useContext, 6 | useMemo, 7 | useRef, 8 | } from 'react' 9 | import * as React from 'react' 10 | import { Fetcher } from './utils/types' 11 | 12 | const Commerce = createContext<CommerceContextValue | {}>({}) 13 | 14 | export type CommerceProps = { 15 | children?: ReactNode 16 | config: CommerceConfig 17 | } 18 | 19 | export type CommerceConfig = { fetcher: Fetcher<any> } & Omit< 20 | CommerceContextValue, 21 | 'fetcherRef' 22 | > 23 | 24 | export type CommerceContextValue = { 25 | fetcherRef: MutableRefObject<Fetcher<any>> 26 | locale: string 27 | cartCookie: string 28 | } 29 | 30 | export function CommerceProvider({ children, config }: CommerceProps) { 31 | if (!config) { 32 | throw new Error('CommerceProvider requires a valid config object') 33 | } 34 | 35 | const fetcherRef = useRef(config.fetcher) 36 | // Because the config is an object, if the parent re-renders this provider 37 | // will re-render every consumer unless we memoize the config 38 | const cfg = useMemo( 39 | () => ({ 40 | fetcherRef, 41 | locale: config.locale, 42 | cartCookie: config.cartCookie, 43 | }), 44 | [config.locale, config.cartCookie] 45 | ) 46 | 47 | return <Commerce.Provider value={cfg}>{children}</Commerce.Provider> 48 | } 49 | 50 | export function useCommerce<T extends CommerceContextValue>() { 51 | return useContext(Commerce) as T 52 | } 53 | -------------------------------------------------------------------------------- /src/commerce/products/use-search.tsx: -------------------------------------------------------------------------------- 1 | import useData from '../utils/use-data' 2 | 3 | const useSearch = useData 4 | 5 | export default useSearch 6 | -------------------------------------------------------------------------------- /src/commerce/use-customer.tsx: -------------------------------------------------------------------------------- 1 | import useData from './utils/use-data' 2 | 3 | const useCustomer = useData 4 | 5 | export default useCustomer 6 | -------------------------------------------------------------------------------- /src/commerce/use-login.tsx: -------------------------------------------------------------------------------- 1 | import useAction from './utils/use-action' 2 | 3 | const useLogin = useAction 4 | 5 | export default useLogin 6 | -------------------------------------------------------------------------------- /src/commerce/use-logout.tsx: -------------------------------------------------------------------------------- 1 | import useAction from './utils/use-action' 2 | 3 | const useLogout = useAction 4 | 5 | export default useLogout 6 | -------------------------------------------------------------------------------- /src/commerce/use-price.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useCommerce } from '.' 3 | 4 | export function formatPrice({ 5 | amount, 6 | currencyCode, 7 | locale, 8 | }: { 9 | amount: number 10 | currencyCode: string 11 | locale: string 12 | }) { 13 | const formatCurrency = new Intl.NumberFormat(locale, { 14 | style: 'currency', 15 | currency: currencyCode, 16 | }) 17 | 18 | return formatCurrency.format(amount) 19 | } 20 | 21 | export function formatVariantPrice({ 22 | amount, 23 | baseAmount, 24 | currencyCode, 25 | locale, 26 | }: { 27 | baseAmount: number 28 | amount: number 29 | currencyCode: string 30 | locale: string 31 | }) { 32 | const hasDiscount = baseAmount > amount 33 | const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' }) 34 | const discount = hasDiscount 35 | ? formatDiscount.format((baseAmount - amount) / baseAmount) 36 | : null 37 | 38 | const price = formatPrice({ amount, currencyCode, locale }) 39 | const basePrice = hasDiscount 40 | ? formatPrice({ amount: baseAmount, currencyCode, locale }) 41 | : null 42 | 43 | return { price, basePrice, discount } 44 | } 45 | 46 | export default function usePrice( 47 | data?: { 48 | amount: number 49 | baseAmount?: number 50 | currencyCode: string 51 | } | null 52 | ) { 53 | const { amount, baseAmount, currencyCode } = data ?? {} 54 | const { locale } = useCommerce() 55 | const value = useMemo(() => { 56 | if (typeof amount !== 'number' || !currencyCode) return '' 57 | 58 | return baseAmount 59 | ? formatVariantPrice({ amount, baseAmount, currencyCode, locale }) 60 | : formatPrice({ amount, currencyCode, locale }) 61 | }, [amount, baseAmount, currencyCode]) 62 | 63 | return typeof value === 'string' ? { price: value } : value 64 | } 65 | -------------------------------------------------------------------------------- /src/commerce/use-signup.tsx: -------------------------------------------------------------------------------- 1 | import useAction from './utils/use-action' 2 | 3 | const useSignup = useAction 4 | 5 | export default useSignup 6 | -------------------------------------------------------------------------------- /src/commerce/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export type ErrorData = { 2 | message: string 3 | code?: string 4 | } 5 | 6 | export type ErrorProps = { 7 | code?: string 8 | } & ( 9 | | { message: string; errors?: never } 10 | | { message?: never; errors: ErrorData[] } 11 | ) 12 | 13 | export class CommerceError extends Error { 14 | code?: string 15 | errors: ErrorData[] 16 | 17 | constructor({ message, code, errors }: ErrorProps) { 18 | const error: ErrorData = message 19 | ? { message, ...(code ? { code } : {}) } 20 | : errors![0] 21 | 22 | super(error.message) 23 | this.errors = message ? [error] : errors! 24 | 25 | if (error.code) this.code = error.code 26 | } 27 | } 28 | 29 | export class FetcherError extends CommerceError { 30 | status: number 31 | 32 | constructor( 33 | options: { 34 | status: number 35 | } & ErrorProps 36 | ) { 37 | super(options) 38 | this.status = options.status 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commerce/utils/types.ts: -------------------------------------------------------------------------------- 1 | // Core fetcher added by CommerceProvider 2 | export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T> 3 | 4 | export type FetcherOptions = { 5 | url?: string 6 | query?: string 7 | method?: string 8 | variables?: any 9 | body?: any 10 | } 11 | 12 | export type HookFetcher<Result, Input = null> = ( 13 | options: HookFetcherOptions | null, 14 | input: Input, 15 | fetch: <T = Result>(options: FetcherOptions) => Promise<T> 16 | ) => Result | Promise<Result> 17 | 18 | export type HookFetcherOptions = { 19 | query?: string 20 | url?: string 21 | method?: string 22 | } 23 | 24 | export type HookInput = [string, string | number | boolean | undefined][] 25 | -------------------------------------------------------------------------------- /src/commerce/utils/use-action.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import type { HookFetcher, HookFetcherOptions } from './types' 3 | import { useCommerce } from '..' 4 | 5 | export default function useAction<T, Input = null>( 6 | options: HookFetcherOptions, 7 | fetcher: HookFetcher<T, Input> 8 | ) { 9 | const { fetcherRef } = useCommerce() 10 | 11 | return useCallback( 12 | (input: Input) => fetcher(options, input, fetcherRef.current), 13 | [fetcher] 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/commerce/utils/use-data.tsx: -------------------------------------------------------------------------------- 1 | import useSWR, { ConfigInterface, responseInterface } from 'swr' 2 | import type { HookInput, HookFetcher, HookFetcherOptions } from './types' 3 | import { CommerceError } from './errors' 4 | import { useCommerce } from '..' 5 | 6 | export type SwrOptions<Result, Input = null> = ConfigInterface< 7 | Result, 8 | CommerceError, 9 | HookFetcher<Result, Input> 10 | > 11 | 12 | export type UseData = <Result = any, Input = null>( 13 | options: HookFetcherOptions | (() => HookFetcherOptions | null), 14 | input: HookInput, 15 | fetcherFn: HookFetcher<Result, Input>, 16 | swrOptions?: SwrOptions<Result, Input> 17 | ) => responseInterface<Result, CommerceError> 18 | 19 | const useData: UseData = (options, input, fetcherFn, swrOptions) => { 20 | const { fetcherRef } = useCommerce() 21 | const fetcher = async ( 22 | url?: string, 23 | query?: string, 24 | method?: string, 25 | ...args: any[] 26 | ) => { 27 | try { 28 | return await fetcherFn( 29 | { url, query, method }, 30 | // Transform the input array into an object 31 | args.reduce((obj, val, i) => { 32 | obj[input[i][0]!] = val 33 | return obj 34 | }, {}), 35 | fetcherRef.current 36 | ) 37 | } catch (error) { 38 | // SWR will not log errors, but any error that's not an instance 39 | // of CommerceError is not welcomed by this hook 40 | if (!(error instanceof CommerceError)) { 41 | console.error(error) 42 | } 43 | throw error 44 | } 45 | } 46 | const response = useSWR( 47 | () => { 48 | const opts = typeof options === 'function' ? options() : options 49 | return opts 50 | ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])] 51 | : null 52 | }, 53 | fetcher, 54 | swrOptions 55 | ) 56 | 57 | return response 58 | } 59 | 60 | export default useData 61 | -------------------------------------------------------------------------------- /src/commerce/wishlist/use-add-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useAddItem = useAction 4 | 5 | export default useAddItem 6 | -------------------------------------------------------------------------------- /src/commerce/wishlist/use-remove-item.tsx: -------------------------------------------------------------------------------- 1 | import useAction from '../utils/use-action' 2 | 3 | const useRemoveItem = useAction 4 | 5 | export default useRemoveItem 6 | -------------------------------------------------------------------------------- /src/commerce/wishlist/use-wishlist.tsx: -------------------------------------------------------------------------------- 1 | import type { responseInterface } from 'swr' 2 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' 3 | import useData, { SwrOptions } from '../utils/use-data' 4 | 5 | export type WishlistResponse<Result> = responseInterface<Result, Error> & { 6 | isEmpty: boolean 7 | } 8 | 9 | export default function useWishlist<Result, Input = null>( 10 | options: HookFetcherOptions, 11 | input: HookInput, 12 | fetcherFn: HookFetcher<Result, Input>, 13 | swrOptions?: SwrOptions<Result, Input> 14 | ) { 15 | const response = useData(options, input, fetcherFn, swrOptions) 16 | return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result> 17 | } 18 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/async-retry@1.2.1": 6 | version "1.2.1" 7 | resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.2.1.tgz#fa9ac165907a8ee78f4924f4e393b656c65b5bb4" 8 | integrity sha512-yMQ6CVgICWtyFNBqJT3zqOc+TnqqEPLo4nKJNPFwcialiylil38Ie6q1ENeFTjvaLOkVim9K5LisHgAKJWidGQ== 9 | 10 | "@types/lodash.debounce@^4.0.6": 11 | version "4.0.6" 12 | resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60" 13 | integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ== 14 | dependencies: 15 | "@types/lodash" "*" 16 | 17 | "@types/lodash@*": 18 | version "4.14.165" 19 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" 20 | integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== 21 | 22 | "@types/lru-cache@4.1.1": 23 | version "4.1.1" 24 | resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40" 25 | integrity sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw== 26 | 27 | "@types/node-fetch@2.3.2": 28 | version "2.3.2" 29 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.3.2.tgz#e01893b176c6fa1367743726380d65bce5d6576b" 30 | integrity sha512-yW0EOebSsQme9yKu09XbdDfle4/SmWZMK4dfteWcSLCYNQQcF+YOv0kIrvm+9pO11/ghA4E6A+RNQqvYj4Nr3A== 31 | dependencies: 32 | "@types/node" "*" 33 | 34 | "@types/node@*": 35 | version "14.14.7" 36 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" 37 | integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== 38 | 39 | "@types/node@10.12.18": 40 | version "10.12.18" 41 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" 42 | integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== 43 | 44 | "@vercel/fetch-cached-dns@^2.0.1": 45 | version "2.0.1" 46 | resolved "https://registry.yarnpkg.com/@vercel/fetch-cached-dns/-/fetch-cached-dns-2.0.1.tgz#b929ba5b4b6f7108abf49adaf03309159047c134" 47 | integrity sha512-4a2IoekfGUgV/dinAB7Tx5oqA+Pg9I/6x/t8n/yduHmdclP5EdWTN4gPrwOKVECKVn2pV1VxAT8q4toSzwa2Eg== 48 | dependencies: 49 | "@types/node-fetch" "2.3.2" 50 | "@zeit/dns-cached-resolve" "2.1.0" 51 | 52 | "@vercel/fetch-retry@^5.0.2": 53 | version "5.0.3" 54 | resolved "https://registry.yarnpkg.com/@vercel/fetch-retry/-/fetch-retry-5.0.3.tgz#cce5d23f6e64f6f525c24e2ac7c78f65d6c5b1f4" 55 | integrity sha512-DIIoBY92r+sQ6iHSf5WjKiYvkdsDIMPWKYATlE0KcUAj2RV6SZK9UWpUzBRKsofXqedOqpVjrI0IE6AWL7JRtg== 56 | dependencies: 57 | async-retry "^1.3.1" 58 | debug "^3.1.0" 59 | 60 | "@vercel/fetch@^6.1.0": 61 | version "6.1.0" 62 | resolved "https://registry.yarnpkg.com/@vercel/fetch/-/fetch-6.1.0.tgz#4959cd264d25e811b46491818a9d9ca5d752a2a9" 63 | integrity sha512-xR0GQggKhPvwEWrqcrobsQFjyR/bDDbX24BkSaRyLzW+8SydKhkBc/mBCUV8h4SBZSlJMJnqhrxjFCZ1uJcqNg== 64 | dependencies: 65 | "@types/async-retry" "1.2.1" 66 | "@vercel/fetch-cached-dns" "^2.0.1" 67 | "@vercel/fetch-retry" "^5.0.2" 68 | agentkeepalive "3.4.1" 69 | debug "3.1.0" 70 | 71 | "@zeit/dns-cached-resolve@2.1.0": 72 | version "2.1.0" 73 | resolved "https://registry.yarnpkg.com/@zeit/dns-cached-resolve/-/dns-cached-resolve-2.1.0.tgz#78583010df1683fdb7b05949b75593c9a8641bc1" 74 | integrity sha512-KD2zyRZEBNs9PJ3/ob7zx0CvR4wM0oV4G5s5gFfPwmM74GpFbUN2pAAivP2AXnUrJ14Nkh8NumNKOzOyc4LbFQ== 75 | dependencies: 76 | "@types/async-retry" "1.2.1" 77 | "@types/lru-cache" "4.1.1" 78 | "@types/node" "10.12.18" 79 | async-retry "1.2.3" 80 | lru-cache "5.1.1" 81 | 82 | agentkeepalive@3.4.1: 83 | version "3.4.1" 84 | resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" 85 | integrity sha512-MPIwsZU9PP9kOrZpyu2042kYA8Fdt/AedQYkYXucHgF9QoD9dXVp0ypuGnHXSR0hTstBxdt85Xkh4JolYfK5wg== 86 | dependencies: 87 | humanize-ms "^1.2.1" 88 | 89 | async-retry@1.2.3: 90 | version "1.2.3" 91 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0" 92 | integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q== 93 | dependencies: 94 | retry "0.12.0" 95 | 96 | async-retry@^1.3.1: 97 | version "1.3.1" 98 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" 99 | integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== 100 | dependencies: 101 | retry "0.12.0" 102 | 103 | debug@3.1.0: 104 | version "3.1.0" 105 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 106 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 107 | dependencies: 108 | ms "2.0.0" 109 | 110 | debug@^3.1.0: 111 | version "3.2.6" 112 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 113 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== 114 | dependencies: 115 | ms "^2.1.1" 116 | 117 | dequal@2.0.2: 118 | version "2.0.2" 119 | resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" 120 | integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== 121 | 122 | humanize-ms@^1.2.1: 123 | version "1.2.1" 124 | resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" 125 | integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= 126 | dependencies: 127 | ms "^2.0.0" 128 | 129 | js-cookie@^2.2.1: 130 | version "2.2.1" 131 | resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" 132 | integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== 133 | 134 | lru-cache@5.1.1: 135 | version "5.1.1" 136 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" 137 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== 138 | dependencies: 139 | yallist "^3.0.2" 140 | 141 | ms@2.0.0: 142 | version "2.0.0" 143 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 144 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 145 | 146 | ms@^2.0.0, ms@^2.1.1: 147 | version "2.1.2" 148 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 149 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 150 | 151 | retry@0.12.0: 152 | version "0.12.0" 153 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" 154 | integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= 155 | 156 | swr@^0.3.8: 157 | version "0.3.8" 158 | resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.8.tgz#b3c3c7fa278913d22dbe1f3f28c07df9da9be944" 159 | integrity sha512-EHRlaqoBtHsB2wOB+dQJ74DrZvaRGu4BaIQrhkD+/rj8/UGo2iQXN+rCcYnV7/VAreBJBmm9+lDkwZmUqWEkKA== 160 | dependencies: 161 | dequal "2.0.2" 162 | 163 | yallist@^3.0.2: 164 | version "3.1.1" 165 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" 166 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== 167 | --------------------------------------------------------------------------------