From aab2e7f7cc97040daa05bdd4124125d0f6b5c7c0 Mon Sep 17 00:00:00 2001
From: Luis Alvarez <luis@vercel.com>
Date: Mon, 8 Feb 2021 10:52:35 -0500
Subject: [PATCH] Replace use-cart with the new hook

---
 components/common/UserNav/UserNav.tsx   |  7 +++-
 framework/bigcommerce/cart/use-cart.tsx | 54 ++-----------------------
 framework/bigcommerce/index.tsx         | 10 +++--
 framework/commerce/cart/use-cart-2.tsx  | 28 -------------
 framework/commerce/cart/use-cart.tsx    | 54 ++++++++++++++++---------
 framework/commerce/cart/use-fake.tsx    |  9 +++--
 framework/commerce/index.tsx            |  6 ++-
 framework/commerce/utils/use-data-2.ts  |  7 ++--
 8 files changed, 66 insertions(+), 109 deletions(-)
 delete mode 100644 framework/commerce/cart/use-cart-2.tsx

diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx
index f8e6373d9..7048cc468 100644
--- a/components/common/UserNav/UserNav.tsx
+++ b/components/common/UserNav/UserNav.tsx
@@ -1,7 +1,10 @@
 import { FC } from 'react'
 import Link from 'next/link'
 import cn from 'classnames'
+import type { BigcommerceProvider } from '@framework'
+import { LineItem } from '@framework/types'
 import useCart from '@framework/cart/use-cart'
+import useFake from '@commerce/cart/use-fake'
 import useCustomer from '@framework/customer/use-customer'
 import { Heart, Bag } from '@components/icons'
 import { useUI } from '@components/ui/context'
@@ -15,12 +18,14 @@ interface Props {
 
 const countItem = (count: number, item: LineItem) => count + item.quantity
 
-const UserNav: FC<Props> = ({ className, children }) => {
+const UserNav: FC<Props> = ({ className }) => {
   const { data } = useCart()
   const { data: customer } = useCustomer()
   const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
   const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
 
+  const x = useFake<BigcommerceProvider>()
+
   return (
     <nav className={cn(s.root, className)}>
       <div className={s.mainContainer}>
diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx
index afa37ec98..8b984ab9f 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 useCommerceCart, { UseCart } from '@commerce/cart/use-cart'
+import { BigcommerceProvider } from '..'
 
-const defaultOpts = {
-  url: '/api/bigcommerce/cart',
-  method: 'GET',
-}
-
-export const fetcher: HookFetcher<Cart | null, CartInput> = async (
-  options,
-  { cartId },
-  fetch
-) => {
-  const data = cartId
-    ? await fetch<BigcommerceCart>({ ...defaultOpts, ...options })
-    : null
-  return data && normalizeCart(data)
-}
-
-export function extendHook(
-  customFetcher: typeof fetcher,
-  swrOptions?: SwrOptions<Cart | null, CartInput>
-) {
-  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 useCommerceCart as UseCart<BigcommerceProvider>
diff --git a/framework/bigcommerce/index.tsx b/framework/bigcommerce/index.tsx
index 268b0bd02..e1584c9e0 100644
--- a/framework/bigcommerce/index.tsx
+++ b/framework/bigcommerce/index.tsx
@@ -49,14 +49,18 @@ const fetcher: Fetcher<any> = async ({
   throw await getError(res)
 }
 
-const useCart: HookHandler<Cart, CartInput> = {
+const useCart: HookHandler<Cart, CartInput, any, any, { isEmpty?: boolean }> = {
   fetchOptions: {
     url: '/api/bigcommerce/cart',
     method: 'GET',
   },
-  fetcher(context) {
-    return undefined as any
+  swrOptions: {
+    revalidateOnFocus: false,
   },
+  // fetcher(context) {
+  //   return undefined as any
+  // },
+  normalizer: normalizeCart,
   onResponse(response) {
     return Object.create(response, {
       isEmpty: {
diff --git a/framework/commerce/cart/use-cart-2.tsx b/framework/commerce/cart/use-cart-2.tsx
deleted file mode 100644
index cfeb85c87..000000000
--- a/framework/commerce/cart/use-cart-2.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-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 '..'
-
-export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
-
-// Input expected by the `useCart` hook
-export type CartInput = {
-  cartId?: Cart['id']
-}
-
-export default function useCart<Data extends Cart | null>(
-  options: HookFetcherOptions,
-  input: HookInput,
-  fetcherFn: HookFetcher<Data, CartInput>,
-  swrOptions?: SwrOptions<Data, CartInput>
-): CartResponse<Data> {
-  const { providerRef, 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)
-
-  return response
-}
diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx
index 0a7ba49ee..77271b593 100644
--- a/framework/commerce/cart/use-cart.tsx
+++ b/framework/commerce/cart/use-cart.tsx
@@ -1,28 +1,46 @@
+import { useMemo } from 'react'
 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 '..'
-
-export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
+import type { HookFetcherFn } from '../utils/types'
+import useData from '../utils/use-data-2'
+import { Provider, useCommerce } from '..'
 
 // Input expected by the `useCart` hook
 export type CartInput = {
   cartId?: Cart['id']
 }
 
-export default function useCart<Data extends Cart | null>(
-  options: HookFetcherOptions,
-  input: HookInput,
-  fetcherFn: HookFetcher<Data, CartInput>,
-  swrOptions?: SwrOptions<Data, CartInput>
-): CartResponse<Data> {
-  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 CartResponse<P extends Provider> = ReturnType<
+  NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>['onResponse']>
+>
 
-  return response
+export type UseCart<P extends Provider> = () => CartResponse<P>
+
+export const fetcher: HookFetcherFn<Cart | null, CartInput> = async ({
+  options,
+  input: { cartId },
+  fetch,
+  normalize,
+}) => {
+  const data = cartId ? await fetch({ ...options }) : null
+  return data && normalize ? normalize(data) : data
+}
+
+export default function useCart<P extends Provider>() {
+  const { providerRef, cartCookie } = useCommerce<P>()
+
+  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!, [], wrapper, opts?.swrOptions)
+  const memoizedResponse = useMemo(
+    () => (opts?.onResponse ? opts.onResponse(response) : response),
+    [response]
+  )
+
+  return memoizedResponse as CartResponse<P>
 }
diff --git a/framework/commerce/cart/use-fake.tsx b/framework/commerce/cart/use-fake.tsx
index 324e1c4d9..2f0083d10 100644
--- a/framework/commerce/cart/use-fake.tsx
+++ b/framework/commerce/cart/use-fake.tsx
@@ -10,7 +10,11 @@ export type CartInput = {
   cartId?: Cart['id']
 }
 
-const fetcher: HookFetcherFn<Cart | null, CartInput> = async ({
+export type CartResponse<P extends Provider> = ReturnType<
+  NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>['onResponse']>
+>
+
+export const fetcher: HookFetcherFn<Cart | null, CartInput> = async ({
   options,
   input: { cartId },
   fetch,
@@ -31,12 +35,11 @@ export default function useFake<P extends Provider>() {
     context.input.cartId = Cookies.get(cartCookie)
     return fetcherFn(context)
   }
-
   const response = useData(options, [], wrapper, opts?.swrOptions)
   const memoizedResponse = useMemo(
     () => (opts?.onResponse ? opts.onResponse(response) : response),
     [response]
   )
 
-  return memoizedResponse
+  return memoizedResponse as CartResponse<P>
 }
diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx
index 91a1be2e0..e52aa539d 100644
--- a/framework/commerce/index.tsx
+++ b/framework/commerce/index.tsx
@@ -21,9 +21,11 @@ export type Provider = CommerceConfig & {
   cartNormalizer(data: any): Cart
 }
 
-export type HookHandler<Data, Input, Result = any, Body = any> = {
+export type HookHandler<Data, Input, Result = any, Body = any, State = {}> = {
   swrOptions?: SwrOptions<Data | null, Input, Result>
-  onResponse?(response: ResponseState<Data | null>): ResponseState<Data | null>
+  onResponse?(
+    response: ResponseState<Data | null>
+  ): ResponseState<Data | null> & State
   onMutation?: any
   fetchOptions?: HookFetcherOptions
 } & (
diff --git a/framework/commerce/utils/use-data-2.ts b/framework/commerce/utils/use-data-2.ts
index 69eb223b0..ed3218840 100644
--- a/framework/commerce/utils/use-data-2.ts
+++ b/framework/commerce/utils/use-data-2.ts
@@ -7,7 +7,7 @@ import type {
 } from './types'
 import defineProperty from './define-property'
 import { CommerceError } from './errors'
-import { useCommerce } from '..'
+import { HookHandler, useCommerce } from '..'
 
 export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
   Data,
@@ -20,7 +20,7 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
 }
 
 export type UseData = <Data = any, Input = null, Result = any>(
-  options: HookFetcherOptions | (() => HookFetcherOptions | null),
+  options: HookHandler<Data, Input, Result>,
   input: HookInput,
   fetcherFn: HookFetcherFn<Data, Input, Result>,
   swrOptions?: SwrOptions<Data, Input, Result>
@@ -43,6 +43,7 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
           return obj
         }, {}),
         fetch: fetcherRef.current,
+        normalize: options.normalizer,
       })
     } catch (error) {
       // SWR will not log errors, but any error that's not an instance
@@ -55,7 +56,7 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
   }
   const response = useSWR(
     () => {
-      const opts = typeof options === 'function' ? options() : options
+      const opts = options.fetchOptions
       return opts
         ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
         : null