4
0
forked from crowetic/commerce

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

This commit is contained in:
okbel 2021-02-02 11:40:29 -03:00
commit b037cc0596
13 changed files with 237 additions and 167 deletions

View File

@ -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'

View File

@ -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}`,
{

View File

@ -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>

View File

@ -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,
})

View File

@ -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]
)
}

View File

@ -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: {

View File

@ -1,5 +0,0 @@
interface Cart extends BaseCart {
lineItems: LineItem[]
}
interface LineItem extends BaseLineItem {}

View 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 {}

View File

@ -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>,

View File

@ -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 products 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 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
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
View 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 products 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 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 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
}

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,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 = {