diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index e33796927..623747666 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -1,6 +1,7 @@ import { FC } from 'react' import Link from 'next/link' import cn from 'classnames' +import type { LineItem } from '@framework/types' import useCart from '@framework/cart/use-cart' import useCustomer from '@framework/customer/use-customer' import { Avatar } from '@components/common' @@ -15,7 +16,7 @@ interface Props { const countItem = (count: number, item: LineItem) => count + item.quantity -const UserNav: FC = ({ className, children }) => { +const UserNav: FC = ({ className }) => { const { data } = useCart() const { data: customer } = useCustomer() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index 82147f575..d1a9403b3 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -42,8 +42,8 @@ const WishlistCard: FC = ({ product }) => { setLoading(true) try { await addItem({ - productId: Number(product.id), - variantId: Number(product.variants[0].id), + productId: product.id, + variantId: product.variants[0].id, }) openSidebar() setLoading(false) diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx index afa37ec98..4f8a5cbcd 100644 --- a/framework/bigcommerce/cart/use-cart.tsx +++ b/framework/bigcommerce/cart/use-cart.tsx @@ -1,52 +1,4 @@ -import type { HookFetcher } from '@commerce/utils/types' -import type { SwrOptions } from '@commerce/utils/use-data' -import useResponse from '@commerce/utils/use-response' -import useCommerceCart, { CartInput } from '@commerce/cart/use-cart' -import { normalizeCart } from '../lib/normalize' -import type { Cart, BigcommerceCart } from '../types' +import useCart, { UseCart } from '@commerce/cart/use-cart' +import type { BigcommerceProvider } from '..' -const defaultOpts = { - url: '/api/bigcommerce/cart', - method: 'GET', -} - -export const fetcher: HookFetcher = async ( - options, - { cartId }, - fetch -) => { - const data = cartId - ? await fetch({ ...defaultOpts, ...options }) - : null - return data && normalizeCart(data) -} - -export function extendHook( - customFetcher: typeof fetcher, - swrOptions?: SwrOptions -) { - const useCart = () => { - const response = useCommerceCart(defaultOpts, [], customFetcher, { - revalidateOnFocus: false, - ...swrOptions, - }) - const res = useResponse(response, { - descriptors: { - isEmpty: { - get() { - return (response.data?.lineItems.length ?? 0) <= 0 - }, - enumerable: true, - }, - }, - }) - - return res - } - - useCart.extend = extendHook - - return useCart -} - -export default extendHook(fetcher) +export default useCart as UseCart diff --git a/framework/bigcommerce/customer/use-customer.tsx b/framework/bigcommerce/customer/use-customer.tsx index f44f16c1f..95adb6fb3 100644 --- a/framework/bigcommerce/customer/use-customer.tsx +++ b/framework/bigcommerce/customer/use-customer.tsx @@ -1,38 +1,4 @@ -import type { HookFetcher } from '@commerce/utils/types' -import type { SwrOptions } from '@commerce/utils/use-data' -import useCommerceCustomer from '@commerce/use-customer' -import type { Customer, CustomerData } from '../api/customers' +import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' +import type { BigcommerceProvider } from '..' -const defaultOpts = { - url: '/api/bigcommerce/customers', - method: 'GET', -} - -export type { Customer } - -export const fetcher: HookFetcher = async ( - options, - _, - fetch -) => { - const data = await fetch({ ...defaultOpts, ...options }) - return data?.customer ?? null -} - -export function extendHook( - customFetcher: typeof fetcher, - swrOptions?: SwrOptions -) { - const useCustomer = () => { - return useCommerceCustomer(defaultOpts, [], customFetcher, { - revalidateOnFocus: false, - ...swrOptions, - }) - } - - useCustomer.extend = extendHook - - return useCustomer -} - -export default extendHook(fetcher) +export default useCustomer as UseCustomer diff --git a/framework/bigcommerce/index.tsx b/framework/bigcommerce/index.tsx index a4c9fffa5..b35785ed2 100644 --- a/framework/bigcommerce/index.tsx +++ b/framework/bigcommerce/index.tsx @@ -1,46 +1,17 @@ -import { ReactNode } from 'react' -import * as React from 'react' +import type { ReactNode } from 'react' import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce, } from '@commerce' -import { FetcherError } from '@commerce/utils/errors' +import { bigcommerceProvider, BigcommerceProvider } from './provider' -async function getText(res: Response) { - try { - return (await res.text()) || res.statusText - } catch (error) { - return res.statusText - } -} - -async function getError(res: Response) { - if (res.headers.get('Content-Type')?.includes('application/json')) { - const data = await res.json() - return new FetcherError({ errors: data.errors, status: res.status }) - } - return new FetcherError({ message: await getText(res), status: res.status }) -} +export { bigcommerceProvider } +export type { BigcommerceProvider } export const bigcommerceConfig: CommerceConfig = { locale: 'en-us', cartCookie: 'bc_cartId', - async fetcher({ url, method = 'GET', variables, body: bodyObj }) { - const hasBody = Boolean(variables || bodyObj) - const body = hasBody - ? JSON.stringify(variables ? { variables } : bodyObj) - : undefined - const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined - const res = await fetch(url!, { method, body, headers }) - - if (res.ok) { - const { data } = await res.json() - return data - } - - throw await getError(res) - }, } export type BigcommerceConfig = Partial @@ -52,10 +23,13 @@ export type BigcommerceProps = { export function CommerceProvider({ children, ...config }: BigcommerceProps) { return ( - + {children} ) } -export const useCommerce = () => useCoreCommerce() +export const useCommerce = () => useCoreCommerce() diff --git a/framework/bigcommerce/provider.tsx b/framework/bigcommerce/provider.tsx new file mode 100644 index 000000000..d3ae4d171 --- /dev/null +++ b/framework/bigcommerce/provider.tsx @@ -0,0 +1,166 @@ +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' +import { normalizeCart } from './lib/normalize' +import type { Wishlist } from './api/wishlist' +import type { Customer, CustomerData } from './api/customers' +import useCustomer from './customer/use-customer' +import type { Cart } from './types' + +async function getText(res: Response) { + try { + return (await res.text()) || res.statusText + } catch (error) { + return res.statusText + } +} + +async function getError(res: Response) { + if (res.headers.get('Content-Type')?.includes('application/json')) { + const data = await res.json() + return new FetcherError({ errors: data.errors, status: res.status }) + } + return new FetcherError({ message: await getText(res), status: res.status }) +} + +const fetcher: Fetcher = async ({ + url, + method = 'GET', + variables, + body: bodyObj, +}) => { + const hasBody = Boolean(variables || bodyObj) + const body = hasBody + ? JSON.stringify(variables ? { variables } : bodyObj) + : undefined + const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined + const res = await fetch(url!, { method, body, headers }) + + if (res.ok) { + const { data } = await res.json() + return data + } + + throw await getError(res) +} + +const useCart: HookHandler< + Cart | null, + {}, + FetchCartInput, + any, + any, + { isEmpty?: boolean } +> = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'GET', + }, + normalizer: normalizeCart, + useHook({ input, useData }) { + 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] + ) + }, +} + +const useWishlist: HookHandler< + Wishlist | null, + { includeProducts?: boolean }, + { customerId?: number; includeProducts: boolean }, + any, + any, + { isEmpty?: boolean } +> = { + fetchOptions: { + 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] + ) + }, +} + +const useCustomerHandler: HookHandler< + Customer | null, + {}, + {}, + CustomerData | null, + any +> = { + fetchOptions: { + url: '/api/bigcommerce/customers', + method: 'GET', + }, + normalizer: (data) => data.customer, + useHook({ input, useData }) { + return useData({ + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + }, +} + +export const bigcommerceProvider = { + locale: 'en-us', + cartCookie: 'bc_cartId', + fetcher, + cartNormalizer: normalizeCart, + cart: { useCart }, + wishlist: { useWishlist }, + customer: { useCustomer: useCustomerHandler }, +} + +export type BigcommerceProvider = typeof 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 0a7ba49ee..6b1a3c789 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,28 +1,71 @@ import Cookies from 'js-cookie' -import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' -import useData, { ResponseState, SwrOptions } from '../utils/use-data' import type { Cart } from '../types' -import { useCommerce } from '..' +import type { + Prop, + HookFetcherFn, + UseHookInput, + UseHookResponse, +} from '../utils/types' +import useData from '../utils/use-data-2' +import { Provider, useCommerce } from '..' -export type CartResponse = ResponseState & { isEmpty?: boolean } - -// Input expected by the `useCart` hook -export type CartInput = { +export type FetchCartInput = { cartId?: Cart['id'] } -export default function useCart( - options: HookFetcherOptions, - input: HookInput, - fetcherFn: HookFetcher, - swrOptions?: SwrOptions -): CartResponse { - const { cartCookie } = useCommerce() - const fetcher: typeof fetcherFn = (options, input, fetch) => { - input.cartId = Cookies.get(cartCookie) - return fetcherFn(options, input, fetch) - } - const response = useData(options, input, fetcher, swrOptions) +export type UseCartHandler

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

= UseHookInput> + +export type CartResponse

= UseHookResponse< + UseCartHandler

+> + +export type UseCart

= Partial< + UseCartInput

+> extends UseCartInput

+ ? (input?: UseCartInput

) => CartResponse

+ : (input: UseCartInput

) => CartResponse

+ +export const fetcher: HookFetcherFn = async ({ + options, + input: { cartId }, + fetch, + normalize, +}) => { + const data = cartId ? await fetch({ ...options }) : null + return data && normalize ? normalize(data) : data +} + +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 useHook = opts?.useHook ?? ((ctx) => ctx.useData()) + + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + + return useHook({ + input, + useData(ctx) { + const response = useData( + { ...opts!, fetcher: wrapper }, + ctx?.input ?? [], + provider.fetcher ?? fetcherRef.current, + ctx?.swrOptions ?? input.swrOptions + ) + return response + }, + }) } diff --git a/framework/commerce/customer/use-customer.tsx b/framework/commerce/customer/use-customer.tsx new file mode 100644 index 000000000..4fb9b430b --- /dev/null +++ b/framework/commerce/customer/use-customer.tsx @@ -0,0 +1,56 @@ +import type { Customer } from '../types' +import type { + Prop, + HookFetcherFn, + UseHookInput, + UseHookResponse, +} from '../utils/types' +import defaultFetcher from '../utils/default-fetcher' +import useData from '../utils/use-data-2' +import { Provider, useCommerce } from '..' + +export type UseCustomerHandler

= Prop< + Prop, + 'useCustomer' +> + +export type UseCustomerInput

= UseHookInput< + UseCustomerHandler

+> + +export type CustomerResponse

= UseHookResponse< + UseCustomerHandler

+> + +export type UseCustomer

= Partial< + UseCustomerInput

+> extends UseCustomerInput

+ ? (input?: UseCustomerInput

) => CustomerResponse

+ : (input: UseCustomerInput

) => CustomerResponse

+ +export const fetcher = defaultFetcher as HookFetcherFn + +export default function useCustomer

( + input: UseCustomerInput

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

() + + const provider = providerRef.current + const opts = provider.customer?.useCustomer + + 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 + }, + }) +} diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 506502ea2..ccfc07e66 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -6,37 +6,60 @@ import { useMemo, useRef, } from 'react' -import * as React from 'react' -import { Fetcher } from './utils/types' +import { Fetcher, HookHandler } from './utils/types' +import type { FetchCartInput } from './cart/use-cart' +import type { Cart, Wishlist, Customer } from './types' -const Commerce = createContext({}) +const Commerce = createContext | {}>({}) -export type CommerceProps = { +export type Provider = CommerceConfig & { + fetcher: Fetcher + cart?: { + useCart?: HookHandler + } + wishlist?: { + useWishlist?: HookHandler + } + customer: { + useCustomer?: HookHandler + } +} + +export type CommerceProps

= { children?: ReactNode + provider: P config: CommerceConfig } -export type CommerceConfig = { fetcher: Fetcher } & Omit< - CommerceContextValue, - 'fetcherRef' +export type CommerceConfig = Omit< + CommerceContextValue, + 'providerRef' | 'fetcherRef' > -export type CommerceContextValue = { - fetcherRef: MutableRefObject> +export type CommerceContextValue

= { + providerRef: MutableRefObject

+ fetcherRef: MutableRefObject locale: string cartCookie: string } -export function CommerceProvider({ children, config }: CommerceProps) { +export function CommerceProvider

({ + provider, + children, + config, +}: CommerceProps

) { if (!config) { throw new Error('CommerceProvider requires a valid config object') } - const fetcherRef = useRef(config.fetcher) + const providerRef = useRef(provider) + // TODO: Remove the fetcherRef + const fetcherRef = useRef(provider.fetcher) // Because the config is an object, if the parent re-renders this provider // will re-render every consumer unless we memoize the config const cfg = useMemo( () => ({ + providerRef, fetcherRef, locale: config.locale, cartCookie: config.cartCookie, @@ -47,6 +70,6 @@ export function CommerceProvider({ children, config }: CommerceProps) { return {children} } -export function useCommerce() { - return useContext(Commerce) as T +export function useCommerce

() { + return useContext(Commerce) as CommerceContextValue

} diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index cc04f52e1..31aaa6fd7 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -1,3 +1,6 @@ +import type { Wishlist as BCWishlist } from '@framework/api/wishlist' +import type { Customer as BCCustomer } from '@framework/api/customers' + export interface Discount { // The value of the discount, can be an amount or percentage value: number @@ -87,6 +90,12 @@ export interface Cart { discounts?: Discount[] } +// TODO: Properly define this type +export interface Wishlist extends BCWishlist {} + +// TODO: Properly define this type +export interface Customer extends BCCustomer {} + /** * Cart mutations */ diff --git a/framework/commerce/use-customer.tsx b/framework/commerce/use-customer.tsx deleted file mode 100644 index 8e2ff3ec2..000000000 --- a/framework/commerce/use-customer.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import useData from './utils/use-data' - -const useCustomer = useData - -export default useCustomer diff --git a/framework/commerce/utils/default-fetcher.ts b/framework/commerce/utils/default-fetcher.ts new file mode 100644 index 000000000..25211a689 --- /dev/null +++ b/framework/commerce/utils/default-fetcher.ts @@ -0,0 +1,12 @@ +import { HookFetcherFn } from './types' + +const defaultFetcher: HookFetcherFn = async ({ + options, + fetch, + normalize, +}) => { + const data = await fetch({ ...options }) + return data && normalize ? normalize(data) : data +} + +export default defaultFetcher diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 010205f62..47da81a7f 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -1,5 +1,21 @@ -// Core fetcher added by CommerceProvider -export type Fetcher = (options: FetcherOptions) => T | Promise +import type { ConfigInterface } from 'swr' +import type { CommerceError } from './errors' +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 + */ +export type PickRequired = Omit & + Required> + +/** + * Core fetcher added by CommerceProvider + */ +export type Fetcher = ( + options: FetcherOptions +) => T | Promise export type FetcherOptions = { url?: string @@ -15,12 +31,76 @@ export type HookFetcher = ( fetch: (options: FetcherOptions) => Promise ) => Data | Promise -export type HookFetcherOptions = { - query?: string - url?: string - method?: string +export type HookFetcherFn< + Data, + Input = never, + Result = any, + Body = any +> = (context: { + options: HookFetcherOptions + input: Input + fetch: (options: FetcherOptions) => Promise + normalize?(data: Result): Data +}) => Data | Promise + +export type HookFetcherOptions = { method?: string } & ( + | { query: string; url?: string } + | { query?: string; url: string } +) + +export type HookInputValue = string | number | boolean | undefined + +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 extends { [k: string]: unknown } = {}, + // Input expected before doing a fetch operation + FetchInput extends HookFetchInput = {}, + // Data returned by the API after a fetch operation + Result = any, + // Body expected by the API endpoint + Body = any, + // Custom state added to the response object of SWR + State = {} +> = { + useHook?(context: { + input: Input & { swrOptions?: SwrOptions } + useData(context?: { + input?: HookFetchInput | HookSwrInput + swrOptions?: SwrOptions + }): ResponseState + }): ResponseState & State + fetchOptions: HookFetcherOptions + fetcher?: HookFetcherFn + normalizer?(data: NonNullable): Data } -export type HookInput = [string, string | number | boolean | undefined][] +export type SwrOptions = ConfigInterface< + Data, + CommerceError, + HookFetcher +> -export type Override = Omit & K +/** + * Returns the property K from type T excluding nullables + */ +export type Prop = NonNullable + +export type UseHookParameters> = Parameters< + Prop +> + +export type UseHookResponse> = ReturnType< + Prop +> + +export type UseHookInput< + H extends HookHandler +> = UseHookParameters[0]['input'] diff --git a/framework/commerce/utils/use-data-2.ts b/framework/commerce/utils/use-data-2.ts new file mode 100644 index 000000000..cc4d2cc5b --- /dev/null +++ b/framework/commerce/utils/use-data-2.ts @@ -0,0 +1,84 @@ +import useSWR, { responseInterface } from 'swr' +import type { + HookHandler, + HookSwrInput, + HookFetchInput, + PickRequired, + Fetcher, + SwrOptions, +} from './types' +import defineProperty from './define-property' +import { CommerceError } from './errors' + +export type ResponseState = responseInterface & { + isLoading: boolean +} + +export type UseData = < + Data = any, + Input extends { [k: string]: unknown } = {}, + FetchInput extends HookFetchInput = {}, + Result = any, + Body = any +>( + options: PickRequired< + HookHandler, + 'fetcher' + >, + input: HookFetchInput | HookSwrInput, + fetcherFn: Fetcher, + swrOptions?: SwrOptions +) => ResponseState + +const useData: UseData = (options, input, fetcherFn, swrOptions) => { + const hookInput = Array.isArray(input) ? input : Object.entries(input) + const fetcher = async ( + url: string, + query?: string, + method?: string, + ...args: any[] + ) => { + try { + return await options.fetcher({ + options: { url, query, method }, + // Transform the input array into an object + input: args.reduce((obj, val, i) => { + obj[hookInput[i][0]!] = val + return obj + }, {}), + fetch: fetcherFn, + normalize: options.normalizer, + }) + } catch (error) { + // SWR will not log errors, but any error that's not an instance + // of CommerceError is not welcomed by this hook + if (!(error instanceof CommerceError)) { + console.error(error) + } + throw error + } + } + const response = useSWR( + () => { + const opts = options.fetchOptions + return opts + ? [opts.url, opts.query, opts.method, ...hookInput.map((e) => e[1])] + : null + }, + fetcher, + swrOptions + ) + + if (!('isLoading' in response)) { + defineProperty(response, 'isLoading', { + get() { + return response.data === undefined + }, + enumerable: true, + }) + } + + return response +} + +export default useData 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..314f0a1c2 100644 --- a/framework/commerce/wishlist/use-wishlist.tsx +++ b/framework/commerce/wishlist/use-wishlist.tsx @@ -1,16 +1,56 @@ -import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' -import useData, { ResponseState, SwrOptions } from '../utils/use-data' +import type { Wishlist } from '../types' +import type { + Prop, + HookFetcherFn, + UseHookInput, + UseHookResponse, +} from '../utils/types' +import defaultFetcher from '../utils/default-fetcher' +import useData from '../utils/use-data-2' +import { Provider, useCommerce } from '..' -export type WishlistResponse = ResponseState & { - isEmpty?: boolean -} +export type UseWishlistHandler

= Prop< + Prop, + 'useWishlist' +> -export default function useWishlist( - options: HookFetcherOptions, - input: HookInput, - fetcherFn: HookFetcher, - swrOptions?: SwrOptions -): WishlistResponse { - const response = useData(options, input, fetcherFn, swrOptions) - return response +export type UseWishlistInput

= UseHookInput< + UseWishlistHandler

+> + +export type WishlistResponse

= UseHookResponse< + UseWishlistHandler

+> + +export type UseWishlist

= Partial< + UseWishlistInput

+> extends UseWishlistInput

+ ? (input?: UseWishlistInput

) => WishlistResponse

+ : (input: UseWishlistInput

) => WishlistResponse

+ +export const fetcher = defaultFetcher as HookFetcherFn + +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 + }, + }) }