mirror of
https://github.com/vercel/commerce.git
synced 2025-04-29 06:17:50 +00:00
Merge branch 'agnostic' of github.com:vercel/commerce into agnostic
This commit is contained in:
commit
85f429c115
@ -1,6 +1,7 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import type { LineItem } from '@framework/types'
|
||||||
import useCart from '@framework/cart/use-cart'
|
import useCart from '@framework/cart/use-cart'
|
||||||
import useCustomer from '@framework/customer/use-customer'
|
import useCustomer from '@framework/customer/use-customer'
|
||||||
import { Avatar } from '@components/common'
|
import { Avatar } from '@components/common'
|
||||||
@ -15,7 +16,7 @@ interface Props {
|
|||||||
|
|
||||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
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 } = useCart()
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
||||||
|
@ -42,8 +42,8 @@ const WishlistCard: FC<Props> = ({ product }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId: Number(product.id),
|
productId: product.id,
|
||||||
variantId: Number(product.variants[0].id),
|
variantId: product.variants[0].id,
|
||||||
})
|
})
|
||||||
openSidebar()
|
openSidebar()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@ -1,52 +1,4 @@
|
|||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
||||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
import type { BigcommerceProvider } from '..'
|
||||||
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'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useCart as UseCart<BigcommerceProvider>
|
||||||
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)
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -1,46 +1,17 @@
|
|||||||
import { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import * as React from 'react'
|
|
||||||
import {
|
import {
|
||||||
CommerceConfig,
|
CommerceConfig,
|
||||||
CommerceProvider as CoreCommerceProvider,
|
CommerceProvider as CoreCommerceProvider,
|
||||||
useCommerce as useCoreCommerce,
|
useCommerce as useCoreCommerce,
|
||||||
} from '@commerce'
|
} from '@commerce'
|
||||||
import { FetcherError } from '@commerce/utils/errors'
|
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||||
|
|
||||||
async function getText(res: Response) {
|
export { bigcommerceProvider }
|
||||||
try {
|
export type { BigcommerceProvider }
|
||||||
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 const bigcommerceConfig: CommerceConfig = {
|
export const bigcommerceConfig: CommerceConfig = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'bc_cartId',
|
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>
|
export type BigcommerceConfig = Partial<CommerceConfig>
|
||||||
@ -52,10 +23,13 @@ export type BigcommerceProps = {
|
|||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||||
return (
|
return (
|
||||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
<CoreCommerceProvider
|
||||||
|
provider={bigcommerceProvider}
|
||||||
|
config={{ ...bigcommerceConfig, ...config }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CoreCommerceProvider>
|
</CoreCommerceProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommerce = () => useCoreCommerce()
|
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||||
|
166
framework/bigcommerce/provider.tsx
Normal file
166
framework/bigcommerce/provider.tsx
Normal file
@ -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
|
@ -1,78 +1,4 @@
|
|||||||
import { HookFetcher } from '@commerce/utils/types'
|
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||||
import { SwrOptions } from '@commerce/utils/use-data'
|
import type { BigcommerceProvider } from '..'
|
||||||
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'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
export default useWishlist as UseWishlist<BigcommerceProvider>
|
||||||
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)
|
|
||||||
|
@ -1,28 +1,71 @@
|
|||||||
import Cookies from 'js-cookie'
|
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 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<Data> = ResponseState<Data> & { isEmpty?: boolean }
|
export type FetchCartInput = {
|
||||||
|
|
||||||
// Input expected by the `useCart` hook
|
|
||||||
export type CartInput = {
|
|
||||||
cartId?: Cart['id']
|
cartId?: Cart['id']
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useCart<Data extends Cart | null>(
|
export type UseCartHandler<P extends Provider> = Prop<
|
||||||
options: HookFetcherOptions,
|
Prop<P, 'cart'>,
|
||||||
input: HookInput,
|
'useCart'
|
||||||
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)
|
|
||||||
|
|
||||||
return response
|
export type UseCartInput<P extends Provider> = UseHookInput<UseCartHandler<P>>
|
||||||
|
|
||||||
|
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 },
|
||||||
|
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 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
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
56
framework/commerce/customer/use-customer.tsx
Normal file
56
framework/commerce/customer/use-customer.tsx
Normal 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-2'
|
||||||
|
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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -6,37 +6,60 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import * as React from 'react'
|
import { Fetcher, HookHandler } from './utils/types'
|
||||||
import { Fetcher } from './utils/types'
|
import type { FetchCartInput } from './cart/use-cart'
|
||||||
|
import type { Cart, Wishlist, Customer } from './types'
|
||||||
|
|
||||||
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<Wishlist | null, any, any>
|
||||||
|
}
|
||||||
|
customer: {
|
||||||
|
useCustomer?: HookHandler<Customer | null, any, any>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommerceProps<P extends Provider> = {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
provider: P
|
||||||
config: CommerceConfig
|
config: CommerceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
export type CommerceConfig = Omit<
|
||||||
CommerceContextValue,
|
CommerceContextValue<any>,
|
||||||
'fetcherRef'
|
'providerRef' | 'fetcherRef'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type CommerceContextValue = {
|
export type CommerceContextValue<P extends Provider> = {
|
||||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
providerRef: MutableRefObject<P>
|
||||||
|
fetcherRef: MutableRefObject<Fetcher>
|
||||||
locale: string
|
locale: string
|
||||||
cartCookie: string
|
cartCookie: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommerceProvider({ children, config }: CommerceProps) {
|
export function CommerceProvider<P extends Provider>({
|
||||||
|
provider,
|
||||||
|
children,
|
||||||
|
config,
|
||||||
|
}: CommerceProps<P>) {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('CommerceProvider requires a valid config object')
|
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
|
// Because the config is an object, if the parent re-renders this provider
|
||||||
// will re-render every consumer unless we memoize the config
|
// will re-render every consumer unless we memoize the config
|
||||||
const cfg = useMemo(
|
const cfg = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
providerRef,
|
||||||
fetcherRef,
|
fetcherRef,
|
||||||
locale: config.locale,
|
locale: config.locale,
|
||||||
cartCookie: config.cartCookie,
|
cartCookie: config.cartCookie,
|
||||||
@ -47,6 +70,6 @@ export function CommerceProvider({ children, config }: CommerceProps) {
|
|||||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommerce<T extends CommerceContextValue>() {
|
export function useCommerce<P extends Provider>() {
|
||||||
return useContext(Commerce) as T
|
return useContext(Commerce) as CommerceContextValue<P>
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
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
|
||||||
value: number
|
value: number
|
||||||
@ -87,6 +90,12 @@ export interface Cart {
|
|||||||
discounts?: Discount[]
|
discounts?: Discount[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Properly define this type
|
||||||
|
export interface Wishlist extends BCWishlist {}
|
||||||
|
|
||||||
|
// TODO: Properly define this type
|
||||||
|
export interface Customer extends BCCustomer {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cart mutations
|
* Cart mutations
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import useData from './utils/use-data'
|
|
||||||
|
|
||||||
const useCustomer = useData
|
|
||||||
|
|
||||||
export default useCustomer
|
|
12
framework/commerce/utils/default-fetcher.ts
Normal file
12
framework/commerce/utils/default-fetcher.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { HookFetcherFn } from './types'
|
||||||
|
|
||||||
|
const defaultFetcher: HookFetcherFn<any> = async ({
|
||||||
|
options,
|
||||||
|
fetch,
|
||||||
|
normalize,
|
||||||
|
}) => {
|
||||||
|
const data = await fetch({ ...options })
|
||||||
|
return data && normalize ? normalize(data) : data
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultFetcher
|
@ -1,5 +1,21 @@
|
|||||||
// Core fetcher added by CommerceProvider
|
import type { ConfigInterface } from 'swr'
|
||||||
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
|
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 = any, B = any> = (
|
||||||
|
options: FetcherOptions<B>
|
||||||
|
) => T | Promise<T>
|
||||||
|
|
||||||
export type FetcherOptions<Body = any> = {
|
export type FetcherOptions<Body = any> = {
|
||||||
url?: string
|
url?: string
|
||||||
@ -15,12 +31,76 @@ export type HookFetcher<Data, Input = null, Result = any> = (
|
|||||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||||
) => Data | Promise<Data>
|
) => Data | Promise<Data>
|
||||||
|
|
||||||
export type HookFetcherOptions = {
|
export type HookFetcherFn<
|
||||||
query?: string
|
Data,
|
||||||
url?: string
|
Input = never,
|
||||||
method?: string
|
Result = any,
|
||||||
|
Body = any
|
||||||
|
> = (context: {
|
||||||
|
options: HookFetcherOptions
|
||||||
|
input: Input
|
||||||
|
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
||||||
|
normalize?(data: Result): Data
|
||||||
|
}) => Data | Promise<Data>
|
||||||
|
|
||||||
|
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<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: NonNullable<Result>): Data
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HookInput = [string, string | number | boolean | undefined][]
|
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||||
|
Data,
|
||||||
|
CommerceError,
|
||||||
|
HookFetcher<Data, Input, Result>
|
||||||
|
>
|
||||||
|
|
||||||
export type Override<T, K> = Omit<T, keyof K> & K
|
/**
|
||||||
|
* 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']
|
||||||
|
84
framework/commerce/utils/use-data-2.ts
Normal file
84
framework/commerce/utils/use-data-2.ts
Normal file
@ -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<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
|
@ -1,5 +1,5 @@
|
|||||||
import useSWR, { ConfigInterface, responseInterface } from 'swr'
|
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 defineProperty from './define-property'
|
||||||
import { CommerceError } from './errors'
|
import { CommerceError } from './errors'
|
||||||
import { useCommerce } from '..'
|
import { useCommerce } from '..'
|
||||||
@ -16,7 +16,7 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
|||||||
|
|
||||||
export type UseData = <Data = any, Input = null, Result = any>(
|
export type UseData = <Data = any, Input = null, Result = any>(
|
||||||
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
||||||
input: HookInput,
|
input: HookSwrInput,
|
||||||
fetcherFn: HookFetcher<Data, Input, Result>,
|
fetcherFn: HookFetcher<Data, Input, Result>,
|
||||||
swrOptions?: SwrOptions<Data, Input, Result>
|
swrOptions?: SwrOptions<Data, Input, Result>
|
||||||
) => ResponseState<Data>
|
) => ResponseState<Data>
|
||||||
|
@ -1,16 +1,56 @@
|
|||||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
import type { Wishlist } from '../types'
|
||||||
import useData, { ResponseState, SwrOptions } from '../utils/use-data'
|
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<Result> = ResponseState<Result> & {
|
export type UseWishlistHandler<P extends Provider> = Prop<
|
||||||
isEmpty?: boolean
|
Prop<P, 'wishlist'>,
|
||||||
}
|
'useWishlist'
|
||||||
|
>
|
||||||
|
|
||||||
export default function useWishlist<Result, Input = null>(
|
export type UseWishlistInput<P extends Provider> = UseHookInput<
|
||||||
options: HookFetcherOptions,
|
UseWishlistHandler<P>
|
||||||
input: HookInput,
|
>
|
||||||
fetcherFn: HookFetcher<Result, Input>,
|
|
||||||
swrOptions?: SwrOptions<Result, Input>
|
export type WishlistResponse<P extends Provider> = UseHookResponse<
|
||||||
): WishlistResponse<Result> {
|
UseWishlistHandler<P>
|
||||||
const response = useData(options, input, fetcherFn, swrOptions)
|
>
|
||||||
|
|
||||||
|
export type UseWishlist<P extends Provider> = Partial<
|
||||||
|
UseWishlistInput<P>
|
||||||
|
> extends UseWishlistInput<P>
|
||||||
|
? (input?: UseWishlistInput<P>) => WishlistResponse<P>
|
||||||
|
: (input: UseWishlistInput<P>) => WishlistResponse<P>
|
||||||
|
|
||||||
|
export const fetcher = defaultFetcher as HookFetcherFn<Wishlist | null>
|
||||||
|
|
||||||
|
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
|
return response
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user