forked from crowetic/commerce
Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic
This commit is contained in:
commit
1450492826
@ -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<BigcommerceProvider>
|
||||
export default useCart as UseCart<BigcommerceProvider>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
|
@ -1,7 +1,10 @@
|
||||
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 useCustomer from './customer/use-customer'
|
||||
import type { Cart } from './types'
|
||||
|
||||
async function getText(res: Response) {
|
||||
@ -43,7 +46,7 @@ const fetcher: Fetcher = async ({
|
||||
|
||||
const useCart: HookHandler<
|
||||
Cart | null,
|
||||
[],
|
||||
{},
|
||||
FetchCartInput,
|
||||
any,
|
||||
any,
|
||||
@ -53,26 +56,31 @@ const useCart: HookHandler<
|
||||
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,
|
||||
},
|
||||
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<
|
||||
Cart | null,
|
||||
[],
|
||||
FetchCartInput,
|
||||
Wishlist | null,
|
||||
{ includeProducts?: boolean },
|
||||
{ customerId?: number; includeProducts: boolean },
|
||||
any,
|
||||
any,
|
||||
{ isEmpty?: boolean }
|
||||
@ -81,18 +89,44 @@ const useWishlist: HookHandler<
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
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,
|
||||
})
|
||||
},
|
||||
onResponse(response) {
|
||||
return Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
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]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -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<Wishlist | null, UseWishlistInput> = (
|
||||
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<Wishlist | null, UseWishlistInput>
|
||||
) {
|
||||
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<BigcommerceProvider>
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { useMemo } from 'react'
|
||||
import Cookies from 'js-cookie'
|
||||
import type { Cart } from '../types'
|
||||
import type { HookFetcherFn } from '../utils/types'
|
||||
import type {
|
||||
Prop,
|
||||
HookFetcherFn,
|
||||
UseHookInput,
|
||||
UseHookResponse,
|
||||
} from '../utils/types'
|
||||
import useData from '../utils/use-data-2'
|
||||
import { Provider, useCommerce } from '..'
|
||||
|
||||
@ -9,18 +13,23 @@ export type FetchCartInput = {
|
||||
cartId?: Cart['id']
|
||||
}
|
||||
|
||||
export type CartResponse<P extends Provider> = ReturnType<
|
||||
NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>['onResponse']>
|
||||
export type UseCartHandler<P extends Provider> = Prop<
|
||||
Prop<P, 'cart'>,
|
||||
'useCart'
|
||||
>
|
||||
|
||||
export type UseCart<P extends Provider> = (
|
||||
...input: UseCartInput<P>
|
||||
) => CartResponse<P>
|
||||
export type UseCartInput<P extends Provider> = UseHookInput<UseCartHandler<P>>
|
||||
|
||||
export type UseCartInput<P extends Provider> = NonNullable<
|
||||
NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>>['input']
|
||||
export type CartResponse<P extends Provider> = UseHookResponse<
|
||||
UseCartHandler<P>
|
||||
>
|
||||
|
||||
export type UseCart<P extends Provider> = Partial<
|
||||
UseCartInput<P>
|
||||
> extends UseCartInput<P>
|
||||
? (input?: UseCartInput<P>) => CartResponse<P>
|
||||
: (input: UseCartInput<P>) => CartResponse<P>
|
||||
|
||||
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
|
||||
options,
|
||||
input: { cartId },
|
||||
@ -31,25 +40,32 @@ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
|
||||
return data && normalize ? normalize(data) : data
|
||||
}
|
||||
|
||||
export default function useCart<P extends Provider>(...input: UseCartInput<P>) {
|
||||
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 useHook = opts?.useHook ?? ((ctx) => ctx.useData())
|
||||
|
||||
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>
|
||||
return useHook({
|
||||
input,
|
||||
useData(ctx) {
|
||||
const response = useData(
|
||||
{ ...opts!, fetcher: wrapper },
|
||||
ctx?.input ?? [],
|
||||
provider.fetcher ?? fetcherRef.current,
|
||||
ctx?.swrOptions ?? input.swrOptions
|
||||
)
|
||||
return response
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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<CommerceContextValue<any> | {}>({})
|
||||
|
||||
export type Provider = CommerceConfig & {
|
||||
fetcher: Fetcher
|
||||
cart?: {
|
||||
useCart?: HookHandler<Cart | null, [...any], FetchCartInput>
|
||||
useCart?: HookHandler<Cart | null, any, FetchCartInput>
|
||||
}
|
||||
wishlist?: {
|
||||
useWishlist?: HookHandler<Cart | null, [...any], FetchCartInput>
|
||||
useWishlist?: HookHandler<Wishlist | null, any, any>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -4,11 +4,15 @@ 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
|
||||
/**
|
||||
* 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
|
||||
/**
|
||||
* Core fetcher added by CommerceProvider
|
||||
*/
|
||||
export type Fetcher<T = any, B = any> = (
|
||||
options: FetcherOptions<B>
|
||||
) => T | Promise<T>
|
||||
@ -29,35 +33,36 @@ export type HookFetcher<Data, Input = null, Result = any> = (
|
||||
|
||||
export type HookFetcherFn<
|
||||
Data,
|
||||
Input = unknown,
|
||||
Input = never,
|
||||
Result = any,
|
||||
Body = any
|
||||
> = (context: {
|
||||
options: HookFetcherOptions | null
|
||||
options: HookFetcherOptions
|
||||
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 HookFetcherOptions = { method?: string } & (
|
||||
| { query: string; url?: string }
|
||||
| { query?: string; url: string }
|
||||
)
|
||||
|
||||
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,
|
||||
FetchInput extends HookFetchInput = {},
|
||||
// Data returned by the API after a fetch operation
|
||||
Result = any,
|
||||
// Body expected by the API endpoint
|
||||
@ -65,11 +70,14 @@ export type HookHandler<
|
||||
// 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
|
||||
useHook?(context: {
|
||||
input: Input & { swrOptions?: SwrOptions<Data, FetchInput, Result> }
|
||||
useData(context?: {
|
||||
input?: HookFetchInput | HookSwrInput
|
||||
swrOptions?: SwrOptions<Data, FetchInput, Result>
|
||||
}): ResponseState<Data>
|
||||
}): ResponseState<Data> & State
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<Data, FetchInput, Result, Body>
|
||||
normalizer?(data: Result): Data
|
||||
}
|
||||
@ -79,3 +87,20 @@ export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||
CommerceError,
|
||||
HookFetcher<Data, Input, Result>
|
||||
>
|
||||
|
||||
/**
|
||||
* Returns the property K from type T excluding nullables
|
||||
*/
|
||||
export type Prop<T, K extends keyof T> = NonNullable<T[K]>
|
||||
|
||||
export type UseHookParameters<H extends HookHandler<any>> = Parameters<
|
||||
Prop<H, 'useHook'>
|
||||
>
|
||||
|
||||
export type UseHookResponse<H extends HookHandler<any>> = ReturnType<
|
||||
Prop<H, 'useHook'>
|
||||
>
|
||||
|
||||
export type UseHookInput<
|
||||
H extends HookHandler<any>
|
||||
> = UseHookParameters<H>[0]['input']
|
||||
|
@ -1,10 +1,11 @@
|
||||
import useSWR, { responseInterface } from 'swr'
|
||||
import type {
|
||||
HookHandler,
|
||||
HookInput,
|
||||
HookSwrInput,
|
||||
HookFetchInput,
|
||||
PickRequired,
|
||||
Fetcher,
|
||||
SwrOptions,
|
||||
} from './types'
|
||||
import defineProperty from './define-property'
|
||||
import { CommerceError } from './errors'
|
||||
@ -15,8 +16,8 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
||||
|
||||
export type UseData = <
|
||||
Data = any,
|
||||
Input = [...any],
|
||||
FetchInput extends HookFetchInput = never,
|
||||
Input extends { [k: string]: unknown } = {},
|
||||
FetchInput extends HookFetchInput = {},
|
||||
Result = any,
|
||||
Body = any
|
||||
>(
|
||||
@ -24,13 +25,15 @@ export type UseData = <
|
||||
HookHandler<Data, Input, FetchInput, Result, Body>,
|
||||
'fetcher'
|
||||
>,
|
||||
input: HookInput,
|
||||
fetcherFn: Fetcher
|
||||
input: HookFetchInput | HookSwrInput,
|
||||
fetcherFn: Fetcher,
|
||||
swrOptions?: SwrOptions<Data, FetchInput, Result>
|
||||
) => ResponseState<Data>
|
||||
|
||||
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,
|
||||
url: string,
|
||||
query?: string,
|
||||
method?: string,
|
||||
...args: any[]
|
||||
@ -40,7 +43,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,11 +62,11 @@ 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,
|
||||
options.swrOptions
|
||||
swrOptions
|
||||
)
|
||||
|
||||
if (!('isLoading' in response)) {
|
||||
|
@ -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<Result> = responseInterface<Result, CommerceError> & {
|
||||
|
||||
export type UseData = <Data = any, Input = null, Result = any>(
|
||||
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
||||
input: HookInput,
|
||||
input: HookSwrInput,
|
||||
fetcherFn: HookFetcher<Data, Input, Result>,
|
||||
swrOptions?: SwrOptions<Data, Input, Result>
|
||||
) => ResponseState<Data>
|
||||
|
@ -1,16 +1,62 @@
|
||||
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 useData from '../utils/use-data-2'
|
||||
import { Provider, useCommerce } from '..'
|
||||
|
||||
export type WishlistResponse<Result> = ResponseState<Result> & {
|
||||
isEmpty?: boolean
|
||||
export type UseWishlistHandler<P extends Provider> = Prop<
|
||||
Prop<P, 'wishlist'>,
|
||||
'useWishlist'
|
||||
>
|
||||
|
||||
export type UseWishlistInput<P extends Provider> = UseHookInput<
|
||||
UseWishlistHandler<P>
|
||||
>
|
||||
|
||||
export type WishlistResponse<P extends Provider> = UseHookResponse<
|
||||
UseWishlistHandler<P>
|
||||
>
|
||||
|
||||
export type UseWishlist<P extends Provider> = Partial<
|
||||
WishlistResponse<P>
|
||||
> extends WishlistResponse<P>
|
||||
? (input?: WishlistResponse<P>) => WishlistResponse<P>
|
||||
: (input: WishlistResponse<P>) => WishlistResponse<P>
|
||||
|
||||
export const fetcher: HookFetcherFn<Wishlist | null> = async ({
|
||||
options,
|
||||
fetch,
|
||||
normalize,
|
||||
}) => {
|
||||
const data = await fetch({ ...options })
|
||||
return data && normalize ? normalize(data) : data
|
||||
}
|
||||
|
||||
export default function useWishlist<Result, Input = null>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, Input>,
|
||||
swrOptions?: SwrOptions<Result, Input>
|
||||
): WishlistResponse<Result> {
|
||||
const response = useData(options, input, fetcherFn, swrOptions)
|
||||
return response
|
||||
export default function useWishlist<P extends Provider>(
|
||||
input: UseWishlistInput<P> = {}
|
||||
) {
|
||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
||||
|
||||
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
|
||||
},
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user