diff --git a/framework/commerce/new-provider.md b/framework/commerce/new-provider.md new file mode 100644 index 000000000..e58b88316 --- /dev/null +++ b/framework/commerce/new-provider.md @@ -0,0 +1,204 @@ +# Adding a new Commerce Provider + +A commerce provider is a headless e-commerce platform that integrates with the [Commerce Framework](./README.md). Right now we have the following providers: + +- BigCommerce ([framework/bigcommerce](../bigcommerce)) +- Shopify ([framework/shopify](../shopify)) + +Adding a commerce provider means adding a provider object with handlers for the [Commerce Hooks](./README.md#commerce-hooks) and a Node.js provider for the [Commerce API](./README.md#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. + +## Adding the provider hooks + +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: + +```tsx +import type { ReactNode } from 'react' +import { + CommerceConfig, + CommerceProvider as CoreCommerceProvider, + useCommerce as useCoreCommerce, +} from '@commerce' +import { bigcommerceProvider, BigcommerceProvider } from './provider' + +export { bigcommerceProvider } +export type { BigcommerceProvider } + +export const bigcommerceConfig: CommerceConfig = { + locale: 'en-us', + cartCookie: 'bc_cartId', +} + +export type BigcommerceConfig = Partial + +export type BigcommerceProps = { + children?: ReactNode + locale: string +} & BigcommerceConfig + +export function CommerceProvider({ children, ...config }: BigcommerceProps) { + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() +``` + +The exported types and components extend from the core ones exported by `@commerce`, which refers to `framework/commerce`. + +The `bigcommerceProvider` object looks like this: + +```tsx +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](./index.ts). + +A hook handler, like `useCart`, looks like this: + +```tsx +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 + +export const handler: SWRHook< + Cart | null, + {}, + FetchCartInput, + { isEmpty?: boolean } +> = { + fetchOptions: { + url: '/api/bigcommerce/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`: + +```tsx +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 + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/bigcommerce/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({ + ...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] + ) + }, +} +``` + +## Adding the Node.js provider API + +TODO + +> The commerce API is currently going through a refactor in https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released. \ No newline at end of file