mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 22:42:33 +00:00
Merge branch 'agnostic' of github.com:vercel/commerce into agnostic
This commit is contained in:
commit
b037cc0596
@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import s from './CartItem.module.css'
|
||||
import { Trash, Plus, Minus } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import type { LineItem } from '@framework/types'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useUpdateItem from '@framework/cart/use-update-item'
|
||||
import useRemoveItem from '@framework/cart/use-remove-item'
|
||||
|
@ -14,6 +14,9 @@ const updateItem: CartHandlers['updateItem'] = async ({
|
||||
})
|
||||
}
|
||||
|
||||
console.log('ITEM', item)
|
||||
console.log('AFTER', parseCartItem(item))
|
||||
|
||||
const { data } = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}/items/${itemId}`,
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ import getCart from './handlers/get-cart'
|
||||
import addItem from './handlers/add-item'
|
||||
import updateItem from './handlers/update-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
import type { Cart, UpdateCartItemHandlerBody } from '../../types'
|
||||
|
||||
type OptionSelections = {
|
||||
option_id: Number
|
||||
@ -23,40 +24,12 @@ export type ItemBody = {
|
||||
|
||||
export type AddItemBody = { item: ItemBody }
|
||||
|
||||
export type UpdateItemBody = { itemId: string; item: ItemBody }
|
||||
|
||||
export type RemoveItemBody = { itemId: string }
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type Cart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
created_time: string
|
||||
discounts?: { id: number; discounted_amount: number }[]
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
export type CartHandlers = {
|
||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
||||
updateItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<UpdateItemBody>
|
||||
>
|
||||
updateItem: BigcommerceHandler<Cart, UpdateCartItemHandlerBody>
|
||||
removeItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<RemoveItemBody>
|
||||
|
@ -1,14 +1,21 @@
|
||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||
import type { ItemBody } from '../cart'
|
||||
import type { CartItemBody, OptionSelections } from '../../types'
|
||||
|
||||
type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
})
|
||||
|
||||
export const parseCartItem = (item: ItemBody) => ({
|
||||
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
||||
quantity: item.quantity,
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
option_selections: item.optionSelections,
|
||||
})
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
ItemBody,
|
||||
UpdateItemBody,
|
||||
Cart as BigcommerceCart,
|
||||
} from '../api/cart'
|
||||
UpdateCartItemBody,
|
||||
UpdateCartItemInput,
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
LineItem,
|
||||
} from '../types'
|
||||
import { fetcher as removeFetcher } from './use-remove-item'
|
||||
import useCart from './use-cart'
|
||||
|
||||
@ -17,9 +19,7 @@ const defaultOpts = {
|
||||
method: 'PUT',
|
||||
}
|
||||
|
||||
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = async (
|
||||
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
|
||||
options,
|
||||
{ itemId, item },
|
||||
fetch
|
||||
@ -30,12 +30,12 @@ export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = async (
|
||||
return removeFetcher(null, { itemId }, fetch)
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new CommerceError({
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch<BigcommerceCart>({
|
||||
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
@ -45,26 +45,41 @@ export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = async (
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = (item?: any) => {
|
||||
const useUpdateItem = <T extends LineItem | undefined = undefined>(
|
||||
item?: T
|
||||
) => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
|
||||
const fn = useCartUpdateItem<Cart | null, UpdateCartItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemInput) => {
|
||||
const data = await fn({
|
||||
itemId: input.id ?? item?.id,
|
||||
item: {
|
||||
productId: input.productId ?? item?.product_id,
|
||||
variantId: input.productId ?? item?.variant_id,
|
||||
quantity: input.quantity,
|
||||
},
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}, cfg?.wait ?? 500),
|
||||
debounce(
|
||||
async (
|
||||
input: T extends LineItem
|
||||
? Partial<UpdateCartItemInput>
|
||||
: UpdateCartItemInput
|
||||
) => {
|
||||
const itemId = input.id ?? item?.id
|
||||
const productId = input.productId ?? item?.productId
|
||||
const variantId = input.productId ?? item?.variantId
|
||||
|
||||
if (!itemId || !productId || !variantId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({
|
||||
itemId,
|
||||
item: { productId, variantId, quantity: input.quantity },
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
cfg?.wait ?? 500
|
||||
),
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Cart as BigcommerceCart } from '../api/cart'
|
||||
import type { Cart, BigcommerceCart, LineItem } from '../types'
|
||||
import update from './immutability'
|
||||
|
||||
function normalizeProductOption(productOption: any) {
|
||||
@ -90,6 +90,7 @@ function normalizeLineItem(item: any): LineItem {
|
||||
return {
|
||||
id: item.id,
|
||||
variantId: String(item.variant_id),
|
||||
productId: String(item.product_id),
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
variant: {
|
||||
|
5
framework/bigcommerce/types.d.ts
vendored
5
framework/bigcommerce/types.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
interface Cart extends BaseCart {
|
||||
lineItems: LineItem[]
|
||||
}
|
||||
|
||||
interface LineItem extends BaseLineItem {}
|
54
framework/bigcommerce/types.ts
Normal file
54
framework/bigcommerce/types.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import * as Core from '@commerce/types'
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type BigcommerceCart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
created_time: string
|
||||
discounts?: { id: number; discounted_amount: number }[]
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
export interface Cart extends Core.Cart {
|
||||
lineItems: LineItem[]
|
||||
}
|
||||
|
||||
export interface LineItem extends Core.LineItem {}
|
||||
|
||||
/**
|
||||
* Cart mutations
|
||||
*/
|
||||
|
||||
export type OptionSelections = {
|
||||
option_id: number
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export interface CartItemBody extends Core.CartItemBody {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export interface UpdateCartItemBody extends Core.UpdateCartItemBody {
|
||||
item: CartItemBody
|
||||
}
|
||||
|
||||
export interface UpdateCartItemInput
|
||||
extends Core.UpdateCartItemInput<CartItemBody> {}
|
||||
|
||||
export interface UpdateCartItemHandlerBody
|
||||
extends Core.UpdateCartItemHandlerBody {}
|
@ -1,15 +1,16 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useData, { ResponseState, SwrOptions } from '../utils/use-data'
|
||||
import type { Cart } from '../types'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
|
||||
|
||||
export type CartInput = {
|
||||
cartId?: BaseCart['id']
|
||||
cartId?: Cart['id']
|
||||
}
|
||||
|
||||
export default function useCart<Data extends BaseCart | null>(
|
||||
export default function useCart<Data extends Cart | null>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Data, CartInput>,
|
||||
|
99
framework/commerce/types.d.ts
vendored
99
framework/commerce/types.d.ts
vendored
@ -45,105 +45,6 @@ interface ProductPrice {
|
||||
extendedListPrice?: number
|
||||
}
|
||||
|
||||
interface DiscountBase {
|
||||
// The value of the discount, can be an amount or percentage
|
||||
value: number
|
||||
}
|
||||
|
||||
interface BaseLineItem {
|
||||
id: string
|
||||
variantId: string
|
||||
name: string
|
||||
quantity: number
|
||||
discounts: DiscountBase[]
|
||||
// A human-friendly unique string automatically generated from the product’s name
|
||||
path: string
|
||||
variant: BaseProductVariant
|
||||
}
|
||||
|
||||
interface Measurement {
|
||||
value: number
|
||||
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
|
||||
}
|
||||
|
||||
interface Image {
|
||||
url: string
|
||||
altText?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
interface BaseProductVariant {
|
||||
id: string
|
||||
// The SKU (stock keeping unit) associated with the product variant.
|
||||
sku: string
|
||||
// The product variant’s 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 variant’s price after all discounts are applied.
|
||||
price: number
|
||||
// Product variant’s 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
|
||||
interface BaseCart {
|
||||
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: BaseLineItem[]
|
||||
// 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?: DiscountBase[]
|
||||
}
|
||||
|
||||
// TODO: Remove this type in favor of BaseCart
|
||||
interface Cart2 extends Entity {
|
||||
id: string | undefined
|
||||
currency: { code: string }
|
||||
taxIncluded?: boolean
|
||||
items: Pick<Product, 'id' | 'name' | 'prices'> & CartItem[]
|
||||
subTotal: number | string
|
||||
total: number | string
|
||||
customerId: Customer['id']
|
||||
}
|
||||
|
||||
interface CartItem extends Entity {
|
||||
quantity: number
|
||||
productId: Product['id']
|
||||
|
111
framework/commerce/types.ts
Normal file
111
framework/commerce/types.ts
Normal file
@ -0,0 +1,111 @@
|
||||
export interface Discount {
|
||||
// The value of the discount, can be an amount or percentage
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface LineItem {
|
||||
id: string
|
||||
variantId: string
|
||||
productId: string
|
||||
name: string
|
||||
quantity: number
|
||||
discounts: Discount[]
|
||||
// A human-friendly unique string automatically generated from the product’s name
|
||||
path: string
|
||||
variant: ProductVariant
|
||||
}
|
||||
|
||||
export interface Measurement {
|
||||
value: number
|
||||
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
url: string
|
||||
altText?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface ProductVariant {
|
||||
id: string
|
||||
// The SKU (stock keeping unit) associated with the product variant.
|
||||
sku: string
|
||||
// The product variant’s 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 variant’s price after all discounts are applied.
|
||||
price: number
|
||||
// Product variant’s 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 interface 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[]
|
||||
}
|
||||
|
||||
// Base cart item body used for cart mutations
|
||||
export interface CartItemBody {
|
||||
variantId: string
|
||||
productId?: string
|
||||
quantity?: number
|
||||
}
|
||||
|
||||
// Body by the update operation
|
||||
export interface UpdateCartItemBody {
|
||||
itemId: string
|
||||
item: CartItemBody
|
||||
}
|
||||
|
||||
// Input expected by the `useUpdateItem` hook
|
||||
export type UpdateCartItemInput<T extends CartItemBody> = T & {
|
||||
id: string
|
||||
}
|
||||
|
||||
// Body expected by the update operation handler
|
||||
export interface UpdateCartItemHandlerBody extends Partial<UpdateCartItemBody> {
|
||||
cartId?: string
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
// Core fetcher added by CommerceProvider
|
||||
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
|
||||
|
||||
export type FetcherOptions = {
|
||||
export type FetcherOptions<Body = any> = {
|
||||
url?: string
|
||||
query?: string
|
||||
method?: string
|
||||
variables?: any
|
||||
body?: any
|
||||
body?: Body
|
||||
}
|
||||
|
||||
export type HookFetcher<Data, Input = null, Result = any> = (
|
||||
options: HookFetcherOptions | null,
|
||||
input: Input,
|
||||
fetch: <T = Result>(options: FetcherOptions) => Promise<T>
|
||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||
) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherOptions = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user