Skip to content

Latest commit

 

History

History
253 lines (207 loc) · 7.34 KB

new-provider.md

File metadata and controls

253 lines (207 loc) · 7.34 KB

Adding a new Commerce Provider

🔔 New providers are on hold until we have a new API for commerce 🔔

A commerce provider is a headless e-commerce platform that integrates with the Commerce Framework. Right now we have the following providers:

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.

Updating the list of known providers

Open ./config.js and add the provider name to the list in PROVIDERS.

Then, open /.env.template and add the provider name in the first line.

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:

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]
      )
    },
}

Adding the Node.js provider API

TODO

The commerce API is currently going through a refactor in vercel/commerce#252 - We'll update the docs once the API is released.