A commerce provider is a headless e-commerce platform that integrates with the Commerce Framework. Right now we have the following providers:
- BigCommerce (framework/bigcommerce)
- Saleor (framework/saleor)
- Shopify (framework/shopify)
Adding a commerce provider means adding a new folder in framework
with a folder structure like the next one:
api
- index.ts
product
- usePrice
- useSearch
- getProduct
- getAllProducts
wishlist
- useWishlist
- useAddItem
- useRemoveItem
auth
- useLogin
- useLogout
- useSignup
customer
- useCustomer
- getCustomerId
- getCustomerWistlist
cart
- useCart
- useAddItem
- useRemoveItem
- useUpdateItem
env.template
index.ts
provider.ts
commerce.config.json
next.config.js
README.md
provider.ts
exports a provider object with handlers for the Commerce Hooks and api/index.ts
exports a Node.js provider for the Commerce API
Important: We use TypeScript for every provider and expect its usage for every new one.
The app imports from the provider directly instead of the core commerce folder (framework/commerce
), but all providers are interchangeable and to achieve it every provider always has to implement the core types and helpers.
The provider folder should only depend on framework/commerce
and dependencies in the main package.json
. In the future we'll move the framework
folder to a package that can be shared easily for multiple apps.
Using BigCommerce as an example. The first thing to do is export a CommerceProvider
component that includes a provider
object with all the handlers that can be used for hooks:
import type { ReactNode } from "react"
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from "@commerce"
import { bigcommerceProvider } from "./provider"
import type { BigcommerceProvider } from "./provider"
export { bigcommerceProvider }
export type { BigcommerceProvider }
export const bigcommerceConfig: CommerceConfig = {
locale: "en-us",
cartCookie: "bc_cartId",
}
export type BigcommerceConfig = Partial<CommerceConfig>
export type BigcommerceProps = {
children?: ReactNode
locale: string
} & BigcommerceConfig
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return (
<CoreCommerceProvider
provider={bigcommerceProvider}
config={{ ...bigcommerceConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
The exported types and components extend from the core ones exported by @commerce
, which refers to framework/commerce
.
The bigcommerceProvider
object looks like this:
import { handler as useCart } from "./cart/use-cart"
import { handler as useAddItem } from "./cart/use-add-item"
import { handler as useUpdateItem } from "./cart/use-update-item"
import { handler as useRemoveItem } from "./cart/use-remove-item"
import { handler as useWishlist } from "./wishlist/use-wishlist"
import { handler as useWishlistAddItem } from "./wishlist/use-add-item"
import { handler as useWishlistRemoveItem } from "./wishlist/use-remove-item"
import { handler as useCustomer } from "./customer/use-customer"
import { handler as useSearch } from "./product/use-search"
import { handler as useLogin } from "./auth/use-login"
import { handler as useLogout } from "./auth/use-logout"
import { handler as useSignup } from "./auth/use-signup"
import fetcher from "./fetcher"
export const bigcommerceProvider = {
locale: "en-us",
cartCookie: "bc_cartId",
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
wishlist: {
useWishlist,
useAddItem: useWishlistAddItem,
useRemoveItem: useWishlistRemoveItem,
},
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type BigcommerceProvider = typeof bigcommerceProvider
The provider object, in this case bigcommerceProvider
, has to match the Provider
type defined in framework/commerce.
A hook handler, like useCart
, looks like this:
import { useMemo } from "react"
import { SWRHook } from "@commerce/utils/types"
import useCart, { UseCart, FetchCartInput } from "@commerce/cart/use-cart"
import { normalizeCart } from "../lib/normalize"
import type { Cart } from "../types"
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<
Cart | null,
{},
FetchCartInput,
{ isEmpty?: boolean }
> = {
fetchOptions: {
url: "/api/cart",
method: "GET",
},
async fetcher({ input: { cartId }, options, fetch }) {
const data = cartId ? await fetch(options) : null
return data && normalizeCart(data)
},
useHook:
({ useData }) =>
(input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (
(response.data?.lineItems.length ?? 0) <= 0
)
},
enumerable: true,
},
}),
[response]
)
},
}
In the case of data fetching hooks like useCart
each handler has to implement the SWRHook
type that's defined in the core types. For mutations it's the MutationHook
, e.g for useAddItem
:
import { useCallback } from "react"
import type { MutationHook } from "@commerce/utils/types"
import { CommerceError } from "@commerce/utils/errors"
import useAddItem, { UseAddItem } from "@commerce/cart/use-add-item"
import { normalizeCart } from "../lib/normalize"
import type {
Cart,
BigcommerceCart,
CartItemBody,
AddCartItemBody,
} from "../types"
import useCart from "./use-cart"
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<Cart, {}, CartItemBody> = {
fetchOptions: {
url: "/api/cart",
method: "POST",
},
async fetcher({ input: item, options, fetch }) {
if (
item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1)
) {
throw new CommerceError({
message:
"The item quantity has to be a valid integer greater than 0",
})
}
const data = await fetch<BigcommerceCart, AddCartItemBody>({
...options,
body: { item },
})
return normalizeCart(data)
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}
TODO
The commerce API is currently going through a refactor in vercel/commerce#252 - We'll update the docs once the API is released.