diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index bb57c3f25..cb7f8600e 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -33,7 +33,7 @@ const CartItem = ({ currencyCode, }) - const updateItem = useUpdateItem(item) + const updateItem = useUpdateItem({ item }) const removeItem = useRemoveItem() const [quantity, setQuantity] = useState(item.quantity) const [removing, setRemoving] = useState(false) diff --git a/framework/bigcommerce/cart/use-add-item.tsx b/framework/bigcommerce/cart/use-add-item.tsx index 7aec2f9e0..d74c23567 100644 --- a/framework/bigcommerce/cart/use-add-item.tsx +++ b/framework/bigcommerce/cart/use-add-item.tsx @@ -1,29 +1,24 @@ -import type { MutationHandler } from '@commerce/utils/types' +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 { - AddCartItemBody, Cart, BigcommerceCart, CartItemBody, + AddCartItemBody, } from '../types' import useCart from './use-cart' -import { BigcommerceProvider } from '..' -const defaultOpts = { - url: '/api/bigcommerce/cart', - method: 'POST', -} +export default useAddItem as UseAddItem -export default useAddItem as UseAddItem - -export const handler: MutationHandler = { +export const handler: MutationHook = { fetchOptions: { url: '/api/bigcommerce/cart', - method: 'GET', + method: 'POST', }, - async fetcher({ input: { item }, options, fetch }) { + async fetcher({ input: item, options, fetch }) { if ( item.quantity && (!Number.isInteger(item.quantity) || item.quantity! < 1) @@ -34,20 +29,22 @@ export const handler: MutationHandler = { } const data = await fetch({ - ...defaultOpts, ...options, body: { item }, }) return normalizeCart(data) }, - useHook() { + useHook: ({ fetch }) => () => { const { mutate } = useCart() - return async function addItem({ input, fetch }) { - const data = await fetch({ input }) - await mutate(data, false) - return data - } + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + await mutate(data, false) + return data + }, + [fetch, mutate] + ) }, } diff --git a/framework/bigcommerce/cart/use-update-item.tsx b/framework/bigcommerce/cart/use-update-item.tsx index d1870c818..f5a09006a 100644 --- a/framework/bigcommerce/cart/use-update-item.tsx +++ b/framework/bigcommerce/cart/use-update-item.tsx @@ -1,9 +1,10 @@ import { useCallback } from 'react' import debounce from 'lodash.debounce' -import type { HookFetcher } from '@commerce/utils/types' +import type { HookContext, HookFetcherContext } from '@commerce/utils/types' import { ValidationError } from '@commerce/utils/errors' -import useCartUpdateItem, { - UpdateItemInput as UseUpdateItemInput, +import useUpdateItem, { + UpdateItemInput as UpdateItemInputBase, + UseUpdateItem, } from '@commerce/cart/use-update-item' import { normalizeCart } from '../lib/normalize' import type { @@ -15,49 +16,50 @@ import type { import { fetcher as removeFetcher } from './use-remove-item' import useCart from './use-cart' -const defaultOpts = { - url: '/api/bigcommerce/cart', - method: 'PUT', -} - export type UpdateItemInput = T extends LineItem - ? Partial> - : UseUpdateItemInput + ? Partial> + : UpdateItemInputBase -export const fetcher: HookFetcher = async ( - options, - { itemId, item }, - fetch -) => { - if (Number.isInteger(item.quantity)) { - // Also allow the update hook to remove an item if the quantity is lower than 1 - if (item.quantity! < 1) { - return removeFetcher(null, { itemId }, fetch) +export default useUpdateItem as UseUpdateItem + +export const handler = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'PUT', + }, + async fetcher({ + input: { itemId, item }, + options, + fetch, + }: HookFetcherContext) { + if (Number.isInteger(item.quantity)) { + // Also allow the update hook to remove an item if the quantity is lower than 1 + if (item.quantity! < 1) { + return removeFetcher(null, { itemId }, fetch) + } + } else if (item.quantity) { + throw new ValidationError({ + message: 'The item quantity has to be a valid integer', + }) } - } else if (item.quantity) { - throw new ValidationError({ - message: 'The item quantity has to be a valid integer', + + const data = await fetch({ + ...options, + body: { itemId, item }, }) - } - const data = await fetch({ - ...defaultOpts, - ...options, - body: { itemId, item }, - }) - - return normalizeCart(data) -} - -function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { - const useUpdateItem = ( - item?: T + return normalizeCart(data) + }, + useHook: ({ fetch }: HookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { + item?: T + wait?: number + } = {} ) => { - const { mutate } = useCart() - const fn = useCartUpdateItem( - defaultOpts, - customFetcher - ) + const { item } = ctx + const { mutate } = useCart() as any return useCallback( debounce(async (input: UpdateItemInput) => { @@ -71,20 +73,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { }) } - const data = await fn({ - itemId, - item: { productId, variantId, quantity: input.quantity }, + const data = await fetch({ + input: { + itemId, + item: { productId, variantId, quantity: input.quantity }, + }, }) await mutate(data, false) return data - }, cfg?.wait ?? 500), - [fn, mutate] + }, ctx.wait ?? 500), + [fetch, mutate] ) - } - - useUpdateItem.extend = extendHook - - return useUpdateItem + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/provider.ts b/framework/bigcommerce/provider.ts index 08192df37..434c1c39e 100644 --- a/framework/bigcommerce/provider.ts +++ b/framework/bigcommerce/provider.ts @@ -1,5 +1,6 @@ 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 useWishlist } from './wishlist/use-wishlist' import { handler as useCustomer } from './customer/use-customer' import { handler as useSearch } from './product/use-search' @@ -9,7 +10,7 @@ export const bigcommerceProvider = { locale: 'en-us', cartCookie: 'bc_cartId', fetcher, - cart: { useCart, useAddItem }, + cart: { useCart, useAddItem, useUpdateItem }, wishlist: { useWishlist }, customer: { useCustomer }, products: { useSearch }, diff --git a/framework/bigcommerce/types.ts b/framework/bigcommerce/types.ts index 16d1ea07a..beeab0223 100644 --- a/framework/bigcommerce/types.ts +++ b/framework/bigcommerce/types.ts @@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & { optionSelections?: OptionSelections } -type X = Core.CartItemBody extends CartItemBody ? any : never -type Y = CartItemBody extends Core.CartItemBody ? any : never - export type GetCartHandlerBody = Core.GetCartHandlerBody export type AddCartItemBody = Core.AddCartItemBody diff --git a/framework/commerce/cart/use-add-item.tsx b/framework/commerce/cart/use-add-item.tsx index 0a70ff30d..715029d18 100644 --- a/framework/commerce/cart/use-add-item.tsx +++ b/framework/commerce/cart/use-add-item.tsx @@ -1,33 +1,7 @@ -import { useCallback } from 'react' -import type { - Prop, - HookFetcherFn, - UseHookInput, - UseHookResponse, -} from '../utils/types' +import useHook, { useHookHandler } from '../utils/use-hook' +import type { MutationHook, HookFetcherFn } from '../utils/types' import type { Cart, CartItemBody, AddCartItemBody } from '../types' -import { Provider, useCommerce } from '..' -import { BigcommerceProvider } from '@framework' - -export type UseAddItemHandler

= Prop< - Prop, - 'useAddItem' -> - -// Input expected by the action returned by the `useAddItem` hook -export type UseAddItemInput

= UseHookInput< - UseAddItemHandler

-> - -export type UseAddItemResult

= ReturnType< - UseHookResponse> -> - -export type UseAddItem

= Partial< - UseAddItemInput

-> extends UseAddItemInput

- ? (input?: UseAddItemInput

) => (input: Input) => UseAddItemResult

- : (input: UseAddItemInput

) => (input: Input) => UseAddItemResult

+import type { Provider } from '..' export const fetcher: HookFetcherFn< Cart, @@ -36,34 +10,15 @@ export const fetcher: HookFetcherFn< return fetch({ ...options, body: input }) } -type X = UseAddItemResult +export type UseAddItem< + H extends MutationHook = MutationHook +> = ReturnType -export default function useAddItem

( - input: UseAddItemInput

-) { - const { providerRef, fetcherRef } = useCommerce

() +const fn = (provider: Provider) => provider.cart?.useAddItem! - const provider = providerRef.current - const opts = provider.cart?.useAddItem - - const fetcherFn = opts?.fetcher ?? fetcher - const useHook = opts?.useHook ?? (() => () => {}) - const fetchFn = provider.fetcher ?? fetcherRef.current - const action = useHook({ input }) - - return useCallback( - function addItem(input: Input) { - return action({ - input, - fetch({ input }) { - return fetcherFn({ - input, - options: opts!.fetchOptions, - fetch: fetchFn, - }) - }, - }) - }, - [input, fetchFn, opts?.fetchOptions] - ) +const useAddItem: UseAddItem = (...args) => { + const handler = useHookHandler(fn, fetcher) + return handler(useHook(fn, fetcher))(...args) } + +export default useAddItem diff --git a/framework/commerce/cart/use-cart-actions.tsx b/framework/commerce/cart/use-cart-actions.tsx index 3ba4b2e1a..5d081f0a8 100644 --- a/framework/commerce/cart/use-cart-actions.tsx +++ b/framework/commerce/cart/use-cart-actions.tsx @@ -1,5 +1,5 @@ import type { HookFetcher, HookFetcherOptions } from '../utils/types' -import useAddItem from './use-add-item' +// import useAddItem from './use-add-item' import useRemoveItem from './use-remove-item' import useUpdateItem from './use-update-item' @@ -9,9 +9,9 @@ export default function useCartActions( options: HookFetcherOptions, fetcher: HookFetcher ) { - const addItem = useAddItem(options, fetcher) + // const addItem = useAddItem(options, fetcher) const updateItem = useUpdateItem(options, fetcher) const removeItem = useRemoveItem(options, fetcher) - return { addItem, updateItem, removeItem } + return { updateItem, removeItem } } diff --git a/framework/commerce/cart/use-update-item.tsx b/framework/commerce/cart/use-update-item.tsx index e1adcb5fb..cc904a14a 100644 --- a/framework/commerce/cart/use-update-item.tsx +++ b/framework/commerce/cart/use-update-item.tsx @@ -1,11 +1,39 @@ -import useAction from '../utils/use-action' -import type { CartItemBody } from '../types' +import useHook, { useHookHandler } from '../utils/use-hook' +import type { MutationHook, HookFetcherFn } from '../utils/types' +import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types' +import type { Provider } from '..' +import debounce from 'lodash.debounce' // Input expected by the action returned by the `useUpdateItem` hook export type UpdateItemInput = T & { id: string } -const useUpdateItem = useAction +export type UseUpdateItem< + H extends MutationHook = MutationHook< + Cart, + { + item?: LineItem + wait?: number + }, + UpdateItemInput, + UpdateCartItemBody + > +> = ReturnType + +export const fetcher: HookFetcherFn = async ({ + options, + input, + fetch, +}) => { + return fetch({ ...options, body: input }) +} + +const fn = (provider: Provider) => provider.cart?.useUpdateItem! + +const useUpdateItem: UseUpdateItem = (input = {}) => { + const handler = useHookHandler(fn, fetcher) + return debounce(handler(useHook(fn, fetcher))(input), input.wait ?? 500) +} export default useUpdateItem diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 243fba2db..935e8610f 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -6,7 +6,12 @@ import { useMemo, useRef, } from 'react' -import { Fetcher, HookHandler, MutationHandler } from './utils/types' +import { + Fetcher, + HookHandler, + MutationHandler, + MutationHook, +} from './utils/types' import type { FetchCartInput } from './cart/use-cart' import type { Cart, Wishlist, Customer, SearchProductsData } from './types' @@ -16,7 +21,9 @@ export type Provider = CommerceConfig & { fetcher: Fetcher cart?: { useCart?: HookHandler - useAddItem?: MutationHandler + useAddItem?: MutationHook + useUpdateItem?: MutationHook + useRemoveItem?: MutationHook } wishlist?: { useWishlist?: HookHandler diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 1d3adef81..120d66366 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -1,3 +1,4 @@ +import { LineItem } from '@framework/types' import type { ConfigInterface } from 'swr' import type { CommerceError } from './errors' import type { ResponseState } from './use-data' @@ -31,16 +32,15 @@ export type HookFetcher = ( fetch: (options: FetcherOptions) => Promise ) => Data | Promise -export type HookFetcherFn< - Data, - Input = never, - Result = any, - Body = any -> = (context: { +export type HookFetcherFn = ( + context: HookFetcherContext +) => Data | Promise + +export type HookFetcherContext = { options: HookFetcherOptions input: Input fetch: (options: FetcherOptions) => Promise -}) => Data | Promise +} export type HookFetcherOptions = { method?: string } & ( | { query: string; url?: string } @@ -53,8 +53,6 @@ export type HookSwrInput = [string, HookInputValue][] export type HookFetchInput = { [k: string]: HookInputValue } -export type HookInput = {} - export type HookHandler< // Data obj returned by the hook and fetch operation Data, @@ -94,6 +92,39 @@ export type MutationHandler< fetcher?: HookFetcherFn } +export type HookFunction< + Input extends { [k: string]: unknown } | {}, + T +> = keyof Input extends never + ? () => T + : Partial extends Input + ? (input?: Input) => T + : (input: Input) => T + +export type MutationHook< + // Data obj returned by the hook and fetch operation + Data, + // Input expected by the hook + Input extends { [k: string]: unknown } = {}, + // Input expected by the action returned by the hook + ActionInput extends { [k: string]: unknown } = {}, + // Input expected before doing a fetch operation + FetchInput extends { [k: string]: unknown } = ActionInput +> = { + useHook( + context: HookContext + ): HookFunction>> + fetchOptions: HookFetcherOptions + fetcher?: HookFetcherFn +} + +export type HookContext< + Data, + FetchInput extends { [k: string]: unknown } = {} +> = { + fetch: (context: { input: FetchInput }) => Data | Promise +} + export type SwrOptions = ConfigInterface< Data, CommerceError, diff --git a/framework/commerce/utils/use-hook.ts b/framework/commerce/utils/use-hook.ts new file mode 100644 index 000000000..b37c33370 --- /dev/null +++ b/framework/commerce/utils/use-hook.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react' +import type { MutationHook } from './types' +import { Provider, useCommerce } from '..' + +export function useHookHandler

( + fn: (provider: P) => MutationHook, + fetcher: any +) { + const { providerRef } = useCommerce

() + const provider = providerRef.current + const opts = fn(provider) + const handler = + opts.useHook ?? + (() => { + const { fetch } = useHook(fn, fetcher) + return (input: any) => fetch({ input }) + }) + + return handler +} + +export default function useHook

( + fn: (provider: P) => MutationHook, + fetcher: any +) { + const { providerRef, fetcherRef } = useCommerce

() + const provider = providerRef.current + const opts = fn(provider) + const fetcherFn = opts.fetcher ?? fetcher + const fetchFn = provider.fetcher ?? fetcherRef.current + const fetch = useCallback( + ({ input }: { input: any }) => { + return fetcherFn({ + input, + options: opts.fetchOptions, + fetch: fetchFn, + }) + }, + [fetchFn, opts.fetchOptions] + ) + + return { fetch } +}