From 5aecb0f303d1e452dc317bcb0e8e68b42e6e4473 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Tue, 9 Feb 2021 13:23:48 -0500 Subject: [PATCH 1/4] Updated how the hook input is handled --- framework/bigcommerce/index.tsx | 2 +- framework/bigcommerce/provider.ts | 4 ++-- framework/commerce/cart/use-cart.tsx | 22 ++++++++++++-------- framework/commerce/index.tsx | 4 ++-- framework/commerce/utils/types.ts | 18 +++++++++++----- framework/commerce/utils/use-data-2.ts | 11 +++++----- framework/commerce/utils/use-data.tsx | 4 ++-- framework/commerce/wishlist/use-wishlist.tsx | 8 +++++-- 8 files changed, 45 insertions(+), 28 deletions(-) diff --git a/framework/bigcommerce/index.tsx b/framework/bigcommerce/index.tsx index 83fbdbcbc..b35785ed2 100644 --- a/framework/bigcommerce/index.tsx +++ b/framework/bigcommerce/index.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import type { ReactNode } from 'react' import { CommerceConfig, CommerceProvider as CoreCommerceProvider, diff --git a/framework/bigcommerce/provider.ts b/framework/bigcommerce/provider.ts index b6385546c..e4ab5c757 100644 --- a/framework/bigcommerce/provider.ts +++ b/framework/bigcommerce/provider.ts @@ -43,7 +43,7 @@ const fetcher: Fetcher = async ({ const useCart: HookHandler< Cart | null, - [], + {}, FetchCartInput, any, any, @@ -71,7 +71,7 @@ const useCart: HookHandler< const useWishlist: HookHandler< Cart | null, - [], + {}, FetchCartInput, any, any, diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index b19e609da..8f40fd055 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,8 +1,8 @@ import { useMemo } from 'react' import Cookies from 'js-cookie' -import type { Cart } from '../types' import type { HookFetcherFn } from '../utils/types' import useData from '../utils/use-data-2' +import type { Cart } from '../types' import { Provider, useCommerce } from '..' export type FetchCartInput = { @@ -13,13 +13,17 @@ export type CartResponse

= ReturnType< NonNullable['useCart']>['onResponse']> > -export type UseCart

= ( - ...input: UseCartInput

-) => CartResponse

+export type UseCartInput

= Parameters< + NonNullable< + NonNullable['useCart']>>['input'] + > +>[0] -export type UseCartInput

= NonNullable< - NonNullable['useCart']>>['input'] -> +export type UseCart

= Partial< + UseCartInput

+> extends UseCartInput

+ ? (input?: UseCartInput

) => CartResponse

+ : (input: UseCartInput

) => CartResponse

export const fetcher: HookFetcherFn = async ({ options, @@ -31,7 +35,7 @@ export const fetcher: HookFetcherFn = async ({ return data && normalize ? normalize(data) : data } -export default function useCart

(...input: UseCartInput

) { +export default function useCart

(input?: UseCartInput

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

() const provider = providerRef.current @@ -43,7 +47,7 @@ export default function useCart

(...input: UseCartInput

) { } const response = useData( { ...opts, fetcher: wrapper }, - input, + opts?.input ? opts.input(input ?? {}) : [], provider.fetcher ?? fetcherRef.current ) const memoizedResponse = useMemo( diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 82e86947d..fa7e3e7e8 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -16,10 +16,10 @@ const Commerce = createContext | {}>({}) export type Provider = CommerceConfig & { fetcher: Fetcher cart?: { - useCart?: HookHandler + useCart?: HookHandler } wishlist?: { - useWishlist?: HookHandler + useWishlist?: HookHandler } } diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index d84ec07f0..6b4f1ddbe 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -4,11 +4,15 @@ import type { ResponseState } from './use-data' export type Override = Omit & K -// Returns the properties in T with the properties in type K changed from optional to required +/** + * Returns the properties in T with the properties in type K changed from optional to required + */ export type PickRequired = Omit & Required> -// Core fetcher added by CommerceProvider +/** + * Core fetcher added by CommerceProvider + */ export type Fetcher = ( options: FetcherOptions ) => T | Promise @@ -47,15 +51,17 @@ export type HookFetcherOptions = { export type HookInputValue = string | number | boolean | undefined -export type HookInput = [string, HookInputValue][] +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, // Input expected by the hook - Input = [...any], + Input extends { [k: string]: unknown } = {}, // Input expected before doing a fetch operation FetchInput extends HookFetchInput = never, // Data returned by the API after a fetch operation @@ -65,7 +71,9 @@ export type HookHandler< // Custom state added to the response object of SWR State = {} > = { - input?: Input + input?( + input: Input & { swrOptions?: SwrOptions } + ): HookFetchInput | HookSwrInput swrOptions?: SwrOptions onResponse?(response: ResponseState): ResponseState & State onMutation?: any diff --git a/framework/commerce/utils/use-data-2.ts b/framework/commerce/utils/use-data-2.ts index 5536bb02c..d9a9e9a39 100644 --- a/framework/commerce/utils/use-data-2.ts +++ b/framework/commerce/utils/use-data-2.ts @@ -1,7 +1,7 @@ import useSWR, { responseInterface } from 'swr' import type { HookHandler, - HookInput, + HookSwrInput, HookFetchInput, PickRequired, Fetcher, @@ -15,7 +15,7 @@ export type ResponseState = responseInterface & { export type UseData = < Data = any, - Input = [...any], + Input extends { [k: string]: unknown } = {}, FetchInput extends HookFetchInput = never, Result = any, Body = any @@ -24,11 +24,12 @@ export type UseData = < HookHandler, 'fetcher' >, - input: HookInput, + input: HookFetchInput | HookSwrInput, fetcherFn: Fetcher ) => ResponseState const useData: UseData = (options, input, fetcherFn) => { + const hookInput = Array.isArray(input) ? input : Object.entries(input) const fetcher = async ( url?: string, query?: string, @@ -40,7 +41,7 @@ const useData: UseData = (options, input, fetcherFn) => { options: { url, query, method }, // Transform the input array into an object input: args.reduce((obj, val, i) => { - obj[input[i][0]!] = val + obj[hookInput[i][0]!] = val return obj }, {}), fetch: fetcherFn, @@ -59,7 +60,7 @@ const useData: UseData = (options, input, fetcherFn) => { () => { const opts = options.fetchOptions return opts - ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])] + ? [opts.url, opts.query, opts.method, ...hookInput.map((e) => e[1])] : null }, fetcher, diff --git a/framework/commerce/utils/use-data.tsx b/framework/commerce/utils/use-data.tsx index 38af46a44..58a1a0a47 100644 --- a/framework/commerce/utils/use-data.tsx +++ b/framework/commerce/utils/use-data.tsx @@ -1,5 +1,5 @@ import useSWR, { ConfigInterface, responseInterface } from 'swr' -import type { HookInput, HookFetcher, HookFetcherOptions } from './types' +import type { HookSwrInput, HookFetcher, HookFetcherOptions } from './types' import defineProperty from './define-property' import { CommerceError } from './errors' import { useCommerce } from '..' @@ -16,7 +16,7 @@ export type ResponseState = responseInterface & { export type UseData = ( options: HookFetcherOptions | (() => HookFetcherOptions | null), - input: HookInput, + input: HookSwrInput, fetcherFn: HookFetcher, swrOptions?: SwrOptions ) => ResponseState diff --git a/framework/commerce/wishlist/use-wishlist.tsx b/framework/commerce/wishlist/use-wishlist.tsx index df8bebe5c..5272766c6 100644 --- a/framework/commerce/wishlist/use-wishlist.tsx +++ b/framework/commerce/wishlist/use-wishlist.tsx @@ -1,4 +1,8 @@ -import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' +import type { + HookSwrInput, + HookFetcher, + HookFetcherOptions, +} from '../utils/types' import useData, { ResponseState, SwrOptions } from '../utils/use-data' export type WishlistResponse = ResponseState & { @@ -7,7 +11,7 @@ export type WishlistResponse = ResponseState & { export default function useWishlist( options: HookFetcherOptions, - input: HookInput, + input: HookSwrInput, fetcherFn: HookFetcher, swrOptions?: SwrOptions ): WishlistResponse { From 0eeb290eb0ad702595329fb529ef66f4f6e25f14 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 10 Feb 2021 10:51:46 -0500 Subject: [PATCH 2/4] Add more options to the hook handler --- .../bigcommerce/{provider.ts => provider.tsx} | 17 ++++ framework/commerce/cart/use-cart.tsx | 85 +++++++++++++------ framework/commerce/utils/types.ts | 24 +++++- 3 files changed, 100 insertions(+), 26 deletions(-) rename framework/bigcommerce/{provider.ts => provider.tsx} (86%) diff --git a/framework/bigcommerce/provider.ts b/framework/bigcommerce/provider.tsx similarity index 86% rename from framework/bigcommerce/provider.ts rename to framework/bigcommerce/provider.tsx index e4ab5c757..0d794edd9 100644 --- a/framework/bigcommerce/provider.ts +++ b/framework/bigcommerce/provider.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react' import { FetcherError } from '@commerce/utils/errors' import type { Fetcher, HookHandler } from '@commerce/utils/types' import type { FetchCartInput } from '@commerce/cart/use-cart' @@ -57,6 +58,22 @@ const useCart: HookHandler< revalidateOnFocus: false, }, normalizer: normalizeCart, + useHook({ input, useData }) { + const response = useData({ input }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, onResponse(response) { return Object.create(response, { isEmpty: { diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index 8f40fd055..4ca20df69 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,23 +1,24 @@ import { useMemo } from 'react' import Cookies from 'js-cookie' -import type { HookFetcherFn } from '../utils/types' -import useData from '../utils/use-data-2' import type { Cart } from '../types' +import type { Prop, HookFetcherFn, UseHookInput } from '../utils/types' +import useData from '../utils/use-data-2' import { Provider, useCommerce } from '..' export type FetchCartInput = { cartId?: Cart['id'] } -export type CartResponse

= ReturnType< - NonNullable['useCart']>['onResponse']> +export type UseCartHandler

= Prop< + Prop, + 'useCart' > -export type UseCartInput

= Parameters< - NonNullable< - NonNullable['useCart']>>['input'] - > ->[0] +export type CartResponse

= ReturnType< + Prop, 'onResponse'> +> + +export type UseCartInput

= UseHookInput> export type UseCart

= Partial< UseCartInput

@@ -35,25 +36,59 @@ export const fetcher: HookFetcherFn = async ({ return data && normalize ? normalize(data) : data } -export default function useCart

(input?: UseCartInput

) { +type X = UseCartInput + +export default function useCart

( + input: UseCartInput

= {} +) { const { providerRef, fetcherRef, cartCookie } = useCommerce

() const provider = providerRef.current const opts = provider.cart?.useCart - const fetcherFn = opts?.fetcher ?? fetcher - const wrapper: typeof fetcher = (context) => { - context.input.cartId = Cookies.get(cartCookie) - return fetcherFn(context) - } - const response = useData( - { ...opts, fetcher: wrapper }, - opts?.input ? opts.input(input ?? {}) : [], - provider.fetcher ?? fetcherRef.current - ) - const memoizedResponse = useMemo( - () => (opts?.onResponse ? opts.onResponse(response) : response), - [response] - ) - return memoizedResponse as CartResponse

+ const { swrOptions, ...hookInput } = input + + return opts?.useHook!({ + input: hookInput, + swrOptions, + useData(ctx) { + const fetcherFn = opts?.fetcher ?? fetcher + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + const response = useData( + { + ...opts, + fetcher: wrapper, + swrOptions: { + ...opts.swrOptions, + ...(ctx?.swrOptions ?? swrOptions), + }, + }, + ctx?.input ?? [], + provider.fetcher ?? fetcherRef.current + ) + return response + }, + }) + + // console.log(i) + + // const fetcherFn = opts?.fetcher ?? fetcher + // const wrapper: typeof fetcher = (context) => { + // context.input.cartId = Cookies.get(cartCookie) + // return fetcherFn(context) + // } + // const response = useData( + // { ...opts, fetcher: wrapper }, + // opts?.input ? opts.input(input ?? {}) : [], + // provider.fetcher ?? fetcherRef.current + // ) + // const memoizedResponse = useMemo( + // () => (opts?.onResponse ? opts.onResponse(response) : response), + // [response] + // ) + + // return memoizedResponse as CartResponse

} diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 6b4f1ddbe..d4d767ae1 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -74,9 +74,16 @@ export type HookHandler< input?( input: Input & { swrOptions?: SwrOptions } ): HookFetchInput | HookSwrInput + useHook?(context: { + input: Input + swrOptions?: SwrOptions + useData(context?: { + input?: HookFetchInput | HookSwrInput + swrOptions?: SwrOptions + }): ResponseState + }): ResponseState & State swrOptions?: SwrOptions onResponse?(response: ResponseState): ResponseState & State - onMutation?: any fetchOptions?: HookFetcherOptions fetcher?: HookFetcherFn normalizer?(data: Result): Data @@ -87,3 +94,18 @@ export type SwrOptions = ConfigInterface< CommerceError, HookFetcher > + +/** + * Returns the property K from type T excluding nullables + */ +export type Prop = NonNullable + +export type UseHookParameters> = Parameters< + Prop +> + +export type UseHookInput< + H extends HookHandler +> = UseHookParameters[0]['input'] & { + swrOptions?: UseHookParameters[0]['swrOptions'] +} From 271ed016311587fa0e62dad4a19ca7fa7e387453 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 10 Feb 2021 15:10:48 -0500 Subject: [PATCH 3/4] Final touches to the hook handler type --- framework/bigcommerce/provider.tsx | 30 ++---------- framework/commerce/cart/use-cart.tsx | 67 +++++++++----------------- framework/commerce/utils/types.ts | 16 +++--- framework/commerce/utils/use-data-2.ts | 8 +-- 4 files changed, 36 insertions(+), 85 deletions(-) diff --git a/framework/bigcommerce/provider.tsx b/framework/bigcommerce/provider.tsx index 0d794edd9..364e87539 100644 --- a/framework/bigcommerce/provider.tsx +++ b/framework/bigcommerce/provider.tsx @@ -54,12 +54,11 @@ const useCart: HookHandler< url: '/api/bigcommerce/cart', method: 'GET', }, - swrOptions: { - revalidateOnFocus: false, - }, normalizer: normalizeCart, useHook({ input, useData }) { - const response = useData({ input }) + const response = useData({ + swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, + }) return useMemo( () => @@ -74,16 +73,6 @@ const useCart: HookHandler< [response] ) }, - onResponse(response) { - return Object.create(response, { - isEmpty: { - get() { - return (response.data?.lineItems.length ?? 0) <= 0 - }, - enumerable: true, - }, - }) - }, } const useWishlist: HookHandler< @@ -98,19 +87,6 @@ const useWishlist: HookHandler< url: '/api/bigcommerce/wishlist', method: 'GET', }, - swrOptions: { - revalidateOnFocus: false, - }, - onResponse(response) { - return Object.create(response, { - isEmpty: { - get() { - return (response.data?.lineItems.length ?? 0) <= 0 - }, - enumerable: true, - }, - }) - }, } export const bigcommerceProvider = { diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index 4ca20df69..9915c7478 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,7 +1,11 @@ -import { useMemo } from 'react' import Cookies from 'js-cookie' import type { Cart } from '../types' -import type { Prop, HookFetcherFn, UseHookInput } from '../utils/types' +import type { + Prop, + HookFetcherFn, + UseHookInput, + UseHookResponse, +} from '../utils/types' import useData from '../utils/use-data-2' import { Provider, useCommerce } from '..' @@ -14,12 +18,12 @@ export type UseCartHandler

= Prop< 'useCart' > -export type CartResponse

= ReturnType< - Prop, 'onResponse'> -> - export type UseCartInput

= UseHookInput> +export type CartResponse

= UseHookResponse< + UseCartHandler

+> + export type UseCart

= Partial< UseCartInput

> extends UseCartInput

@@ -36,8 +40,6 @@ export const fetcher: HookFetcherFn = async ({ return data && normalize ? normalize(data) : data } -type X = UseCartInput - export default function useCart

( input: UseCartInput

= {} ) { @@ -46,49 +48,24 @@ export default function useCart

( const provider = providerRef.current const opts = provider.cart?.useCart - const { swrOptions, ...hookInput } = input + const fetcherFn = opts?.fetcher ?? fetcher + const useHook = opts?.useHook ?? ((ctx) => ctx.useData()) - return opts?.useHook!({ - input: hookInput, - swrOptions, + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + + return useHook({ + input, useData(ctx) { - const fetcherFn = opts?.fetcher ?? fetcher - const wrapper: typeof fetcher = (context) => { - context.input.cartId = Cookies.get(cartCookie) - return fetcherFn(context) - } const response = useData( - { - ...opts, - fetcher: wrapper, - swrOptions: { - ...opts.swrOptions, - ...(ctx?.swrOptions ?? swrOptions), - }, - }, + { ...opts, fetcher: wrapper }, ctx?.input ?? [], - provider.fetcher ?? fetcherRef.current + provider.fetcher ?? fetcherRef.current, + ctx?.swrOptions ?? input.swrOptions ) return response }, }) - - // console.log(i) - - // const fetcherFn = opts?.fetcher ?? fetcher - // const wrapper: typeof fetcher = (context) => { - // context.input.cartId = Cookies.get(cartCookie) - // return fetcherFn(context) - // } - // const response = useData( - // { ...opts, fetcher: wrapper }, - // opts?.input ? opts.input(input ?? {}) : [], - // provider.fetcher ?? fetcherRef.current - // ) - // const memoizedResponse = useMemo( - // () => (opts?.onResponse ? opts.onResponse(response) : response), - // [response] - // ) - - // return memoizedResponse as CartResponse

} diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index d4d767ae1..7a4b73f93 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -71,19 +71,13 @@ export type HookHandler< // Custom state added to the response object of SWR State = {} > = { - input?( - input: Input & { swrOptions?: SwrOptions } - ): HookFetchInput | HookSwrInput useHook?(context: { - input: Input - swrOptions?: SwrOptions + input: Input & { swrOptions?: SwrOptions } useData(context?: { input?: HookFetchInput | HookSwrInput swrOptions?: SwrOptions }): ResponseState }): ResponseState & State - swrOptions?: SwrOptions - onResponse?(response: ResponseState): ResponseState & State fetchOptions?: HookFetcherOptions fetcher?: HookFetcherFn normalizer?(data: Result): Data @@ -104,8 +98,10 @@ export type UseHookParameters> = Parameters< Prop > +export type UseHookResponse> = ReturnType< + Prop +> + export type UseHookInput< H extends HookHandler -> = UseHookParameters[0]['input'] & { - swrOptions?: UseHookParameters[0]['swrOptions'] -} +> = UseHookParameters[0]['input'] diff --git a/framework/commerce/utils/use-data-2.ts b/framework/commerce/utils/use-data-2.ts index d9a9e9a39..e79daf632 100644 --- a/framework/commerce/utils/use-data-2.ts +++ b/framework/commerce/utils/use-data-2.ts @@ -5,6 +5,7 @@ import type { HookFetchInput, PickRequired, Fetcher, + SwrOptions, } from './types' import defineProperty from './define-property' import { CommerceError } from './errors' @@ -25,10 +26,11 @@ export type UseData = < 'fetcher' >, input: HookFetchInput | HookSwrInput, - fetcherFn: Fetcher + fetcherFn: Fetcher, + swrOptions?: SwrOptions ) => ResponseState -const useData: UseData = (options, input, fetcherFn) => { +const useData: UseData = (options, input, fetcherFn, swrOptions) => { const hookInput = Array.isArray(input) ? input : Object.entries(input) const fetcher = async ( url?: string, @@ -64,7 +66,7 @@ const useData: UseData = (options, input, fetcherFn) => { : null }, fetcher, - options.swrOptions + swrOptions ) if (!('isLoading' in response)) { From 8966f0b583844094f36e50a523c054787ec9dbd9 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Thu, 11 Feb 2021 02:43:09 -0500 Subject: [PATCH 4/4] Moved useWishlist to use new handler --- framework/bigcommerce/cart/use-cart.tsx | 4 +- framework/bigcommerce/provider.tsx | 47 ++++++++++- .../bigcommerce/wishlist/use-wishlist.tsx | 80 +------------------ framework/commerce/cart/use-cart.tsx | 2 +- framework/commerce/index.tsx | 6 +- framework/commerce/types.ts | 5 ++ framework/commerce/utils/types.ts | 17 ++-- framework/commerce/utils/use-data-2.ts | 4 +- framework/commerce/wishlist/use-wishlist.tsx | 70 ++++++++++++---- 9 files changed, 124 insertions(+), 111 deletions(-) diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx index ba005ec59..4f8a5cbcd 100644 --- a/framework/bigcommerce/cart/use-cart.tsx +++ b/framework/bigcommerce/cart/use-cart.tsx @@ -1,4 +1,4 @@ -import useCommerceCart, { UseCart } from '@commerce/cart/use-cart' +import useCart, { UseCart } from '@commerce/cart/use-cart' import type { BigcommerceProvider } from '..' -export default useCommerceCart as UseCart +export default useCart as UseCart diff --git a/framework/bigcommerce/provider.tsx b/framework/bigcommerce/provider.tsx index 364e87539..60106f7f8 100644 --- a/framework/bigcommerce/provider.tsx +++ b/framework/bigcommerce/provider.tsx @@ -3,6 +3,8 @@ import { FetcherError } from '@commerce/utils/errors' import type { Fetcher, HookHandler } from '@commerce/utils/types' import type { FetchCartInput } from '@commerce/cart/use-cart' import { normalizeCart } from './lib/normalize' +import type { Wishlist } from './api/wishlist' +import useCustomer from './customer/use-customer' import type { Cart } from './types' async function getText(res: Response) { @@ -76,9 +78,9 @@ const useCart: HookHandler< } const useWishlist: HookHandler< - Cart | null, - {}, - FetchCartInput, + Wishlist | null, + { includeProducts?: boolean }, + { customerId?: number; includeProducts: boolean }, any, any, { isEmpty?: boolean } @@ -87,6 +89,45 @@ const useWishlist: HookHandler< url: '/api/bigcommerce/wishlist', method: 'GET', }, + fetcher({ input: { customerId, includeProducts }, options, fetch }) { + if (!customerId) return null + + // Use a dummy base as we only care about the relative path + const url = new URL(options.url!, 'http://a') + + if (includeProducts) url.searchParams.set('products', '1') + + return fetch({ + url: url.pathname + url.search, + method: options.method, + }) + }, + useHook({ input, useData }) { + const { data: customer } = useCustomer() + const response = useData({ + input: [ + ['customerId', customer?.id], + ['includeProducts', input.includeProducts], + ], + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.items?.length || 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, } export const bigcommerceProvider = { diff --git a/framework/bigcommerce/wishlist/use-wishlist.tsx b/framework/bigcommerce/wishlist/use-wishlist.tsx index 730bf19dc..dfa3d9dbc 100644 --- a/framework/bigcommerce/wishlist/use-wishlist.tsx +++ b/framework/bigcommerce/wishlist/use-wishlist.tsx @@ -1,78 +1,4 @@ -import { HookFetcher } from '@commerce/utils/types' -import { SwrOptions } from '@commerce/utils/use-data' -import useResponse from '@commerce/utils/use-response' -import useCommerceWishlist from '@commerce/wishlist/use-wishlist' -import type { Wishlist } from '../api/wishlist' -import useCustomer from '../customer/use-customer' +import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' +import type { BigcommerceProvider } from '..' -const defaultOpts = { - url: '/api/bigcommerce/wishlist', - method: 'GET', -} - -export type { Wishlist } - -export interface UseWishlistOptions { - includeProducts?: boolean -} - -export interface UseWishlistInput extends UseWishlistOptions { - customerId?: number -} - -export const fetcher: HookFetcher = ( - options, - { customerId, includeProducts }, - fetch -) => { - if (!customerId) return null - - // Use a dummy base as we only care about the relative path - const url = new URL(options?.url ?? defaultOpts.url, 'http://a') - - if (includeProducts) url.searchParams.set('products', '1') - - return fetch({ - url: url.pathname + url.search, - method: options?.method ?? defaultOpts.method, - }) -} - -export function extendHook( - customFetcher: typeof fetcher, - swrOptions?: SwrOptions -) { - const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => { - const { data: customer } = useCustomer() - const response = useCommerceWishlist( - defaultOpts, - [ - ['customerId', customer?.id], - ['includeProducts', includeProducts], - ], - customFetcher, - { - revalidateOnFocus: false, - ...swrOptions, - } - ) - const res = useResponse(response, { - descriptors: { - isEmpty: { - get() { - return (response.data?.items?.length || 0) <= 0 - }, - set: (x) => x, - }, - }, - }) - - return res - } - - useWishlist.extend = extendHook - - return useWishlist -} - -export default extendHook(fetcher) +export default useWishlist as UseWishlist diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index 9915c7478..6b1a3c789 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -60,7 +60,7 @@ export default function useCart

( input, useData(ctx) { const response = useData( - { ...opts, fetcher: wrapper }, + { ...opts!, fetcher: wrapper }, ctx?.input ?? [], provider.fetcher ?? fetcherRef.current, ctx?.swrOptions ?? input.swrOptions diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index fa7e3e7e8..cb4136e3b 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -8,18 +8,18 @@ import { } from 'react' import * as React from 'react' import { Fetcher, HookHandler } from './utils/types' -import { Cart } from './types' import type { FetchCartInput } from './cart/use-cart' +import type { Cart, Wishlist } from './types' const Commerce = createContext | {}>({}) export type Provider = CommerceConfig & { fetcher: Fetcher cart?: { - useCart?: HookHandler + useCart?: HookHandler } wishlist?: { - useWishlist?: HookHandler + useWishlist?: HookHandler } } diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index cc04f52e1..743a93e4e 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -1,3 +1,5 @@ +import type { Wishlist as BCWishlist } from '@framework/api/wishlist' + export interface Discount { // The value of the discount, can be an amount or percentage value: number @@ -87,6 +89,9 @@ export interface Cart { discounts?: Discount[] } +// TODO: Properly define this type +export interface Wishlist extends BCWishlist {} + /** * Cart mutations */ diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 7a4b73f93..dbde3e7ec 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -33,21 +33,20 @@ export type HookFetcher = ( export type HookFetcherFn< Data, - Input = unknown, + Input = never, Result = any, Body = any > = (context: { - options: HookFetcherOptions | null + options: HookFetcherOptions input: Input fetch: (options: FetcherOptions) => Promise normalize?(data: Result): Data }) => Data | Promise -export type HookFetcherOptions = { - query?: string - url?: string - method?: string -} +export type HookFetcherOptions = { method?: string } & ( + | { query: string; url?: string } + | { query?: string; url: string } +) export type HookInputValue = string | number | boolean | undefined @@ -63,7 +62,7 @@ export type HookHandler< // Input expected by the hook Input extends { [k: string]: unknown } = {}, // Input expected before doing a fetch operation - FetchInput extends HookFetchInput = never, + FetchInput extends HookFetchInput = {}, // Data returned by the API after a fetch operation Result = any, // Body expected by the API endpoint @@ -78,7 +77,7 @@ export type HookHandler< swrOptions?: SwrOptions }): ResponseState }): ResponseState & State - fetchOptions?: HookFetcherOptions + fetchOptions: HookFetcherOptions fetcher?: HookFetcherFn normalizer?(data: Result): Data } diff --git a/framework/commerce/utils/use-data-2.ts b/framework/commerce/utils/use-data-2.ts index e79daf632..cc4d2cc5b 100644 --- a/framework/commerce/utils/use-data-2.ts +++ b/framework/commerce/utils/use-data-2.ts @@ -17,7 +17,7 @@ export type ResponseState = responseInterface & { export type UseData = < Data = any, Input extends { [k: string]: unknown } = {}, - FetchInput extends HookFetchInput = never, + FetchInput extends HookFetchInput = {}, Result = any, Body = any >( @@ -33,7 +33,7 @@ export type UseData = < const useData: UseData = (options, input, fetcherFn, swrOptions) => { const hookInput = Array.isArray(input) ? input : Object.entries(input) const fetcher = async ( - url?: string, + url: string, query?: string, method?: string, ...args: any[] diff --git a/framework/commerce/wishlist/use-wishlist.tsx b/framework/commerce/wishlist/use-wishlist.tsx index 5272766c6..c2e0d2dc1 100644 --- a/framework/commerce/wishlist/use-wishlist.tsx +++ b/framework/commerce/wishlist/use-wishlist.tsx @@ -1,20 +1,62 @@ +import type { Wishlist } from '../types' import type { - HookSwrInput, - HookFetcher, - HookFetcherOptions, + Prop, + HookFetcherFn, + UseHookInput, + UseHookResponse, } from '../utils/types' -import useData, { ResponseState, SwrOptions } from '../utils/use-data' +import useData from '../utils/use-data-2' +import { Provider, useCommerce } from '..' -export type WishlistResponse = ResponseState & { - isEmpty?: boolean +export type UseWishlistHandler

= Prop< + Prop, + 'useWishlist' +> + +export type UseWishlistInput

= UseHookInput< + UseWishlistHandler

+> + +export type WishlistResponse

= UseHookResponse< + UseWishlistHandler

+> + +export type UseWishlist

= Partial< + WishlistResponse

+> extends WishlistResponse

+ ? (input?: WishlistResponse

) => WishlistResponse

+ : (input: WishlistResponse

) => WishlistResponse

+ +export const fetcher: HookFetcherFn = async ({ + options, + fetch, + normalize, +}) => { + const data = await fetch({ ...options }) + return data && normalize ? normalize(data) : data } -export default function useWishlist( - options: HookFetcherOptions, - input: HookSwrInput, - fetcherFn: HookFetcher, - swrOptions?: SwrOptions -): WishlistResponse { - const response = useData(options, input, fetcherFn, swrOptions) - return response +export default function useWishlist

( + input: UseWishlistInput

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

() + + const provider = providerRef.current + const opts = provider.wishlist?.useWishlist + + const fetcherFn = opts?.fetcher ?? fetcher + const useHook = opts?.useHook ?? ((ctx) => ctx.useData()) + + return useHook({ + input, + useData(ctx) { + const response = useData( + { ...opts!, fetcher: fetcherFn }, + ctx?.input ?? [], + provider.fetcher ?? fetcherRef.current, + ctx?.swrOptions ?? input.swrOptions + ) + return response + }, + }) }