mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
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 commit23c8ed7c2d
, reversing changes made tobf50965a39
. * change readme * Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic" This reverts commitbf50965a39
, reversing changes made to0dad4ddedb
. * Revert "Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic"" This reverts commitc9a43f1bce
. * 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 commit23c8ed7c2d
, reversing changes made tobf50965a39
. * change readme * Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic" This reverts commitbf50965a39
, reversing changes made to0dad4ddedb
. * Revert "Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic"" This reverts commitc9a43f1bce
. * 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:
6
framework/bigcommerce/.env.template
Normal file
6
framework/bigcommerce/.env.template
Normal file
@@ -0,0 +1,6 @@
|
||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_URL=
|
||||
BIGCOMMERCE_STORE_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
||||
BIGCOMMERCE_CHANNEL_ID=
|
@@ -1,27 +1,25 @@
|
||||
# Table of Contents
|
||||
|
||||
Table of Contents
|
||||
=================
|
||||
|
||||
* [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks)
|
||||
* [Installation](#installation)
|
||||
* [General Usage](#general-usage)
|
||||
* [CommerceProvider](#commerceprovider)
|
||||
* [useLogin hook](#uselogin-hook)
|
||||
* [useLogout](#uselogout)
|
||||
* [useCustomer](#usecustomer)
|
||||
* [useSignup](#usesignup)
|
||||
* [usePrice](#useprice)
|
||||
* [Cart Hooks](#cart-hooks)
|
||||
* [useCart](#usecart)
|
||||
* [useAddItem](#useadditem)
|
||||
* [useUpdateItem](#useupdateitem)
|
||||
* [useRemoveItem](#useremoveitem)
|
||||
* [Wishlist Hooks](#wishlist-hooks)
|
||||
* [Product Hooks and API](#product-hooks-and-api)
|
||||
* [useSearch](#usesearch)
|
||||
* [getAllProducts](#getallproducts)
|
||||
* [getProduct](#getproduct)
|
||||
* [More](#more)
|
||||
- [BigCommerce Storefront Data Hooks](#bigcommerce-storefront-data-hooks)
|
||||
- [Installation](#installation)
|
||||
- [General Usage](#general-usage)
|
||||
- [CommerceProvider](#commerceprovider)
|
||||
- [useLogin hook](#uselogin-hook)
|
||||
- [useLogout](#uselogout)
|
||||
- [useCustomer](#usecustomer)
|
||||
- [useSignup](#usesignup)
|
||||
- [usePrice](#useprice)
|
||||
- [Cart Hooks](#cart-hooks)
|
||||
- [useCart](#usecart)
|
||||
- [useAddItem](#useadditem)
|
||||
- [useUpdateItem](#useupdateitem)
|
||||
- [useRemoveItem](#useremoveitem)
|
||||
- [Wishlist Hooks](#wishlist-hooks)
|
||||
- [Product Hooks and API](#product-hooks-and-api)
|
||||
- [useSearch](#usesearch)
|
||||
- [getAllProducts](#getallproducts)
|
||||
- [getProduct](#getproduct)
|
||||
- [More](#more)
|
||||
|
||||
# BigCommerce Storefront Data Hooks
|
||||
|
||||
@@ -49,6 +47,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_URL=<>
|
||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
||||
BIGCOMMERCE_CHANNEL_ID=<>
|
||||
```
|
||||
|
||||
## General Usage
|
||||
@@ -193,13 +192,11 @@ Returns the current cart data for use
|
||||
...
|
||||
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||
|
||||
const countItem = (count: number, item: any) => count + item.quantity
|
||||
const countItems = (count: number, items: any[]) =>
|
||||
items.reduce(countItem, count)
|
||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||
|
||||
const CartNumber = () => {
|
||||
const { data } = useCart()
|
||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
||||
|
||||
return itemsCount > 0 ? <span>{itemsCount}</span> : null
|
||||
}
|
||||
@@ -235,7 +232,7 @@ import useUpdateItem from '@bigcommerce/storefront-data-hooks/cart/use-update-it
|
||||
const CartItem = ({ item }) => {
|
||||
const [quantity, setQuantity] = useState(item.quantity)
|
||||
const updateItem = useUpdateItem(item)
|
||||
|
||||
|
||||
const updateQuantity = async (e) => {
|
||||
const val = e.target.value
|
||||
await updateItem({ quantity: val })
|
||||
@@ -264,7 +261,7 @@ import useRemoveItem from '@bigcommerce/storefront-data-hooks/cart/use-remove-it
|
||||
|
||||
const RemoveButton = ({ item }) => {
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
|
||||
const handleRemove = async () => {
|
||||
await removeItem({ id: item.id })
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ import { parseCartItem } from '../../utils/parse-item'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
|
||||
// Return current cart info
|
||||
const addItem: CartHandlers['addItem'] = async ({
|
||||
res,
|
||||
body: { cartId, item },
|
||||
@@ -26,8 +25,14 @@ const addItem: CartHandlers['addItem'] = async ({
|
||||
}),
|
||||
}
|
||||
const { data } = cartId
|
||||
? await config.storeApiFetch(`/v3/carts/${cartId}/items?include=line_items.physical_items.options`, options)
|
||||
: await config.storeApiFetch('/v3/carts?include=line_items.physical_items.options', options)
|
||||
? await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}/items?include=line_items.physical_items.options`,
|
||||
options
|
||||
)
|
||||
: await config.storeApiFetch(
|
||||
'/v3/carts?include=line_items.physical_items.options',
|
||||
options
|
||||
)
|
||||
|
||||
// Create or update the cart cookie
|
||||
res.setHeader(
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import type { BigcommerceCart } from '../../../types'
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { Cart, CartHandlers } from '..'
|
||||
import type { CartHandlers } from '../'
|
||||
|
||||
// Return current cart info
|
||||
const getCart: CartHandlers['getCart'] = async ({
|
||||
@@ -8,11 +9,13 @@ const getCart: CartHandlers['getCart'] = async ({
|
||||
body: { cartId },
|
||||
config,
|
||||
}) => {
|
||||
let result: { data?: Cart } = {}
|
||||
let result: { data?: BigcommerceCart } = {}
|
||||
|
||||
if (cartId) {
|
||||
try {
|
||||
result = await config.storeApiFetch(`/v3/carts/${cartId}?include=line_items.physical_items.options`)
|
||||
result = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}?include=line_items.physical_items.options`
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||
// Remove the cookie if it exists but the cart wasn't found
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
|
||||
// Return current cart info
|
||||
const removeItem: CartHandlers['removeItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId },
|
||||
|
@@ -2,7 +2,6 @@ import { parseCartItem } from '../../utils/parse-item'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartHandlers } from '..'
|
||||
|
||||
// Return current cart info
|
||||
const updateItem: CartHandlers['updateItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId, item },
|
||||
|
@@ -8,63 +8,25 @@ import getCart from './handlers/get-cart'
|
||||
import addItem from './handlers/add-item'
|
||||
import updateItem from './handlers/update-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
|
||||
type OptionSelections = {
|
||||
option_id: Number
|
||||
option_value: Number|String
|
||||
}
|
||||
|
||||
export type ItemBody = {
|
||||
productId: number
|
||||
variantId: number
|
||||
quantity?: number
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
// TODO: add missing fields
|
||||
}
|
||||
import type {
|
||||
BigcommerceCart,
|
||||
GetCartHandlerBody,
|
||||
AddCartItemHandlerBody,
|
||||
UpdateCartItemHandlerBody,
|
||||
RemoveCartItemHandlerBody,
|
||||
} from '../../types'
|
||||
|
||||
export type CartHandlers = {
|
||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
||||
updateItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<UpdateItemBody>
|
||||
>
|
||||
removeItem: BigcommerceHandler<
|
||||
Cart,
|
||||
{ cartId?: string } & Partial<RemoveItemBody>
|
||||
>
|
||||
getCart: BigcommerceHandler<BigcommerceCart, GetCartHandlerBody>
|
||||
addItem: BigcommerceHandler<BigcommerceCart, AddCartItemHandlerBody>
|
||||
updateItem: BigcommerceHandler<BigcommerceCart, UpdateCartItemHandlerBody>
|
||||
removeItem: BigcommerceHandler<BigcommerceCart, RemoveCartItemHandlerBody>
|
||||
}
|
||||
|
||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
|
||||
// TODO: a complete implementation should have schema validation for `req.body`
|
||||
const cartApi: BigcommerceApiHandler<Cart, CartHandlers> = async (
|
||||
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import getAllProducts, { ProductEdge } from '../../operations/get-all-products'
|
||||
import { Product } from '@commerce/types'
|
||||
import getAllProducts, { ProductEdge } from '../../../product/get-all-products'
|
||||
import type { ProductsHandlers } from '../products'
|
||||
|
||||
const SORT: { [key: string]: string | undefined } = {
|
||||
@@ -6,6 +7,7 @@ const SORT: { [key: string]: string | undefined } = {
|
||||
trending: 'total_sold',
|
||||
price: 'price',
|
||||
}
|
||||
|
||||
const LIMIT = 12
|
||||
|
||||
// Return current cart info
|
||||
@@ -44,21 +46,25 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
||||
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
||||
url.pathname + url.search
|
||||
)
|
||||
|
||||
const entityIds = data.map((p) => p.id)
|
||||
const found = entityIds.length > 0
|
||||
|
||||
// We want the GraphQL version of each product
|
||||
const graphqlData = await getAllProducts({
|
||||
variables: { first: LIMIT, entityIds },
|
||||
config,
|
||||
})
|
||||
|
||||
// Put the products in an object that we can use to get them by id
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: ProductEdge
|
||||
[k: number]: Product
|
||||
}>((prods, p) => {
|
||||
prods[p.node.entityId] = p
|
||||
prods[Number(p.id)] = p
|
||||
return prods
|
||||
}, {})
|
||||
const products: ProductEdge[] = found ? [] : graphqlData.products
|
||||
|
||||
const products: Product[] = found ? [] : graphqlData.products
|
||||
|
||||
// Populate the products array with the graphql products, in the order
|
||||
// assigned by the list of entity ids
|
||||
|
@@ -1,27 +1,27 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
import isAllowedMethod from '../utils/is-allowed-method'
|
||||
import createApiHandler, {
|
||||
BigcommerceApiHandler,
|
||||
BigcommerceHandler,
|
||||
} from '../utils/create-api-handler'
|
||||
import { BigcommerceApiError } from '../utils/errors'
|
||||
import type { ProductEdge } from '../operations/get-all-products'
|
||||
import getProducts from './handlers/get-products'
|
||||
|
||||
export type SearchProductsData = {
|
||||
products: ProductEdge[]
|
||||
products: Product[]
|
||||
found: boolean
|
||||
}
|
||||
|
||||
export type ProductsHandlers = {
|
||||
getProducts: BigcommerceHandler<
|
||||
SearchProductsData,
|
||||
{ search?: 'string'; category?: string; brand?: string; sort?: string }
|
||||
{ search?: string; category?: string; brand?: string; sort?: string }
|
||||
>
|
||||
}
|
||||
|
||||
const METHODS = ['GET']
|
||||
|
||||
// TODO: a complete implementation should have schema validation for `req.body`
|
||||
// TODO(lf): a complete implementation should have schema validation for `req.body`
|
||||
const productsApi: BigcommerceApiHandler<
|
||||
SearchProductsData,
|
||||
ProductsHandlers
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import login from '../../operations/login'
|
||||
import login from '../../../auth/login'
|
||||
import type { LoginHandlers } from '../login'
|
||||
|
||||
const invalidCredentials = /invalid credentials/i
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import login from '../../operations/login'
|
||||
import login from '../../../auth/login'
|
||||
import { SignupHandlers } from '../signup'
|
||||
|
||||
const signup: SignupHandlers['signup'] = async ({
|
||||
|
@@ -1,14 +1,28 @@
|
||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||
import type { ItemBody } from '../cart'
|
||||
import type { CartItemBody, OptionSelections } from '../../types'
|
||||
|
||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
type BCWishlistItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
}
|
||||
|
||||
type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
item: WishlistItemBody
|
||||
): BCWishlistItemBody => ({
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
})
|
||||
|
||||
export const parseCartItem = (item: ItemBody) => ({
|
||||
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
||||
quantity: item.quantity,
|
||||
product_id: item.productId,
|
||||
variant_id: item.variantId,
|
||||
option_selections: item.optionSelections
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
option_selections: item.optionSelections,
|
||||
})
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { ProductNode } from '../operations/get-all-products'
|
||||
import type { ProductNode } from '../../product/get-all-products'
|
||||
import type { RecursivePartial } from './types'
|
||||
|
||||
export default function setProductLocaleMeta(
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { WishlistHandlers } from '..'
|
||||
import getCustomerId from '../../operations/get-customer-id'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import getCustomerId from '../../../customer/get-customer-id'
|
||||
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
||||
import { parseWishlistItem } from '../../utils/parse-item'
|
||||
|
||||
// Returns the wishlist of the signed customer
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import getCustomerId from '../../operations/get-customer-id'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import getCustomerId from '../../../customer/get-customer-id'
|
||||
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
||||
import type { Wishlist, WishlistHandlers } from '..'
|
||||
|
||||
// Return wishlist info
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import getCustomerId from '../../operations/get-customer-id'
|
||||
import getCustomerId from '../../../customer/get-customer-id'
|
||||
import getCustomerWishlist, {
|
||||
Wishlist,
|
||||
} from '../../operations/get-customer-wishlist'
|
||||
} from '../../../customer/get-customer-wishlist'
|
||||
import type { WishlistHandlers } from '..'
|
||||
|
||||
// Return current wishlist info
|
||||
|
@@ -7,24 +7,25 @@ import { BigcommerceApiError } from '../utils/errors'
|
||||
import type {
|
||||
Wishlist,
|
||||
WishlistItem,
|
||||
} from '../operations/get-customer-wishlist'
|
||||
} from '../../customer/get-customer-wishlist'
|
||||
import getWishlist from './handlers/get-wishlist'
|
||||
import addItem from './handlers/add-item'
|
||||
import removeItem from './handlers/remove-item'
|
||||
import type { Product, ProductVariant, Customer } from '@commerce/types'
|
||||
|
||||
export type { Wishlist, WishlistItem }
|
||||
|
||||
export type ItemBody = {
|
||||
productId: number
|
||||
variantId: number
|
||||
productId: Product['id']
|
||||
variantId: ProductVariant['id']
|
||||
}
|
||||
|
||||
export type AddItemBody = { item: ItemBody }
|
||||
|
||||
export type RemoveItemBody = { itemId: string }
|
||||
export type RemoveItemBody = { itemId: Product['id'] }
|
||||
|
||||
export type WishlistBody = {
|
||||
customer_id: number
|
||||
customer_id: Customer['entityId']
|
||||
is_public: number
|
||||
name: string
|
||||
items: any[]
|
||||
|
3
framework/bigcommerce/auth/index.ts
Normal file
3
framework/bigcommerce/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
@@ -1,8 +1,8 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import type { LoginMutation, LoginMutationVariables } from '../../schema'
|
||||
import type { RecursivePartial } from '../utils/types'
|
||||
import concatHeader from '../utils/concat-cookie'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import type { LoginMutation, LoginMutationVariables } from '../schema'
|
||||
import type { RecursivePartial } from '../api/utils/types'
|
||||
import concatHeader from '../api/utils/concat-cookie'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
mutation login($email: String!, $password: String!) {
|
||||
@@ -54,7 +54,7 @@ async function login({
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
cookie = cookie.replace('; Secure', '')
|
||||
// SameSite=none can't be set unless the cookie is Secure
|
||||
// bc seems to sometimes send back SameSite=None rather than none so make
|
||||
// bc seems to sometimes send back SameSite=None rather than none so make
|
||||
// this case insensitive
|
||||
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
||||
}
|
40
framework/bigcommerce/auth/use-login.tsx
Normal file
40
framework/bigcommerce/auth/use-login.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||
import type { LoginBody } from '../api/customers/login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null, {}, LoginBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers/login',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
25
framework/bigcommerce/auth/use-logout.tsx
Normal file
25
framework/bigcommerce/auth/use-logout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers/logout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function logout() {
|
||||
const data = await fetch()
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
44
framework/bigcommerce/auth/use-signup.tsx
Normal file
44
framework/bigcommerce/auth/use-signup.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||
import type { SignupBody } from '../api/customers/signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<null, {}, SignupBody, SignupBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers/signup',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({
|
||||
input: { firstName, lastName, email, password },
|
||||
options,
|
||||
fetch,
|
||||
}) {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
4
framework/bigcommerce/cart/index.ts
Normal file
4
framework/bigcommerce/cart/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as useCart } from './use-cart'
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
||||
export { default as useUpdateItem } from './use-update-item'
|
@@ -1,56 +1,50 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCartAddItem from '@commerce/cart/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/cart'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
CartItemBody,
|
||||
AddCartItemBody,
|
||||
} from '../types'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
})
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Cart, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
) => {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = () => {
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
const data = await fn({ item: input })
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,13 +0,0 @@
|
||||
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() {
|
||||
const addItem = useAddItem()
|
||||
const updateItem = useUpdateItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
}
|
@@ -1,50 +1,41 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
||||
import type { Cart } from '../api/cart'
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type { Cart } from '../types'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
}
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
export type { Cart }
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, CartInput> = (
|
||||
options,
|
||||
{ cartId },
|
||||
fetch
|
||||
) => {
|
||||
return cartId ? fetch({ ...defaultOpts, ...options }) : null
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
||||
) {
|
||||
const useCart = () => {
|
||||
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
export const handler: SWRHook<
|
||||
Cart | null,
|
||||
{},
|
||||
FetchCartInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { cartId }, options, fetch }) {
|
||||
const data = cartId ? await fetch(options) : null
|
||||
return data && normalizeCart(data)
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||
})
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
Object.defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return Object.values(response.data?.line_items ?? {}).every(
|
||||
(items) => !items.length
|
||||
)
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useCart.extend = extendHook
|
||||
|
||||
return useCart
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,51 +1,71 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/cart'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useRemoveItem, {
|
||||
RemoveItemInput as RemoveItemInputBase,
|
||||
UseRemoveItem,
|
||||
} from '@commerce/cart/use-remove-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
RemoveCartItemBody,
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
LineItem,
|
||||
} from '../types'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'DELETE',
|
||||
}
|
||||
export type RemoveItemFn<T = any> = T extends LineItem
|
||||
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
||||
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string
|
||||
}
|
||||
export type RemoveItemInput<T = any> = T extends LineItem
|
||||
? Partial<RemoveItemInputBase>
|
||||
: RemoveItemInputBase
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (item?: any) => {
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<RemoveCartItemBody>) {
|
||||
const data = await fetch<BigcommerceCart>({
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: { item?: T } = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||
const itemId = input?.id ?? item?.id
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
const data = await fn({ itemId: input.id ?? item?.id })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
if (!itemId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
const data = await fetch({ input: { itemId } })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}
|
||||
|
||||
return useRemoveItem
|
||||
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,70 +1,97 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
||||
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
||||
import { fetcher as removeFetcher } from './use-remove-item'
|
||||
import useCart, { Cart } from './use-cart'
|
||||
import type {
|
||||
MutationHookContext,
|
||||
HookFetcherContext,
|
||||
} from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useUpdateItem, {
|
||||
UpdateItemInput as UpdateItemInputBase,
|
||||
UseUpdateItem,
|
||||
} from '@commerce/cart/use-update-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
UpdateCartItemBody,
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
LineItem,
|
||||
} from '../types'
|
||||
import { handler as removeItemHandler } from './use-remove-item'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
}
|
||||
export type UpdateItemInput<T = any> = T extends LineItem
|
||||
? Partial<UpdateItemInputBase<LineItem>>
|
||||
: UpdateItemInputBase<LineItem>
|
||||
|
||||
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
|
||||
options,
|
||||
{ itemId, item },
|
||||
fetch
|
||||
) => {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeFetcher(null, { itemId }, fetch)
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId, item },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<UpdateCartItemBody>) {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeItemHandler.fetcher({
|
||||
options: removeItemHandler.fetchOptions,
|
||||
input: { itemId },
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
|
||||
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = (item?: any) => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({
|
||||
fetch,
|
||||
}: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: {
|
||||
item?: T
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart() as any
|
||||
|
||||
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,
|
||||
debounce(async (input: UpdateItemInput<T>) => {
|
||||
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 fetch({
|
||||
input: {
|
||||
itemId,
|
||||
item: { productId, variantId, quantity: input.quantity },
|
||||
},
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}, cfg?.wait ?? 500),
|
||||
[fn, mutate]
|
||||
}, ctx.wait ?? 500),
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useUpdateItem.extend = extendHook
|
||||
|
||||
return useUpdateItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
6
framework/bigcommerce/commerce.config.json
Normal file
6
framework/bigcommerce/commerce.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"provider": "bigcommerce",
|
||||
"features": {
|
||||
"wishlist": true
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { definitions } from '../api/definitions/store-content'
|
||||
|
||||
export type Page = definitions['page_Full']
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { definitions } from '../api/definitions/store-content'
|
||||
|
||||
export type Page = definitions['page_Full']
|
||||
|
||||
@@ -38,9 +38,9 @@ async function getPage({
|
||||
config = getConfig(config)
|
||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||
// required in case there's a custom `url`
|
||||
const { data } = await config.storeApiFetch<RecursivePartial<{ data: Page[] }>>(
|
||||
url || `/v3/content/pages?id=${variables.id}&include=body`
|
||||
)
|
||||
const { data } = await config.storeApiFetch<
|
||||
RecursivePartial<{ data: Page[] }>
|
||||
>(url || `/v3/content/pages?id=${variables.id}&include=body`)
|
||||
const firstPage = data?.[0]
|
||||
const page = firstPage as RecursiveRequired<typeof firstPage>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { categoryTreeItemFragment } from '../fragments/category-tree'
|
||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { categoryTreeItemFragment } from '../api/fragments/category-tree'
|
||||
|
||||
// Get 3 levels of categories
|
||||
export const getSiteInfoQuery = /* GraphQL */ `
|
@@ -1,5 +1,5 @@
|
||||
import { GetCustomerIdQuery } from '../../schema'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import { GetCustomerIdQuery } from '../schema'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
|
||||
export const getCustomerIdQuery = /* GraphQL */ `
|
||||
query getCustomerId {
|
@@ -1,7 +1,7 @@
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { definitions } from '../definitions/wishlist'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import { definitions } from '../api/definitions/wishlist'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import getAllProducts, { ProductEdge } from '../product/get-all-products'
|
||||
|
||||
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
|
||||
items?: WishlistItem[]
|
||||
@@ -68,14 +68,15 @@ async function getCustomerWishlist({
|
||||
const productsById = graphqlData.products.reduce<{
|
||||
[k: number]: ProductEdge
|
||||
}>((prods, p) => {
|
||||
prods[p.node.entityId] = p
|
||||
prods[Number(p.id)] = p as any
|
||||
return prods
|
||||
}, {})
|
||||
// Populate the wishlist items with the graphql products
|
||||
wishlist.items.forEach((item) => {
|
||||
const product = item && productsById[item.product_id!]
|
||||
if (item && product) {
|
||||
item.product = product.node
|
||||
// @ts-ignore Fix this type when the wishlist type is properly defined
|
||||
item.product = product
|
||||
}
|
||||
})
|
||||
}
|
1
framework/bigcommerce/customer/index.ts
Normal file
1
framework/bigcommerce/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
24
framework/bigcommerce/customer/use-customer.tsx
Normal file
24
framework/bigcommerce/customer/use-customer.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||
import type { Customer, CustomerData } from '../api/customers'
|
||||
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export const handler: SWRHook<Customer | null> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/customers',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch<CustomerData | null>(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
41
framework/bigcommerce/fetcher.ts
Normal file
41
framework/bigcommerce/fetcher.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { Fetcher } from '@commerce/utils/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)
|
||||
}
|
||||
|
||||
export default fetcher
|
@@ -1,46 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||
|
||||
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 })
|
||||
}
|
||||
export { bigcommerceProvider }
|
||||
export type { BigcommerceProvider }
|
||||
|
||||
export const bigcommerceConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
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>
|
||||
@@ -52,10 +23,13 @@ export type BigcommerceProps = {
|
||||
|
||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
||||
return (
|
||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
||||
<CoreCommerceProvider
|
||||
provider={bigcommerceProvider}
|
||||
config={{ ...bigcommerceConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||
|
13
framework/bigcommerce/lib/immutability.ts
Normal file
13
framework/bigcommerce/lib/immutability.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import update, { Context } from 'immutability-helper'
|
||||
|
||||
const c = new Context()
|
||||
|
||||
c.extend('$auto', function (value, object) {
|
||||
return object ? c.update(object, value) : c.update({}, value)
|
||||
})
|
||||
|
||||
c.extend('$autoArray', function (value, object) {
|
||||
return object ? c.update(object, value) : c.update([], value)
|
||||
})
|
||||
|
||||
export default c.update
|
113
framework/bigcommerce/lib/normalize.ts
Normal file
113
framework/bigcommerce/lib/normalize.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Product } from '@commerce/types'
|
||||
import type { Cart, BigcommerceCart, LineItem } from '../types'
|
||||
import update from './immutability'
|
||||
|
||||
function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
node: {
|
||||
entityId,
|
||||
values: { edges },
|
||||
...rest
|
||||
},
|
||||
} = productOption
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
values: edges?.map(({ node }: any) => node),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProduct(productNode: any): Product {
|
||||
const {
|
||||
entityId: id,
|
||||
productOptions,
|
||||
prices,
|
||||
path,
|
||||
id: _,
|
||||
options: _0,
|
||||
} = productNode
|
||||
|
||||
return update(productNode, {
|
||||
id: { $set: String(id) },
|
||||
images: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
url: urlOriginal,
|
||||
alt: altText,
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
variants: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||
id: entityId,
|
||||
options: productOptions?.edges
|
||||
? productOptions.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
$set: productOptions.edges
|
||||
? productOptions?.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
},
|
||||
brand: {
|
||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
||||
},
|
||||
slug: {
|
||||
$set: path?.replace(/^\/+|\/+$/g, ''),
|
||||
},
|
||||
price: {
|
||||
$set: {
|
||||
value: prices?.price.value,
|
||||
currencyCode: prices?.price.currencyCode,
|
||||
},
|
||||
},
|
||||
$unset: ['entityId'],
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeCart(data: BigcommerceCart): Cart {
|
||||
return {
|
||||
id: data.id,
|
||||
customerId: String(data.customer_id),
|
||||
email: data.email,
|
||||
createdAt: data.created_time,
|
||||
currency: data.currency,
|
||||
taxesIncluded: data.tax_included,
|
||||
lineItems: data.line_items.physical_items.map(normalizeLineItem),
|
||||
lineItemsSubtotalPrice: data.base_amount,
|
||||
subtotalPrice: data.base_amount + data.discount_amount,
|
||||
totalPrice: data.cart_amount,
|
||||
discounts: data.discounts?.map((discount) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
id: String(item.variant_id),
|
||||
sku: item.sku,
|
||||
name: item.name,
|
||||
image: {
|
||||
url: item.image_url,
|
||||
},
|
||||
requiresShipping: item.is_require_shipping,
|
||||
price: item.sale_price,
|
||||
listPrice: item.list_price,
|
||||
},
|
||||
path: item.url.split('/')[3],
|
||||
discounts: item.discounts.map((discount: any) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
}
|
||||
}
|
8
framework/bigcommerce/next.config.js
Normal file
8
framework/bigcommerce/next.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: ['cdn11.bigcommerce.com'],
|
||||
},
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
import type {
|
||||
GetAllProductPathsQuery,
|
||||
GetAllProductPathsQueryVariables,
|
||||
} from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
} from '../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
|
||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
||||
query getAllProductPaths($first: Int = 100) {
|
@@ -1,12 +1,14 @@
|
||||
import type {
|
||||
GetAllProductsQuery,
|
||||
GetAllProductsQueryVariables,
|
||||
} from '../../schema'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||
import { productConnectionFragment } from '../fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
} from '../schema'
|
||||
import type { Product } from '@commerce/types'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||
import filterEdges from '../api/utils/filter-edges'
|
||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||
import { productConnectionFragment } from '../api/fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { normalizeProduct } from '../lib/normalize'
|
||||
|
||||
export const getAllProductsQuery = /* GraphQL */ `
|
||||
query getAllProducts(
|
||||
@@ -72,7 +74,7 @@ async function getAllProducts(opts?: {
|
||||
variables?: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
}): Promise<GetAllProductsResult>
|
||||
}): Promise<{ products: Product[] }>
|
||||
|
||||
async function getAllProducts<
|
||||
T extends Record<keyof GetAllProductsResult, any[]>,
|
||||
@@ -93,7 +95,8 @@ async function getAllProducts({
|
||||
variables?: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
} = {}): Promise<GetAllProductsResult> {
|
||||
// TODO: fix the product type here
|
||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||
config = getConfig(config)
|
||||
|
||||
const locale = vars.locale || config.locale
|
||||
@@ -126,7 +129,7 @@ async function getAllProducts({
|
||||
})
|
||||
}
|
||||
|
||||
return { products }
|
||||
return { products: products.map(({ node }) => normalizeProduct(node as any)) }
|
||||
}
|
||||
|
||||
export default getAllProducts
|
@@ -1,7 +1,9 @@
|
||||
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
|
||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||
import { productInfoFragment } from '../fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
import type { GetProductQuery, GetProductQueryVariables } from '../schema'
|
||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||
import { productInfoFragment } from '../api/fragments/product'
|
||||
import { BigcommerceConfig, getConfig } from '../api'
|
||||
import { normalizeProduct } from '../lib/normalize'
|
||||
import type { Product } from '@commerce/types'
|
||||
|
||||
export const getProductQuery = /* GraphQL */ `
|
||||
query getProduct(
|
||||
@@ -92,7 +94,7 @@ async function getProduct({
|
||||
variables: ProductVariables
|
||||
config?: BigcommerceConfig
|
||||
preview?: boolean
|
||||
}): Promise<GetProductResult> {
|
||||
}): Promise<Product | {} | any> {
|
||||
config = getConfig(config)
|
||||
|
||||
const locale = vars.locale || config.locale
|
||||
@@ -109,7 +111,8 @@ async function getProduct({
|
||||
if (locale && config.applyLocale) {
|
||||
setProductLocaleMeta(product)
|
||||
}
|
||||
return { product }
|
||||
|
||||
return { product: normalizeProduct(product as any) }
|
||||
}
|
||||
|
||||
return {}
|
4
framework/bigcommerce/product/index.ts
Normal file
4
framework/bigcommerce/product/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as usePrice } from './use-price'
|
||||
export { default as useSearch } from './use-search'
|
||||
export { default as getProduct } from './get-product'
|
||||
export { default as getAllProducts } from './get-all-products'
|
2
framework/bigcommerce/product/use-price.tsx
Normal file
2
framework/bigcommerce/product/use-price.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from '@commerce/product/use-price'
|
||||
export { default } from '@commerce/product/use-price'
|
53
framework/bigcommerce/product/use-search.tsx
Normal file
53
framework/bigcommerce/product/use-search.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
||||
import type { SearchProductsData } from '../api/catalog/products'
|
||||
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const handler: SWRHook<
|
||||
SearchProductsData,
|
||||
SearchProductsInput,
|
||||
SearchProductsInput
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) => (input = {}) => {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceSearch from '@commerce/products/use-search'
|
||||
import type { SearchProductsData } from '../api/catalog/products'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/catalog/products',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number
|
||||
brandId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
|
||||
options,
|
||||
{ search, categoryId, brandId, sort },
|
||||
fetch
|
||||
) => {
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
url.searchParams.set('category', String(categoryId))
|
||||
if (Number.isInteger(brandId)) url.searchParams.set('brand', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options?.method ?? defaultOpts.method,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
|
||||
) {
|
||||
const useSearch = (input: SearchProductsInput = {}) => {
|
||||
const response = useCommerceSearch(
|
||||
defaultOpts,
|
||||
[
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
customFetcher,
|
||||
{ revalidateOnFocus: false, ...swrOptions }
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useSearch.extend = extendHook
|
||||
|
||||
return useSearch
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
34
framework/bigcommerce/provider.ts
Normal file
34
framework/bigcommerce/provider.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { handler as useCart } from './cart/use-cart'
|
||||
import { handler as useAddItem } from './cart/use-add-item'
|
||||
import { handler as useUpdateItem } from './cart/use-update-item'
|
||||
import { handler as useRemoveItem } from './cart/use-remove-item'
|
||||
|
||||
import { handler as useWishlist } from './wishlist/use-wishlist'
|
||||
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
|
||||
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
|
||||
|
||||
import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useSearch } from './product/use-search'
|
||||
|
||||
import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogout } from './auth/use-logout'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
|
||||
import fetcher from './fetcher'
|
||||
|
||||
export const bigcommerceProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
wishlist: {
|
||||
useWishlist,
|
||||
useAddItem: useWishlistAddItem,
|
||||
useRemoveItem: useWishlistRemoveItem,
|
||||
},
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
}
|
||||
|
||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
58
framework/bigcommerce/types.ts
Normal file
58
framework/bigcommerce/types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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 type Cart = Core.Cart & {
|
||||
lineItems: LineItem[]
|
||||
}
|
||||
|
||||
export type LineItem = Core.LineItem
|
||||
|
||||
/**
|
||||
* Cart mutations
|
||||
*/
|
||||
|
||||
export type OptionSelections = {
|
||||
option_id: number
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
export type GetCartHandlerBody = Core.GetCartHandlerBody
|
||||
|
||||
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
||||
|
||||
export type AddCartItemHandlerBody = Core.AddCartItemHandlerBody<CartItemBody>
|
||||
|
||||
export type UpdateCartItemBody = Core.UpdateCartItemBody<CartItemBody>
|
||||
|
||||
export type UpdateCartItemHandlerBody = Core.UpdateCartItemHandlerBody<CartItemBody>
|
||||
|
||||
export type RemoveCartItemBody = Core.RemoveCartItemBody
|
||||
|
||||
export type RemoveCartItemHandlerBody = Core.RemoveCartItemHandlerBody
|
@@ -1,38 +0,0 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceCustomer from '@commerce/use-customer'
|
||||
import type { Customer, CustomerData } from './api/customers'
|
||||
|
||||
const defaultOpts = {
|
||||
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,54 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceLogin from '@commerce/use-login'
|
||||
import type { LoginBody } from './api/customers/login'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/login',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type LoginInput = LoginBody
|
||||
|
||||
export const fetcher: HookFetcher<null, LoginBody> = (
|
||||
options,
|
||||
{ email, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogin = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login(input: LoginInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogin.extend = extendHook
|
||||
|
||||
return useLogin
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@@ -1,38 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import useCommerceLogout from '@commerce/use-logout'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/logout',
|
||||
method: 'GET',
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useLogout = () => {
|
||||
const { mutate } = useCustomer()
|
||||
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function login() {
|
||||
const data = await fn(null)
|
||||
await mutate(null, false)
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useLogout.extend = extendHook
|
||||
|
||||
return useLogout
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
@@ -1,2 +0,0 @@
|
||||
export * from '@commerce/use-price'
|
||||
export { default } from '@commerce/use-price'
|
@@ -1,54 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCommerceSignup from '@commerce/use-signup'
|
||||
import type { SignupBody } from './api/customers/signup'
|
||||
import useCustomer from './use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/customers/signup',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
export type SignupInput = SignupBody
|
||||
|
||||
export const fetcher: HookFetcher<null, SignupBody> = (
|
||||
options,
|
||||
{ firstName, lastName, email, password },
|
||||
fetch
|
||||
) => {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useSignup = () => {
|
||||
const { revalidate } = useCustomer()
|
||||
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function signup(input: SignupInput) {
|
||||
const data = await fn(input)
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn]
|
||||
)
|
||||
}
|
||||
|
||||
useSignup.extend = extendHook
|
||||
|
||||
return useSignup
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
3
framework/bigcommerce/wishlist/index.ts
Normal file
3
framework/bigcommerce/wishlist/index.ts
Normal 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'
|
@@ -1,39 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
||||
import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
|
||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export type AddItemInput = ItemBody
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
||||
options,
|
||||
{ item },
|
||||
fetch
|
||||
) => {
|
||||
// TODO: add validations before doing the fetch
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = (opts?: UseWishlistOptions) => {
|
||||
export const handler: MutationHook<any, {}, ItemBody, AddItemBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'POST',
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||
const { revalidate } = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
async function addItem(item) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
@@ -41,17 +26,12 @@ export function extendHook(customFetcher: typeof fetcher) {
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({ item: input })
|
||||
// TODO: add validations before doing the fetch
|
||||
const data = await fetch({ input: { item } })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn, revalidate, customer]
|
||||
[fetch, revalidate, customer]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,43 +1,32 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
||||
import useRemoveItem, {
|
||||
RemoveItemInput,
|
||||
UseRemoveItem,
|
||||
} from '@commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemBody, Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist, { UseWishlistInput } from './use-wishlist'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'DELETE',
|
||||
}
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export type RemoveItemInput = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
||||
options,
|
||||
{ itemId },
|
||||
fetch
|
||||
) => {
|
||||
return fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId },
|
||||
})
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
||||
export const handler: MutationHook<
|
||||
Wishlist | null,
|
||||
{ wishlist?: UseWishlistInput },
|
||||
RemoveItemInput,
|
||||
RemoveItemBody
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'DELETE',
|
||||
},
|
||||
useHook: ({ fetch }) => ({ wishlist } = {}) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const { revalidate } = useWishlist(opts)
|
||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const { revalidate } = useWishlist(wishlist)
|
||||
|
||||
return useCallback(
|
||||
async function removeItem(input: RemoveItemInput) {
|
||||
async function removeItem(input) {
|
||||
if (!customer) {
|
||||
// A signed customer is required in order to have a wishlist
|
||||
throw new CommerceError({
|
||||
@@ -45,17 +34,11 @@ export function extendHook(customFetcher: typeof fetcher) {
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({ itemId: String(input.id) })
|
||||
const data = await fetch({ input: { itemId: String(input.id) } })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fn, revalidate, customer]
|
||||
[fetch, revalidate, customer]
|
||||
)
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
|
||||
return useRemoveItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-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 useWishlistActions() {
|
||||
const addItem = useAddItem()
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
return { addItem, removeItem }
|
||||
}
|
@@ -1,76 +1,60 @@
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { SwrOptions } from '@commerce/utils/use-data'
|
||||
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||
import type { Wishlist } from '../api/wishlist'
|
||||
import useCustomer from '../use-customer'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
}
|
||||
export type UseWishlistInput = { includeProducts?: boolean }
|
||||
|
||||
export type { Wishlist }
|
||||
export default useWishlist as UseWishlist<typeof handler>
|
||||
|
||||
export interface UseWishlistOptions {
|
||||
includeProducts?: boolean
|
||||
}
|
||||
export const handler: SWRHook<
|
||||
Wishlist | null,
|
||||
UseWishlistInput,
|
||||
{ customerId?: number } & UseWishlistInput,
|
||||
{ isEmpty?: boolean }
|
||||
> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||
if (!customerId) return null
|
||||
|
||||
export interface UseWishlistInput extends UseWishlistOptions {
|
||||
customerId?: number
|
||||
}
|
||||
// Use a dummy base as we only care about the relative path
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = (
|
||||
options,
|
||||
{ customerId, includeProducts },
|
||||
fetch
|
||||
) => {
|
||||
if (!customerId) return null
|
||||
if (includeProducts) url.searchParams.set('products', '1')
|
||||
|
||||
// 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 = {}) => {
|
||||
return fetch({
|
||||
url: url.pathname + url.search,
|
||||
method: options.method,
|
||||
})
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useCommerceWishlist(
|
||||
defaultOpts,
|
||||
[
|
||||
const response = useData({
|
||||
input: [
|
||||
['customerId', customer?.entityId],
|
||||
['includeProducts', includeProducts],
|
||||
['includeProducts', input?.includeProducts],
|
||||
],
|
||||
customFetcher,
|
||||
{
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
}
|
||||
)
|
||||
|
||||
// Uses a getter to only calculate the prop when required
|
||||
// response.data is also a getter and it's better to not trigger it early
|
||||
Object.defineProperty(response, 'isEmpty', {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
...input?.swrOptions,
|
||||
},
|
||||
set: (x) => x,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
useWishlist.extend = extendHook
|
||||
|
||||
return useWishlist
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
Reference in New Issue
Block a user