Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic

This commit is contained in:
cond0r 2021-02-15 00:21:43 +02:00
commit af201cccdc
16 changed files with 1143 additions and 1882 deletions

View File

@ -52,6 +52,10 @@ type Action =
type: 'SET_MODAL_VIEW' type: 'SET_MODAL_VIEW'
view: MODAL_VIEWS view: MODAL_VIEWS
} }
| {
type: 'SET_USER_AVATAR'
value: string
}
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW' type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
type ToastText = string type ToastText = string
@ -147,6 +151,9 @@ export const UIProvider: FC = (props) => {
const openToast = () => dispatch({ type: 'OPEN_TOAST' }) const openToast = () => dispatch({ type: 'OPEN_TOAST' })
const closeToast = () => dispatch({ type: 'CLOSE_TOAST' }) const closeToast = () => dispatch({ type: 'CLOSE_TOAST' })
const setUserAvatar = (value: string) =>
dispatch({ type: 'SET_USER_AVATAR', value })
const setModalView = (view: MODAL_VIEWS) => const setModalView = (view: MODAL_VIEWS) =>
dispatch({ type: 'SET_MODAL_VIEW', view }) dispatch({ type: 'SET_MODAL_VIEW', view })
@ -164,6 +171,7 @@ export const UIProvider: FC = (props) => {
setModalView, setModalView,
openToast, openToast,
closeToast, closeToast,
setUserAvatar,
}), }),
[state] [state]
) )

View File

@ -1,38 +1,4 @@
import type { HookFetcher } from '@commerce/utils/types' import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { SwrOptions } from '@commerce/utils/use-data' import type { BigcommerceProvider } from '..'
import useCommerceCustomer from '@commerce/use-customer'
import type { Customer, CustomerData } from '../api/customers'
const defaultOpts = { export default useCustomer as UseCustomer<BigcommerceProvider>
url: '/api/bigcommerce/customers',
method: 'GET',
}
export type { Customer }
export const fetcher: HookFetcher<Customer | null> = async (
options,
_,
fetch
) => {
const data = await fetch<CustomerData | null>({ ...defaultOpts, ...options })
return data?.customer ?? null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Customer | null>
) {
const useCustomer = () => {
return useCommerceCustomer(defaultOpts, [], customFetcher, {
revalidateOnFocus: false,
...swrOptions,
})
}
useCustomer.extend = extendHook
return useCustomer
}
export default extendHook(fetcher)

View File

@ -1,63 +1,4 @@
import type { HookFetcher } from '@commerce/utils/types' import useSearch, { UseSearch } from '@commerce/products/use-search'
import type { SwrOptions } from '@commerce/utils/use-data' import type { BigcommerceProvider } from '..'
import useCommerceSearch from '@commerce/products/use-search'
import type { SearchProductsData } from '../api/catalog/products'
const defaultOpts = { export default useSearch as UseSearch<BigcommerceProvider>
url: '/api/bigcommerce/catalog/products',
method: 'GET',
}
export type SearchProductsInput = {
search?: string
categoryId?: number
brandId?: number
sort?: string
}
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
options,
{ search, categoryId, brandId, sort },
fetch
) => {
// Use a dummy base as we only care about the relative path
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
if (search) url.searchParams.set('search', search)
if (Number.isInteger(categoryId))
url.searchParams.set('category', String(categoryId))
if (Number.isInteger(brandId)) url.searchParams.set('brand', String(brandId))
if (sort) url.searchParams.set('sort', sort)
return fetch({
url: url.pathname + url.search,
method: options?.method ?? defaultOpts.method,
})
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
) {
const useSearch = (input: SearchProductsInput = {}) => {
const response = useCommerceSearch(
defaultOpts,
[
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
],
customFetcher,
{ revalidateOnFocus: false, ...swrOptions }
)
return response
}
useSearch.extend = extendHook
return useSearch
}
export default extendHook(fetcher)

View File

@ -4,6 +4,8 @@ import type { Fetcher, HookHandler } from '@commerce/utils/types'
import type { FetchCartInput } from '@commerce/cart/use-cart' import type { FetchCartInput } from '@commerce/cart/use-cart'
import { normalizeCart } from './lib/normalize' import { normalizeCart } from './lib/normalize'
import type { Wishlist } from './api/wishlist' import type { Wishlist } from './api/wishlist'
import type { Customer, CustomerData } from './api/customers'
import type { SearchProductsData } from './api/catalog/products'
import useCustomer from './customer/use-customer' import useCustomer from './customer/use-customer'
import type { Cart } from './types' import type { Cart } from './types'
@ -48,15 +50,16 @@ const useCart: HookHandler<
Cart | null, Cart | null,
{}, {},
FetchCartInput, FetchCartInput,
any,
any,
{ isEmpty?: boolean } { isEmpty?: boolean }
> = { > = {
fetchOptions: { fetchOptions: {
url: '/api/bigcommerce/cart', url: '/api/bigcommerce/cart',
method: 'GET', method: 'GET',
}, },
normalizer: normalizeCart, async fetcher({ input: { cartId }, options, fetch }) {
const data = cartId ? await fetch(options) : null
return data && normalizeCart(data)
},
useHook({ input, useData }) { useHook({ input, useData }) {
const response = useData({ const response = useData({
swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, swrOptions: { revalidateOnFocus: false, ...input.swrOptions },
@ -81,8 +84,6 @@ const useWishlist: HookHandler<
Wishlist | null, Wishlist | null,
{ includeProducts?: boolean }, { includeProducts?: boolean },
{ customerId?: number; includeProducts: boolean }, { customerId?: number; includeProducts: boolean },
any,
any,
{ isEmpty?: boolean } { isEmpty?: boolean }
> = { > = {
fetchOptions: { fetchOptions: {
@ -130,6 +131,73 @@ const useWishlist: HookHandler<
}, },
} }
const useCustomerHandler: HookHandler<Customer | null> = {
fetchOptions: {
url: '/api/bigcommerce/customers',
method: 'GET',
},
async fetcher({ options, fetch }) {
const data = await fetch<CustomerData | null>(options)
return data?.customer ?? null
},
useHook({ input, useData }) {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}
export type SearchProductsInput = {
search?: string
categoryId?: number
brandId?: number
sort?: string
}
const useSearch: HookHandler<
SearchProductsData,
SearchProductsInput,
SearchProductsInput
> = {
fetchOptions: {
url: '/api/bigcommerce/catalog/products',
method: 'GET',
},
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
// Use a dummy base as we only care about the relative path
const url = new URL(options.url!, 'http://a')
if (search) url.searchParams.set('search', search)
if (Number.isInteger(categoryId))
url.searchParams.set('category', String(categoryId))
if (Number.isInteger(brandId))
url.searchParams.set('brand', String(brandId))
if (sort) url.searchParams.set('sort', sort)
return fetch({
url: url.pathname + url.search,
method: options.method,
})
},
useHook({ input, useData }) {
return useData({
input: [
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}
export const bigcommerceProvider = { export const bigcommerceProvider = {
locale: 'en-us', locale: 'en-us',
cartCookie: 'bc_cartId', cartCookie: 'bc_cartId',
@ -137,6 +205,8 @@ export const bigcommerceProvider = {
cartNormalizer: normalizeCart, cartNormalizer: normalizeCart,
cart: { useCart }, cart: { useCart },
wishlist: { useWishlist }, wishlist: { useWishlist },
customer: { useCustomer: useCustomerHandler },
products: { useSearch },
} }
export type BigcommerceProvider = typeof bigcommerceProvider export type BigcommerceProvider = typeof bigcommerceProvider

View File

@ -6,7 +6,7 @@ import type {
UseHookInput, UseHookInput,
UseHookResponse, UseHookResponse,
} from '../utils/types' } from '../utils/types'
import useData from '../utils/use-data-2' import useData from '../utils/use-data'
import { Provider, useCommerce } from '..' import { Provider, useCommerce } from '..'
export type FetchCartInput = { export type FetchCartInput = {
@ -34,10 +34,8 @@ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options, options,
input: { cartId }, input: { cartId },
fetch, fetch,
normalize,
}) => { }) => {
const data = cartId ? await fetch({ ...options }) : null return cartId ? await fetch({ ...options }) : null
return data && normalize ? normalize(data) : data
} }
export default function useCart<P extends Provider>( export default function useCart<P extends Provider>(

View File

@ -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'
import { Provider, useCommerce } from '..'
export type UseCustomerHandler<P extends Provider> = Prop<
Prop<P, 'customer'>,
'useCustomer'
>
export type UseCustomerInput<P extends Provider> = UseHookInput<
UseCustomerHandler<P>
>
export type CustomerResponse<P extends Provider> = UseHookResponse<
UseCustomerHandler<P>
>
export type UseCustomer<P extends Provider> = Partial<
UseCustomerInput<P>
> extends UseCustomerInput<P>
? (input?: UseCustomerInput<P>) => CustomerResponse<P>
: (input: UseCustomerInput<P>) => CustomerResponse<P>
export const fetcher = defaultFetcher as HookFetcherFn<Customer | null>
export default function useCustomer<P extends Provider>(
input: UseCustomerInput<P> = {}
) {
const { providerRef, fetcherRef } = useCommerce<P>()
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
},
})
}

View File

@ -6,10 +6,9 @@ import {
useMemo, useMemo,
useRef, useRef,
} from 'react' } from 'react'
import * as React from 'react'
import { Fetcher, HookHandler } from './utils/types' import { Fetcher, HookHandler } from './utils/types'
import type { FetchCartInput } from './cart/use-cart' import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist } from './types' import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
const Commerce = createContext<CommerceContextValue<any> | {}>({}) const Commerce = createContext<CommerceContextValue<any> | {}>({})
@ -21,6 +20,12 @@ export type Provider = CommerceConfig & {
wishlist?: { wishlist?: {
useWishlist?: HookHandler<Wishlist | null, any, any> useWishlist?: HookHandler<Wishlist | null, any, any>
} }
customer: {
useCustomer?: HookHandler<Customer | null, any, any>
}
products: {
useSearch?: HookHandler<SearchProductsData, any, any>
}
} }
export type CommerceProps<P extends Provider> = { export type CommerceProps<P extends Provider> = {

View File

@ -1,5 +1,57 @@
import type { SearchProductsData } from '../types'
import type {
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import defaultFetcher from '../utils/default-fetcher'
import useData from '../utils/use-data' import useData from '../utils/use-data'
import { Provider, useCommerce } from '..'
import { BigcommerceProvider } from '@framework'
const useSearch = useData export type UseSearchHandler<P extends Provider> = Prop<
Prop<P, 'products'>,
'useSearch'
>
export default useSearch export type UseSeachInput<P extends Provider> = UseHookInput<
UseSearchHandler<P>
>
export type SearchResponse<P extends Provider> = UseHookResponse<
UseSearchHandler<P>
>
export type UseSearch<P extends Provider> = Partial<
UseSeachInput<P>
> extends UseSeachInput<P>
? (input?: UseSeachInput<P>) => SearchResponse<P>
: (input: UseSeachInput<P>) => SearchResponse<P>
export const fetcher = defaultFetcher as HookFetcherFn<SearchProductsData>
export default function useSearch<P extends Provider>(
input: UseSeachInput<P> = {}
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.products?.useSearch
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
},
})
}

View File

@ -1,4 +1,6 @@
import type { Wishlist as BCWishlist } from '@framework/api/wishlist' import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
import type { Customer as BCCustomer } from '@framework/api/customers'
import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products'
export interface Discount { export interface Discount {
// The value of the discount, can be an amount or percentage // The value of the discount, can be an amount or percentage
@ -92,6 +94,12 @@ export interface Cart {
// TODO: Properly define this type // TODO: Properly define this type
export interface Wishlist extends BCWishlist {} export interface Wishlist extends BCWishlist {}
// TODO: Properly define this type
export interface Customer extends BCCustomer {}
// TODO: Properly define this type
export interface SearchProductsData extends BCSearchProductsData {}
/** /**
* Cart mutations * Cart mutations
*/ */

View File

@ -1,5 +0,0 @@
import useData from './utils/use-data'
const useCustomer = useData
export default useCustomer

View File

@ -0,0 +1,6 @@
import type { HookFetcherFn } from './types'
const defaultFetcher: HookFetcherFn<any> = ({ options, fetch }) =>
fetch(options)
export default defaultFetcher

View File

@ -40,7 +40,6 @@ export type HookFetcherFn<
options: HookFetcherOptions options: HookFetcherOptions
input: Input input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T> fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
normalize?(data: Result): Data
}) => Data | Promise<Data> }) => Data | Promise<Data>
export type HookFetcherOptions = { method?: string } & ( export type HookFetcherOptions = { method?: string } & (
@ -63,23 +62,18 @@ export type HookHandler<
Input extends { [k: string]: unknown } = {}, Input extends { [k: string]: unknown } = {},
// Input expected before doing a fetch operation // Input expected before doing a fetch operation
FetchInput extends HookFetchInput = {}, 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 // Custom state added to the response object of SWR
State = {} State = {}
> = { > = {
useHook?(context: { useHook?(context: {
input: Input & { swrOptions?: SwrOptions<Data, FetchInput, Result> } input: Input & { swrOptions?: SwrOptions<Data, FetchInput> }
useData(context?: { useData(context?: {
input?: HookFetchInput | HookSwrInput input?: HookFetchInput | HookSwrInput
swrOptions?: SwrOptions<Data, FetchInput, Result> swrOptions?: SwrOptions<Data, FetchInput>
}): ResponseState<Data> }): ResponseState<Data>
}): ResponseState<Data> & State }): ResponseState<Data> & State
fetchOptions: HookFetcherOptions fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput, Result, Body> fetcher?: HookFetcherFn<Data, FetchInput>
normalizer?(data: Result): Data
} }
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface< export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<

View File

@ -1,84 +0,0 @@
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<Result> = responseInterface<Result, CommerceError> & {
isLoading: boolean
}
export type UseData = <
Data = any,
Input extends { [k: string]: unknown } = {},
FetchInput extends HookFetchInput = {},
Result = any,
Body = any
>(
options: PickRequired<
HookHandler<Data, Input, FetchInput, Result, Body>,
'fetcher'
>,
input: HookFetchInput | HookSwrInput,
fetcherFn: Fetcher,
swrOptions?: SwrOptions<Data, FetchInput, Result>
) => ResponseState<Data>
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

View File

@ -1,44 +1,48 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr' import useSWR, { responseInterface } from 'swr'
import type { HookSwrInput, HookFetcher, HookFetcherOptions } from './types' import type {
HookHandler,
HookSwrInput,
HookFetchInput,
PickRequired,
Fetcher,
SwrOptions,
} from './types'
import defineProperty from './define-property' import defineProperty from './define-property'
import { CommerceError } from './errors' import { CommerceError } from './errors'
import { useCommerce } from '..'
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
Data,
CommerceError,
HookFetcher<Data, Input, Result>
>
export type ResponseState<Result> = responseInterface<Result, CommerceError> & { export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
isLoading: boolean isLoading: boolean
} }
export type UseData = <Data = any, Input = null, Result = any>( export type UseData = <
options: HookFetcherOptions | (() => HookFetcherOptions | null), Data = any,
input: HookSwrInput, Input extends { [k: string]: unknown } = {},
fetcherFn: HookFetcher<Data, Input, Result>, FetchInput extends HookFetchInput = {}
swrOptions?: SwrOptions<Data, Input, Result> >(
options: PickRequired<HookHandler<Data, Input, FetchInput>, 'fetcher'>,
input: HookFetchInput | HookSwrInput,
fetcherFn: Fetcher,
swrOptions?: SwrOptions<Data, FetchInput>
) => ResponseState<Data> ) => ResponseState<Data>
const useData: UseData = (options, input, fetcherFn, swrOptions) => { const useData: UseData = (options, input, fetcherFn, swrOptions) => {
const { fetcherRef } = useCommerce() const hookInput = Array.isArray(input) ? input : Object.entries(input)
const fetcher = async ( const fetcher = async (
url?: string, url: string,
query?: string, query?: string,
method?: string, method?: string,
...args: any[] ...args: any[]
) => { ) => {
try { try {
return await fetcherFn( return await options.fetcher({
{ url, query, method }, options: { url, query, method },
// Transform the input array into an object // Transform the input array into an object
args.reduce((obj, val, i) => { input: args.reduce((obj, val, i) => {
obj[input[i][0]!] = val obj[hookInput[i][0]!] = val
return obj return obj
}, {}), }, {}),
fetcherRef.current fetch: fetcherFn,
) })
} catch (error) { } catch (error) {
// SWR will not log errors, but any error that's not an instance // SWR will not log errors, but any error that's not an instance
// of CommerceError is not welcomed by this hook // of CommerceError is not welcomed by this hook
@ -50,9 +54,9 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
} }
const response = useSWR( const response = useSWR(
() => { () => {
const opts = typeof options === 'function' ? options() : options const opts = options.fetchOptions
return opts 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 : null
}, },
fetcher, fetcher,

View File

@ -5,7 +5,8 @@ import type {
UseHookInput, UseHookInput,
UseHookResponse, UseHookResponse,
} from '../utils/types' } from '../utils/types'
import useData from '../utils/use-data-2' import defaultFetcher from '../utils/default-fetcher'
import useData from '../utils/use-data'
import { Provider, useCommerce } from '..' import { Provider, useCommerce } from '..'
export type UseWishlistHandler<P extends Provider> = Prop< export type UseWishlistHandler<P extends Provider> = Prop<
@ -22,19 +23,12 @@ export type WishlistResponse<P extends Provider> = UseHookResponse<
> >
export type UseWishlist<P extends Provider> = Partial< export type UseWishlist<P extends Provider> = Partial<
WishlistResponse<P> UseWishlistInput<P>
> extends WishlistResponse<P> > extends UseWishlistInput<P>
? (input?: WishlistResponse<P>) => WishlistResponse<P> ? (input?: UseWishlistInput<P>) => WishlistResponse<P>
: (input: WishlistResponse<P>) => WishlistResponse<P> : (input: UseWishlistInput<P>) => WishlistResponse<P>
export const fetcher: HookFetcherFn<Wishlist | null> = async ({ export const fetcher = defaultFetcher as HookFetcherFn<Wishlist | null>
options,
fetch,
normalize,
}) => {
const data = await fetch({ ...options })
return data && normalize ? normalize(data) : data
}
export default function useWishlist<P extends Provider>( export default function useWishlist<P extends Provider>(
input: UseWishlistInput<P> = {} input: UseWishlistInput<P> = {}

2518
yarn.lock

File diff suppressed because it is too large Load Diff