4
0
forked from crowetic/commerce
commerce/framework/commerce/new-provider.md
Jakub Neander 3b2bf654fe
Updated Saleor Provider (#356)
* Initial work, copied from the Shopify provider

* Added basis setup and type generation for the products queries

* refactor: adjust the types

* task: relax the Node.js constraint

* fix: page/product properties

* disable unknown fields

* mention Saleor in the README

* setup debugging for Next.js

* Check nextjs-commerce bug if no images are added for a product

* fix: client/server pecularities for env visibility

Must prefix with `NEXT_PUBLIC_` so that the API URL is
visible on the client

* re: make search work with Saleor API (WIP)

* task: update deps

* task: move to Webpack 5.x

* saleor: initial cart integration

* update deps

* saleor: shall the cart appear!

* task: remove deprecated packages

* saleor: adding/removing from the cart

* saleor: preliminary signup process

* saleor: fix the prices in the cart

* update deps

* update deps

* Added the options for a variant to the product page

* Mapped options to variants

* Mapped options to variants

* saleor: refine the auth process

* saleor: remove unused code

* saleor: handle customer find via refresh

temporary solution

* saleor: update deps

* saleor: fix the session handling

* saleor: fix the variants

* saleor: simplify the naming for GraphQL statements

* saleor: fix the type for collection

* saleor: arrange the error codes

* saleor: integrate collections

* saleor: fix product sorting

* saleor: set cookie location

* saleor: update the schema

* saleor: attach checkout to customer

* saleor: fix the checkout flow

* saleor: unify GraphQL naming approach

* task: update deps

* Add the env variables for saleor to the template

* task: prettier

* saleor: stub API for build/typescript compilation

thanks @cond0r

* task: temporarily disable for the `build`

* saleor: refactor GraphQL queries

* saleor: adjust the config

* task: update dependencies

* revert: Next.js to `10.0.9`

* saleor: fix the checkout fetch query

* task: update dependencies

* saleor: adapt for displaying featured products

* saleor: update the provider structure

* saleor: make the home page representable

* feature/cart: display the variant name (cond)

Co-authored-by: Patryk Zawadzki <patrys@room-303.com>
Co-authored-by: royderks <10717410+royderks@users.noreply.github.com>
2021-06-10 01:46:28 -05:00

7.0 KiB

Adding a new Commerce Provider

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.

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, 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 https://github.com/vercel/commerce/pull/252 - We'll update the docs once the API is released.