mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 22:42:33 +00:00
Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic
This commit is contained in:
commit
23c8ed7c2d
@ -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 { Heart, Bag } from '@components/icons'
|
||||
@ -15,7 +16,7 @@ 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()
|
||||
|
@ -42,8 +42,8 @@ const WishlistCard: FC<Props> = ({ 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)
|
||||
|
@ -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 type { 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>
|
||||
|
@ -1,46 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import * as React 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<CommerceConfig>
|
||||
@ -52,10 +23,13 @@ export type BigcommerceProps = {
|
||||
|
||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||
return (
|
||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
||||
<CoreCommerceProvider
|
||||
provider={bigcommerceProvider}
|
||||
config={{ ...bigcommerceConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||
|
108
framework/bigcommerce/provider.ts
Normal file
108
framework/bigcommerce/provider.ts
Normal file
@ -0,0 +1,108 @@
|
||||
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 { 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',
|
||||
},
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
normalizer: normalizeCart,
|
||||
onResponse(response) {
|
||||
return Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const useWishlist: HookHandler<
|
||||
Cart | null,
|
||||
[],
|
||||
FetchCartInput,
|
||||
any,
|
||||
any,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
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 = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
fetcher,
|
||||
cartNormalizer: normalizeCart,
|
||||
cart: { useCart },
|
||||
wishlist: { useWishlist },
|
||||
}
|
||||
|
||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
@ -1,28 +1,55 @@
|
||||
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 '..'
|
||||
import type { HookFetcherFn } from '../utils/types'
|
||||
import useData from '../utils/use-data-2'
|
||||
import { Provider, useCommerce } from '..'
|
||||
|
||||
export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
|
||||
|
||||
// Input expected by the `useCart` hook
|
||||
export type CartInput = {
|
||||
export type FetchCartInput = {
|
||||
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> = (
|
||||
...input: UseCartInput<P>
|
||||
) => CartResponse<P>
|
||||
|
||||
export type UseCartInput<P extends Provider> = NonNullable<
|
||||
NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>>['input']
|
||||
>
|
||||
|
||||
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = 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>(...input: UseCartInput<P>) {
|
||||
const { providerRef, fetcherRef, 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, fetcher: wrapper },
|
||||
input,
|
||||
provider.fetcher ?? fetcherRef.current
|
||||
)
|
||||
const memoizedResponse = useMemo(
|
||||
() => (opts?.onResponse ? opts.onResponse(response) : response),
|
||||
[response]
|
||||
)
|
||||
|
||||
return memoizedResponse as CartResponse<P>
|
||||
}
|
||||
|
@ -7,36 +7,57 @@ import {
|
||||
useRef,
|
||||
} from 'react'
|
||||
import * as React from 'react'
|
||||
import { Fetcher } from './utils/types'
|
||||
import { Fetcher, HookHandler } from './utils/types'
|
||||
import { Cart } from './types'
|
||||
import type { FetchCartInput } from './cart/use-cart'
|
||||
|
||||
const Commerce = createContext<CommerceContextValue | {}>({})
|
||||
const Commerce = createContext<CommerceContextValue<any> | {}>({})
|
||||
|
||||
export type CommerceProps = {
|
||||
export type Provider = CommerceConfig & {
|
||||
fetcher: Fetcher
|
||||
cart?: {
|
||||
useCart?: HookHandler<Cart | null, [...any], FetchCartInput>
|
||||
}
|
||||
wishlist?: {
|
||||
useWishlist?: HookHandler<Cart | null, [...any], FetchCartInput>
|
||||
}
|
||||
}
|
||||
|
||||
export type CommerceProps<P extends Provider> = {
|
||||
children?: ReactNode
|
||||
provider: P
|
||||
config: CommerceConfig
|
||||
}
|
||||
|
||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
||||
CommerceContextValue,
|
||||
'fetcherRef'
|
||||
export type CommerceConfig = Omit<
|
||||
CommerceContextValue<any>,
|
||||
'providerRef' | 'fetcherRef'
|
||||
>
|
||||
|
||||
export type CommerceContextValue = {
|
||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
||||
export type CommerceContextValue<P extends Provider> = {
|
||||
providerRef: MutableRefObject<P>
|
||||
fetcherRef: MutableRefObject<Fetcher>
|
||||
locale: string
|
||||
cartCookie: string
|
||||
}
|
||||
|
||||
export function CommerceProvider({ children, config }: CommerceProps) {
|
||||
export function CommerceProvider<P extends Provider>({
|
||||
provider,
|
||||
children,
|
||||
config,
|
||||
}: CommerceProps<P>) {
|
||||
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 +68,6 @@ export function CommerceProvider({ children, config }: CommerceProps) {
|
||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
||||
}
|
||||
|
||||
export function useCommerce<T extends CommerceContextValue>() {
|
||||
return useContext(Commerce) as T
|
||||
export function useCommerce<P extends Provider>() {
|
||||
return useContext(Commerce) as CommerceContextValue<P>
|
||||
}
|
||||
|
@ -1,5 +1,17 @@
|
||||
import type { ConfigInterface } from 'swr'
|
||||
import type { CommerceError } from './errors'
|
||||
import type { ResponseState } from './use-data'
|
||||
|
||||
export type Override<T, K> = Omit<T, keyof K> & K
|
||||
|
||||
// Returns the properties in T with the properties in type K changed from optional to required
|
||||
export type PickRequired<T, K extends keyof T> = Omit<T, K> &
|
||||
Required<Pick<T, K>>
|
||||
|
||||
// Core fetcher added by CommerceProvider
|
||||
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
|
||||
export type Fetcher<T = any, B = any> = (
|
||||
options: FetcherOptions<B>
|
||||
) => T | Promise<T>
|
||||
|
||||
export type FetcherOptions<Body = any> = {
|
||||
url?: string
|
||||
@ -15,12 +27,55 @@ export type HookFetcher<Data, Input = null, Result = any> = (
|
||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||
) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherFn<
|
||||
Data,
|
||||
Input = unknown,
|
||||
Result = any,
|
||||
Body = any
|
||||
> = (context: {
|
||||
options: HookFetcherOptions | null
|
||||
input: Input
|
||||
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
||||
normalize?(data: Result): Data
|
||||
}) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherOptions = {
|
||||
query?: string
|
||||
url?: string
|
||||
method?: string
|
||||
}
|
||||
|
||||
export type HookInput = [string, string | number | boolean | undefined][]
|
||||
export type HookInputValue = string | number | boolean | undefined
|
||||
|
||||
export type Override<T, K> = Omit<T, keyof K> & K
|
||||
export type HookInput = [string, HookInputValue][]
|
||||
|
||||
export type HookFetchInput = { [k: string]: HookInputValue }
|
||||
|
||||
export type HookHandler<
|
||||
// Data obj returned by the hook and fetch operation
|
||||
Data,
|
||||
// Input expected by the hook
|
||||
Input = [...any],
|
||||
// Input expected before doing a fetch operation
|
||||
FetchInput extends HookFetchInput = never,
|
||||
// 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 = {}
|
||||
> = {
|
||||
input?: Input
|
||||
swrOptions?: SwrOptions<Data, FetchInput, Result>
|
||||
onResponse?(response: ResponseState<Data>): ResponseState<Data> & State
|
||||
onMutation?: any
|
||||
fetchOptions?: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<Data, FetchInput, Result, Body>
|
||||
normalizer?(data: Result): Data
|
||||
}
|
||||
|
||||
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||
Data,
|
||||
CommerceError,
|
||||
HookFetcher<Data, Input, Result>
|
||||
>
|
||||
|
81
framework/commerce/utils/use-data-2.ts
Normal file
81
framework/commerce/utils/use-data-2.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import useSWR, { responseInterface } from 'swr'
|
||||
import type {
|
||||
HookHandler,
|
||||
HookInput,
|
||||
HookFetchInput,
|
||||
PickRequired,
|
||||
Fetcher,
|
||||
} from './types'
|
||||
import defineProperty from './define-property'
|
||||
import { CommerceError } from './errors'
|
||||
|
||||
export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export type UseData = <
|
||||
Data = any,
|
||||
Input = [...any],
|
||||
FetchInput extends HookFetchInput = never,
|
||||
Result = any,
|
||||
Body = any
|
||||
>(
|
||||
options: PickRequired<
|
||||
HookHandler<Data, Input, FetchInput, Result, Body>,
|
||||
'fetcher'
|
||||
>,
|
||||
input: HookInput,
|
||||
fetcherFn: Fetcher
|
||||
) => ResponseState<Data>
|
||||
|
||||
const useData: UseData = (options, input, fetcherFn) => {
|
||||
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[input[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, ...input.map((e) => e[1])]
|
||||
: null
|
||||
},
|
||||
fetcher,
|
||||
options.swrOptions
|
||||
)
|
||||
|
||||
if (!('isLoading' in response)) {
|
||||
defineProperty(response, 'isLoading', {
|
||||
get() {
|
||||
return response.data === undefined
|
||||
},
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export default useData
|
Loading…
x
Reference in New Issue
Block a user