({
+ handler: loginEndpoint,
+ handlers,
+})
+
+export default loginApi
diff --git a/framework/commercejs/api/endpoints/login/login.ts b/framework/commercejs/api/endpoints/login/login.ts
new file mode 100644
index 000000000..b9088ad22
--- /dev/null
+++ b/framework/commercejs/api/endpoints/login/login.ts
@@ -0,0 +1,33 @@
+import { serialize } from 'cookie'
+import sdkFetcherFunction from '../../utils/sdk-fetch'
+import { getDeploymentUrl } from '../../../utils/get-deployment-url'
+import type { LoginEndpoint } from '.'
+
+const login: LoginEndpoint['handlers']['login'] = async ({
+ req,
+ res,
+ config: { sdkFetch, customerCookie },
+}) => {
+ const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
+ const redirectUrl = getDeploymentUrl()
+ try {
+ const loginToken = req.query?.token as string
+ if (!loginToken) {
+ res.redirect(redirectUrl)
+ }
+ const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
+ res.setHeader(
+ 'Set-Cookie',
+ serialize(customerCookie, jwt, {
+ secure: process.env.NODE_ENV === 'production',
+ maxAge: 60 * 60 * 24,
+ path: '/',
+ })
+ )
+ res.redirect(redirectUrl)
+ } catch {
+ res.redirect(redirectUrl)
+ }
+}
+
+export default login
diff --git a/framework/commercejs/api/endpoints/logout/index.ts b/framework/commercejs/api/endpoints/logout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/commercejs/api/endpoints/logout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/commercejs/api/endpoints/signup/index.ts b/framework/commercejs/api/endpoints/signup/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/commercejs/api/endpoints/signup/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/commercejs/api/endpoints/wishlist/index.tsx b/framework/commercejs/api/endpoints/wishlist/index.tsx
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/commercejs/api/endpoints/wishlist/index.tsx
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/commercejs/api/index.ts b/framework/commercejs/api/index.ts
new file mode 100644
index 000000000..faccd5a01
--- /dev/null
+++ b/framework/commercejs/api/index.ts
@@ -0,0 +1,46 @@
+import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
+import { getCommerceApi as commerceApi } from '@commerce/api'
+
+import getAllPages from './operations/get-all-pages'
+import getPage from './operations/get-page'
+import getSiteInfo from './operations/get-site-info'
+import getAllProductPaths from './operations/get-all-product-paths'
+import getAllProducts from './operations/get-all-products'
+import getProduct from './operations/get-product'
+import sdkFetch from './utils/sdk-fetch'
+import createGraphqlFetcher from './utils/graphql-fetch'
+import { API_URL, CART_COOKIE, CUSTOMER_COOKIE } from '../constants'
+
+export interface CommercejsConfig extends CommerceAPIConfig {
+ sdkFetch: typeof sdkFetch
+}
+
+const config: CommercejsConfig = {
+ commerceUrl: API_URL,
+ cartCookie: CART_COOKIE,
+ cartCookieMaxAge: 2592000,
+ customerCookie: CUSTOMER_COOKIE,
+ apiToken: '',
+ fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
+ sdkFetch,
+}
+
+const operations = {
+ getAllPages,
+ getPage,
+ getSiteInfo,
+ getAllProductPaths,
+ getAllProducts,
+ getProduct,
+}
+
+export const provider = { config, operations }
+
+export type Provider = typeof provider
+export type CommercejsAPI = CommerceAPI
+
+export function getCommerceApi
(
+ customProvider: P = provider as any
+): CommercejsAPI
{
+ return commerceApi(customProvider as any)
+}
diff --git a/framework/commercejs/api/operations/get-all-pages.ts b/framework/commercejs/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..c8c9e41b2
--- /dev/null
+++ b/framework/commercejs/api/operations/get-all-pages.ts
@@ -0,0 +1,21 @@
+import type { CommercejsConfig } from '..'
+import { GetAllPagesOperation } from '../../types/page'
+
+export type Page = { url: string }
+export type GetAllPagesResult = { pages: Page[] }
+
+export default function getAllPagesOperation() {
+ async function getAllPages({
+ config,
+ preview,
+ }: {
+ url?: string
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ return Promise.resolve({
+ pages: [],
+ })
+ }
+ return getAllPages
+}
diff --git a/framework/commercejs/api/operations/get-all-product-paths.ts b/framework/commercejs/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..570d43b13
--- /dev/null
+++ b/framework/commercejs/api/operations/get-all-product-paths.ts
@@ -0,0 +1,35 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type {
+ GetAllProductPathsOperation,
+ CommercejsProduct,
+} from '../../types/product'
+
+import type { CommercejsConfig, Provider } from '..'
+
+export type GetAllProductPathsResult = {
+ products: Array<{ path: string }>
+}
+
+export default function getAllProductPathsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProductPaths({
+ config,
+ }: {
+ config?: Partial
+ } = {}): Promise {
+ const { sdkFetch } = commerce.getConfig(config)
+ const { data } = await sdkFetch('products', 'list')
+
+ // Match a path for every product retrieved
+ const productPaths = data.map(({ permalink }: CommercejsProduct) => ({
+ path: `/${permalink}`,
+ }))
+
+ return {
+ products: productPaths,
+ }
+ }
+
+ return getAllProductPaths
+}
diff --git a/framework/commercejs/api/operations/get-all-products.ts b/framework/commercejs/api/operations/get-all-products.ts
new file mode 100644
index 000000000..14e49d2d3
--- /dev/null
+++ b/framework/commercejs/api/operations/get-all-products.ts
@@ -0,0 +1,29 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { GetAllProductsOperation } from '../../types/product'
+import type { CommercejsConfig, Provider } from '../index'
+
+import { normalizeProduct } from '../../utils/normalize-product'
+
+export default function getAllProductsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProducts({
+ config,
+ }: {
+ config?: Partial
+ } = {}): Promise {
+ const { sdkFetch } = commerce.getConfig(config)
+ const { data } = await sdkFetch('products', 'list', {
+ sortBy: 'sort_order',
+ })
+
+ const productsFormatted =
+ data?.map((product) => normalizeProduct(product)) || []
+
+ return {
+ products: productsFormatted,
+ }
+ }
+
+ return getAllProducts
+}
diff --git a/framework/commercejs/api/operations/get-page.ts b/framework/commercejs/api/operations/get-page.ts
new file mode 100644
index 000000000..f4b69c90d
--- /dev/null
+++ b/framework/commercejs/api/operations/get-page.ts
@@ -0,0 +1,15 @@
+import { GetPageOperation } from '../../types/page'
+
+export type Page = any
+export type GetPageResult = { page?: Page }
+
+export type PageVariables = {
+ id: number
+}
+
+export default function getPageOperation() {
+ async function getPage(): Promise {
+ return Promise.resolve({})
+ }
+ return getPage
+}
diff --git a/framework/commercejs/api/operations/get-product.ts b/framework/commercejs/api/operations/get-product.ts
new file mode 100644
index 000000000..f71aab278
--- /dev/null
+++ b/framework/commercejs/api/operations/get-product.ts
@@ -0,0 +1,44 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { GetProductOperation } from '../../types/product'
+import type { CommercejsConfig, Provider } from '../index'
+import { normalizeProduct } from '../../utils/normalize-product'
+
+export default function getProductOperation({
+ commerce,
+}: OperationContext) {
+ async function getProduct({
+ config,
+ variables,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ const { sdkFetch } = commerce.getConfig(config)
+
+ // Fetch a product by its permalink.
+ const product = await sdkFetch(
+ 'products',
+ 'retrieve',
+ variables?.slug || '',
+ {
+ type: 'permalink',
+ }
+ )
+
+ const { data: variants } = await sdkFetch(
+ 'products',
+ 'getVariants',
+ product.id
+ )
+
+ const productFormatted = normalizeProduct(product, variants)
+
+ return {
+ product: productFormatted,
+ }
+ }
+
+ return getProduct
+}
diff --git a/framework/commercejs/api/operations/get-site-info.ts b/framework/commercejs/api/operations/get-site-info.ts
new file mode 100644
index 000000000..922fd7e76
--- /dev/null
+++ b/framework/commercejs/api/operations/get-site-info.ts
@@ -0,0 +1,36 @@
+import type { OperationContext } from '@commerce/api/operations'
+import type { Category, GetSiteInfoOperation } from '../../types/site'
+import { normalizeCategory } from '../../utils/normalize-category'
+import type { CommercejsConfig, Provider } from '../index'
+
+export type GetSiteInfoResult<
+ T extends { categories: any[]; brands: any[] } = {
+ categories: Category[]
+ brands: any[]
+ }
+> = T
+
+export default function getSiteInfoOperation({
+ commerce,
+}: OperationContext) {
+ async function getSiteInfo({
+ config,
+ }: {
+ query?: string
+ variables?: any
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ const { sdkFetch } = commerce.getConfig(config)
+ const { data: categories } = await sdkFetch('categories', 'list')
+
+ const formattedCategories = categories.map(normalizeCategory)
+
+ return {
+ categories: formattedCategories,
+ brands: [],
+ }
+ }
+
+ return getSiteInfo
+}
diff --git a/framework/commercejs/api/operations/index.ts b/framework/commercejs/api/operations/index.ts
new file mode 100644
index 000000000..84b04a978
--- /dev/null
+++ b/framework/commercejs/api/operations/index.ts
@@ -0,0 +1,6 @@
+export { default as getAllPages } from './get-all-pages'
+export { default as getPage } from './get-page'
+export { default as getSiteInfo } from './get-site-info'
+export { default as getProduct } from './get-product'
+export { default as getAllProducts } from './get-all-products'
+export { default as getAllProductPaths } from './get-all-product-paths'
diff --git a/framework/commercejs/api/utils/graphql-fetch.ts b/framework/commercejs/api/utils/graphql-fetch.ts
new file mode 100644
index 000000000..805177405
--- /dev/null
+++ b/framework/commercejs/api/utils/graphql-fetch.ts
@@ -0,0 +1,14 @@
+import type { GraphQLFetcher } from '@commerce/api'
+import type { CommercejsConfig } from '../'
+
+import { FetcherError } from '@commerce/utils/errors'
+
+const fetchGraphqlApi: (getConfig: () => CommercejsConfig) => GraphQLFetcher =
+ () => async () => {
+ throw new FetcherError({
+ errors: [{ message: 'GraphQL fetch is not implemented' }],
+ status: 500,
+ })
+ }
+
+export default fetchGraphqlApi
diff --git a/framework/commercejs/api/utils/sdk-fetch.ts b/framework/commercejs/api/utils/sdk-fetch.ts
new file mode 100644
index 000000000..c0123ac34
--- /dev/null
+++ b/framework/commercejs/api/utils/sdk-fetch.ts
@@ -0,0 +1,19 @@
+import { commerce } from '../../lib/commercejs'
+import Commerce from '@chec/commerce.js'
+
+type MethodKeys = {
+ [K in keyof T]: T[K] extends (...args: any) => infer R ? K : never
+}[keyof T]
+
+// Calls the relevant Commerce.js SDK method based on resource and method arguments.
+export default async function sdkFetch<
+ Resource extends keyof Commerce,
+ Method extends MethodKeys
+>(
+ resource: Resource,
+ method: Method,
+ ...variables: Parameters
+): Promise> {
+ const data = await commerce[resource][method](...variables)
+ return data
+}
diff --git a/framework/commercejs/auth/index.ts b/framework/commercejs/auth/index.ts
new file mode 100644
index 000000000..36e757a89
--- /dev/null
+++ b/framework/commercejs/auth/index.ts
@@ -0,0 +1,3 @@
+export { default as useLogin } from './use-login'
+export { default as useLogout } from './use-logout'
+export { default as useSignup } from './use-signup'
diff --git a/framework/commercejs/auth/use-login.tsx b/framework/commercejs/auth/use-login.tsx
new file mode 100644
index 000000000..7bc9fd534
--- /dev/null
+++ b/framework/commercejs/auth/use-login.tsx
@@ -0,0 +1,34 @@
+import { useCallback } from 'react'
+import { MutationHook } from '@commerce/utils/types'
+import useLogin, { UseLogin } from '@commerce/auth/use-login'
+import type { LoginHook } from '@commerce/types/login'
+import { getDeploymentUrl } from '../utils/get-deployment-url'
+
+export default useLogin as UseLogin
+
+const getLoginCallbackUrl = () => {
+ const baseUrl = getDeploymentUrl()
+ const API_ROUTE_PATH = 'api/login'
+ return `${baseUrl}/${API_ROUTE_PATH}`
+}
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'customer',
+ method: 'login',
+ },
+ async fetcher({ input, options: { query, method }, fetch }) {
+ await fetch({
+ query,
+ method,
+ variables: [input.email, getLoginCallbackUrl()],
+ })
+ return null
+ },
+ useHook: ({ fetch }) =>
+ function useHook() {
+ return useCallback(async function login(input) {
+ return fetch({ input })
+ }, [])
+ },
+}
diff --git a/framework/commercejs/auth/use-logout.tsx b/framework/commercejs/auth/use-logout.tsx
new file mode 100644
index 000000000..6b841637f
--- /dev/null
+++ b/framework/commercejs/auth/use-logout.tsx
@@ -0,0 +1,27 @@
+import { useCallback } from 'react'
+import Cookies from 'js-cookie'
+import { MutationHook } from '@commerce/utils/types'
+import useLogout, { UseLogout } from '@commerce/auth/use-logout'
+import type { LogoutHook } from '@commerce/types/logout'
+import useCustomer from '../customer/use-customer'
+import { CUSTOMER_COOKIE } from '../constants'
+
+export default useLogout as UseLogout
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '_',
+ method: '_',
+ },
+ useHook: () => () => {
+ const { mutate } = useCustomer()
+ return useCallback(
+ async function logout() {
+ Cookies.remove(CUSTOMER_COOKIE)
+ await mutate(null, false)
+ return null
+ },
+ [mutate]
+ )
+ },
+}
diff --git a/framework/commercejs/auth/use-signup.tsx b/framework/commercejs/auth/use-signup.tsx
new file mode 100644
index 000000000..07fabce0f
--- /dev/null
+++ b/framework/commercejs/auth/use-signup.tsx
@@ -0,0 +1,17 @@
+import { MutationHook } from '@commerce/utils/types'
+import useSignup, { UseSignup } from '@commerce/auth/use-signup'
+
+export default useSignup as UseSignup
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher() {
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () =>
+ () => {},
+}
diff --git a/framework/commercejs/cart/index.ts b/framework/commercejs/cart/index.ts
new file mode 100644
index 000000000..3b8ba990e
--- /dev/null
+++ b/framework/commercejs/cart/index.ts
@@ -0,0 +1,4 @@
+export { default as useCart } from './use-cart'
+export { default as useAddItem } from './use-add-item'
+export { default as useRemoveItem } from './use-remove-item'
+export { default as useUpdateItem } from './use-update-item'
diff --git a/framework/commercejs/cart/use-add-item.tsx b/framework/commercejs/cart/use-add-item.tsx
new file mode 100644
index 000000000..24dd057b6
--- /dev/null
+++ b/framework/commercejs/cart/use-add-item.tsx
@@ -0,0 +1,45 @@
+import type { AddItemHook } from '@commerce/types/cart'
+import type { MutationHook } from '@commerce/utils/types'
+import { useCallback } from 'react'
+import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
+import type { CommercejsCart } from '../types/cart'
+import { normalizeCart } from '../utils/normalize-cart'
+import useCart from './use-cart'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'cart',
+ method: 'add',
+ },
+ async fetcher({ input: item, options, fetch }) {
+ // Frontend stringifies variantId even if undefined.
+ const hasVariant = !item.variantId || item.variantId !== 'undefined'
+
+ const variables = [item.productId, item?.quantity || 1]
+ if (hasVariant) {
+ variables.push(item.variantId)
+ }
+
+ const { cart } = await fetch<{ cart: CommercejsCart }>({
+ query: options.query,
+ method: options.method,
+ variables,
+ })
+ return normalizeCart(cart)
+ },
+ useHook: ({ fetch }) =>
+ function useHook() {
+ const { mutate } = useCart()
+
+ return useCallback(
+ async function addItem(input) {
+ const cart = await fetch({ input })
+ await mutate(cart, false)
+ return cart
+ },
+ [mutate]
+ )
+ },
+}
diff --git a/framework/commercejs/cart/use-cart.tsx b/framework/commercejs/cart/use-cart.tsx
new file mode 100644
index 000000000..beb807362
--- /dev/null
+++ b/framework/commercejs/cart/use-cart.tsx
@@ -0,0 +1,41 @@
+import { useMemo } from 'react'
+import type { GetCartHook } from '@commerce/types/cart'
+import { SWRHook } from '@commerce/utils/types'
+import useCart, { UseCart } from '@commerce/cart/use-cart'
+import type { CommercejsCart } from '../types/cart'
+import { normalizeCart } from '../utils/normalize-cart'
+
+export default useCart as UseCart
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: 'cart',
+ method: 'retrieve',
+ },
+ async fetcher({ options, fetch }) {
+ const cart = await fetch({
+ query: options.query,
+ method: options.method,
+ })
+ return normalizeCart(cart)
+ },
+ useHook: ({ useData }) =>
+ function useHook(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]
+ )
+ },
+}
diff --git a/framework/commercejs/cart/use-remove-item.tsx b/framework/commercejs/cart/use-remove-item.tsx
new file mode 100644
index 000000000..9b492e9eb
--- /dev/null
+++ b/framework/commercejs/cart/use-remove-item.tsx
@@ -0,0 +1,36 @@
+import { useCallback } from 'react'
+import type { MutationHook } from '@commerce/utils/types'
+import type { RemoveItemHook } from '@commerce/types/cart'
+import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
+import type { CommercejsCart } from '../types/cart'
+import { normalizeCart } from '../utils/normalize-cart'
+import useCart from './use-cart'
+
+export default useRemoveItem as UseRemoveItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'cart',
+ method: 'remove',
+ },
+ async fetcher({ input, options, fetch }) {
+ const { cart } = await fetch<{ cart: CommercejsCart }>({
+ query: options.query,
+ method: options.method,
+ variables: input.itemId,
+ })
+ return normalizeCart(cart)
+ },
+ useHook: ({ fetch }) =>
+ function useHook() {
+ const { mutate } = useCart()
+ return useCallback(
+ async function removeItem(input) {
+ const cart = await fetch({ input: { itemId: input.id } })
+ await mutate(cart, false)
+ return cart
+ },
+ [mutate]
+ )
+ },
+}
diff --git a/framework/commercejs/cart/use-update-item.tsx b/framework/commercejs/cart/use-update-item.tsx
new file mode 100644
index 000000000..8e890de49
--- /dev/null
+++ b/framework/commercejs/cart/use-update-item.tsx
@@ -0,0 +1,76 @@
+import type { UpdateItemHook, LineItem } from '@commerce/types/cart'
+import type {
+ HookFetcherContext,
+ MutationHookContext,
+} from '@commerce/utils/types'
+import { ValidationError } from '@commerce/utils/errors'
+import debounce from 'lodash.debounce'
+import { useCallback } from 'react'
+import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
+import type { CommercejsCart } from '../types/cart'
+import { normalizeCart } from '../utils/normalize-cart'
+import useCart from './use-cart'
+
+export default useUpdateItem as UseUpdateItem
+
+export type UpdateItemActionInput = T extends LineItem
+ ? Partial
+ : UpdateItemHook['actionInput']
+
+export const handler = {
+ fetchOptions: {
+ query: 'cart',
+ method: 'update',
+ },
+ async fetcher({ input, options, fetch }: HookFetcherContext) {
+ const variables = [input.itemId, { quantity: input.item.quantity }]
+ const { cart } = await fetch<{ cart: CommercejsCart }>({
+ query: options.query,
+ method: options.method,
+ variables,
+ })
+ return normalizeCart(cart)
+ },
+ useHook:
+ ({ fetch }: MutationHookContext) =>
+ (
+ ctx: {
+ item?: T
+ wait?: number
+ } = {}
+ ) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { mutate } = useCart() as any
+ const { item } = ctx
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ return useCallback(
+ debounce(async (input: UpdateItemActionInput) => {
+ const itemId = input.id ?? item?.id
+ const productId = input.productId ?? item?.productId
+ const variantId = input.productId ?? item?.variantId
+ const quantity = input?.quantity ?? item?.quantity
+
+ if (!itemId || !productId || !variantId) {
+ throw new ValidationError({
+ message: 'Invalid input for updating cart item',
+ })
+ }
+
+ const cart = await fetch({
+ input: {
+ itemId,
+ item: {
+ quantity,
+ productId,
+ variantId,
+ },
+ },
+ })
+ await mutate(cart, false)
+ return cart
+ }, ctx.wait ?? 500),
+ [mutate, item]
+ )
+ },
+}
diff --git a/framework/commercejs/checkout/index.ts b/framework/commercejs/checkout/index.ts
new file mode 100644
index 000000000..306621059
--- /dev/null
+++ b/framework/commercejs/checkout/index.ts
@@ -0,0 +1,2 @@
+export { default as useSubmitCheckout } from './use-submit-checkout'
+export { default as useCheckout } from './use-checkout'
diff --git a/framework/commercejs/checkout/use-checkout.tsx b/framework/commercejs/checkout/use-checkout.tsx
new file mode 100644
index 000000000..f41b01a59
--- /dev/null
+++ b/framework/commercejs/checkout/use-checkout.tsx
@@ -0,0 +1,52 @@
+import type { GetCheckoutHook } from '@commerce/types/checkout'
+
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
+import useSubmitCheckout from './use-submit-checkout'
+import { useCheckoutContext } from '@components/checkout/context'
+
+export default useCheckout as UseCheckout
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '_',
+ method: '_',
+ },
+ useHook: () =>
+ function useHook() {
+ const { cardFields, addressFields } = useCheckoutContext()
+ const submit = useSubmitCheckout()
+
+ // Basic validation - check that at least one field has a value.
+ const hasEnteredCard = Object.values(cardFields).some(
+ (fieldValue) => !!fieldValue
+ )
+ const hasEnteredAddress = Object.values(addressFields).some(
+ (fieldValue) => !!fieldValue
+ )
+
+ const response = useMemo(
+ () => ({
+ data: {
+ hasPayment: hasEnteredCard,
+ hasShipping: hasEnteredAddress,
+ },
+ }),
+ [hasEnteredCard, hasEnteredAddress]
+ )
+
+ return useMemo(
+ () =>
+ Object.create(response, {
+ submit: {
+ get() {
+ return submit
+ },
+ enumerable: true,
+ },
+ }),
+ [submit, response]
+ )
+ },
+}
diff --git a/framework/commercejs/checkout/use-submit-checkout.tsx b/framework/commercejs/checkout/use-submit-checkout.tsx
new file mode 100644
index 000000000..77039ef51
--- /dev/null
+++ b/framework/commercejs/checkout/use-submit-checkout.tsx
@@ -0,0 +1,38 @@
+import type { SubmitCheckoutHook } from '@commerce/types/checkout'
+import type { MutationHook } from '@commerce/utils/types'
+
+import { useCallback } from 'react'
+import useSubmitCheckout, {
+ UseSubmitCheckout,
+} from '@commerce/checkout/use-submit-checkout'
+import { useCheckoutContext } from '@components/checkout/context'
+
+export default useSubmitCheckout as UseSubmitCheckout
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ url: '/api/checkout',
+ method: 'POST',
+ },
+ async fetcher({ input: item, options, fetch }) {
+ const data = await fetch({
+ ...options,
+ body: { item },
+ })
+ return data
+ },
+ useHook: ({ fetch }) =>
+ function useHook() {
+ const { cardFields, addressFields } = useCheckoutContext()
+
+ return useCallback(
+ async function onSubmitCheckout(input) {
+ const data = await fetch({
+ input: { card: cardFields, address: addressFields },
+ })
+ return data
+ },
+ [cardFields, addressFields]
+ )
+ },
+}
diff --git a/framework/commercejs/commerce.config.json b/framework/commercejs/commerce.config.json
new file mode 100644
index 000000000..ba52b04c4
--- /dev/null
+++ b/framework/commercejs/commerce.config.json
@@ -0,0 +1,10 @@
+{
+ "provider": "commercejs",
+ "features": {
+ "cart": true,
+ "search": true,
+ "customCheckout": true,
+ "customerAuth": true,
+ "wishlist": false
+ }
+}
diff --git a/framework/commercejs/constants.ts b/framework/commercejs/constants.ts
new file mode 100644
index 000000000..33a767793
--- /dev/null
+++ b/framework/commercejs/constants.ts
@@ -0,0 +1,4 @@
+export const CART_COOKIE = 'commercejs_cart_id'
+export const CUSTOMER_COOKIE = 'commercejs_customer_token'
+export const API_URL = 'https://api.chec.io/v1'
+export const LOCALE = 'en-us'
diff --git a/framework/commercejs/customer/address/index.ts b/framework/commercejs/customer/address/index.ts
new file mode 100644
index 000000000..1fb07c055
--- /dev/null
+++ b/framework/commercejs/customer/address/index.ts
@@ -0,0 +1,2 @@
+export { default as useAddresses } from './use-addresses'
+export { default as useAddItem } from './use-add-item'
diff --git a/framework/commercejs/customer/address/use-add-item.tsx b/framework/commercejs/customer/address/use-add-item.tsx
new file mode 100644
index 000000000..3e0022761
--- /dev/null
+++ b/framework/commercejs/customer/address/use-add-item.tsx
@@ -0,0 +1,25 @@
+import type { AddItemHook } from '@commerce/types/customer/address'
+import type { MutationHook } from '@commerce/utils/types'
+import { useCallback } from 'react'
+import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
+import { useCheckoutContext } from '@components/checkout/context'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: '_',
+ method: '_',
+ },
+ useHook: () =>
+ function useHook() {
+ const { setAddressFields } = useCheckoutContext()
+ return useCallback(
+ async function addItem(input) {
+ setAddressFields(input)
+ return undefined
+ },
+ [setAddressFields]
+ )
+ },
+}
diff --git a/framework/commercejs/customer/address/use-addresses.tsx b/framework/commercejs/customer/address/use-addresses.tsx
new file mode 100644
index 000000000..5d0ad0ab6
--- /dev/null
+++ b/framework/commercejs/customer/address/use-addresses.tsx
@@ -0,0 +1,34 @@
+import type { GetAddressesHook } from '@commerce/types/customer/address'
+
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useAddresses, {
+ UseAddresses,
+} from '@commerce/customer/address/use-addresses'
+
+export default useAddresses as UseAddresses
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ url: '_',
+ method: '_',
+ },
+ useHook: () =>
+ function useHook() {
+ return useMemo(
+ () =>
+ Object.create(
+ {},
+ {
+ isEmpty: {
+ get() {
+ return true
+ },
+ enumerable: true,
+ },
+ }
+ ),
+ []
+ )
+ },
+}
diff --git a/framework/commercejs/customer/card/index.ts b/framework/commercejs/customer/card/index.ts
new file mode 100644
index 000000000..4048ca29a
--- /dev/null
+++ b/framework/commercejs/customer/card/index.ts
@@ -0,0 +1,2 @@
+export { default as useCards } from './use-cards'
+export { default as useAddItem } from './use-add-item'
diff --git a/framework/commercejs/customer/card/use-add-item.tsx b/framework/commercejs/customer/card/use-add-item.tsx
new file mode 100644
index 000000000..d83c44cd5
--- /dev/null
+++ b/framework/commercejs/customer/card/use-add-item.tsx
@@ -0,0 +1,25 @@
+import type { AddItemHook } from '@commerce/types/customer/card'
+import type { MutationHook } from '@commerce/utils/types'
+import { useCallback } from 'react'
+import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
+import { useCheckoutContext } from '@components/checkout/context'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ url: '_',
+ method: '_',
+ },
+ useHook: () =>
+ function useHook() {
+ const { setCardFields } = useCheckoutContext()
+ return useCallback(
+ async function addItem(input) {
+ setCardFields(input)
+ return undefined
+ },
+ [setCardFields]
+ )
+ },
+}
diff --git a/framework/commercejs/customer/card/use-cards.tsx b/framework/commercejs/customer/card/use-cards.tsx
new file mode 100644
index 000000000..2372eaa53
--- /dev/null
+++ b/framework/commercejs/customer/card/use-cards.tsx
@@ -0,0 +1,31 @@
+import type { GetCardsHook } from '@commerce/types/customer/card'
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCard, { UseCards } from '@commerce/customer/card/use-cards'
+
+export default useCard as UseCards
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '_',
+ method: '_',
+ },
+ useHook: () =>
+ function useHook() {
+ return useMemo(
+ () =>
+ Object.create(
+ {},
+ {
+ isEmpty: {
+ get() {
+ return true
+ },
+ enumerable: true,
+ },
+ }
+ ),
+ []
+ )
+ },
+}
diff --git a/framework/commercejs/customer/index.ts b/framework/commercejs/customer/index.ts
new file mode 100644
index 000000000..6c903ecc5
--- /dev/null
+++ b/framework/commercejs/customer/index.ts
@@ -0,0 +1 @@
+export { default as useCustomer } from './use-customer'
diff --git a/framework/commercejs/customer/use-customer.tsx b/framework/commercejs/customer/use-customer.tsx
new file mode 100644
index 000000000..3f91b5abe
--- /dev/null
+++ b/framework/commercejs/customer/use-customer.tsx
@@ -0,0 +1,44 @@
+import Cookies from 'js-cookie'
+import { decode } from 'jsonwebtoken'
+import { SWRHook } from '@commerce/utils/types'
+import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
+import { CUSTOMER_COOKIE, API_URL } from '../constants'
+import type { CustomerHook } from '../types/customer'
+
+export default useCustomer as UseCustomer
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: 'customer',
+ method: '_request',
+ },
+ async fetcher({ options, fetch }) {
+ const token = Cookies.get(CUSTOMER_COOKIE)
+ if (!token) {
+ return null
+ }
+
+ const decodedToken = decode(token) as { cid: string }
+ const customer = await fetch({
+ query: options.query,
+ method: options.method,
+ variables: [
+ `${API_URL}/customers/${decodedToken.cid}`,
+ 'get',
+ null,
+ {},
+ token,
+ ],
+ })
+ return customer
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ return useData({
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input?.swrOptions,
+ },
+ })
+ },
+}
diff --git a/framework/commercejs/fetcher.ts b/framework/commercejs/fetcher.ts
new file mode 100644
index 000000000..c556582bf
--- /dev/null
+++ b/framework/commercejs/fetcher.ts
@@ -0,0 +1,61 @@
+import { commerce } from './lib/commercejs'
+import type { Fetcher } from '@commerce/utils/types'
+import { FetcherError } from '@commerce/utils/errors'
+
+function isValidSDKQuery(query?: string): query is keyof typeof commerce {
+ if (!query) return false
+ return query in commerce
+}
+
+// Fetches from an API route within /api/endpoints directory
+const customFetcher: Fetcher = async ({ method, url, body }) => {
+ const response = await fetch(url!, {
+ method,
+ body: body ? JSON.stringify(body) : undefined,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ })
+ .then((response) => response.json())
+ .then((response) => response.data)
+
+ return response
+}
+
+const fetcher: Fetcher = async ({ url, query, method, variables, body }) => {
+ // If a URL is passed, it means that the fetch needs to be passed on to a custom API route.
+ const isCustomFetch = !!url
+ if (isCustomFetch) {
+ const data = await customFetcher({ url, method, body })
+ return data
+ }
+
+ // Fetch using the Commerce.js SDK, but make sure that it's a valid method.
+ if (!isValidSDKQuery(query)) {
+ throw new FetcherError({
+ errors: [
+ { message: `Query ${query} does not exist on Commerce.js SDK.` },
+ ],
+ status: 400,
+ })
+ }
+
+ const resource: any = commerce[query]
+
+ if (!method || !resource[method]) {
+ throw new FetcherError({
+ errors: [
+ {
+ message: `Method ${method} does not exist on Commerce.js SDK ${query} resource.`,
+ },
+ ],
+ status: 400,
+ })
+ }
+
+ const variablesArgument = Array.isArray(variables) ? variables : [variables]
+ const data = await resource[method](...variablesArgument)
+ return data
+}
+
+export default fetcher
diff --git a/framework/commercejs/index.tsx b/framework/commercejs/index.tsx
new file mode 100644
index 000000000..c1ca7e4be
--- /dev/null
+++ b/framework/commercejs/index.tsx
@@ -0,0 +1,9 @@
+import { commercejsProvider, CommercejsProvider } from './provider'
+import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
+
+export { commercejsProvider }
+export type { CommercejsProvider }
+
+export const CommerceProvider = getCommerceProvider(commercejsProvider)
+
+export const useCommerce = () => useCoreCommerce()
diff --git a/framework/commercejs/lib/commercejs.ts b/framework/commercejs/lib/commercejs.ts
new file mode 100644
index 000000000..8acea540b
--- /dev/null
+++ b/framework/commercejs/lib/commercejs.ts
@@ -0,0 +1,11 @@
+import Commerce from '@chec/commerce.js'
+
+const commercejsPublicKey = process.env
+ .NEXT_PUBLIC_COMMERCEJS_PUBLIC_KEY as string
+const devEnvironment = process.env.NODE_ENV === 'development'
+
+if (devEnvironment && !commercejsPublicKey) {
+ throw Error('A Commerce.js public API key must be provided')
+}
+
+export const commerce = new Commerce(commercejsPublicKey, devEnvironment)
diff --git a/framework/commercejs/next.config.js b/framework/commercejs/next.config.js
new file mode 100644
index 000000000..0c9e96b4c
--- /dev/null
+++ b/framework/commercejs/next.config.js
@@ -0,0 +1,16 @@
+const commerce = require('./commerce.config.json')
+
+module.exports = {
+ commerce,
+ images: {
+ domains: ['cdn.chec.io'],
+ },
+ rewrites() {
+ return [
+ {
+ source: '/api/login/:token',
+ destination: '/api/login?token=:token',
+ },
+ ]
+ },
+}
diff --git a/framework/commercejs/product/index.ts b/framework/commercejs/product/index.ts
new file mode 100644
index 000000000..426a3edcd
--- /dev/null
+++ b/framework/commercejs/product/index.ts
@@ -0,0 +1,2 @@
+export { default as usePrice } from './use-price'
+export { default as useSearch } from './use-search'
diff --git a/framework/commercejs/product/use-price.tsx b/framework/commercejs/product/use-price.tsx
new file mode 100644
index 000000000..0174faf5e
--- /dev/null
+++ b/framework/commercejs/product/use-price.tsx
@@ -0,0 +1,2 @@
+export * from '@commerce/product/use-price'
+export { default } from '@commerce/product/use-price'
diff --git a/framework/commercejs/product/use-search.tsx b/framework/commercejs/product/use-search.tsx
new file mode 100644
index 000000000..e0561dc1f
--- /dev/null
+++ b/framework/commercejs/product/use-search.tsx
@@ -0,0 +1,53 @@
+import { SWRHook } from '@commerce/utils/types'
+import useSearch, { UseSearch } from '@commerce/product/use-search'
+import { SearchProductsHook } from '@commerce/types/product'
+import type { CommercejsProduct } from '../types/product'
+import { getProductSearchVariables } from '../utils/product-search'
+import { normalizeProduct } from '../utils/normalize-product'
+
+export default useSearch as UseSearch
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: 'products',
+ method: 'list',
+ },
+ async fetcher({ input, options, fetch }) {
+ const { data, meta } = await fetch<{
+ data: CommercejsProduct[]
+ meta: {
+ pagination: {
+ total: number
+ }
+ }
+ }>({
+ query: options.query,
+ method: options.method,
+ variables: getProductSearchVariables(input),
+ })
+
+ const formattedProducts =
+ data?.map((product) => normalizeProduct(product)) || []
+
+ return {
+ products: formattedProducts,
+ found: meta.pagination.total > 0,
+ }
+ },
+ useHook:
+ ({ useData }) =>
+ (input = {}) => {
+ return useData({
+ input: [
+ ['search', input.search],
+ ['categoryId', input.categoryId],
+ ['brandId', input.brandId],
+ ['sort', input.sort],
+ ],
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input.swrOptions,
+ },
+ })
+ },
+}
diff --git a/framework/commercejs/provider.ts b/framework/commercejs/provider.ts
new file mode 100644
index 000000000..d596fa9b2
--- /dev/null
+++ b/framework/commercejs/provider.ts
@@ -0,0 +1,55 @@
+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 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 { handler as useCheckout } from './checkout/use-checkout'
+import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
+
+import { handler as useCards } from './customer/card/use-cards'
+import { handler as useAddCardItem } from './customer/card/use-add-item'
+
+import { handler as useAddresses } from './customer/address/use-addresses'
+import { handler as useAddAddressItem } from './customer/address/use-add-item'
+
+import { CART_COOKIE, CUSTOMER_COOKIE, LOCALE } from './constants'
+import { default as sdkFetcher } from './fetcher'
+
+export const commercejsProvider = {
+ locale: LOCALE,
+ cartCookie: CART_COOKIE,
+ customerCookie: CUSTOMER_COOKIE,
+ fetcher: sdkFetcher,
+ cart: {
+ useCart,
+ useAddItem,
+ useUpdateItem,
+ useRemoveItem,
+ },
+ checkout: {
+ useCheckout,
+ useSubmitCheckout,
+ },
+ customer: {
+ useCustomer,
+ card: {
+ useCards,
+ useAddItem: useAddCardItem,
+ },
+ address: {
+ useAddresses,
+ useAddItem: useAddAddressItem,
+ },
+ },
+ products: { useSearch },
+ auth: { useLogin, useLogout, useSignup },
+}
+
+export type CommercejsProvider = typeof commercejsProvider
diff --git a/framework/commercejs/types/cart.ts b/framework/commercejs/types/cart.ts
new file mode 100644
index 000000000..fe7b7bdc2
--- /dev/null
+++ b/framework/commercejs/types/cart.ts
@@ -0,0 +1,4 @@
+export * from '@commerce/types/cart'
+
+export type { Cart as CommercejsCart } from '@chec/commerce.js/types/cart'
+export type { LineItem as CommercejsLineItem } from '@chec/commerce.js/types/line-item'
diff --git a/framework/commercejs/types/checkout.ts b/framework/commercejs/types/checkout.ts
new file mode 100644
index 000000000..47b6a63b9
--- /dev/null
+++ b/framework/commercejs/types/checkout.ts
@@ -0,0 +1,3 @@
+export * from '@commerce/types/checkout'
+
+export type { CheckoutCapture as CommercejsCheckoutCapture } from '@chec/commerce.js/types/checkout-capture'
diff --git a/framework/commercejs/types/common.ts b/framework/commercejs/types/common.ts
new file mode 100644
index 000000000..b52c33a4d
--- /dev/null
+++ b/framework/commercejs/types/common.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/common'
diff --git a/framework/commercejs/types/customer.ts b/framework/commercejs/types/customer.ts
new file mode 100644
index 000000000..87c9afcc4
--- /dev/null
+++ b/framework/commercejs/types/customer.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/customer'
diff --git a/framework/commercejs/types/index.ts b/framework/commercejs/types/index.ts
new file mode 100644
index 000000000..7ab0b7f64
--- /dev/null
+++ b/framework/commercejs/types/index.ts
@@ -0,0 +1,25 @@
+import * as Cart from './cart'
+import * as Checkout from './checkout'
+import * as Common from './common'
+import * as Customer from './customer'
+import * as Login from './login'
+import * as Logout from './logout'
+import * as Page from './page'
+import * as Product from './product'
+import * as Signup from './signup'
+import * as Site from './site'
+import * as Wishlist from './wishlist'
+
+export type {
+ Cart,
+ Checkout,
+ Common,
+ Customer,
+ Login,
+ Logout,
+ Page,
+ Product,
+ Signup,
+ Site,
+ Wishlist,
+}
diff --git a/framework/commercejs/types/login.ts b/framework/commercejs/types/login.ts
new file mode 100644
index 000000000..97f879078
--- /dev/null
+++ b/framework/commercejs/types/login.ts
@@ -0,0 +1,9 @@
+import { LoginBody, LoginTypes } from '@commerce/types/login'
+export * from '@commerce/types/login'
+
+export type LoginHook = {
+ data: null
+ actionInput: LoginBody
+ fetcherInput: LoginBody
+ body: T['body']
+}
diff --git a/framework/commercejs/types/logout.ts b/framework/commercejs/types/logout.ts
new file mode 100644
index 000000000..9f0a466af
--- /dev/null
+++ b/framework/commercejs/types/logout.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/logout'
diff --git a/framework/commercejs/types/page.ts b/framework/commercejs/types/page.ts
new file mode 100644
index 000000000..20ec8ea38
--- /dev/null
+++ b/framework/commercejs/types/page.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/page'
diff --git a/framework/commercejs/types/product.ts b/framework/commercejs/types/product.ts
new file mode 100644
index 000000000..4db475d95
--- /dev/null
+++ b/framework/commercejs/types/product.ts
@@ -0,0 +1,4 @@
+export * from '@commerce/types/product'
+
+export type { Product as CommercejsProduct } from '@chec/commerce.js/types/product'
+export type { Variant as CommercejsVariant } from '@chec/commerce.js/types/variant'
diff --git a/framework/commercejs/types/signup.ts b/framework/commercejs/types/signup.ts
new file mode 100644
index 000000000..58543c6f6
--- /dev/null
+++ b/framework/commercejs/types/signup.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/signup'
diff --git a/framework/commercejs/types/site.ts b/framework/commercejs/types/site.ts
new file mode 100644
index 000000000..8fd61a07d
--- /dev/null
+++ b/framework/commercejs/types/site.ts
@@ -0,0 +1,3 @@
+export * from '@commerce/types/site'
+
+export type { Category as CommercejsCategory } from '@chec/commerce.js/types/category'
diff --git a/framework/commercejs/types/wishlist.ts b/framework/commercejs/types/wishlist.ts
new file mode 100644
index 000000000..8907fbf82
--- /dev/null
+++ b/framework/commercejs/types/wishlist.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/wishlist'
diff --git a/framework/commercejs/utils/get-deployment-url.ts b/framework/commercejs/utils/get-deployment-url.ts
new file mode 100644
index 000000000..b0926abc7
--- /dev/null
+++ b/framework/commercejs/utils/get-deployment-url.ts
@@ -0,0 +1,12 @@
+export const getDeploymentUrl = () => {
+ // Custom environment variable.
+ if (process.env.NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL) {
+ return process.env.NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL
+ }
+ // Automatic Vercel deployment URL.
+ if (process.env.NEXT_PUBLIC_VERCEL_URL) {
+ return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
+ }
+ // Assume local development.
+ return 'http://localhost:3000'
+}
diff --git a/framework/commercejs/utils/normalize-cart.ts b/framework/commercejs/utils/normalize-cart.ts
new file mode 100644
index 000000000..c01ea6dea
--- /dev/null
+++ b/framework/commercejs/utils/normalize-cart.ts
@@ -0,0 +1,74 @@
+import type {
+ Cart,
+ LineItem,
+ CommercejsCart,
+ CommercejsLineItem,
+} from '../types/cart'
+
+type CommercejsLineItemType = CommercejsLineItem & { image: { url: string } }
+
+const normalizeLineItem = (
+ commercejsLineItem: CommercejsLineItemType
+): LineItem => {
+ const {
+ id,
+ sku,
+ quantity,
+ price,
+ product_id,
+ product_name,
+ permalink,
+ variant,
+ image,
+ selected_options,
+ } = commercejsLineItem
+ return {
+ id,
+ variantId: variant?.id ?? '',
+ productId: product_id,
+ name: product_name,
+ quantity,
+ discounts: [],
+ path: permalink,
+ options: selected_options?.map(({ group_name, option_name }) => ({
+ name: group_name,
+ value: option_name,
+ })),
+ variant: {
+ id: variant?.id ?? id,
+ sku: variant?.sku ?? sku,
+ name: product_name,
+ requiresShipping: false,
+ price: variant?.price?.raw ?? price.raw,
+ listPrice: variant?.price?.raw ?? price.raw,
+ image: {
+ url: image?.url,
+ },
+ },
+ }
+}
+
+export const normalizeCart = (commercejsCart: CommercejsCart): Cart => {
+ const {
+ id,
+ created,
+ subtotal: { raw: rawPrice },
+ currency,
+ line_items,
+ } = commercejsCart
+
+ return {
+ id,
+ createdAt: new Date(created * 1000).toISOString(),
+ currency: {
+ code: currency.code,
+ },
+ taxesIncluded: false,
+ lineItems: line_items.map((item) => {
+ return normalizeLineItem(item as CommercejsLineItemType)
+ }),
+ lineItemsSubtotalPrice: rawPrice,
+ subtotalPrice: rawPrice,
+ totalPrice: rawPrice,
+ }
+}
diff --git a/framework/commercejs/utils/normalize-category.ts b/framework/commercejs/utils/normalize-category.ts
new file mode 100644
index 000000000..e47969e81
--- /dev/null
+++ b/framework/commercejs/utils/normalize-category.ts
@@ -0,0 +1,14 @@
+import type { Category } from '@commerce/types/site'
+import type { Category as CommercejsCategory } from '@chec/commerce.js/types/category'
+
+export function normalizeCategory(
+ commercejsCatgeory: CommercejsCategory
+): Category {
+ const { id, name, slug } = commercejsCatgeory
+ return {
+ id,
+ name,
+ slug,
+ path: slug,
+ }
+}
diff --git a/framework/commercejs/utils/normalize-checkout.ts b/framework/commercejs/utils/normalize-checkout.ts
new file mode 100644
index 000000000..2cc996f88
--- /dev/null
+++ b/framework/commercejs/utils/normalize-checkout.ts
@@ -0,0 +1,63 @@
+import type { CardFields } from '@commerce/types/customer/card'
+import type { AddressFields } from '@commerce/types/customer/address'
+import type { CommercejsCheckoutCapture } from '../types/checkout'
+
+/**
+ * Creates a checkout payload suitable for test checkouts.
+ * 1. Hard-codes the payment values for the Commerce.js test gateway.
+ * 2. Hard-codes the email until an email field exists on the checkout form.
+ * 3. Gets as much as much checkout info as possible from the checkout form, and uses fallback values.
+ */
+export function normalizeTestCheckout({
+ paymentInfo,
+ shippingInfo,
+ shippingOption,
+}: {
+ paymentInfo?: CardFields
+ shippingInfo?: AddressFields
+ shippingOption: string
+}): CommercejsCheckoutCapture {
+ const firstName =
+ shippingInfo?.firstName || paymentInfo?.firstName || 'Nextjs'
+ const lastName = shippingInfo?.lastName || paymentInfo?.lastName || 'Commerce'
+ const fullName = `${firstName} ${lastName}`
+ const postalCode = shippingInfo?.zipCode || paymentInfo?.zipCode || '94103'
+ const street =
+ shippingInfo?.streetNumber || paymentInfo?.streetNumber || 'Test Street'
+ const townCity = shippingInfo?.city || paymentInfo?.city || 'Test Town'
+
+ return {
+ payment: {
+ gateway: 'test_gateway',
+ card: {
+ number: '4242 4242 4242 4242',
+ expiry_month: '01',
+ expiry_year: '2024',
+ cvc: '123',
+ postal_zip_code: postalCode,
+ },
+ },
+ customer: {
+ email: 'nextcommerce@test.com',
+ firstname: firstName,
+ lastname: lastName,
+ },
+ shipping: {
+ name: fullName,
+ street,
+ town_city: townCity,
+ country: 'US',
+ },
+ billing: {
+ name: fullName,
+ street,
+ town_city: townCity,
+ postal_zip_code: postalCode,
+ county_state: 'California',
+ country: 'US',
+ },
+ fulfillment: {
+ shipping_method: shippingOption,
+ },
+ }
+}
diff --git a/framework/commercejs/utils/normalize-product.ts b/framework/commercejs/utils/normalize-product.ts
new file mode 100644
index 000000000..86c42d922
--- /dev/null
+++ b/framework/commercejs/utils/normalize-product.ts
@@ -0,0 +1,77 @@
+import type {
+ Product,
+ CommercejsProduct,
+ CommercejsVariant,
+} from '../types/product'
+
+function getOptionsFromVariantGroups(
+ variantGroups: CommercejsProduct['variant_groups']
+): Product['options'] {
+ const optionsFromVariantGroups = variantGroups.map(
+ ({ id, name: variantName, options }) => ({
+ id,
+ displayName: variantName,
+ values: options.map(({ name: optionName }) => ({
+ label: optionName,
+ })),
+ })
+ )
+ return optionsFromVariantGroups
+}
+
+function normalizeVariants(
+ variants: Array = [],
+ variantGroups: CommercejsProduct['variant_groups']
+) {
+ if (!Array.isArray(variants)) return []
+ return variants?.map((variant) => ({
+ id: variant.id,
+ options: Object.entries(variant.options).map(
+ ([variantGroupId, variantOptionId]) => {
+ const variantGroupFromId = variantGroups.find(
+ (group) => group.id === variantGroupId
+ )
+ const valueLabel = variantGroupFromId?.options.find(
+ (option) => option.id === variantOptionId
+ )?.name
+
+ return {
+ id: variantOptionId,
+ displayName: variantGroupFromId?.name || '',
+ __typename: 'MultipleChoiceOption' as 'MultipleChoiceOption',
+ values: [
+ {
+ label: valueLabel || '',
+ },
+ ],
+ }
+ }
+ ),
+ }))
+}
+
+export function normalizeProduct(
+ commercejsProduct: CommercejsProduct,
+ commercejsProductVariants: Array = []
+): Product {
+ const { id, name, description, permalink, assets, price, variant_groups } =
+ commercejsProduct
+ return {
+ id,
+ name,
+ description,
+ descriptionHtml: description,
+ slug: permalink,
+ path: permalink,
+ images: assets.map(({ url, description, filename }) => ({
+ url,
+ alt: description || filename,
+ })),
+ price: {
+ value: price.raw,
+ currencyCode: 'USD',
+ },
+ variants: normalizeVariants(commercejsProductVariants, variant_groups),
+ options: getOptionsFromVariantGroups(variant_groups),
+ }
+}
diff --git a/framework/commercejs/utils/product-search.ts b/framework/commercejs/utils/product-search.ts
new file mode 100644
index 000000000..b1ee96681
--- /dev/null
+++ b/framework/commercejs/utils/product-search.ts
@@ -0,0 +1,54 @@
+import { SearchProductsBody } from '@commerce/types/product'
+
+const getFilterVariables = ({
+ search,
+ categoryId,
+}: {
+ search?: string
+ categoryId?: string | number
+}) => {
+ let filterVariables: { [key: string]: any } = {}
+ if (search) {
+ filterVariables.query = search
+ }
+ if (categoryId) {
+ filterVariables['category_id'] = categoryId
+ }
+ return filterVariables
+}
+
+const getSortVariables = ({ sort }: { sort?: string }) => {
+ let sortVariables: { [key: string]: any } = {}
+ switch (sort) {
+ case 'trending-desc':
+ case 'latest-desc':
+ sortVariables = {
+ sortBy: 'updated',
+ sortDirection: 'desc',
+ }
+ break
+ case 'price-asc':
+ sortVariables = {
+ sortBy: 'price',
+ sortDirection: 'asc',
+ }
+ break
+ case 'price-desc':
+ sortVariables = {
+ sortBy: 'price',
+ sortDirection: 'desc',
+ }
+ break
+ }
+ return sortVariables
+}
+
+export const getProductSearchVariables = (input: SearchProductsBody) => {
+ const { search, categoryId, sort } = input
+ const filterVariables = getFilterVariables({ search, categoryId })
+ const sortVariables = getSortVariables({ sort })
+ return {
+ ...filterVariables,
+ ...sortVariables,
+ }
+}
diff --git a/framework/commercejs/wishlist/use-add-item.tsx b/framework/commercejs/wishlist/use-add-item.tsx
new file mode 100644
index 000000000..75f067c3a
--- /dev/null
+++ b/framework/commercejs/wishlist/use-add-item.tsx
@@ -0,0 +1,13 @@
+import { useCallback } from 'react'
+
+export function emptyHook() {
+ const useEmptyHook = async (options = {}) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
+ }
+
+ return useEmptyHook
+}
+
+export default emptyHook
diff --git a/framework/commercejs/wishlist/use-remove-item.tsx b/framework/commercejs/wishlist/use-remove-item.tsx
new file mode 100644
index 000000000..a2d3a8a05
--- /dev/null
+++ b/framework/commercejs/wishlist/use-remove-item.tsx
@@ -0,0 +1,17 @@
+import { useCallback } from 'react'
+
+type Options = {
+ includeProducts?: boolean
+}
+
+export function emptyHook(options?: Options) {
+ const useEmptyHook = async ({ id }: { id: string | number }) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
+ }
+
+ return useEmptyHook
+}
+
+export default emptyHook
diff --git a/framework/commercejs/wishlist/use-wishlist.tsx b/framework/commercejs/wishlist/use-wishlist.tsx
new file mode 100644
index 000000000..125ee2686
--- /dev/null
+++ b/framework/commercejs/wishlist/use-wishlist.tsx
@@ -0,0 +1,40 @@
+import { HookFetcher } from '@commerce/utils/types'
+
+export type Wishlist = {
+ items: [
+ {
+ product_id: number
+ variant_id: number
+ id: number
+ product: any
+ }
+ ]
+}
+
+export interface UseWishlistOptions {
+ includeProducts?: boolean
+}
+
+export interface UseWishlistInput extends UseWishlistOptions {
+ customerId?: number
+}
+
+export const fetcher: HookFetcher = () => {
+ return null
+}
+
+export function extendHook(
+ customFetcher: typeof fetcher,
+ // swrOptions?: SwrOptions
+ swrOptions?: any
+) {
+ const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
+ return { data: null }
+ }
+
+ useWishlist.extend = extendHook
+
+ return useWishlist
+}
+
+export default extendHook(fetcher)
diff --git a/package-lock.json b/package-lock.json
index 196a45582..882bd6216 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "@chec/commerce.js": "^2.8.0",
"@react-spring/web": "^9.2.1",
"@spree/storefront-api-v2-sdk": "^5.0.1",
"@vercel/fetch": "^6.1.1",
@@ -47,6 +48,7 @@
"@graphql-codegen/typescript-operations": "^1.18.1",
"@next/bundle-analyzer": "^10.2.3",
"@types/body-scroll-lock": "^2.6.1",
+ "@types/chec__commerce.js": "^2.8.4",
"@types/cookie": "^0.4.0",
"@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",
@@ -1282,6 +1284,15 @@
"to-fast-properties": "^2.0.0"
}
},
+ "node_modules/@chec/commerce.js": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@chec/commerce.js/-/commerce.js-2.8.0.tgz",
+ "integrity": "sha512-OPBphT/hU33iDp52zzYOqz/oSXLhEuhGVUg2UNvYtmBW4eCNmtsM0dqW0+wu+6K0d6fZojurCBdVQMKb2R7l3g==",
+ "dependencies": {
+ "@babel/runtime": "^7.7.4",
+ "axios": "^0.21.1"
+ }
+ },
"node_modules/@csstools/convert-colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
@@ -2509,6 +2520,15 @@
"integrity": "sha512-PPFm/2A6LfKmSpvMg58gHtSqwwMChbcKKGhSCRIhY4MyFzhY8moAN6HrTCpOeZQUqkFdTFfMqr7njeqGLKt72Q==",
"dev": true
},
+ "node_modules/@types/chec__commerce.js": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/@types/chec__commerce.js/-/chec__commerce.js-2.8.4.tgz",
+ "integrity": "sha512-hyR2OXEB3gIRp/ESWOQaFStefBG+C5OdnkxGC1Gmp0ePVzl/wk5FyvaK5NsT1ddNC/y1YsmDAVPe+DArr6/9Jg==",
+ "dev": true,
+ "dependencies": {
+ "@types/chec__commerce.js": "*"
+ }
+ },
"node_modules/@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
@@ -3254,6 +3274,14 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "0.21.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
+ "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "dependencies": {
+ "follow-redirects": "^1.14.0"
+ }
+ },
"node_modules/axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -5809,6 +5837,25 @@
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.14.5",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
+ "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
@@ -16303,6 +16350,15 @@
"to-fast-properties": "^2.0.0"
}
},
+ "@chec/commerce.js": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@chec/commerce.js/-/commerce.js-2.8.0.tgz",
+ "integrity": "sha512-OPBphT/hU33iDp52zzYOqz/oSXLhEuhGVUg2UNvYtmBW4eCNmtsM0dqW0+wu+6K0d6fZojurCBdVQMKb2R7l3g==",
+ "requires": {
+ "@babel/runtime": "^7.7.4",
+ "axios": "^0.21.1"
+ }
+ },
"@csstools/convert-colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
@@ -17254,6 +17310,15 @@
"integrity": "sha512-PPFm/2A6LfKmSpvMg58gHtSqwwMChbcKKGhSCRIhY4MyFzhY8moAN6HrTCpOeZQUqkFdTFfMqr7njeqGLKt72Q==",
"dev": true
},
+ "@types/chec__commerce.js": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/@types/chec__commerce.js/-/chec__commerce.js-2.8.4.tgz",
+ "integrity": "sha512-hyR2OXEB3gIRp/ESWOQaFStefBG+C5OdnkxGC1Gmp0ePVzl/wk5FyvaK5NsT1ddNC/y1YsmDAVPe+DArr6/9Jg==",
+ "dev": true,
+ "requires": {
+ "@types/chec__commerce.js": "*"
+ }
+ },
"@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
@@ -17843,6 +17908,14 @@
"integrity": "sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg==",
"dev": true
},
+ "axios": {
+ "version": "0.21.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
+ "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "requires": {
+ "follow-redirects": "^1.14.0"
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -19897,6 +19970,11 @@
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
"dev": true
},
+ "follow-redirects": {
+ "version": "1.14.5",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
+ "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
+ },
"foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
diff --git a/package.json b/package.json
index f0fa51e02..0252f5392 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"node": ">=14.x"
},
"dependencies": {
+ "@chec/commerce.js": "^2.8.0",
"@react-spring/web": "^9.2.1",
"@spree/storefront-api-v2-sdk": "^5.0.1",
"@vercel/fetch": "^6.1.1",
@@ -59,6 +60,7 @@
"@graphql-codegen/typescript-operations": "^1.18.1",
"@next/bundle-analyzer": "^10.2.3",
"@types/body-scroll-lock": "^2.6.1",
+ "@types/chec__commerce.js": "^2.8.4",
"@types/cookie": "^0.4.0",
"@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",
diff --git a/tsconfig.json b/tsconfig.json
index 340929669..3373fb42f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -34,6 +34,7 @@
"./framework/shopify",
"./framework/swell",
"./framework/vendure",
- "./framework/saleor"
+ "./framework/saleor",
+ "./framework/commercejs"
]
}