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)
|
||||
|
@@ -32,5 +32,3 @@ export interface CommerceAPIFetchOptions<Variables> {
|
||||
variables?: Variables
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
// TODO: define interfaces for all the available operations and API endpoints
|
||||
|
19
framework/commerce/auth/use-login.tsx
Normal file
19
framework/commerce/auth/use-login.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { MutationHook, HookFetcherFn } from '../utils/types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
export type UseLogin<
|
||||
H extends MutationHook<any, any, any> = MutationHook<null, {}, {}>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<null, {}> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.auth?.useLogin!
|
||||
|
||||
const useLogin: UseLogin = (...args) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(...args)
|
||||
}
|
||||
|
||||
export default useLogin
|
19
framework/commerce/auth/use-logout.tsx
Normal file
19
framework/commerce/auth/use-logout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
export type UseLogout<
|
||||
H extends MutationHook<any, any, any> = MutationHook<null>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<null> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.auth?.useLogout!
|
||||
|
||||
const useLogout: UseLogout = (...args) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(...args)
|
||||
}
|
||||
|
||||
export default useLogout
|
19
framework/commerce/auth/use-signup.tsx
Normal file
19
framework/commerce/auth/use-signup.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
export type UseSignup<
|
||||
H extends MutationHook<any, any, any> = MutationHook<null>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<null> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.auth?.useSignup!
|
||||
|
||||
const useSignup: UseSignup = (...args) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(...args)
|
||||
}
|
||||
|
||||
export default useSignup
|
@@ -1,5 +1,23 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
const useAddItem = useAction
|
||||
export type UseAddItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<
|
||||
Cart,
|
||||
AddCartItemBody<CartItemBody>
|
||||
> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.cart?.useAddItem!
|
||||
|
||||
const useAddItem: UseAddItem = (...args) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(...args)
|
||||
}
|
||||
|
||||
export default useAddItem
|
||||
|
@@ -1,17 +0,0 @@
|
||||
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
import useUpdateItem from './use-update-item'
|
||||
|
||||
// This hook is probably not going to be used, but it's here
|
||||
// to show how a commerce should be structuring it
|
||||
export default function useCartActions<T, Input>(
|
||||
options: HookFetcherOptions,
|
||||
fetcher: HookFetcher<T, Input>
|
||||
) {
|
||||
const addItem = useAddItem<T, Input>(options, fetcher)
|
||||
const updateItem = useUpdateItem<T, Input>(options, fetcher)
|
||||
const removeItem = useRemoveItem<T, Input>(options, fetcher)
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
}
|
@@ -1,31 +1,41 @@
|
||||
import type { responseInterface } from 'swr'
|
||||
import Cookies from 'js-cookie'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useData, { SwrOptions } from '../utils/use-data'
|
||||
import { useCommerce } from '..'
|
||||
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||
import type { Cart } from '../types'
|
||||
import { Provider, useCommerce } from '..'
|
||||
|
||||
export type CartResponse<Result> = responseInterface<Result, Error> & {
|
||||
isEmpty: boolean
|
||||
export type FetchCartInput = {
|
||||
cartId?: Cart['id']
|
||||
}
|
||||
|
||||
export type CartInput = {
|
||||
cartId: string | undefined
|
||||
export type UseCart<
|
||||
H extends SWRHook<any, any, any> = SWRHook<
|
||||
Cart | null,
|
||||
{},
|
||||
FetchCartInput,
|
||||
{ isEmpty?: boolean }
|
||||
>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
|
||||
options,
|
||||
input: { cartId },
|
||||
fetch,
|
||||
}) => {
|
||||
return cartId ? await fetch({ ...options }) : null
|
||||
}
|
||||
|
||||
export default function useCart<Result>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, CartInput>,
|
||||
swrOptions?: SwrOptions<Result, CartInput>
|
||||
) {
|
||||
const fn = (provider: Provider) => provider.cart?.useCart!
|
||||
|
||||
const useCart: UseCart = (input) => {
|
||||
const hook = useHook(fn)
|
||||
const { cartCookie } = useCommerce()
|
||||
|
||||
const fetcher: typeof fetcherFn = (options, input, fetch) => {
|
||||
input.cartId = Cookies.get(cartCookie)
|
||||
return fetcherFn(options, input, fetch)
|
||||
const fetcherFn = hook.fetcher ?? fetcher
|
||||
const wrapper: typeof fetcher = (context) => {
|
||||
context.input.cartId = Cookies.get(cartCookie)
|
||||
return fetcherFn(context)
|
||||
}
|
||||
|
||||
const response = useData(options, input, fetcher, swrOptions)
|
||||
|
||||
return Object.assign(response, { isEmpty: true }) as CartResponse<Result>
|
||||
return useSWRHook({ ...hook, fetcher: wrapper })(input)
|
||||
}
|
||||
|
||||
export default useCart
|
||||
|
@@ -1,5 +1,35 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||
import type { Cart, LineItem, RemoveCartItemBody } from '../types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
const useRemoveItem = useAction
|
||||
/**
|
||||
* Input expected by the action returned by the `useRemoveItem` hook
|
||||
*/
|
||||
export type RemoveItemInput = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UseRemoveItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<
|
||||
Cart | null,
|
||||
{ item?: LineItem },
|
||||
RemoveItemInput,
|
||||
RemoveCartItemBody
|
||||
>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<
|
||||
Cart | null,
|
||||
RemoveCartItemBody
|
||||
> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.cart?.useRemoveItem!
|
||||
|
||||
const useRemoveItem: UseRemoveItem = (input) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(input)
|
||||
}
|
||||
|
||||
export default useRemoveItem
|
||||
|
@@ -1,5 +1,38 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
const useUpdateItem = useAction
|
||||
/**
|
||||
* Input expected by the action returned by the `useUpdateItem` hook
|
||||
*/
|
||||
export type UpdateItemInput<T extends CartItemBody> = T & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UseUpdateItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<
|
||||
Cart | null,
|
||||
{
|
||||
item?: LineItem
|
||||
wait?: number
|
||||
},
|
||||
UpdateItemInput<CartItemBody>,
|
||||
UpdateCartItemBody<CartItemBody>
|
||||
>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<
|
||||
Cart | null,
|
||||
UpdateCartItemBody<CartItemBody>
|
||||
> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.cart?.useUpdateItem!
|
||||
|
||||
const useUpdateItem: UseUpdateItem = (input) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(input)
|
||||
}
|
||||
|
||||
export default useUpdateItem
|
||||
|
20
framework/commerce/customer/use-customer.tsx
Normal file
20
framework/commerce/customer/use-customer.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||
import { SWRFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||
import type { Customer } from '../types'
|
||||
import { Provider } from '..'
|
||||
|
||||
export type UseCustomer<
|
||||
H extends SWRHook<any, any, any> = SWRHook<Customer | null>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<Customer | null, any> = SWRFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.customer?.useCustomer!
|
||||
|
||||
const useCustomer: UseCustomer = (input) => {
|
||||
const hook = useHook(fn)
|
||||
return useSWRHook({ fetcher, ...hook })(input)
|
||||
}
|
||||
|
||||
export default useCustomer
|
@@ -6,37 +6,73 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import * as React from 'react'
|
||||
import { Fetcher } from './utils/types'
|
||||
import { Fetcher, SWRHook, MutationHook } from './utils/types'
|
||||
import type { FetchCartInput } from './cart/use-cart'
|
||||
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
|
||||
|
||||
const Commerce = createContext<CommerceContextValue | {}>({})
|
||||
const Commerce = createContext<CommerceContextValue<any> | {}>({})
|
||||
|
||||
export type CommerceProps = {
|
||||
export type Provider = CommerceConfig & {
|
||||
fetcher: Fetcher
|
||||
cart?: {
|
||||
useCart?: SWRHook<Cart | null, any, FetchCartInput>
|
||||
useAddItem?: MutationHook<any, any, any>
|
||||
useUpdateItem?: MutationHook<any, any, any>
|
||||
useRemoveItem?: MutationHook<any, any, any>
|
||||
}
|
||||
wishlist?: {
|
||||
useWishlist?: SWRHook<Wishlist | null, any, any>
|
||||
useAddItem?: MutationHook<any, any, any>
|
||||
useRemoveItem?: MutationHook<any, any, any>
|
||||
}
|
||||
customer?: {
|
||||
useCustomer?: SWRHook<Customer | null, any, any>
|
||||
}
|
||||
products?: {
|
||||
useSearch?: SWRHook<SearchProductsData, any, any>
|
||||
}
|
||||
auth?: {
|
||||
useSignup?: MutationHook<any, any, any>
|
||||
useLogin?: MutationHook<any, any, any>
|
||||
useLogout?: MutationHook<any, any, any>
|
||||
}
|
||||
}
|
||||
|
||||
export type CommerceProps<P extends Provider> = {
|
||||
children?: ReactNode
|
||||
provider: P
|
||||
config: CommerceConfig
|
||||
}
|
||||
|
||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
||||
CommerceContextValue,
|
||||
'fetcherRef'
|
||||
export type CommerceConfig = Omit<
|
||||
CommerceContextValue<any>,
|
||||
'providerRef' | 'fetcherRef'
|
||||
>
|
||||
|
||||
export type CommerceContextValue = {
|
||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
||||
export type CommerceContextValue<P extends Provider> = {
|
||||
providerRef: MutableRefObject<P>
|
||||
fetcherRef: MutableRefObject<Fetcher>
|
||||
locale: string
|
||||
cartCookie: string
|
||||
}
|
||||
|
||||
export function CommerceProvider({ children, config }: CommerceProps) {
|
||||
export function CommerceProvider<P extends Provider>({
|
||||
provider,
|
||||
children,
|
||||
config,
|
||||
}: CommerceProps<P>) {
|
||||
if (!config) {
|
||||
throw new Error('CommerceProvider requires a valid config object')
|
||||
}
|
||||
|
||||
const fetcherRef = useRef(config.fetcher)
|
||||
const providerRef = useRef(provider)
|
||||
// TODO: Remove the fetcherRef
|
||||
const fetcherRef = useRef(provider.fetcher)
|
||||
// Because the config is an object, if the parent re-renders this provider
|
||||
// will re-render every consumer unless we memoize the config
|
||||
const cfg = useMemo(
|
||||
() => ({
|
||||
providerRef,
|
||||
fetcherRef,
|
||||
locale: config.locale,
|
||||
cartCookie: config.cartCookie,
|
||||
@@ -47,6 +83,6 @@ export function CommerceProvider({ children, config }: CommerceProps) {
|
||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
||||
}
|
||||
|
||||
export function useCommerce<T extends CommerceContextValue>() {
|
||||
return useContext(Commerce) as T
|
||||
export function useCommerce<P extends Provider>() {
|
||||
return useContext(Commerce) as CommerceContextValue<P>
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useCommerce } from '.'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export function formatPrice({
|
||||
amount,
|
20
framework/commerce/product/use-search.tsx
Normal file
20
framework/commerce/product/use-search.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||
import { SWRFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||
import type { SearchProductsData } from '../types'
|
||||
import { Provider } from '..'
|
||||
|
||||
export type UseSearch<
|
||||
H extends SWRHook<any, any, any> = SWRHook<SearchProductsData>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<SearchProductsData, any> = SWRFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.products?.useSearch!
|
||||
|
||||
const useSearch: UseSearch = (input) => {
|
||||
const hook = useHook(fn)
|
||||
return useSWRHook({ fetcher, ...hook })(input)
|
||||
}
|
||||
|
||||
export default useSearch
|
@@ -1,5 +0,0 @@
|
||||
import useData from '../utils/use-data'
|
||||
|
||||
const useSearch = useData
|
||||
|
||||
export default useSearch
|
203
framework/commerce/types.ts
Normal file
203
framework/commerce/types.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import type { Wishlist as BCWishlist } from '../bigcommerce/api/wishlist'
|
||||
import type { Customer as BCCustomer } from '../bigcommerce/api/customers'
|
||||
import type { SearchProductsData as BCSearchProductsData } from '../bigcommerce/api/catalog/products'
|
||||
|
||||
export type Discount = {
|
||||
// The value of the discount, can be an amount or percentage
|
||||
value: number
|
||||
}
|
||||
|
||||
export type LineItem = {
|
||||
id: string
|
||||
variantId: string
|
||||
productId: string
|
||||
name: string
|
||||
quantity: number
|
||||
discounts: Discount[]
|
||||
// A human-friendly unique string automatically generated from the product’s name
|
||||
path: string
|
||||
variant: ProductVariant
|
||||
}
|
||||
|
||||
export type Measurement = {
|
||||
value: number
|
||||
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
|
||||
}
|
||||
|
||||
export type Image = {
|
||||
url: string
|
||||
altText?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export type ProductVariant = {
|
||||
id: string
|
||||
// The SKU (stock keeping unit) associated with the product variant.
|
||||
sku: string
|
||||
// The product variant’s title, or the product's name.
|
||||
name: string
|
||||
// Whether a customer needs to provide a shipping address when placing
|
||||
// an order for the product variant.
|
||||
requiresShipping: boolean
|
||||
// The product variant’s price after all discounts are applied.
|
||||
price: number
|
||||
// Product variant’s price, as quoted by the manufacturer/distributor.
|
||||
listPrice: number
|
||||
// Image associated with the product variant. Falls back to the product image
|
||||
// if no image is available.
|
||||
image?: Image
|
||||
// Indicates whether this product variant is in stock.
|
||||
isInStock?: boolean
|
||||
// Indicates if the product variant is available for sale.
|
||||
availableForSale?: boolean
|
||||
// The variant's weight. If a weight was not explicitly specified on the
|
||||
// variant this will be the product's weight.
|
||||
weight?: Measurement
|
||||
// The variant's height. If a height was not explicitly specified on the
|
||||
// variant, this will be the product's height.
|
||||
height?: Measurement
|
||||
// The variant's width. If a width was not explicitly specified on the
|
||||
// variant, this will be the product's width.
|
||||
width?: Measurement
|
||||
// The variant's depth. If a depth was not explicitly specified on the
|
||||
// variant, this will be the product's depth.
|
||||
depth?: Measurement
|
||||
}
|
||||
|
||||
// Shopping cart, a.k.a Checkout
|
||||
export type Cart = {
|
||||
id: string
|
||||
// ID of the customer to which the cart belongs.
|
||||
customerId?: string
|
||||
// The email assigned to this cart
|
||||
email?: string
|
||||
// The date and time when the cart was created.
|
||||
createdAt: string
|
||||
// The currency used for this cart
|
||||
currency: { code: string }
|
||||
// Specifies if taxes are included in the line items.
|
||||
taxesIncluded: boolean
|
||||
lineItems: LineItem[]
|
||||
// The sum of all the prices of all the items in the cart.
|
||||
// Duties, taxes, shipping and discounts excluded.
|
||||
lineItemsSubtotalPrice: number
|
||||
// Price of the cart before duties, shipping and taxes.
|
||||
subtotalPrice: number
|
||||
// The sum of all the prices of all the items in the cart.
|
||||
// Duties, taxes and discounts included.
|
||||
totalPrice: number
|
||||
// Discounts that have been applied on the cart.
|
||||
discounts?: Discount[]
|
||||
}
|
||||
|
||||
// TODO: Properly define this type
|
||||
export interface Wishlist extends BCWishlist {}
|
||||
|
||||
// TODO: Properly define this type
|
||||
export interface Customer extends BCCustomer {}
|
||||
|
||||
// TODO: Properly define this type
|
||||
export interface SearchProductsData extends BCSearchProductsData {}
|
||||
|
||||
/**
|
||||
* Cart mutations
|
||||
*/
|
||||
|
||||
// Base cart item body used for cart mutations
|
||||
export type CartItemBody = {
|
||||
variantId: string
|
||||
productId?: string
|
||||
quantity?: number
|
||||
}
|
||||
|
||||
// Body used by the `getCart` operation handler
|
||||
export type GetCartHandlerBody = {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Body used by the add item to cart operation
|
||||
export type AddCartItemBody<T extends CartItemBody> = {
|
||||
item: T
|
||||
}
|
||||
|
||||
// Body expected by the add item to cart operation handler
|
||||
export type AddCartItemHandlerBody<T extends CartItemBody> = Partial<
|
||||
AddCartItemBody<T>
|
||||
> & {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Body used by the update cart item operation
|
||||
export type UpdateCartItemBody<T extends CartItemBody> = {
|
||||
itemId: string
|
||||
item: T
|
||||
}
|
||||
|
||||
// Body expected by the update cart item operation handler
|
||||
export type UpdateCartItemHandlerBody<T extends CartItemBody> = Partial<
|
||||
UpdateCartItemBody<T>
|
||||
> & {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
// Body used by the remove cart item operation
|
||||
export type RemoveCartItemBody = {
|
||||
itemId: string
|
||||
}
|
||||
|
||||
// Body expected by the remove cart item operation handler
|
||||
export type RemoveCartItemHandlerBody = Partial<RemoveCartItemBody> & {
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporal types
|
||||
*/
|
||||
|
||||
interface Entity {
|
||||
id: string | number
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export interface Product extends Entity {
|
||||
name: string
|
||||
description: string
|
||||
slug?: string
|
||||
path?: string
|
||||
images: ProductImage[]
|
||||
variants: ProductVariant2[]
|
||||
price: ProductPrice
|
||||
options: ProductOption[]
|
||||
sku?: string
|
||||
}
|
||||
|
||||
interface ProductOption extends Entity {
|
||||
displayName: string
|
||||
values: ProductOptionValues[]
|
||||
}
|
||||
|
||||
interface ProductOptionValues {
|
||||
label: string
|
||||
hexColors?: string[]
|
||||
}
|
||||
|
||||
interface ProductImage {
|
||||
url: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
interface ProductVariant2 {
|
||||
id: string | number
|
||||
options: ProductOption[]
|
||||
}
|
||||
|
||||
interface ProductPrice {
|
||||
value: number
|
||||
currencyCode: 'USD' | 'ARS' | string | undefined
|
||||
retailPrice?: number
|
||||
salePrice?: number
|
||||
listPrice?: number
|
||||
extendedSalePrice?: number
|
||||
extendedListPrice?: number
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
import useData from './utils/use-data'
|
||||
|
||||
const useCustomer = useData
|
||||
|
||||
export default useCustomer
|
@@ -1,5 +0,0 @@
|
||||
import useAction from './utils/use-action'
|
||||
|
||||
const useLogin = useAction
|
||||
|
||||
export default useLogin
|
@@ -1,5 +0,0 @@
|
||||
import useAction from './utils/use-action'
|
||||
|
||||
const useLogout = useAction
|
||||
|
||||
export default useLogout
|
@@ -1,5 +0,0 @@
|
||||
import useAction from './utils/use-action'
|
||||
|
||||
const useSignup = useAction
|
||||
|
||||
export default useSignup
|
12
framework/commerce/utils/default-fetcher.ts
Normal file
12
framework/commerce/utils/default-fetcher.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { HookFetcherFn } from './types'
|
||||
|
||||
export const SWRFetcher: HookFetcherFn<any, any> = ({ options, fetch }) =>
|
||||
fetch(options)
|
||||
|
||||
export const mutationFetcher: HookFetcherFn<any, any> = ({
|
||||
input,
|
||||
options,
|
||||
fetch,
|
||||
}) => fetch({ ...options, body: input })
|
||||
|
||||
export default SWRFetcher
|
37
framework/commerce/utils/define-property.ts
Normal file
37
framework/commerce/utils/define-property.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Taken from https://fettblog.eu/typescript-assertion-signatures/
|
||||
|
||||
type InferValue<Prop extends PropertyKey, Desc> = Desc extends {
|
||||
get(): any
|
||||
value: any
|
||||
}
|
||||
? never
|
||||
: Desc extends { value: infer T }
|
||||
? Record<Prop, T>
|
||||
: Desc extends { get(): infer T }
|
||||
? Record<Prop, T>
|
||||
: never
|
||||
|
||||
type DefineProperty<
|
||||
Prop extends PropertyKey,
|
||||
Desc extends PropertyDescriptor
|
||||
> = Desc extends { writable: any; set(val: any): any }
|
||||
? never
|
||||
: Desc extends { writable: any; get(): any }
|
||||
? never
|
||||
: Desc extends { writable: false }
|
||||
? Readonly<InferValue<Prop, Desc>>
|
||||
: Desc extends { writable: true }
|
||||
? InferValue<Prop, Desc>
|
||||
: Readonly<InferValue<Prop, Desc>>
|
||||
|
||||
export default function defineProperty<
|
||||
Obj extends object,
|
||||
Key extends PropertyKey,
|
||||
PDesc extends PropertyDescriptor
|
||||
>(
|
||||
obj: Obj,
|
||||
prop: Key,
|
||||
val: PDesc
|
||||
): asserts obj is Obj & DefineProperty<Key, PDesc> {
|
||||
Object.defineProperty(obj, prop, val)
|
||||
}
|
@@ -26,6 +26,14 @@ export class CommerceError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// Used for errors that come from a bad implementation of the hooks
|
||||
export class ValidationError extends CommerceError {
|
||||
constructor(options: ErrorProps) {
|
||||
super(options)
|
||||
this.code = 'validation_error'
|
||||
}
|
||||
}
|
||||
|
||||
export class FetcherError extends CommerceError {
|
||||
status: number
|
||||
|
||||
|
@@ -1,24 +1,131 @@
|
||||
// Core fetcher added by CommerceProvider
|
||||
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
|
||||
import type { ConfigInterface } from 'swr'
|
||||
import type { CommerceError } from './errors'
|
||||
import type { ResponseState } from './use-data'
|
||||
|
||||
export type FetcherOptions = {
|
||||
/**
|
||||
* Returns the properties in T with the properties in type K, overriding properties defined in T
|
||||
*/
|
||||
export type Override<T, K> = Omit<T, keyof K> & K
|
||||
|
||||
/**
|
||||
* Returns the properties in T with the properties in type K changed from optional to required
|
||||
*/
|
||||
export type PickRequired<T, K extends keyof T> = Omit<T, K> &
|
||||
{
|
||||
[P in K]-?: NonNullable<T[P]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Core fetcher added by CommerceProvider
|
||||
*/
|
||||
export type Fetcher<T = any, B = any> = (
|
||||
options: FetcherOptions<B>
|
||||
) => T | Promise<T>
|
||||
|
||||
export type FetcherOptions<Body = any> = {
|
||||
url?: string
|
||||
query?: string
|
||||
method?: string
|
||||
variables?: any
|
||||
body?: any
|
||||
body?: Body
|
||||
}
|
||||
|
||||
export type HookFetcher<Result, Input = null> = (
|
||||
export type HookFetcher<Data, Input = null, Result = any> = (
|
||||
options: HookFetcherOptions | null,
|
||||
input: Input,
|
||||
fetch: <T = Result>(options: FetcherOptions) => Promise<T>
|
||||
) => Result | Promise<Result>
|
||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||
) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherOptions = {
|
||||
query?: string
|
||||
url?: string
|
||||
method?: string
|
||||
export type HookFetcherFn<Data, Input = undefined, Result = any, Body = any> = (
|
||||
context: HookFetcherContext<Input, Result, Body>
|
||||
) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherContext<Input = undefined, Result = any, Body = any> = {
|
||||
options: HookFetcherOptions
|
||||
input: Input
|
||||
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
||||
}
|
||||
|
||||
export type HookInput = [string, string | number | boolean | undefined][]
|
||||
export type HookFetcherOptions = { method?: string } & (
|
||||
| { query: string; url?: string }
|
||||
| { query?: string; url: string }
|
||||
)
|
||||
|
||||
export type HookInputValue = string | number | boolean | undefined
|
||||
|
||||
export type HookSWRInput = [string, HookInputValue][]
|
||||
|
||||
export type HookFetchInput = { [k: string]: HookInputValue }
|
||||
|
||||
export type HookFunction<
|
||||
Input extends { [k: string]: unknown } | null,
|
||||
T
|
||||
> = keyof Input extends never
|
||||
? () => T
|
||||
: Partial<Input> extends Input
|
||||
? (input?: Input) => T
|
||||
: (input: Input) => T
|
||||
|
||||
export type SWRHook<
|
||||
// Data obj returned by the hook and fetch operation
|
||||
Data,
|
||||
// Input expected by the hook
|
||||
Input extends { [k: string]: unknown } = {},
|
||||
// Input expected before doing a fetch operation
|
||||
FetchInput extends HookFetchInput = {},
|
||||
// Custom state added to the response object of SWR
|
||||
State = {}
|
||||
> = {
|
||||
useHook(
|
||||
context: SWRHookContext<Data, FetchInput>
|
||||
): HookFunction<
|
||||
Input & { swrOptions?: SwrOptions<Data, FetchInput> },
|
||||
ResponseState<Data> & State
|
||||
>
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||
}
|
||||
|
||||
export type SWRHookContext<
|
||||
Data,
|
||||
FetchInput extends { [k: string]: unknown } = {}
|
||||
> = {
|
||||
useData(context?: {
|
||||
input?: HookFetchInput | HookSWRInput
|
||||
swrOptions?: SwrOptions<Data, FetchInput>
|
||||
}): ResponseState<Data>
|
||||
}
|
||||
|
||||
export type MutationHook<
|
||||
// Data obj returned by the hook and fetch operation
|
||||
Data,
|
||||
// Input expected by the hook
|
||||
Input extends { [k: string]: unknown } = {},
|
||||
// Input expected by the action returned by the hook
|
||||
ActionInput extends { [k: string]: unknown } = {},
|
||||
// Input expected before doing a fetch operation
|
||||
FetchInput extends { [k: string]: unknown } = ActionInput
|
||||
> = {
|
||||
useHook(
|
||||
context: MutationHookContext<Data, FetchInput>
|
||||
): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||
}
|
||||
|
||||
export type MutationHookContext<
|
||||
Data,
|
||||
FetchInput extends { [k: string]: unknown } | null = {}
|
||||
> = {
|
||||
fetch: keyof FetchInput extends never
|
||||
? () => Data | Promise<Data>
|
||||
: Partial<FetchInput> extends FetchInput
|
||||
? (context?: { input?: FetchInput }) => Data | Promise<Data>
|
||||
: (context: { input: FetchInput }) => Data | Promise<Data>
|
||||
}
|
||||
|
||||
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||
Data,
|
||||
CommerceError,
|
||||
HookFetcher<Data, Input, Result>
|
||||
>
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { HookFetcher, HookFetcherOptions } from './types'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export default function useAction<T, Input = null>(
|
||||
options: HookFetcherOptions,
|
||||
fetcher: HookFetcher<T, Input>
|
||||
) {
|
||||
const { fetcherRef } = useCommerce()
|
||||
|
||||
return useCallback(
|
||||
(input: Input) => fetcher(options, input, fetcherRef.current),
|
||||
[fetcher]
|
||||
)
|
||||
}
|
@@ -1,39 +1,47 @@
|
||||
import useSWR, { ConfigInterface, responseInterface } from 'swr'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
|
||||
import useSWR, { responseInterface } from 'swr'
|
||||
import type {
|
||||
HookSWRInput,
|
||||
HookFetchInput,
|
||||
Fetcher,
|
||||
SwrOptions,
|
||||
HookFetcherOptions,
|
||||
HookFetcherFn,
|
||||
} from './types'
|
||||
import defineProperty from './define-property'
|
||||
import { CommerceError } from './errors'
|
||||
import { useCommerce } from '..'
|
||||
|
||||
export type SwrOptions<Result, Input = null> = ConfigInterface<
|
||||
Result,
|
||||
CommerceError,
|
||||
HookFetcher<Result, Input>
|
||||
>
|
||||
export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export type UseData = <Result = any, Input = null>(
|
||||
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, Input>,
|
||||
swrOptions?: SwrOptions<Result, Input>
|
||||
) => responseInterface<Result, CommerceError>
|
||||
export type UseData = <Data = any, FetchInput extends HookFetchInput = {}>(
|
||||
options: {
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher: HookFetcherFn<Data, FetchInput>
|
||||
},
|
||||
input: HookFetchInput | HookSWRInput,
|
||||
fetcherFn: Fetcher,
|
||||
swrOptions?: SwrOptions<Data, FetchInput>
|
||||
) => ResponseState<Data>
|
||||
|
||||
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
|
||||
const { fetcherRef } = useCommerce()
|
||||
const hookInput = Array.isArray(input) ? input : Object.entries(input)
|
||||
const fetcher = async (
|
||||
url?: string,
|
||||
url: string,
|
||||
query?: string,
|
||||
method?: string,
|
||||
...args: any[]
|
||||
) => {
|
||||
try {
|
||||
return await fetcherFn(
|
||||
{ url, query, method },
|
||||
return await options.fetcher({
|
||||
options: { url, query, method },
|
||||
// Transform the input array into an object
|
||||
args.reduce((obj, val, i) => {
|
||||
obj[input[i][0]!] = val
|
||||
input: args.reduce((obj, val, i) => {
|
||||
obj[hookInput[i][0]!] = val
|
||||
return obj
|
||||
}, {}),
|
||||
fetcherRef.current
|
||||
)
|
||||
fetch: fetcherFn,
|
||||
})
|
||||
} catch (error) {
|
||||
// SWR will not log errors, but any error that's not an instance
|
||||
// of CommerceError is not welcomed by this hook
|
||||
@@ -45,15 +53,24 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
|
||||
}
|
||||
const response = useSWR(
|
||||
() => {
|
||||
const opts = typeof options === 'function' ? options() : options
|
||||
const opts = options.fetchOptions
|
||||
return opts
|
||||
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
|
||||
? [opts.url, opts.query, opts.method, ...hookInput.map((e) => e[1])]
|
||||
: null
|
||||
},
|
||||
fetcher,
|
||||
swrOptions
|
||||
)
|
||||
|
||||
if (!('isLoading' in response)) {
|
||||
defineProperty(response, 'isLoading', {
|
||||
get() {
|
||||
return response.data === undefined
|
||||
},
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
50
framework/commerce/utils/use-hook.ts
Normal file
50
framework/commerce/utils/use-hook.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react'
|
||||
import { Provider, useCommerce } from '..'
|
||||
import type { MutationHook, PickRequired, SWRHook } from './types'
|
||||
import useData from './use-data'
|
||||
|
||||
export function useFetcher() {
|
||||
const { providerRef, fetcherRef } = useCommerce()
|
||||
return providerRef.current.fetcher ?? fetcherRef.current
|
||||
}
|
||||
|
||||
export function useHook<
|
||||
P extends Provider,
|
||||
H extends MutationHook<any, any, any> | SWRHook<any, any, any>
|
||||
>(fn: (provider: P) => H) {
|
||||
const { providerRef } = useCommerce<P>()
|
||||
const provider = providerRef.current
|
||||
return fn(provider)
|
||||
}
|
||||
|
||||
export function useSWRHook<H extends SWRHook<any, any, any>>(
|
||||
hook: PickRequired<H, 'fetcher'>
|
||||
) {
|
||||
const fetcher = useFetcher()
|
||||
|
||||
return hook.useHook({
|
||||
useData(ctx) {
|
||||
const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions)
|
||||
return response
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useMutationHook<H extends MutationHook<any, any, any>>(
|
||||
hook: PickRequired<H, 'fetcher'>
|
||||
) {
|
||||
const fetcher = useFetcher()
|
||||
|
||||
return hook.useHook({
|
||||
fetch: useCallback(
|
||||
({ input } = {}) => {
|
||||
return hook.fetcher({
|
||||
input,
|
||||
options: hook.fetchOptions,
|
||||
fetch: fetcher,
|
||||
})
|
||||
},
|
||||
[fetcher, hook.fetchOptions]
|
||||
),
|
||||
})
|
||||
}
|
3
framework/commerce/wishlist/index.ts
Normal file
3
framework/commerce/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,5 +1,19 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { MutationHook } from '../utils/types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
const useAddItem = useAction
|
||||
export type UseAddItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<any, {}, {}>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.wishlist?.useAddItem!
|
||||
|
||||
const useAddItem: UseAddItem = (...args) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(...args)
|
||||
}
|
||||
|
||||
export default useAddItem
|
||||
|
@@ -1,5 +1,28 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, MutationHook } from '../utils/types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
const useRemoveItem = useAction
|
||||
export type RemoveItemInput = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export type UseRemoveItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<
|
||||
any | null,
|
||||
{ wishlist?: any },
|
||||
RemoveItemInput,
|
||||
{}
|
||||
>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<any | null, {}> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.wishlist?.useRemoveItem!
|
||||
|
||||
const useRemoveItem: UseRemoveItem = (input) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(input)
|
||||
}
|
||||
|
||||
export default useRemoveItem
|
||||
|
@@ -1,17 +1,25 @@
|
||||
import type { responseInterface } from 'swr'
|
||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useData, { SwrOptions } from '../utils/use-data'
|
||||
import { useHook, useSWRHook } from '../utils/use-hook'
|
||||
import { SWRFetcher } from '../utils/default-fetcher'
|
||||
import type { HookFetcherFn, SWRHook } from '../utils/types'
|
||||
import type { Wishlist } from '../types'
|
||||
import type { Provider } from '..'
|
||||
|
||||
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
|
||||
isEmpty: boolean
|
||||
export type UseWishlist<
|
||||
H extends SWRHook<any, any, any> = SWRHook<
|
||||
Wishlist | null,
|
||||
{ includeProducts?: boolean },
|
||||
{ customerId?: number; includeProducts: boolean },
|
||||
{ isEmpty?: boolean }
|
||||
>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<Wishlist | null, any> = SWRFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.wishlist?.useWishlist!
|
||||
|
||||
const useWishlist: UseWishlist = (input) => {
|
||||
const hook = useHook(fn)
|
||||
return useSWRHook({ fetcher, ...hook })(input)
|
||||
}
|
||||
|
||||
export default function useWishlist<Result, Input = null>(
|
||||
options: HookFetcherOptions,
|
||||
input: HookInput,
|
||||
fetcherFn: HookFetcher<Result, Input>,
|
||||
swrOptions?: SwrOptions<Result, Input>
|
||||
) {
|
||||
const response = useData(options, input, fetcherFn, swrOptions)
|
||||
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
|
||||
}
|
||||
export default useWishlist
|
||||
|
41
framework/commerce/with-config.js
Normal file
41
framework/commerce/with-config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* This file is expected to be used in next.config.js only
|
||||
*/
|
||||
|
||||
const merge = require('deepmerge')
|
||||
|
||||
const PROVIDERS = ['bigcommerce', 'shopify']
|
||||
|
||||
function getProviderName() {
|
||||
// TODO: OSOT.
|
||||
return process.env.BIGCOMMERCE_STOREFRONT_API_URL ? 'bigcommerce' : null
|
||||
}
|
||||
|
||||
module.exports = (nextConfig = {}) => {
|
||||
const commerce = nextConfig.commerce || {}
|
||||
const name = commerce.provider || getProviderName()
|
||||
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
`The commerce provider is missing, please add a valid provider name or its environment variables`
|
||||
)
|
||||
}
|
||||
if (!PROVIDERS.includes(name)) {
|
||||
throw new Error(
|
||||
`The commerce provider "${name}" can't be found, please use one of "${PROVIDERS.join(
|
||||
', '
|
||||
)}"`
|
||||
)
|
||||
}
|
||||
|
||||
const commerceNextConfig = require(`../${name}/next.config`)
|
||||
const config = merge(commerceNextConfig, nextConfig)
|
||||
|
||||
config.env = config.env || {}
|
||||
|
||||
Object.entries(config.commerce.features).forEach(([k, v]) => {
|
||||
if (v) config.env[`COMMERCE_${k.toUpperCase()}_ENABLED`] = true
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
2
framework/shopify/.env.template
Normal file
2
framework/shopify/.env.template
Normal file
@@ -0,0 +1,2 @@
|
||||
SHOPIFY_STORE_DOMAIN=
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
260
framework/shopify/README.md
Normal file
260
framework/shopify/README.md
Normal file
@@ -0,0 +1,260 @@
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Modifications](#modifications)
|
||||
- [Adding item to Cart](#adding-item-to-cart)
|
||||
- [Proceed to Checkout](#proceed-to-checkout)
|
||||
- [General Usage](#general-usage)
|
||||
- [CommerceProvider](#commerceprovider)
|
||||
- [useCommerce](#usecommerce)
|
||||
- [Hooks](#hooks)
|
||||
- [usePrice](#useprice)
|
||||
- [useAddItem](#useadditem)
|
||||
- [useRemoveItem](#useremoveitem)
|
||||
- [useUpdateItem](#useupdateitem)
|
||||
- [APIs](#apis)
|
||||
- [getProduct](#getproduct)
|
||||
- [getAllProducts](#getallproducts)
|
||||
- [getAllCollections](#getallcollections)
|
||||
- [getAllPages](#getallpages)
|
||||
|
||||
# Shopify Storefront Data Hooks
|
||||
|
||||
Collection of hooks and data fetching functions to integrate Shopify in a React application. Designed to work with [Next.js Commerce](https://demo.vercel.store/).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```
|
||||
yarn install shopify-buy
|
||||
yarn install -D @types/shopify-buy
|
||||
```
|
||||
|
||||
3. Environment variables need to be set:
|
||||
|
||||
```
|
||||
SHOPIFY_STORE_DOMAIN=
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
|
||||
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||
```
|
||||
|
||||
4. Point the framework to `shopify` by updating `tsconfig.json`:
|
||||
|
||||
```
|
||||
"@framework/*": ["framework/shopify/*"],
|
||||
"@framework": ["framework/shopify"]
|
||||
```
|
||||
|
||||
### Modifications
|
||||
|
||||
These modifications are temporarily until contributions are made to remove them.
|
||||
|
||||
#### Adding item to Cart
|
||||
|
||||
```js
|
||||
// components/product/ProductView/ProductView.tsx
|
||||
const ProductView: FC<Props> = ({ product }) => {
|
||||
const addToCart = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await addItem({
|
||||
productId: product.id,
|
||||
variantId: variant ? variant.id : product.variants[0].id,
|
||||
})
|
||||
openSidebar()
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Proceed to Checkout
|
||||
|
||||
```js
|
||||
// components/cart/CartSidebarView/CartSidebarView.tsx
|
||||
import { useCommerce } from '@framework'
|
||||
|
||||
const CartSidebarView: FC = () => {
|
||||
const { checkout } = useCommerce()
|
||||
return (
|
||||
<Button href={checkout.webUrl} Component="a" width="100%">
|
||||
Proceed to Checkout
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## General Usage
|
||||
|
||||
### CommerceProvider
|
||||
|
||||
Provider component that creates the commerce context for children.
|
||||
|
||||
```js
|
||||
import { CommerceProvider } from '@framework'
|
||||
|
||||
const App = ({ children }) => {
|
||||
return <CommerceProvider locale={locale}>{children}</CommerceProvider>
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
### useCommerce
|
||||
|
||||
Returns the configs that are defined in the nearest `CommerceProvider`. Also provides access to Shopify's `checkout` and `shop`.
|
||||
|
||||
```js
|
||||
import { useCommerce } from 'nextjs-commerce-shopify'
|
||||
|
||||
const { checkout, shop } = useCommerce()
|
||||
```
|
||||
|
||||
- `checkout`: The information required to checkout items and pay ([Documentation](https://shopify.dev/docs/storefront-api/reference/checkouts/checkout)).
|
||||
- `shop`: Represents a collection of the general settings and information about the shop ([Documentation](https://shopify.dev/docs/storefront-api/reference/online-store/shop/index)).
|
||||
|
||||
## Hooks
|
||||
|
||||
### usePrice
|
||||
|
||||
Display the product variant price according to currency and locale.
|
||||
|
||||
```js
|
||||
import usePrice from '@framework/product/use-price'
|
||||
|
||||
const { price } = usePrice({
|
||||
amount,
|
||||
})
|
||||
```
|
||||
|
||||
Takes in either `amount` or `variant`:
|
||||
|
||||
- `amount`: A price value for a particular item if the amount is known.
|
||||
- `variant`: A shopify product variant. Price will be extracted from the variant.
|
||||
|
||||
### useAddItem
|
||||
|
||||
```js
|
||||
import { useAddItem } from '@framework/cart'
|
||||
|
||||
const AddToCartButton = ({ variantId, quantity }) => {
|
||||
const addItem = useAddItem()
|
||||
|
||||
const addToCart = async () => {
|
||||
await addItem({
|
||||
variantId,
|
||||
})
|
||||
}
|
||||
|
||||
return <button onClick={addToCart}>Add To Cart</button>
|
||||
}
|
||||
```
|
||||
|
||||
### useRemoveItem
|
||||
|
||||
```js
|
||||
import { useRemoveItem } from '@framework/cart'
|
||||
|
||||
const RemoveButton = ({ item }) => {
|
||||
const removeItem = useRemoveItem()
|
||||
|
||||
const handleRemove = async () => {
|
||||
await removeItem({ id: item.id })
|
||||
}
|
||||
|
||||
return <button onClick={handleRemove}>Remove</button>
|
||||
}
|
||||
```
|
||||
|
||||
### useUpdateItem
|
||||
|
||||
```js
|
||||
import { useUpdateItem } from '@framework/cart'
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
max={99}
|
||||
min={0}
|
||||
value={quantity}
|
||||
onChange={updateQuantity}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## APIs
|
||||
|
||||
Collections of APIs to fetch data from a Shopify store.
|
||||
|
||||
The data is fetched using the [Shopify JavaScript Buy SDK](https://github.com/Shopify/js-buy-sdk#readme). Read the [Shopify Storefront API reference](https://shopify.dev/docs/storefront-api/reference) for more information.
|
||||
|
||||
### getProduct
|
||||
|
||||
Get a single product by its `handle`.
|
||||
|
||||
```js
|
||||
import getProduct from '@framework/product/get-product'
|
||||
import { getConfig } from '@framework/api'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
const product = await getProduct({
|
||||
variables: { slug },
|
||||
config,
|
||||
})
|
||||
```
|
||||
|
||||
### getAllProducts
|
||||
|
||||
```js
|
||||
import getAllProducts from '@framework/product/get-all-products'
|
||||
import { getConfig } from '@framework/api'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
const { products } = await getAllProducts({
|
||||
variables: { first: 12 },
|
||||
config,
|
||||
})
|
||||
```
|
||||
|
||||
### getAllCollections
|
||||
|
||||
```js
|
||||
import getAllCollections from '@framework/product/get-all-collections'
|
||||
import { getConfig } from '@framework/api'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
const collections = await getAllCollections({
|
||||
config,
|
||||
})
|
||||
```
|
||||
|
||||
### getAllPages
|
||||
|
||||
```js
|
||||
import getAllPages from '@framework/common/get-all-pages'
|
||||
import { getConfig } from '@framework/api'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
const pages = await getAllPages({
|
||||
variables: { first: 12 },
|
||||
config,
|
||||
})
|
||||
```
|
1
framework/shopify/api/cart/index.ts
Normal file
1
framework/shopify/api/cart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/shopify/api/catalog/index.ts
Normal file
1
framework/shopify/api/catalog/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/shopify/api/catalog/products.ts
Normal file
1
framework/shopify/api/catalog/products.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function () {}
|
46
framework/shopify/api/checkout/index.ts
Normal file
46
framework/shopify/api/checkout/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import isAllowedMethod from '../utils/is-allowed-method'
|
||||
import createApiHandler, {
|
||||
ShopifyApiHandler,
|
||||
} from '../utils/create-api-handler'
|
||||
|
||||
import {
|
||||
SHOPIFY_CHECKOUT_ID_COOKIE,
|
||||
SHOPIFY_CHECKOUT_URL_COOKIE,
|
||||
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
|
||||
} from '../../const'
|
||||
|
||||
import { getConfig } from '..'
|
||||
import associateCustomerWithCheckoutMutation from '../../utils/mutations/associate-customer-with-checkout'
|
||||
|
||||
const METHODS = ['GET']
|
||||
|
||||
const checkoutApi: ShopifyApiHandler<any> = async (req, res, config) => {
|
||||
if (!isAllowedMethod(req, res, METHODS)) return
|
||||
|
||||
config = getConfig()
|
||||
|
||||
const { cookies } = req
|
||||
const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE]
|
||||
const customerCookie = cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE]
|
||||
|
||||
if (customerCookie) {
|
||||
try {
|
||||
await config.fetch(associateCustomerWithCheckoutMutation, {
|
||||
variables: {
|
||||
checkoutId: cookies[SHOPIFY_CHECKOUT_ID_COOKIE],
|
||||
customerAccessToken: cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (checkoutUrl) {
|
||||
res.redirect(checkoutUrl)
|
||||
} else {
|
||||
res.redirect('/cart')
|
||||
}
|
||||
}
|
||||
|
||||
export default createApiHandler(checkoutApi, {}, {})
|
1
framework/shopify/api/customer.ts
Normal file
1
framework/shopify/api/customer.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/shopify/api/customers/index.ts
Normal file
1
framework/shopify/api/customers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function () {}
|
1
framework/shopify/api/customers/login.ts
Normal file
1
framework/shopify/api/customers/login.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function () {}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user