Agnostic UI (#199)

* changes

* Progress

* Normalized Products output

* Progress

* Restored Index Agnostic

* Progress

* Reordering

* Moved normalizer to BC function

* Removed Futures

* More Types

* More Types

* More Types

* Fix useCallback

* Progress, Changes types, readme and restoring functionality

* Changes

* TS Issues

* Changes

* Normalizer

* Normalizing more operations

* Normalizing more operations

* changes

* Merge Issues

* Cleanup

* change

* changes

* index.ts broke my tree shaking

* slug

* Normalized Options and Swatches

* Restored Add to cart

* Correct Variant Added to Cart

* Normalizing Cart Responses

* Changes

* changes breaking

* Adding immutable normalizer for Product

* Cart Normalized

* changes

* Progress

* More updates

* Removed some comments

* Add loading state for data hooks

* Bug fix

* Changed the way isEmpty works

* Improve navbar performance

* Added useResponse hook

* added useResponse to useWhishlist

* Added husky and lint-staged

* Ran prettier fix

* Added more cart types

* Moved types.d.ts to the commerce folder

* Minor changes

* Moved normalizer to happen after fetch

* updated useCart types

* Updated normalizer for useData

* Added new normalizer for the cart to the UI

* More corrections for useCart

* Updated cart update hooks

* Removed import

* Progress

* Switch away from global types

* Making multiple changes

* Improved types for operations

* Moved and updated cart types

* Updated the useAddItem and useRemoveItem hooks

* Minor life improvement

* Minor change

* Implement Shopify Provider

* Update README.md

* Update README.md

* normalizations & missing files

* Update index.ts

* fixes

* Update normalize.ts

* fix: cart error on first load

* shopify checkout redirect & api handler

* Update get-checkout-id.ts

* userAvatar

* Fix: color option

* Update normalize.ts

* changes

* Update next.config.js

* start customer auth & signup

* Update config.ts

* Login, Sign Up, Log Out, and checkout & customer association

* Automatic login after sign-up

* Update handle-login.ts

* MOving stuff around and adding temporal new files

* changes

* Replace use-cart with the new hook

* Removed old hook

* Improved HookHandler type

* Moved types

* Simplified useData types

* Updated Fetcher type

* Moved SwrOptions type

* Removed duplicated fetcher

* Moved provider to its own file

* Added proper type for fetch input

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

This reverts commit 23c8ed7c2d, reversing
changes made to bf50965a39.

* change readme

* Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic"

This reverts commit bf50965a39, reversing
changes made to 0dad4ddedb.

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

This reverts commit c9a43f1bce.

* align with upstream changes

* Updated how the hook input is handled

* Add more options to the hook handler

* Final touches to the hook handler type

* Moved useWishlist to use new handler

* Move useCustomer to the new hook

* Added a default fetcher

* query all products for vendors & paths, improve search

* Update use-search.tsx

* fix cart after upstream changes

* Shopify Provider (#186)

* Start of Shopify provider

* add missing comment to documentation

* add missing env vars to documentation

* update reference to types file

* Moved useSearch to the new hook

* Removed old use-data lib

* Removed generics for result and body

* Removed normalizr

* Wishlist

* New changes and initial Features API

* Fixed some types

* Fixed more types

* fixes after upstream changes

* Fixed product types

* Fixed another product type

* Updated type

* Fixed remaining issues with types

* Added a MutationHandler

* Moved the handlers to each hook

* Moved the fetcher to its own file

* Moved handler to each hook

* Added initial version of useAddItem

* Added better mutation types, and moved some hooks

* Removed use-cart-actions

* Added initial version of useAddItem

* Updated types

* Update use-add-item.tsx

* changes

* Changes

* Reordering and changes

* Adding Features APO

* Adding wishlist api

* Implementing FeaturesAPI with Wishlist

* Removing bug with touchstart

* Adding tyni typing

* moved use-remove-item

* Removed MutationHandler type

* Moved more hooks and updated types to make them smaller

* Moved data hooks to new format

* Removed no longer required types

* Removed useResponse helper

* Updated useData type

* Moved wishlist use-add-item

* Moved wishlist use-remove-item to provider

* Moved use-login and use-logout

* Update use-signup

* Removed use-action helper

* Moved auth & cart hooks + several fixes

* Updated cart item, fixed deprecations

* Update next.config.js

* Updates to wishlist feature

* Moved the features to be environment variable only

* More changes for wishlist config

* Disable wishlist

* Removed useWishlistActions

* Updated readme

* updates

* typos

* Updated the way the provider config is set

* Removed features.ts

* Removed bootstrap.js

* Aligned with upstream changes

* Updates

* shopify: changes

* shopify: changes

* Update next.config.js

* Shopify Provider Updates (#209)

* Implement Shopify Provider

* Update README.md

* Update README.md

* normalizations & missing files

* Update index.ts

* fixes

* Update normalize.ts

* fix: cart error on first load

* shopify checkout redirect & api handler

* Update get-checkout-id.ts

* Fix: color option

* Update normalize.ts

* changes

* Update next.config.js

* start customer auth & signup

* Update config.ts

* Login, Sign Up, Log Out, and checkout & customer association

* Automatic login after sign-up

* Update handle-login.ts

* changes

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

This reverts commit 23c8ed7c2d, reversing
changes made to bf50965a39.

* change readme

* Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic"

This reverts commit bf50965a39, reversing
changes made to 0dad4ddedb.

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

This reverts commit c9a43f1bce.

* align with upstream changes

* query all products for vendors & paths, improve search

* Update use-search.tsx

* fix cart after upstream changes

* fixes after upstream changes

* Moved handler to each hook

* Added initial version of useAddItem

* Updated types

* Update use-add-item.tsx

* Moved auth & cart hooks + several fixes

* Updated cart item, fixed deprecations

* Update next.config.js

* Aligned with upstream changes

* Updates

* Update next.config.js

* Updated the commerce config structure

* Removed @framework imports within framework providers

* Fixed types

* changes

* Adding extra config

* Adding shopify commit

* Adding env templates to the providers

* Ignore some types

* Adding link for Cart

* Adding customCheckout

* multiple changes to fix the wishlist

* Shopify Provier Updates (#212)

* changes

* Adding shopify commit

* Changed to query page by id

* Fixed page query, Changed use-search GraphQl query

* Update use-search.tsx

* remove unused util

* Changed cookie expiration

* Update tsconfig.json

Co-authored-by: okbel <curciobel@gmail.com>

* Bump and adding dependency

* Adding color checks

* Now colors work with lighter colors

* Stable commerce.config.json

* Updated main readme

* Readme changes

* Default to bigcommerce

Co-authored-by: bc <bc@bcs-MacBook-Pro.fibertel.com.ar>
Co-authored-by: Luis Alvarez <luis@vercel.com>
Co-authored-by: cond0r <pinte_catalin@yahoo.com>
Co-authored-by: Peter Mekhaeil <4616064+petermekhaeil@users.noreply.github.com>
This commit is contained in:
B
2021-03-04 07:57:25 -03:00
committed by GitHub
parent b121078151
commit 9b71bd77fc
232 changed files with 20545 additions and 1895 deletions

View File

@@ -32,5 +32,3 @@ export interface CommerceAPIFetchOptions<Variables> {
variables?: Variables
preview?: boolean
}
// TODO: define interfaces for all the available operations and API endpoints

View File

@@ -0,0 +1,19 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook, HookFetcherFn } from '../utils/types'
import type { Provider } from '..'
export type UseLogin<
H extends MutationHook<any, any, any> = MutationHook<null, {}, {}>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null, {}> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useLogin!
const useLogin: UseLogin = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useLogin

View File

@@ -0,0 +1,19 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Provider } from '..'
export type UseLogout<
H extends MutationHook<any, any, any> = MutationHook<null>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useLogout!
const useLogout: UseLogout = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useLogout

View File

@@ -0,0 +1,19 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Provider } from '..'
export type UseSignup<
H extends MutationHook<any, any, any> = MutationHook<null>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useSignup!
const useSignup: UseSignup = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useSignup

View File

@@ -1,5 +1,23 @@
import useAction from '../utils/use-action'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
import type { Provider } from '..'
const useAddItem = useAction
export type UseAddItem<
H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart,
AddCartItemBody<CartItemBody>
> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useAddItem!
const useAddItem: UseAddItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useAddItem

View File

@@ -1,17 +0,0 @@
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-item'
// This hook is probably not going to be used, but it's here
// to show how a commerce should be structuring it
export default function useCartActions<T, Input>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const addItem = useAddItem<T, Input>(options, fetcher)
const updateItem = useUpdateItem<T, Input>(options, fetcher)
const removeItem = useRemoveItem<T, Input>(options, fetcher)
return { addItem, updateItem, removeItem }
}

View File

@@ -1,31 +1,41 @@
import type { responseInterface } from 'swr'
import Cookies from 'js-cookie'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
import { useCommerce } from '..'
import { useHook, useSWRHook } from '../utils/use-hook'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Cart } from '../types'
import { Provider, useCommerce } from '..'
export type CartResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
export type FetchCartInput = {
cartId?: Cart['id']
}
export type CartInput = {
cartId: string | undefined
export type UseCart<
H extends SWRHook<any, any, any> = SWRHook<
Cart | null,
{},
FetchCartInput,
{ isEmpty?: boolean }
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options,
input: { cartId },
fetch,
}) => {
return cartId ? await fetch({ ...options }) : null
}
export default function useCart<Result>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<Result, CartInput>,
swrOptions?: SwrOptions<Result, CartInput>
) {
const fn = (provider: Provider) => provider.cart?.useCart!
const useCart: UseCart = (input) => {
const hook = useHook(fn)
const { cartCookie } = useCommerce()
const fetcher: typeof fetcherFn = (options, input, fetch) => {
input.cartId = Cookies.get(cartCookie)
return fetcherFn(options, input, fetch)
const fetcherFn = hook.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context)
}
const response = useData(options, input, fetcher, swrOptions)
return Object.assign(response, { isEmpty: true }) as CartResponse<Result>
return useSWRHook({ ...hook, fetcher: wrapper })(input)
}
export default useCart

View File

@@ -1,5 +1,35 @@
import useAction from '../utils/use-action'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, LineItem, RemoveCartItemBody } from '../types'
import type { Provider } from '..'
const useRemoveItem = useAction
/**
* Input expected by the action returned by the `useRemoveItem` hook
*/
export type RemoveItemInput = {
id: string
}
export type UseRemoveItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart | null,
{ item?: LineItem },
RemoveItemInput,
RemoveCartItemBody
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart | null,
RemoveCartItemBody
> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useRemoveItem

View File

@@ -1,5 +1,38 @@
import useAction from '../utils/use-action'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
import type { Provider } from '..'
const useUpdateItem = useAction
/**
* Input expected by the action returned by the `useUpdateItem` hook
*/
export type UpdateItemInput<T extends CartItemBody> = T & {
id: string
}
export type UseUpdateItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart | null,
{
item?: LineItem
wait?: number
},
UpdateItemInput<CartItemBody>,
UpdateCartItemBody<CartItemBody>
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart | null,
UpdateCartItemBody<CartItemBody>
> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useUpdateItem!
const useUpdateItem: UseUpdateItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useUpdateItem

View File

@@ -0,0 +1,20 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Customer } from '../types'
import { Provider } from '..'
export type UseCustomer<
H extends SWRHook<any, any, any> = SWRHook<Customer | null>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<Customer | null, any> = SWRFetcher
const fn = (provider: Provider) => provider.customer?.useCustomer!
const useCustomer: UseCustomer = (input) => {
const hook = useHook(fn)
return useSWRHook({ fetcher, ...hook })(input)
}
export default useCustomer

View File

@@ -6,37 +6,73 @@ import {
useMemo,
useRef,
} from 'react'
import * as React from 'react'
import { Fetcher } from './utils/types'
import { Fetcher, SWRHook, MutationHook } from './utils/types'
import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
const Commerce = createContext<CommerceContextValue | {}>({})
const Commerce = createContext<CommerceContextValue<any> | {}>({})
export type CommerceProps = {
export type Provider = CommerceConfig & {
fetcher: Fetcher
cart?: {
useCart?: SWRHook<Cart | null, any, FetchCartInput>
useAddItem?: MutationHook<any, any, any>
useUpdateItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
}
wishlist?: {
useWishlist?: SWRHook<Wishlist | null, any, any>
useAddItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
}
customer?: {
useCustomer?: SWRHook<Customer | null, any, any>
}
products?: {
useSearch?: SWRHook<SearchProductsData, any, any>
}
auth?: {
useSignup?: MutationHook<any, any, any>
useLogin?: MutationHook<any, any, any>
useLogout?: MutationHook<any, any, any>
}
}
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 +83,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>
}

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { useCommerce } from '.'
import { useCommerce } from '..'
export function formatPrice({
amount,

View File

@@ -0,0 +1,20 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { SearchProductsData } from '../types'
import { Provider } from '..'
export type UseSearch<
H extends SWRHook<any, any, any> = SWRHook<SearchProductsData>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<SearchProductsData, any> = SWRFetcher
const fn = (provider: Provider) => provider.products?.useSearch!
const useSearch: UseSearch = (input) => {
const hook = useHook(fn)
return useSWRHook({ fetcher, ...hook })(input)
}
export default useSearch

View File

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

203
framework/commerce/types.ts Normal file
View File

@@ -0,0 +1,203 @@
import type { Wishlist as BCWishlist } from '../bigcommerce/api/wishlist'
import type { Customer as BCCustomer } from '../bigcommerce/api/customers'
import type { SearchProductsData as BCSearchProductsData } from '../bigcommerce/api/catalog/products'
export type Discount = {
// The value of the discount, can be an amount or percentage
value: number
}
export type LineItem = {
id: string
variantId: string
productId: string
name: string
quantity: number
discounts: Discount[]
// A human-friendly unique string automatically generated from the products name
path: string
variant: ProductVariant
}
export type Measurement = {
value: number
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
}
export type Image = {
url: string
altText?: string
width?: number
height?: number
}
export type ProductVariant = {
id: string
// The SKU (stock keeping unit) associated with the product variant.
sku: string
// The product variants title, or the product's name.
name: string
// Whether a customer needs to provide a shipping address when placing
// an order for the product variant.
requiresShipping: boolean
// The product variants price after all discounts are applied.
price: number
// Product variants price, as quoted by the manufacturer/distributor.
listPrice: number
// Image associated with the product variant. Falls back to the product image
// if no image is available.
image?: Image
// Indicates whether this product variant is in stock.
isInStock?: boolean
// Indicates if the product variant is available for sale.
availableForSale?: boolean
// The variant's weight. If a weight was not explicitly specified on the
// variant this will be the product's weight.
weight?: Measurement
// The variant's height. If a height was not explicitly specified on the
// variant, this will be the product's height.
height?: Measurement
// The variant's width. If a width was not explicitly specified on the
// variant, this will be the product's width.
width?: Measurement
// The variant's depth. If a depth was not explicitly specified on the
// variant, this will be the product's depth.
depth?: Measurement
}
// Shopping cart, a.k.a Checkout
export type Cart = {
id: string
// ID of the customer to which the cart belongs.
customerId?: string
// The email assigned to this cart
email?: string
// The date and time when the cart was created.
createdAt: string
// The currency used for this cart
currency: { code: string }
// Specifies if taxes are included in the line items.
taxesIncluded: boolean
lineItems: LineItem[]
// The sum of all the prices of all the items in the cart.
// Duties, taxes, shipping and discounts excluded.
lineItemsSubtotalPrice: number
// Price of the cart before duties, shipping and taxes.
subtotalPrice: number
// The sum of all the prices of all the items in the cart.
// Duties, taxes and discounts included.
totalPrice: number
// Discounts that have been applied on the cart.
discounts?: Discount[]
}
// TODO: Properly define this type
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
*/
// Base cart item body used for cart mutations
export type CartItemBody = {
variantId: string
productId?: string
quantity?: number
}
// Body used by the `getCart` operation handler
export type GetCartHandlerBody = {
cartId?: string
}
// Body used by the add item to cart operation
export type AddCartItemBody<T extends CartItemBody> = {
item: T
}
// Body expected by the add item to cart operation handler
export type AddCartItemHandlerBody<T extends CartItemBody> = Partial<
AddCartItemBody<T>
> & {
cartId?: string
}
// Body used by the update cart item operation
export type UpdateCartItemBody<T extends CartItemBody> = {
itemId: string
item: T
}
// Body expected by the update cart item operation handler
export type UpdateCartItemHandlerBody<T extends CartItemBody> = Partial<
UpdateCartItemBody<T>
> & {
cartId?: string
}
// Body used by the remove cart item operation
export type RemoveCartItemBody = {
itemId: string
}
// Body expected by the remove cart item operation handler
export type RemoveCartItemHandlerBody = Partial<RemoveCartItemBody> & {
cartId?: string
}
/**
* Temporal types
*/
interface Entity {
id: string | number
[prop: string]: any
}
export interface Product extends Entity {
name: string
description: string
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant2[]
price: ProductPrice
options: ProductOption[]
sku?: string
}
interface ProductOption extends Entity {
displayName: string
values: ProductOptionValues[]
}
interface ProductOptionValues {
label: string
hexColors?: string[]
}
interface ProductImage {
url: string
alt?: string
}
interface ProductVariant2 {
id: string | number
options: ProductOption[]
}
interface ProductPrice {
value: number
currencyCode: 'USD' | 'ARS' | string | undefined
retailPrice?: number
salePrice?: number
listPrice?: number
extendedSalePrice?: number
extendedListPrice?: number
}

View File

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

View File

@@ -1,5 +0,0 @@
import useAction from './utils/use-action'
const useLogin = useAction
export default useLogin

View File

@@ -1,5 +0,0 @@
import useAction from './utils/use-action'
const useLogout = useAction
export default useLogout

View File

@@ -1,5 +0,0 @@
import useAction from './utils/use-action'
const useSignup = useAction
export default useSignup

View File

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

View File

@@ -0,0 +1,37 @@
// Taken from https://fettblog.eu/typescript-assertion-signatures/
type InferValue<Prop extends PropertyKey, Desc> = Desc extends {
get(): any
value: any
}
? never
: Desc extends { value: infer T }
? Record<Prop, T>
: Desc extends { get(): infer T }
? Record<Prop, T>
: never
type DefineProperty<
Prop extends PropertyKey,
Desc extends PropertyDescriptor
> = Desc extends { writable: any; set(val: any): any }
? never
: Desc extends { writable: any; get(): any }
? never
: Desc extends { writable: false }
? Readonly<InferValue<Prop, Desc>>
: Desc extends { writable: true }
? InferValue<Prop, Desc>
: Readonly<InferValue<Prop, Desc>>
export default function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor
>(
obj: Obj,
prop: Key,
val: PDesc
): asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.defineProperty(obj, prop, val)
}

View File

@@ -26,6 +26,14 @@ export class CommerceError extends Error {
}
}
// Used for errors that come from a bad implementation of the hooks
export class ValidationError extends CommerceError {
constructor(options: ErrorProps) {
super(options)
this.code = 'validation_error'
}
}
export class FetcherError extends CommerceError {
status: number

View File

@@ -1,24 +1,131 @@
// Core fetcher added by CommerceProvider
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
import type { ConfigInterface } from 'swr'
import type { CommerceError } from './errors'
import type { ResponseState } from './use-data'
export type FetcherOptions = {
/**
* Returns the properties in T with the properties in type K, overriding properties defined in T
*/
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> &
{
[P in K]-?: NonNullable<T[P]>
}
/**
* Core fetcher added by CommerceProvider
*/
export type Fetcher<T = any, B = any> = (
options: FetcherOptions<B>
) => T | Promise<T>
export type FetcherOptions<Body = any> = {
url?: string
query?: string
method?: string
variables?: any
body?: any
body?: Body
}
export type HookFetcher<Result, Input = null> = (
export type HookFetcher<Data, Input = null, Result = any> = (
options: HookFetcherOptions | null,
input: Input,
fetch: <T = Result>(options: FetcherOptions) => Promise<T>
) => Result | Promise<Result>
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
) => Data | Promise<Data>
export type HookFetcherOptions = {
query?: string
url?: string
method?: string
export type HookFetcherFn<Data, Input = undefined, Result = any, Body = any> = (
context: HookFetcherContext<Input, Result, Body>
) => Data | Promise<Data>
export type HookFetcherContext<Input = undefined, Result = any, Body = any> = {
options: HookFetcherOptions
input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
}
export type HookInput = [string, string | number | boolean | undefined][]
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 HookFunction<
Input extends { [k: string]: unknown } | null,
T
> = keyof Input extends never
? () => T
: Partial<Input> extends Input
? (input?: Input) => T
: (input: Input) => T
export type SWRHook<
// 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 = {},
// Custom state added to the response object of SWR
State = {}
> = {
useHook(
context: SWRHookContext<Data, FetchInput>
): HookFunction<
Input & { swrOptions?: SwrOptions<Data, FetchInput> },
ResponseState<Data> & State
>
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput>
}
export type SWRHookContext<
Data,
FetchInput extends { [k: string]: unknown } = {}
> = {
useData(context?: {
input?: HookFetchInput | HookSWRInput
swrOptions?: SwrOptions<Data, FetchInput>
}): ResponseState<Data>
}
export type MutationHook<
// Data obj returned by the hook and fetch operation
Data,
// Input expected by the hook
Input extends { [k: string]: unknown } = {},
// Input expected by the action returned by the hook
ActionInput extends { [k: string]: unknown } = {},
// Input expected before doing a fetch operation
FetchInput extends { [k: string]: unknown } = ActionInput
> = {
useHook(
context: MutationHookContext<Data, FetchInput>
): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput>
}
export type MutationHookContext<
Data,
FetchInput extends { [k: string]: unknown } | null = {}
> = {
fetch: keyof FetchInput extends never
? () => Data | Promise<Data>
: Partial<FetchInput> extends FetchInput
? (context?: { input?: FetchInput }) => Data | Promise<Data>
: (context: { input: FetchInput }) => Data | Promise<Data>
}
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
Data,
CommerceError,
HookFetcher<Data, Input, Result>
>

View File

@@ -1,15 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher, HookFetcherOptions } from './types'
import { useCommerce } from '..'
export default function useAction<T, Input = null>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const { fetcherRef } = useCommerce()
return useCallback(
(input: Input) => fetcher(options, input, fetcherRef.current),
[fetcher]
)
}

View File

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

View File

@@ -0,0 +1,50 @@
import { useCallback } from 'react'
import { Provider, useCommerce } from '..'
import type { MutationHook, PickRequired, SWRHook } from './types'
import useData from './use-data'
export function useFetcher() {
const { providerRef, fetcherRef } = useCommerce()
return providerRef.current.fetcher ?? fetcherRef.current
}
export function useHook<
P extends Provider,
H extends MutationHook<any, any, any> | SWRHook<any, any, any>
>(fn: (provider: P) => H) {
const { providerRef } = useCommerce<P>()
const provider = providerRef.current
return fn(provider)
}
export function useSWRHook<H extends SWRHook<any, any, any>>(
hook: PickRequired<H, 'fetcher'>
) {
const fetcher = useFetcher()
return hook.useHook({
useData(ctx) {
const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions)
return response
},
})
}
export function useMutationHook<H extends MutationHook<any, any, any>>(
hook: PickRequired<H, 'fetcher'>
) {
const fetcher = useFetcher()
return hook.useHook({
fetch: useCallback(
({ input } = {}) => {
return hook.fetcher({
input,
options: hook.fetchOptions,
fetch: fetcher,
})
},
[fetcher, hook.fetchOptions]
),
})
}

View File

@@ -0,0 +1,3 @@
export { default as useAddItem } from './use-add-item'
export { default as useWishlist } from './use-wishlist'
export { default as useRemoveItem } from './use-remove-item'

View File

@@ -1,5 +1,19 @@
import useAction from '../utils/use-action'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook } from '../utils/types'
import type { Provider } from '..'
const useAddItem = useAction
export type UseAddItem<
H extends MutationHook<any, any, any> = MutationHook<any, {}, {}>
> = ReturnType<H['useHook']>
export const fetcher = mutationFetcher
const fn = (provider: Provider) => provider.wishlist?.useAddItem!
const useAddItem: UseAddItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useAddItem

View File

@@ -1,5 +1,28 @@
import useAction from '../utils/use-action'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Provider } from '..'
const useRemoveItem = useAction
export type RemoveItemInput = {
id: string | number
}
export type UseRemoveItem<
H extends MutationHook<any, any, any> = MutationHook<
any | null,
{ wishlist?: any },
RemoveItemInput,
{}
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<any | null, {}> = mutationFetcher
const fn = (provider: Provider) => provider.wishlist?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useRemoveItem

View File

@@ -1,17 +1,25 @@
import type { responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Wishlist } from '../types'
import type { Provider } from '..'
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
export type UseWishlist<
H extends SWRHook<any, any, any> = SWRHook<
Wishlist | null,
{ includeProducts?: boolean },
{ customerId?: number; includeProducts: boolean },
{ isEmpty?: boolean }
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<Wishlist | null, any> = SWRFetcher
const fn = (provider: Provider) => provider.wishlist?.useWishlist!
const useWishlist: UseWishlist = (input) => {
const hook = useHook(fn)
return useSWRHook({ fetcher, ...hook })(input)
}
export default function useWishlist<Result, Input = null>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<Result, Input>,
swrOptions?: SwrOptions<Result, Input>
) {
const response = useData(options, input, fetcherFn, swrOptions)
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
}
export default useWishlist

View File

@@ -0,0 +1,41 @@
/**
* This file is expected to be used in next.config.js only
*/
const merge = require('deepmerge')
const PROVIDERS = ['bigcommerce', 'shopify']
function getProviderName() {
// TODO: OSOT.
return process.env.BIGCOMMERCE_STOREFRONT_API_URL ? 'bigcommerce' : null
}
module.exports = (nextConfig = {}) => {
const commerce = nextConfig.commerce || {}
const name = commerce.provider || getProviderName()
if (!name) {
throw new Error(
`The commerce provider is missing, please add a valid provider name or its environment variables`
)
}
if (!PROVIDERS.includes(name)) {
throw new Error(
`The commerce provider "${name}" can't be found, please use one of "${PROVIDERS.join(
', '
)}"`
)
}
const commerceNextConfig = require(`../${name}/next.config`)
const config = merge(commerceNextConfig, nextConfig)
config.env = config.env || {}
Object.entries(config.commerce.features).forEach(([k, v]) => {
if (v) config.env[`COMMERCE_${k.toUpperCase()}_ENABLED`] = true
})
return config
}