diff --git a/.changeset/calm-parents-hide.md b/.changeset/calm-parents-hide.md new file mode 100644 index 00000000..33d8c4f1 --- /dev/null +++ b/.changeset/calm-parents-hide.md @@ -0,0 +1,5 @@ +--- +"@elasticpath/react-shopper-hooks": minor +--- + +New interface for useCart diff --git a/.changeset/metal-queens-sing.md b/.changeset/metal-queens-sing.md new file mode 100644 index 00000000..ee30af26 --- /dev/null +++ b/.changeset/metal-queens-sing.md @@ -0,0 +1,5 @@ +--- +"@elasticpath/d2c-schematics": minor +--- + +fix shopper hooks version diff --git a/apps/composable-cli-docs/docs/products/PXM/how-to/storefront-show-products.mdx b/apps/composable-cli-docs/docs/products/PXM/how-to/storefront-show-products.mdx index 2d02c274..bb173c2c 100644 --- a/apps/composable-cli-docs/docs/products/PXM/how-to/storefront-show-products.mdx +++ b/apps/composable-cli-docs/docs/products/PXM/how-to/storefront-show-products.mdx @@ -103,4 +103,59 @@ This endpoint doesn't require any parameters, although it support additional opt ### Filtering Products -You can filter products by using the `filter` query parameter. For example, to filter products by category, you can use the `category` filter: \ No newline at end of file +You can filter products by using the `filter` query parameter. For example, to filter products by category, you can use the `category` filter: + +## Get Product by ID + +You can get a single product in your storefront using the [PXM catalog view (shopper) endpoints, get a product](https://elasticpath.dev/docs/pxm/catalogs/shopper-catalog/get-a-product). + + + + + + ```typescript + client.ShopperCatalog.Products.Get({productId: productId}).then((product) => { + console.log(product.id) + }) + ``` + + + + + + ```tsx + import { useProduct } from "@elasticpath/react-shopper-hooks" + + export default function Products() { + const { data: product, isLoading } = useProduct() + + return ( +
+ {isLoading && Loading...} + {product && {product.attributes.name}} +
+ ) + } + ``` + +
+ + + ```js + fetch("https://useast.api.elasticpath.com/catalog/products/${productId}", { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer XXXX" + } + }).then(response => response.json()) + .then(data => console.log(data)); + ``` + + +
+ +Returns the specified product from the catalog. The product must be in the live status. + +If you have multiple catalog rules defined, the rule that best matches the shopperʼs context is used to determine which catalog is retrieved. For information about how rules are matched, see [Resolving Catalog Rules](https://elasticpath.dev/docs/pxm/catalogs/shopper-catalog/catalog-shopper-overview#resolving-catalog-rules). + +You can see the parent nodes a product is associated with in the bread_crumbs and bread_crumb_nodes metadata for each product. This is useful if you want to improve how your shoppers search your store, for example. For more information, see [Catalog Releases Overview](https://elasticpath.dev/docs/pxm/catalogs/catalog-latest-release/overview). \ No newline at end of file diff --git a/apps/composable-cli-docs/package.json b/apps/composable-cli-docs/package.json index 2adfcbfb..48a59710 100644 --- a/apps/composable-cli-docs/package.json +++ b/apps/composable-cli-docs/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "start": "docusaurus start", + "start": "docusaurus start --port 3005", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/packages/d2c-schematics/utility/latest-versions/package.json b/packages/d2c-schematics/utility/latest-versions/package.json index 99f2a13f..868e8efe 100644 --- a/packages/d2c-schematics/utility/latest-versions/package.json +++ b/packages/d2c-schematics/utility/latest-versions/package.json @@ -4,8 +4,8 @@ "private": true, "dependencies": { "@moltin/sdk": "^27.0.0", - "@elasticpath/react-shopper-hooks": "^0.6.2", - "@elasticpath/shopper-common": "^0.2.1", + "@elasticpath/react-shopper-hooks": "0.6.2", + "@elasticpath/shopper-common": "0.2.1", "clsx": "^1.2.1", "cookies-next": "^4.0.0", "focus-visible": "^5.2.0", diff --git a/packages/react-shopper-hooks/src/account/account-provider.tsx b/packages/react-shopper-hooks/src/account/account-provider.tsx index e92f12d1..0e991134 100644 --- a/packages/react-shopper-hooks/src/account/account-provider.tsx +++ b/packages/react-shopper-hooks/src/account/account-provider.tsx @@ -1,9 +1,13 @@ import React, { createContext, ReactNode } from "react" -import { AccountMember } from "@moltin/sdk" +import Cookies from "js-cookie" +import { AccountCredentials } from "./types" +import { AccountTokenBase, AccountMember } from "@moltin/sdk" -interface AccountState { +export interface AccountState { accountCookieName: string profile: AccountMember | null + getSelectedAccountToken: () => AccountTokenBase | undefined + getAccountMemberTokens: () => Record | undefined } export const AccountProviderContext = createContext(null) @@ -19,9 +23,43 @@ export const AccountProvider = ({ children, accountCookieName = ACCOUNT_MEMBER_TOKEN_STR, }: AccountProviderProps) => { + function getParsedCookie(): AccountCredentials | undefined { + const cookie = Cookies.get(accountCookieName) + return cookie && JSON.parse(cookie) + } + + function getAccountMemberTokens(): + | Record + | undefined { + const parsedCookie = getParsedCookie() + + if (!parsedCookie) { + return undefined + } + + return parsedCookie.accounts + } + + function getSelectedAccountToken(): AccountTokenBase | undefined { + const parsedCookie = getParsedCookie() + + if (!parsedCookie) { + return undefined + } + + const token = parsedCookie.accounts[parsedCookie.selected] + + return token + } + return ( {children} diff --git a/packages/react-shopper-hooks/src/account/hooks/use-account-addresses.tsx b/packages/react-shopper-hooks/src/account/hooks/use-account-addresses.tsx new file mode 100644 index 00000000..b73478cb --- /dev/null +++ b/packages/react-shopper-hooks/src/account/hooks/use-account-addresses.tsx @@ -0,0 +1,37 @@ +import { useElasticPath } from "../../elasticpath" +import { UseQueryOptionsWrapper } from "../../types" +import { AccountAddress, ResourcePage } from "@moltin/sdk" +import { useQuery, UseQueryResult } from "@tanstack/react-query" +import { queryKeysFactory } from "../../shared/util/query-keys-factory" + +const ACCOUNT_ADDRESSES_QUERY_KEY = "account-addresses" as const + +export const accountAddressesQueryKeys = queryKeysFactory( + ACCOUNT_ADDRESSES_QUERY_KEY, +) +type AccountAddressesQueryKey = typeof accountAddressesQueryKeys + +export function useAccountAddresses( + accountId: string, + options?: UseQueryOptionsWrapper< + ResourcePage, + Error, + ReturnType + > & { ep?: { accountMemberToken?: string } }, +): Partial> & + Omit, Error>, "data"> { + const { client } = useElasticPath() + const { data, ...rest } = useQuery({ + queryKey: [...accountAddressesQueryKeys.list({ accountId })], + queryFn: () => + client.AccountAddresses.All({ + account: accountId, + ...(options?.ep?.accountMemberToken && { + token: options.ep.accountMemberToken, + }), + }), + ...options, + }) + + return { ...data, ...rest } as const +} diff --git a/packages/react-shopper-hooks/src/account/hooks/use-account-member.tsx b/packages/react-shopper-hooks/src/account/hooks/use-account-member.tsx index 617ba8d9..4c1361d2 100644 --- a/packages/react-shopper-hooks/src/account/hooks/use-account-member.tsx +++ b/packages/react-shopper-hooks/src/account/hooks/use-account-member.tsx @@ -1,15 +1,13 @@ import { useElasticPath } from "../../elasticpath/elasticpath" import { UseQueryOptionsWrapper } from "../../types" import { AccountMember, Resource } from "@moltin/sdk" -import { useQuery } from "@tanstack/react-query" +import { useQuery, UseQueryResult } from "@tanstack/react-query" import { queryKeysFactory } from "../../shared/util/query-keys-factory" -import { UseQueryResult } from "@tanstack/react-query/src/types" const ACCOUNT_MEMBER_QUERY_KEY = "account-member" as const export const accountMemberQueryKeys = queryKeysFactory(ACCOUNT_MEMBER_QUERY_KEY) type AccountMemberQueryKey = typeof accountMemberQueryKeys -type Temp = UseQueryResult | undefined, Error> export function useAccountMember( id: string, diff --git a/packages/react-shopper-hooks/src/account/hooks/use-authed-account-member.tsx b/packages/react-shopper-hooks/src/account/hooks/use-authed-account-member.tsx index 88e446d2..c9360c67 100644 --- a/packages/react-shopper-hooks/src/account/hooks/use-authed-account-member.tsx +++ b/packages/react-shopper-hooks/src/account/hooks/use-authed-account-member.tsx @@ -1,18 +1,23 @@ import { useAccountMember } from "./use-account-member" -import { useContext } from "react" +import { useContext, useEffect, useState } from "react" import { AccountProviderContext } from "../account-provider" import Cookies from "js-cookie" -import { - createCookieTokenStore, - resolveAccountMemberIdFromToken, -} from "../login-account" -import { useElasticPath } from "../../elasticpath/elasticpath" -import { AccountMember, Resource } from "@moltin/sdk" +import { AccountMember, AccountTokenBase, Resource } from "@moltin/sdk" import { UseQueryResult } from "@tanstack/react-query/src/types" +import { AccountCredentials } from "../types" export function useAuthedAccountMember(): Partial> & - Omit, Error>, "data"> { + Omit, Error>, "data"> & { + accountMemberTokens?: Record + selectedAccountToken?: AccountTokenBase + } { const ctx = useContext(AccountProviderContext) + const [accountMemberTokens, setAccountMemberTokens] = useState< + Record | undefined + >() + const [selectedAccountToken, setSelectedAccountToken] = useState< + AccountTokenBase | undefined + >() if (!ctx) { throw new Error( @@ -20,19 +25,26 @@ export function useAuthedAccountMember(): Partial> & ) } - const { client } = useElasticPath() - - const tokenStore = createCookieTokenStore(ctx.accountCookieName) - const authedAccountMemberId = resolveAccountMemberIdFromToken( - client, - tokenStore, - ) const accountCookie = Cookies.get(ctx.accountCookieName) + const parsedAccountCookie: AccountCredentials | undefined = + accountCookie && JSON.parse(accountCookie) + + const selectedAccount = + parsedAccountCookie?.accounts[parsedAccountCookie?.selected] - const result = useAccountMember(authedAccountMemberId ?? "", { - enabled: !!accountCookie && !!authedAccountMemberId, - ep: { accountMemberToken: accountCookie }, + const result = useAccountMember(parsedAccountCookie?.accountMemberId ?? "", { + enabled: !!accountCookie && !!parsedAccountCookie?.accountMemberId, + ep: { accountMemberToken: selectedAccount?.token }, }) - return { ...result } as const + useEffect(() => { + setAccountMemberTokens(ctx.getAccountMemberTokens()) + setSelectedAccountToken(ctx.getSelectedAccountToken()) + }, [result.data]) + + return { + ...result, + accountMemberTokens, + selectedAccountToken, + } as const } diff --git a/packages/react-shopper-hooks/src/account/index.ts b/packages/react-shopper-hooks/src/account/index.ts index e8c635f3..850ba992 100644 --- a/packages/react-shopper-hooks/src/account/index.ts +++ b/packages/react-shopper-hooks/src/account/index.ts @@ -1,3 +1,4 @@ export * from "./hooks/use-account-member" export * from "./hooks/use-authed-account-member" export * from "./account-provider" +export * from "./hooks/use-account-addresses" diff --git a/packages/react-shopper-hooks/src/account/login-account.ts b/packages/react-shopper-hooks/src/account/login-account.ts index d1954562..d2db435c 100644 --- a/packages/react-shopper-hooks/src/account/login-account.ts +++ b/packages/react-shopper-hooks/src/account/login-account.ts @@ -1,4 +1,4 @@ -import { AccountMember, Moltin, Resource } from "@moltin/sdk" +import { AccountMember, AccountTokenBase, Moltin, Resource } from "@moltin/sdk" import Cookies from "js-cookie" import jwtDecode from "jwt-decode" @@ -57,7 +57,12 @@ export async function resolveAccountMember( return undefined } - const decodedToken = token ? jwtDecode<{ sub?: string }>(token) : undefined + const parsedToken: AccountTokenBase & { account_member_id: string } = + JSON.parse(token) + + const decodedToken = parsedToken?.token + ? jwtDecode<{ sub?: string }>(parsedToken.token) + : undefined if (!decodedToken) { return undefined @@ -89,24 +94,3 @@ export async function resolveAccountMember( return undefined } } - -export function resolveAccountMemberIdFromToken( - client: Moltin, - tokenStore: TokenStore, -) { - const token = tokenStore.getToken() - - if (!token) { - return undefined - } - - const decodedToken = token ? jwtDecode<{ sub?: string }>(token) : undefined - - if (!decodedToken) { - return undefined - } - - const { sub: accountMemberId } = decodedToken - - return accountMemberId -} diff --git a/packages/react-shopper-hooks/src/account/types.ts b/packages/react-shopper-hooks/src/account/types.ts new file mode 100644 index 00000000..cdbcd85f --- /dev/null +++ b/packages/react-shopper-hooks/src/account/types.ts @@ -0,0 +1,7 @@ +import { AccountTokenBase } from "@moltin/sdk" + +export type AccountCredentials = { + accountMemberId: string + accounts: Record + selected: string +} diff --git a/packages/react-shopper-hooks/src/cart/cart-provider.tsx b/packages/react-shopper-hooks/src/cart/cart-provider.tsx index c9a4dc28..26f17498 100644 --- a/packages/react-shopper-hooks/src/cart/cart-provider.tsx +++ b/packages/react-shopper-hooks/src/cart/cart-provider.tsx @@ -1,98 +1,179 @@ -import React, { - createContext, - ReactNode, - useEffect, - useReducer, - useState, -} from "react" +import React, { createContext, ReactNode } from "react" import { Cart, CartIncluded, ResourceIncluded, - Moltin as EPCCClient, + CartItem, + CartItemsResponse, } from "@moltin/sdk" -import { CartAction, CartState } from "./types/cart-reducer-types" -import { cartReducer } from "./cart-reducer" -import { getCart } from "./service/cart" -import { getInitialState } from "./util/get-initial-cart-state" +import { CartState } from "./types/cart-types" +import { enhanceCartResponse } from "./util/enhance-cart-response" import { StoreEvent } from "../shared" -import { useStore } from "../store" +import { cartQueryKeys, useGetCart } from "./hooks/use-get-cart" +import { useUpdateCartItem } from "./hooks/use-update-cart-items" +import { useQueryClient } from "@tanstack/react-query" +import { useRemoveCartItem } from "./hooks/use-remove-cart-item" +import { + useAddBundleProductToCart, + useAddProductToCart, + useAddPromotionToCart, + useDeleteCartItems, +} from "./hooks" +import { useRemovePromotionCode } from "./hooks/use-remove-promotion" export const CartItemsContext = createContext< - | { - state: CartState - dispatch: (action: CartAction) => void - resolveCartId: () => string - client: EPCCClient + | ({ + state: CartState | undefined + cartId?: string emit?: (event: StoreEvent) => void - } + useScopedUpdateCartItem: () => ReturnType + useScopedRemoveCartItem: () => ReturnType + useScopedAddPromotion: () => ReturnType + useScopedRemovePromotion: () => ReturnType + useScopedAddProductToCart: () => ReturnType + useScopedAddBundleProductToCart: () => ReturnType< + typeof useAddBundleProductToCart + > + useClearCart: () => ReturnType + } & Omit, "data">) | undefined >(undefined) export interface CartProviderProps { children: ReactNode - client?: EPCCClient - resolveCartId: () => string - cart?: ResourceIncluded + cartId?: string + initialState?: { + cart?: ResourceIncluded + } emit?: (event: StoreEvent) => void } export function CartProvider({ - cart, + initialState, children, emit, - resolveCartId, - client: overrideClient, + cartId = "", }: CartProviderProps) { - const { client: storeClient } = useStore() + const queryClient = useQueryClient() - const [state, dispatch] = useReducer(cartReducer, getInitialState(cart)) - const [client] = useState(overrideClient ?? storeClient) + const { data: rawCartData, ...rest } = useGetCart(cartId, { + initialData: initialState?.cart, + }) - useEffect(() => { - if (state.kind === "uninitialised-cart-state") { - _initialiseCart(dispatch, resolveCartId, client, emit) - } - }, [state, dispatch, emit, client]) + async function invalidateCartQuery() { + return queryClient.invalidateQueries({ + queryKey: cartQueryKeys.detail(cartId), + }) + } + + function setCartQueryData(updatedData: CartItemsResponse) { + // Updates the cart items in the query cache + return queryClient.setQueryData( + cartQueryKeys.detail(cartId), + createCartItemsUpdater(updatedData.data), + ) + } + + const state = + rawCartData && + enhanceCartResponse({ + data: rawCartData, + included: rest.included, + }) + + const updateCartItem = () => + useUpdateCartItem(cartId, { + onSuccess: (updatedData) => { + setCartQueryData(updatedData) + invalidateCartQuery() + }, + }) + + const addProductToCart = () => + useAddProductToCart(cartId, { + onSuccess: (updatedData) => { + setCartQueryData(updatedData) + invalidateCartQuery() + }, + }) + + const removeCartItem = () => + useRemoveCartItem(cartId, { + onSuccess: (updatedData) => { + setCartQueryData(updatedData) + invalidateCartQuery() + }, + }) + + const addPromotion = () => + useAddPromotionToCart(cartId, { + onSuccess: (updatedData) => { + setCartQueryData(updatedData) + invalidateCartQuery() + }, + }) + + const removePromotion = () => + useRemovePromotionCode(cartId, { + onSuccess: (updatedData) => { + setCartQueryData(updatedData) + invalidateCartQuery() + }, + }) + + const addBundleItemToCart = () => + useAddBundleProductToCart(cartId, { + onSuccess: (updatedData) => { + setCartQueryData(updatedData) + invalidateCartQuery() + }, + }) + + const clearCart = () => + useDeleteCartItems(cartId, { + onSuccess: async (updatedData) => { + setCartQueryData(updatedData) + await invalidateCartQuery() + }, + }) return ( {children} ) } -async function _initialiseCart( - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - client: EPCCClient, - emit?: (event: StoreEvent) => void, -) { - const cartId = resolveCartId() - - dispatch({ - type: "initialise-cart", - }) - - const resp = await getCart(cartId, client) - - dispatch({ - type: "update-cart", - payload: { - id: resp.data.id, - meta: resp.data.meta, - items: resp.included?.items ?? [], - }, - }) - - if (emit) { - emit({ - type: "success", - scope: "cart", - action: "init", - message: "Initialised cart", - }) +function createCartItemsUpdater(updatedData: CartItem[]) { + return function cartItemsUpdater( + oldData: ResourceIncluded, + ) { + return { + ...oldData, + included: { + items: updatedData, + }, + } + } +} +export const useCart = () => { + const context = React.useContext(CartItemsContext) + if (!context) { + throw new Error("useCart must be used within a CartProvider") } + return context } diff --git a/packages/react-shopper-hooks/src/cart/cart-reducer.ts b/packages/react-shopper-hooks/src/cart/cart-reducer.ts deleted file mode 100644 index dbd8d0d2..00000000 --- a/packages/react-shopper-hooks/src/cart/cart-reducer.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Cart } from "@moltin/sdk" -import { - CartAction, - CartState, - CustomCartItem, - PresentCartState, - RegularCartItem, -} from "./types/cart-reducer-types" -import { groupCartItems } from "./util/group-cart-items" -import { isNonEmpty } from "@elasticpath/shopper-common" - -export function calculateCartNumbers( - meta?: Cart["meta"], -): Pick { - const { without_tax, with_tax } = meta?.display_price ?? {} - - if (!with_tax?.formatted) { - throw Error( - "Unexpected value was undefined: display_price.with_tax.formatted can't calculate cart numbers.", - ) - } - - if (!without_tax?.formatted) { - throw Error( - "Unexpected value was undefined: display_price.without_tax.formatted can't calculate cart numbers.", - ) - } - - return { - withTax: with_tax.formatted, - withoutTax: without_tax.formatted, - } -} - -export function cartReducer(state: CartState, action: CartAction): CartState { - switch (action.type) { - case "initialise-cart": { - if (state.kind !== "uninitialised-cart-state") { - return state - } - return { - kind: "loading-cart-state", - } - } - case "updating-cart": { - if ( - state.kind === "present-cart-state" || - state.kind === "empty-cart-state" - ) { - return { - kind: "updating-cart-state", - previousCart: state, - updateAction: action.payload.action, - } - } - return state - } - case "failed-cart-update": { - if (state.kind === "updating-cart-state") { - return state.previousCart - } - return state - } - case "update-cart": - if ( - state.kind !== "updating-cart-state" && - state.kind !== "loading-cart-state" - ) { - return state - } - const { id, meta, items } = action.payload - - if (!items || items.length < 1) { - return { - kind: "empty-cart-state", - id, - } - } - - const filteredItems = items.filter( - (item) => item.type === "cart_item" || item.type === "custom_item", - ) as (RegularCartItem | CustomCartItem)[] - - if (!isNonEmpty(filteredItems)) { - return { - kind: "empty-cart-state", - id, - } - } - - const groupedItems = groupCartItems(items) - return { - kind: "present-cart-state", - groupedItems: groupedItems, - id, - items: filteredItems, - ...calculateCartNumbers(meta), - } - default: - return state - } -} diff --git a/packages/react-shopper-hooks/src/cart/hooks/index.ts b/packages/react-shopper-hooks/src/cart/hooks/index.ts new file mode 100644 index 00000000..135b0575 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/index.ts @@ -0,0 +1,9 @@ +export * from "./use-get-cart" +export * from "./use-remove-cart-item" +export * from "./use-update-cart-items" +export * from "./use-add-product" +export * from "./use-add-promotion" +export * from "./use-add-bundle-product-to-cart" +export * from "./use-checkout" +export * from "./use-add-custom-item" +export * from "./use-delete-cart-items" diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-add-bundle-product-to-cart.ts b/packages/react-shopper-hooks/src/cart/hooks/use-add-bundle-product-to-cart.ts new file mode 100644 index 00000000..62f180e1 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-add-bundle-product-to-cart.ts @@ -0,0 +1,51 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { CartAdditionalHeaders, CartItemsResponse } from "@moltin/sdk" +import { useElasticPath } from "../../elasticpath" +import { SelectedOptions } from "../types/bundle.type" + +export type CartAddBundleProductReq = { + productId: string + selectedOptions: SelectedOptions + quantity?: number + data?: any + isSku?: boolean + token?: string + additionalHeaders?: CartAdditionalHeaders +} + +export const useAddBundleProductToCart = ( + cartId: string, + options?: UseMutationOptions< + CartItemsResponse, + Error, + CartAddBundleProductReq + >, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + productId, + quantity, + data, + selectedOptions, + isSku, + token, + additionalHeaders, + }) => { + return client.Cart(cartId).AddProduct( + productId, + quantity, + { + bundle_configuration: { + selected_options: selectedOptions, + }, + ...data, + }, + isSku, + token, + additionalHeaders, + ) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-add-custom-item.ts b/packages/react-shopper-hooks/src/cart/hooks/use-add-custom-item.ts new file mode 100644 index 00000000..c40726f4 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-add-custom-item.ts @@ -0,0 +1,31 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { CartItemsResponse } from "@moltin/sdk" +import { useElasticPath } from "../../elasticpath" + +export type CartAddCustomItemReq = { + type: "custom_item" + name: string + description?: string + sku?: string + quantity: number + price: { + amount: number + includes_tax: boolean + } + custom_inputs?: Record + shipping_group_id?: string + tax?: any[] +} + +export const useAddCustomItemToCart = ( + cartId: string, + options?: UseMutationOptions, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async (props) => { + return client.Cart(cartId).AddCustomItem(props) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-add-product.ts b/packages/react-shopper-hooks/src/cart/hooks/use-add-product.ts new file mode 100644 index 00000000..8fa74668 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-add-product.ts @@ -0,0 +1,34 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { CartAdditionalHeaders, CartItemsResponse } from "@moltin/sdk" + +type CartAddProductReq = { + productId: string + quantity?: number + data?: any + isSku?: boolean + token?: string + additionalHeaders?: CartAdditionalHeaders +} + +export const useAddProductToCart = ( + cartId: string, + options?: UseMutationOptions, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + productId, + quantity, + data, + isSku, + token, + additionalHeaders, + }) => { + return client + .Cart(cartId) + .AddProduct(productId, quantity, data, isSku, token, additionalHeaders) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-add-promotion.ts b/packages/react-shopper-hooks/src/cart/hooks/use-add-promotion.ts new file mode 100644 index 00000000..2f647fed --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-add-promotion.ts @@ -0,0 +1,21 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { CartItemsResponse } from "@moltin/sdk" + +type CartAddPromotionReq = { + code: string + token?: string +} + +export const useAddPromotionToCart = ( + cartId: string, + options?: UseMutationOptions, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ code, token }: CartAddPromotionReq) => { + return client.Cart(cartId).AddPromotion(code, token) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-checkout.ts b/packages/react-shopper-hooks/src/cart/hooks/use-checkout.ts new file mode 100644 index 00000000..8f56fcd2 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-checkout.ts @@ -0,0 +1,75 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { + Address, + CartAdditionalHeaders, + CheckoutCustomer, + CheckoutCustomerObject, + Order, + Resource, +} from "@moltin/sdk" + +export type UseCheckoutReq = { + customer: string | CheckoutCustomer | CheckoutCustomerObject + billingAddress: Partial
+ shippingAddress?: Partial
+ additionalHeaders?: CartAdditionalHeaders +} + +export const useCheckout = ( + cartId: string, + options?: UseMutationOptions, Error, UseCheckoutReq>, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + customer, + billingAddress, + shippingAddress, + additionalHeaders, + }: UseCheckoutReq) => { + return client + .Cart(cartId) + .Checkout(customer, billingAddress, shippingAddress, additionalHeaders) + }, + ...options, + }) +} + +export type UseCheckoutWithAccountReq = { + contact: string | CheckoutCustomer | CheckoutCustomerObject + billingAddress: Partial
+ token: string + shippingAddress?: Partial
+ additionalHeaders?: CartAdditionalHeaders +} + +export const useCheckoutWithAccount = ( + cartId: string, + options?: UseMutationOptions< + Resource, + Error, + UseCheckoutWithAccountReq + >, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + contact, + billingAddress, + shippingAddress, + token, + additionalHeaders, + }: UseCheckoutWithAccountReq) => { + return client.Cart(cartId).CheckoutWithAccountManagementToken( + contact, + billingAddress, + shippingAddress, + token, + // @ts-ignore TODO: Fix type definition in js-sdk + additionalHeaders, + ) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-delete-cart-items.ts b/packages/react-shopper-hooks/src/cart/hooks/use-delete-cart-items.ts new file mode 100644 index 00000000..d75db1f9 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-delete-cart-items.ts @@ -0,0 +1,22 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { CartItemsResponse } from "@moltin/sdk" + +type CartDeleteCartItemsReq = {} + +export const useDeleteCartItems = ( + cartId: string, + options?: UseMutationOptions< + CartItemsResponse, + Error, + CartDeleteCartItemsReq + >, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async () => { + return client.Cart(cartId).RemoveAllItems() + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-get-cart.ts b/packages/react-shopper-hooks/src/cart/hooks/use-get-cart.ts new file mode 100644 index 00000000..98aa9fca --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-get-cart.ts @@ -0,0 +1,28 @@ +import { useElasticPath } from "../../elasticpath/elasticpath" +import { UseQueryOptionsWrapper } from "../../types" +import type { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk" +import { useQuery, UseQueryResult } from "@tanstack/react-query" +import { queryKeysFactory } from "../../shared/util/query-keys-factory" + +const CARTS_QUERY_KEY = "carts" as const + +export const cartQueryKeys = queryKeysFactory(CARTS_QUERY_KEY) +type CartQueryKey = typeof cartQueryKeys + +export function useGetCart( + id: string, + options?: UseQueryOptionsWrapper< + ResourceIncluded, + Error, + ReturnType + >, +): Partial> & + Omit, Error>, "data"> { + const { client } = useElasticPath() + const { data, ...rest } = useQuery({ + queryKey: cartQueryKeys.detail(id), + queryFn: () => client.Cart(id).With("items").Get(), + ...options, + }) + return { ...data, ...rest } as const +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-remove-cart-item.ts b/packages/react-shopper-hooks/src/cart/hooks/use-remove-cart-item.ts new file mode 100644 index 00000000..886f47a0 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-remove-cart-item.ts @@ -0,0 +1,20 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { CartItemsResponse } from "@moltin/sdk" + +type CartUpdateReq = { + itemId: string +} + +export const useRemoveCartItem = ( + cartId: string, + options?: UseMutationOptions, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ itemId }: CartUpdateReq) => { + return client.Cart(cartId).RemoveItem(itemId) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-remove-promotion.ts b/packages/react-shopper-hooks/src/cart/hooks/use-remove-promotion.ts new file mode 100644 index 00000000..14d8bdbd --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-remove-promotion.ts @@ -0,0 +1,32 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { CartItemsResponse } from "@moltin/sdk" + +type CartRemovePromotionCodeReq = { + code: string +} + +export const useRemovePromotionCode = ( + cartId: string, + options?: UseMutationOptions< + CartItemsResponse, + Error, + CartRemovePromotionCodeReq + >, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ code }: CartRemovePromotionCodeReq) => { + return client.request.send( + `carts/${cartId}/discounts/${code}`, + "DELETE", + undefined, + undefined, + client, + undefined, + "v2", + ) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/hooks/use-update-cart-items.ts b/packages/react-shopper-hooks/src/cart/hooks/use-update-cart-items.ts new file mode 100644 index 00000000..81bffe00 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/hooks/use-update-cart-items.ts @@ -0,0 +1,21 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { CartItemsResponse } from "@moltin/sdk" + +type CartUpdateReq = { + itemId: string + quantity: number +} + +export const useUpdateCartItem = ( + cartId: string, + options?: UseMutationOptions, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ itemId, quantity }: CartUpdateReq) => { + return client.Cart(cartId).UpdateItem(itemId, quantity) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/cart/index.ts b/packages/react-shopper-hooks/src/cart/index.ts index d8261e29..b6e71cd1 100644 --- a/packages/react-shopper-hooks/src/cart/index.ts +++ b/packages/react-shopper-hooks/src/cart/index.ts @@ -1,4 +1,4 @@ export * from "./cart-provider" -export * from "./use-cart-hook" -export * from "./types/cart-reducer-types" -export * from "./util/get-present-cart-state" +export * from "./types/cart-types" +export * from "./hooks" +export * from "./util" diff --git a/packages/react-shopper-hooks/src/cart/service/cart.ts b/packages/react-shopper-hooks/src/cart/service/cart.ts deleted file mode 100644 index 9dfe0f18..00000000 --- a/packages/react-shopper-hooks/src/cart/service/cart.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { CartItemsResponse, Moltin as EPCCClient } from "@moltin/sdk" -import { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk" -import { SelectedOptions } from "../../cart/types/bundle.type" - -export async function removeCartItem( - id: string, - itemId: string, - client: EPCCClient, -): Promise { - return client.Cart(id).RemoveItem(itemId) -} - -export async function removeAllCartItems( - id: string, - client: EPCCClient, -): Promise { - return client.Cart(id).RemoveAllItems() -} - -export async function updateCartItem( - id: string, - productId: string, - quantity: number, - client: EPCCClient, -): Promise { - return client.Cart(id).UpdateItem(productId, quantity) -} - -export async function addPromotion( - id: string, - promoCode: string, - client: EPCCClient, -): Promise { - return client.Cart(id).AddPromotion(promoCode) -} - -export async function addProductToCart( - cartId: string, - productId: string, - quantity: number, - client: EPCCClient, -): Promise { - return client.Cart(cartId).AddProduct(productId, quantity) -} - -export async function addBundleProductToCart( - cartId: string, - productId: string, - selectedOptions: SelectedOptions, - quantity: number, - client: EPCCClient, -): Promise { - return client.Cart(cartId).AddProduct(productId, quantity, { - bundle_configuration: { - selected_options: selectedOptions, - }, - }) -} - -export interface CustomItemRequest { - type: "custom_item" - name: string - quantity: number - price: { - amount: number - includes_tax?: boolean - } - sku?: string - description?: string - custom_inputs?: Record -} - -export async function addCustomItemToCart( - cartId: string, - customItem: CustomItemRequest, - client: EPCCClient, -): Promise { - return client.Cart(cartId).AddCustomItem(customItem) -} - -export async function getCart( - cartId: string, - client: EPCCClient, -): Promise> { - return client.Cart(cartId).With("items").Get() -} diff --git a/packages/react-shopper-hooks/src/cart/service/checkout.ts b/packages/react-shopper-hooks/src/cart/service/checkout.ts deleted file mode 100644 index f5d41efe..00000000 --- a/packages/react-shopper-hooks/src/cart/service/checkout.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - Order, - PaymentRequestBody, - Address, - CheckoutCustomerObject, - Moltin as EPCCClient, - ConfirmPaymentResponse, -} from "@moltin/sdk"; - -export function checkout( - id: string, - customer: CheckoutCustomerObject, - billing: Partial
, - shipping: Partial
, - client: EPCCClient -): Promise<{ data: Order }> { - return client.Cart(id).Checkout(customer, billing, shipping); -} - -export function makePayment( - payment: PaymentRequestBody, - orderId: string, - client: EPCCClient -): Promise { - return client.Orders.Payment(orderId, payment); -} \ No newline at end of file diff --git a/packages/react-shopper-hooks/src/cart/types/cart-reducer-types.ts b/packages/react-shopper-hooks/src/cart/types/cart-types.ts similarity index 51% rename from packages/react-shopper-hooks/src/cart/types/cart-reducer-types.ts rename to packages/react-shopper-hooks/src/cart/types/cart-types.ts index faed5cc9..1ad0b338 100644 --- a/packages/react-shopper-hooks/src/cart/types/cart-reducer-types.ts +++ b/packages/react-shopper-hooks/src/cart/types/cart-types.ts @@ -1,13 +1,8 @@ import { Cart, CartItem } from "@moltin/sdk" -import { - DeepReadonly, - ReadonlyNonEmptyArray, -} from "@elasticpath/shopper-common" +import { DeepReadonly } from "@elasticpath/shopper-common" /** --------------------------------- Cart State --------------------------------- */ -interface CartStateBase {} - export type PromotionCartItem = DeepReadonly & { readonly type: "promotion_item" } @@ -18,10 +13,6 @@ export type RegularCartItem = DeepReadonly & { readonly type: "cart_item" } -export interface LoadingCartState extends CartStateBase { - readonly kind: "loading-cart-state" -} - /** * Cart items seperated into their respective groups by type property * cart_item, promotion_item and custom_item. @@ -43,91 +34,30 @@ export interface GroupedCartItems { readonly custom: CustomCartItem[] } -/** - * State the cart is in when there is no items in the cart - */ -export interface EmptyCartState extends CartStateBase { - readonly kind: "empty-cart-state" - - /** - * the cart id sometimes referred to as the reference - */ - readonly id: string -} - -/** - * State the cart is in before it has fetched the cart data - * this will happen if the provider is not given an initial cart state. - */ -export interface UninitialisedCartState extends CartStateBase { - readonly kind: "uninitialised-cart-state" -} - export type RefinedCartItem = RegularCartItem | CustomCartItem /** - * State the cart is in when there are items in the cart. cart_item or custom_item - * - * promotion_items can't exist in a cart without any other cart items. + * State of the cart. */ -export interface PresentCartState extends CartStateBase { - readonly kind: "present-cart-state" - - /** - * the cart id sometimes referred to as the reference - */ - readonly id: string - +export type CartState = { /** * items property is all items excluding promotion items */ - readonly items: ReadonlyNonEmptyArray + readonly items: ReadonlyArray /** - * Cart items grouped by their respective types cart_item, custom_item, promotion_item + * Extended cart properties */ - readonly groupedItems: GroupedCartItems - - /** - * Total of the cart including tax - */ - readonly withTax: string - - /** - * Total of the cart without tax - */ - readonly withoutTax: string -} + readonly __extended: { + /** + * Cart items grouped by their respective types cart_item, custom_item, promotion_item + */ + readonly groupedItems: GroupedCartItems + } +} & Cart type UpdatingAction = "add" | "remove" | "update" | "empty" | "checkout" -/** - * State the cart is in when there is an update actions in progress. - */ -export interface UpdatingCartState extends CartStateBase { - readonly kind: "updating-cart-state" - - /** - * State of the cart when the updating event was triggered. - */ - readonly previousCart: PresentCartState | EmptyCartState - - /** - * What type of update action is being performed during this update. - */ - readonly updateAction: UpdatingAction -} - -/** - * Representing a state the cart can be in. - */ -export type CartState = - | PresentCartState - | LoadingCartState - | UpdatingCartState - | EmptyCartState - | UninitialisedCartState - /** --------------------------------- Cart Actions --------------------------------- */ /** diff --git a/packages/react-shopper-hooks/src/cart/use-cart-hook.ts b/packages/react-shopper-hooks/src/cart/use-cart-hook.ts deleted file mode 100644 index 5c9c7199..00000000 --- a/packages/react-shopper-hooks/src/cart/use-cart-hook.ts +++ /dev/null @@ -1,547 +0,0 @@ -import { useContext } from "react" -import { - addBundleProductToCart, - addCustomItemToCart, - addProductToCart, - addPromotion, - CustomItemRequest, - removeAllCartItems, - removeCartItem, - updateCartItem, -} from "./service/cart" -import { CartItemsContext } from "./cart-provider" -import { CartAction, CartState } from "./types/cart-reducer-types" -import { - CartItemsResponse, - ConfirmPaymentResponse, - Moltin as EPCCClient, - Order, - OrderBillingAddress, - OrderShippingAddress, - PaymentRequestBody, -} from "@moltin/sdk" -import { StoreCartAction, StoreEvent } from "../shared/types/event-types" -import { StoreError } from "../shared/types/error-types" -import { checkout, makePayment } from "../cart/service/checkout" -import { SelectedOptions } from "../cart/types/bundle.type" - -export function useCart() { - const context = useContext(CartItemsContext) - - if (context === undefined) { - throw new Error("useCartItems must be used within a CartProvider") - } - - const { state, dispatch, emit, resolveCartId, client } = context - - return { - addProductToCart: _addProductToCart(client, dispatch, resolveCartId, emit), - addBundleProductToCart: _addBundleProductToCart( - client, - dispatch, - resolveCartId, - emit, - ), - removeCartItem: _removeCartItem(client, dispatch, resolveCartId, emit), - emptyCart: _emptyCart(client, dispatch, state, resolveCartId, emit), - addPromotionToCart: _addPromotionToCart( - client, - dispatch, - resolveCartId, - emit, - ), - updateCartItem: _updateCartItem(client, dispatch, resolveCartId, emit), - addCustomItemToCart: _addCustomItemToCart( - client, - dispatch, - resolveCartId, - emit, - ), - stripeIntent: _stripeIntent(dispatch, resolveCartId, client, emit), - checkout: _checkout(dispatch, resolveCartId, client, emit), - accountCheckout: _accountCheckout(dispatch, resolveCartId, client, emit), - isUpdatingCart: state.kind === "updating-cart-state", - state, - } -} - -function _stripeIntent( - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - client: EPCCClient, - _emit?: (event: StoreEvent) => void, -) { - return async ( - email: string, - shippingAddress: Partial, - sameAsShipping?: boolean, - billingAddress?: Partial, - ): Promise<{ data: Order }> => { - const cartId = resolveCartId() - - const customer = `${shippingAddress.first_name} ${shippingAddress.last_name}` - return await checkout( - cartId, - { - email, - name: customer, - }, - billingAddress && !sameAsShipping ? billingAddress : shippingAddress, - shippingAddress, - client, - ) - } -} - -function _checkout( - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - client: EPCCClient, - _emit?: (event: StoreEvent) => void, -) { - return async ( - email: string, - shippingAddress: Partial, - payment: PaymentRequestBody, - sameAsShipping?: boolean, - billingAddress?: Partial, - ): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "checkout" }, - }) - - const customer = `${shippingAddress.first_name} ${shippingAddress.last_name}` - const orderResponse = await checkout( - cartId, - { - email, - name: customer, - }, - billingAddress && !sameAsShipping ? billingAddress : shippingAddress, - shippingAddress, - client, - ) - - const paymentResponse = await makePayment( - payment, - orderResponse.data.id, - client, - ) - - const response = await removeAllCartItems(cartId, client) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - - return paymentResponse - } -} - -function _accountCheckout( - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - client: EPCCClient, - _emit?: (event: StoreEvent) => void, -) { - return async ( - name: string, - email: string, - token: string, - shippingAddress: Partial, - payment: PaymentRequestBody, - sameAsShipping?: boolean, - billingAddress?: Partial, - ): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "checkout" }, - }) - - const customer = `${shippingAddress.first_name} ${shippingAddress.last_name}` - - const orderResponse = await client - .Cart(cartId) - .CheckoutWithAccountManagementToken( - { - email, - name: customer, - }, - billingAddress && !sameAsShipping ? billingAddress : shippingAddress, - shippingAddress, - // @ts-ignore - token, // TODO types are once again wrong and the api of this method needs work we should be able to set the authed account onto the sdk - ) - - const paymentResponse = await makePayment( - payment, - orderResponse.data.id, - client, - ) - - const response = await removeAllCartItems(cartId, client) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - - return paymentResponse - } -} - -function _updateCartItem( - client: EPCCClient, - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async (itemId: string, quantity: number): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "update" }, - }) - - try { - const response = await updateCartItem(cartId, itemId, quantity, client) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - attemptEmitSuccess("update-cart", "Updated cart item", eventEmitter) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "update-cart", - "Failed to update product in cart", - eventEmitter, - ) - throw err - } - } -} - -function _addProductToCart( - client: EPCCClient, - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async (productId: string, quantity: number): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "add" }, - }) - - try { - const response = await addProductToCart( - cartId, - productId, - quantity, - client, - ) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - attemptEmitSuccess( - "add-to-cart", - `Added "${resolveProductName(response, productId)}" to cart`, - eventEmitter, - ) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "add-to-cart", - "Failed to add product to cart", - eventEmitter, - ) - throw err - } - } -} - -function _addBundleProductToCart( - client: EPCCClient, - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async ( - productId: string, - selectedOptions: SelectedOptions, - quantity: number, - ): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "add" }, - }) - - try { - const response = await addBundleProductToCart( - cartId, - productId, - selectedOptions, - quantity, - client, - ) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - attemptEmitSuccess( - "add-to-cart", - `Added "${resolveProductName(response, productId)}" to cart`, - eventEmitter, - ) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "add-to-cart", - "Failed to add product to cart", - eventEmitter, - ) - throw err - } - } -} - -function resolveProductName( - cartItems: CartItemsResponse, - productId: string, -): string { - const maybeProduct = cartItems.data.find((i) => i.product_id === productId) - if (maybeProduct) { - return maybeProduct.name - } - return "Unknown" -} - -function _addCustomItemToCart( - client: EPCCClient, - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async (customItem: CustomItemRequest): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "add" }, - }) - - try { - const response = await addCustomItemToCart(cartId, customItem, client) - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - attemptEmitSuccess( - "add-to-cart", - `Added ${customItem.name} to cart`, - eventEmitter, - ) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "add-to-cart", - "Failed to add custom item to cart", - eventEmitter, - ) - throw err - } - } -} - -function _addPromotionToCart( - client: EPCCClient, - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async (promoCode: string): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "add" }, - }) - - try { - const response = await addPromotion(cartId, promoCode, client) - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - attemptEmitSuccess("add-to-cart", "Added promotion to cart", eventEmitter) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "add-to-cart", - "Failed to add promotion to cart", - eventEmitter, - ) - throw err - } - } -} - -function _removeCartItem( - client: EPCCClient, - dispatch: (action: CartAction) => void, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async (itemId: string): Promise => { - const cartId = resolveCartId() - - dispatch({ - type: "updating-cart", - payload: { action: "remove" }, - }) - - try { - const response = await removeCartItem(cartId, itemId, client) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - attemptEmitSuccess( - "remove-from-cart", - `Removed item from cart`, - eventEmitter, - ) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "remove-from-cart", - "Failed to remove product from cart", - eventEmitter, - ) - throw err - } - } -} - -function _emptyCart( - client: EPCCClient, - dispatch: (action: CartAction) => void, - state: CartState, - resolveCartId: () => string, - eventEmitter?: (event: StoreEvent) => void, -) { - return async (): Promise => { - const cartId = resolveCartId() - - if (state.kind === "present-cart-state") { - dispatch({ - type: "updating-cart", - payload: { action: "empty" }, - }) - - try { - const response = await removeAllCartItems(cartId, client) - - dispatch({ - type: "update-cart", - payload: { id: cartId, meta: response.meta, items: response.data }, - }) - - attemptEmitSuccess("empty-cart", "Emptied cart", eventEmitter) - } catch (err) { - dispatch({ - type: "failed-cart-update", - }) - attemptEmitError( - err, - "empty-cart", - "Failed to empty cart", - eventEmitter, - ) - throw err - } - } - } -} - -function createError(err: unknown, msg: string): StoreError { - if (err instanceof Error) { - return { - type: "cart-store-error", - // @ts-ignore TODO - cause: new Error(msg, { cause: err }), - } - } - - return { - type: "cart-store-error", - cause: new Error(`${msg} - The cause of the error is unknown`), - } -} - -function attemptEmitError( - err: unknown, - action: StoreCartAction, - msg: string, - emitter?: (event: StoreEvent) => void, -): void { - if (emitter) { - emitter({ - type: "error", - scope: "cart", - message: msg, - action, - cause: createError(err, msg), - }) - } -} - -function attemptEmitSuccess( - action: StoreCartAction, - msg: string, - emitter?: (event: StoreEvent) => void, -): void { - if (emitter) { - emitter({ - type: "success", - scope: "cart", - message: msg, - action, - }) - } -} diff --git a/packages/react-shopper-hooks/src/cart/util/enhance-cart-response.ts b/packages/react-shopper-hooks/src/cart/util/enhance-cart-response.ts new file mode 100644 index 00000000..bd02f4e5 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/util/enhance-cart-response.ts @@ -0,0 +1,39 @@ +import { Cart, CartIncluded, CartItem, ResourceIncluded } from "@moltin/sdk" +import { CartState, RefinedCartItem } from "../../cart" +import { groupCartItems } from "./group-cart-items" + +export function enhanceCartResponse( + cart: ResourceIncluded, +): CartState { + const items = !!cart.included?.items + ? enhanceCartItems(cart.included.items) + : [] + + const groupedItems = groupCartItems(cart.included?.items ?? []) + + return { + items: items as ReadonlyArray, + __extended: { + groupedItems, + }, + ...cart.data, + } +} + +function sortItemByCreatedAsc(a: CartItem, b: CartItem) { + return ( + new Date(a.meta.timestamps.created_at).getTime() - + new Date(b.meta.timestamps.created_at).getTime() + ) +} + +export function enhanceCartItems(items: CartItem[]) { + const enhanced = + items + ?.filter( + (item) => item.type === "cart_item" || item.type === "custom_item", + ) + .sort(sortItemByCreatedAsc) ?? [] + + return enhanced as ReadonlyArray +} diff --git a/packages/react-shopper-hooks/src/cart/util/get-initial-cart-state.ts b/packages/react-shopper-hooks/src/cart/util/get-initial-cart-state.ts deleted file mode 100644 index 145485a8..00000000 --- a/packages/react-shopper-hooks/src/cart/util/get-initial-cart-state.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk" -import { CartState, CustomCartItem, RegularCartItem } from "../../cart" -import { isNonEmpty } from "@elasticpath/shopper-common" -import { groupCartItems } from "./group-cart-items" -import { calculateCartNumbers } from "../cart-reducer" - -export function getInitialState( - cart?: ResourceIncluded, -): CartState { - if (!cart) { - return { - kind: "uninitialised-cart-state", - } - } - - if (!cart.included?.items) { - return { - kind: "empty-cart-state", - id: cart.data.id, - } - } - - const items = cart.included.items.filter( - (item) => item.type === "cart_item" || item.type === "custom_item", - ) as (RegularCartItem | CustomCartItem)[] - - if (!isNonEmpty(items)) { - return { - kind: "empty-cart-state", - id: cart.data.id, - } - } - - const groupedItems = groupCartItems(cart.included.items) - return { - kind: "present-cart-state", - items, - groupedItems: groupedItems, - id: cart.data.id, - ...calculateCartNumbers(cart.data.meta), - } -} diff --git a/packages/react-shopper-hooks/src/cart/util/get-present-cart-state.ts b/packages/react-shopper-hooks/src/cart/util/get-present-cart-state.ts deleted file mode 100644 index e8e61844..00000000 --- a/packages/react-shopper-hooks/src/cart/util/get-present-cart-state.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CartState, PresentCartState } from "../../cart" - -export function getPresentCartState( - state: CartState, -): PresentCartState | undefined { - return state.kind === "present-cart-state" - ? state - : state.kind === "updating-cart-state" && - state.previousCart.kind === "present-cart-state" - ? state.previousCart - : undefined -} - -export function getPresentCartStateCheckout( - state: CartState, -): PresentCartState | undefined { - return state.kind === "present-cart-state" - ? state - : state.kind === "updating-cart-state" && - state.updateAction === "checkout" && - state.previousCart.kind === "present-cart-state" - ? state.previousCart - : undefined -} diff --git a/packages/react-shopper-hooks/src/cart/util/index.ts b/packages/react-shopper-hooks/src/cart/util/index.ts new file mode 100644 index 00000000..fc202909 --- /dev/null +++ b/packages/react-shopper-hooks/src/cart/util/index.ts @@ -0,0 +1,2 @@ +export * from "./enhance-cart-response" +export * from "./group-cart-items" diff --git a/packages/react-shopper-hooks/src/currency/hooks/index.ts b/packages/react-shopper-hooks/src/currency/hooks/index.ts new file mode 100644 index 00000000..aaee86ef --- /dev/null +++ b/packages/react-shopper-hooks/src/currency/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-currencies" diff --git a/packages/react-shopper-hooks/src/currency/hooks/use-currencies.tsx b/packages/react-shopper-hooks/src/currency/hooks/use-currencies.tsx new file mode 100644 index 00000000..c7fcf01f --- /dev/null +++ b/packages/react-shopper-hooks/src/currency/hooks/use-currencies.tsx @@ -0,0 +1,27 @@ +import { useElasticPath } from "../../elasticpath" +import { UseQueryOptionsWrapper } from "../../types" +import type { Currency, ResourcePage } from "@moltin/sdk" +import { useQuery, UseQueryResult } from "@tanstack/react-query" +import { queryKeysFactory } from "../../shared/util/query-keys-factory" + +const CURRENCY_QUERY_KEY = "currency" as const + +export const currencyQueryKeys = queryKeysFactory(CURRENCY_QUERY_KEY) +type CurrencyQueryKey = typeof currencyQueryKeys + +export function useCurrencies( + options?: UseQueryOptionsWrapper< + ResourcePage, + Error, + ReturnType + >, +): Partial> & + Omit, Error>, "data"> { + const { client } = useElasticPath() + const { data, ...rest } = useQuery({ + queryKey: currencyQueryKeys.list(), + queryFn: () => client.Currencies.All(), + ...options, + }) + return { ...data, ...rest } as const +} diff --git a/packages/react-shopper-hooks/src/currency/index.ts b/packages/react-shopper-hooks/src/currency/index.ts new file mode 100644 index 00000000..351e313d --- /dev/null +++ b/packages/react-shopper-hooks/src/currency/index.ts @@ -0,0 +1 @@ +export * from "./hooks" diff --git a/packages/react-shopper-hooks/src/index.ts b/packages/react-shopper-hooks/src/index.ts index 1c9bb9ce..12ebe77c 100644 --- a/packages/react-shopper-hooks/src/index.ts +++ b/packages/react-shopper-hooks/src/index.ts @@ -1,9 +1,11 @@ export * from "./cart" export * from "./event" export * from "./shared" -export * from "./payment-gateway-register" export * from "./store" export * from "./product" export * from "./account" export * from "./elasticpath" +export * from "./payments" +export * from "./currency" +export * from "./order" export * from "@elasticpath/shopper-common" diff --git a/packages/react-shopper-hooks/src/order/hooks/index.ts b/packages/react-shopper-hooks/src/order/hooks/index.ts new file mode 100644 index 00000000..be069fed --- /dev/null +++ b/packages/react-shopper-hooks/src/order/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-order-confirm" diff --git a/packages/react-shopper-hooks/src/order/hooks/use-order-confirm.ts b/packages/react-shopper-hooks/src/order/hooks/use-order-confirm.ts new file mode 100644 index 00000000..f4e9e059 --- /dev/null +++ b/packages/react-shopper-hooks/src/order/hooks/use-order-confirm.ts @@ -0,0 +1,29 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { ConfirmPaymentResponse, ConfirmPaymentBody } from "@moltin/sdk" + +export type UseOrderConfirmReq = { + orderId: string + transactionId: string + options: ConfirmPaymentBody +} + +export const useOrderConfirm = ( + options?: UseMutationOptions< + ConfirmPaymentResponse, + Error, + UseOrderConfirmReq + >, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ + orderId, + transactionId, + options, + }: UseOrderConfirmReq) => { + return client.Orders.Confirm(orderId, transactionId, options) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/order/index.ts b/packages/react-shopper-hooks/src/order/index.ts new file mode 100644 index 00000000..351e313d --- /dev/null +++ b/packages/react-shopper-hooks/src/order/index.ts @@ -0,0 +1 @@ +export * from "./hooks" diff --git a/packages/react-shopper-hooks/src/payment-gateway-register/index.ts b/packages/react-shopper-hooks/src/payment-gateway-register/index.ts deleted file mode 100644 index 9a5802b7..00000000 --- a/packages/react-shopper-hooks/src/payment-gateway-register/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./payment-gateway-provider" -export * from "./types/payment-gateway-reducer-types" -export * from "./use-payment-gateway" diff --git a/packages/react-shopper-hooks/src/payment-gateway-register/payment-gateway-provider.tsx b/packages/react-shopper-hooks/src/payment-gateway-register/payment-gateway-provider.tsx deleted file mode 100644 index fb263b58..00000000 --- a/packages/react-shopper-hooks/src/payment-gateway-register/payment-gateway-provider.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { createContext, ReactNode, useReducer } from "react" -import { PGRAction, PGRState } from "./types/payment-gateway-reducer-types" -import { pgrReducer } from "./payment-gateway-reducer" - -export const PGRContext = createContext< - { state: PGRState; dispatch: (action: PGRAction) => void } | undefined ->(undefined) - -interface PGRProviderProps { - children: ReactNode -} - -export function PGRProvider({ children }: PGRProviderProps) { - const [state, dispatch] = useReducer(pgrReducer, { - kind: "uninitialized-payment-gateway-register-state", - }) - - return ( - - {children} - - ) -} diff --git a/packages/react-shopper-hooks/src/payment-gateway-register/payment-gateway-reducer.ts b/packages/react-shopper-hooks/src/payment-gateway-register/payment-gateway-reducer.ts deleted file mode 100644 index 4c8cdda4..00000000 --- a/packages/react-shopper-hooks/src/payment-gateway-register/payment-gateway-reducer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PGRAction, PGRState } from "./types/payment-gateway-reducer-types"; - -export function pgrReducer(state: PGRState, action: PGRAction): PGRState { - switch (action.type) { - case "update-payment-gateway-register": { - const { type, resolvePayment } = action.payload; - return { - kind: "registered-payment-gateway-register-state", - resolvePayment, - type, - }; - } - default: - return state; - } -} diff --git a/packages/react-shopper-hooks/src/payment-gateway-register/types/payment-gateway-reducer-types.ts b/packages/react-shopper-hooks/src/payment-gateway-register/types/payment-gateway-reducer-types.ts deleted file mode 100644 index 0566f3de..00000000 --- a/packages/react-shopper-hooks/src/payment-gateway-register/types/payment-gateway-reducer-types.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PaymentRequestBody } from "@moltin/sdk"; - -/** --------------------------------- Payment Gateway Register (PGR) State --------------------------------- */ - -interface PaymentGatewayStateBase {} - -/** - * State the PGR is in when no gateways have been registered - */ -export interface UninitializedPGRState extends PaymentGatewayStateBase { - readonly kind: "uninitialized-payment-gateway-register-state"; -} - -export type SupportedGateway = "braintree" | "manual"; - -export type ResolvePaymentFunction = RegisteredPGRState["resolvePayment"]; - -/** - * State the PGR is in when a payment gateway has been registered and is ready to process payments - */ -export interface RegisteredPGRState extends PaymentGatewayStateBase { - readonly kind: "registered-payment-gateway-register-state"; - - /** - * the type of payment gateway that has been registered - */ - readonly type: SupportedGateway; - - /** - * this function when called will resolve the payment specification for the registered gateway - */ - readonly resolvePayment: () => - | Promise - | PaymentRequestBody; -} - -/** - * Representing a state the PGR can be in. - */ -export type PGRState = UninitializedPGRState | RegisteredPGRState; - -/** --------------------------------- Payment Gateway Register (PGR) Actions --------------------------------- */ - -/** - * Register the cart with updated items and meta - */ -export interface UpdatePGRAction { - type: "update-payment-gateway-register"; - payload: { - type: SupportedGateway; - resolvePayment: ResolvePaymentFunction; - }; -} - -/** - * Actions that can be performed to change the state of the cart - */ -export type PGRAction = UpdatePGRAction; diff --git a/packages/react-shopper-hooks/src/payment-gateway-register/use-payment-gateway.ts b/packages/react-shopper-hooks/src/payment-gateway-register/use-payment-gateway.ts deleted file mode 100644 index e109389a..00000000 --- a/packages/react-shopper-hooks/src/payment-gateway-register/use-payment-gateway.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useContext } from "react" -import { PGRContext } from "./payment-gateway-provider" -import { - PGRAction, - PGRState, - ResolvePaymentFunction, - SupportedGateway, -} from "./types/payment-gateway-reducer-types" - -export function usePaymentGateway(): { - registerGateway: ( - resolvePayment: ResolvePaymentFunction, - type: SupportedGateway, - ) => void - state: PGRState -} { - const context = useContext(PGRContext) - - if (context === undefined) { - throw new Error("usePaymentGateway must be used within a PGRProvider") - } - - const { state, dispatch } = context - - return { - registerGateway: _registerGateway(dispatch), - state, - } -} - -function _registerGateway(dispatch: (action: PGRAction) => void) { - return ( - resolvePayment: ResolvePaymentFunction, - type: SupportedGateway, - ): void => { - dispatch({ - type: "update-payment-gateway-register", - payload: { type, resolvePayment }, - }) - } -} diff --git a/packages/react-shopper-hooks/src/payments/hooks/index.ts b/packages/react-shopper-hooks/src/payments/hooks/index.ts new file mode 100644 index 00000000..3e371f94 --- /dev/null +++ b/packages/react-shopper-hooks/src/payments/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-payments" diff --git a/packages/react-shopper-hooks/src/payments/hooks/use-payments.ts b/packages/react-shopper-hooks/src/payments/hooks/use-payments.ts new file mode 100644 index 00000000..a2262c55 --- /dev/null +++ b/packages/react-shopper-hooks/src/payments/hooks/use-payments.ts @@ -0,0 +1,20 @@ +import { useMutation, UseMutationOptions } from "@tanstack/react-query" +import { useElasticPath } from "../../elasticpath" +import { ConfirmPaymentResponse, PaymentRequestBody } from "@moltin/sdk" + +export type UsePaymentsReq = { + orderId: string + payment: PaymentRequestBody +} + +export const usePayments = ( + options?: UseMutationOptions, +) => { + const { client } = useElasticPath() + return useMutation({ + mutationFn: async ({ orderId, payment }: UsePaymentsReq) => { + return client.Orders.Payment(orderId, payment) + }, + ...options, + }) +} diff --git a/packages/react-shopper-hooks/src/payments/index.ts b/packages/react-shopper-hooks/src/payments/index.ts new file mode 100644 index 00000000..351e313d --- /dev/null +++ b/packages/react-shopper-hooks/src/payments/index.ts @@ -0,0 +1 @@ +export * from "./hooks" diff --git a/packages/react-shopper-hooks/src/product/hooks/index.ts b/packages/react-shopper-hooks/src/product/hooks/index.ts new file mode 100644 index 00000000..a99982f1 --- /dev/null +++ b/packages/react-shopper-hooks/src/product/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-product" diff --git a/packages/react-shopper-hooks/src/product/hooks/use-product.ts b/packages/react-shopper-hooks/src/product/hooks/use-product.ts new file mode 100644 index 00000000..e3c3cad9 --- /dev/null +++ b/packages/react-shopper-hooks/src/product/hooks/use-product.ts @@ -0,0 +1,39 @@ +import { useElasticPath } from "../../elasticpath/elasticpath" +import { UseQueryOptionsWrapper } from "../../types" +import type { Moltin } from "@moltin/sdk" +import { useQuery, UseQueryResult } from "@tanstack/react-query" +import { queryKeysFactory } from "../../shared/util/query-keys-factory" +import { ProductResponse, ShopperCatalogResource } from "@moltin/sdk" + +const PRODUCTS_QUERY_KEY = "products" as const + +export const productQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY) +type ProductQueryKey = typeof productQueryKeys + +export type UseProductParams = NonNullable< + Parameters +>[0] + +export function useProduct( + params: UseProductParams, + options?: UseQueryOptionsWrapper< + ShopperCatalogResource, + Error, + ReturnType + >, +): Partial> & + Omit, Error>, "data"> { + const { client } = useElasticPath() + + const { data, ...rest } = useQuery({ + queryKey: [...productQueryKeys.detail(params.productId)], + queryFn: () => + client.ShopperCatalog.Products.With([ + "main_image", + "component_products", + "files", + ]).Get(params), + ...options, + }) + return { ...data, ...rest } as const +} diff --git a/packages/react-shopper-hooks/src/product/index.ts b/packages/react-shopper-hooks/src/product/index.ts index cd1f8b72..33db85d8 100644 --- a/packages/react-shopper-hooks/src/product/index.ts +++ b/packages/react-shopper-hooks/src/product/index.ts @@ -1,3 +1,4 @@ export * from "./bundle" export * from "./simple" export * from "./variation" +export * from "./hooks" diff --git a/packages/react-shopper-hooks/src/store/store-provider.tsx b/packages/react-shopper-hooks/src/store/store-provider.tsx index 5a696dd2..9b36cc94 100644 --- a/packages/react-shopper-hooks/src/store/store-provider.tsx +++ b/packages/react-shopper-hooks/src/store/store-provider.tsx @@ -1,46 +1,36 @@ import { StoreProviderProps } from "./types/store-context-types" -import { PGRProvider } from "../payment-gateway-register/payment-gateway-provider" import { emitter } from "../event/event-context" import { CartProvider } from "../cart" -import React, { createContext, Dispatch, SetStateAction, useState } from "react" +import React, { createContext } from "react" import type { Moltin as EPCCClient } from "@moltin/sdk" import { NavigationNode } from "@elasticpath/shopper-common" +import { useElasticPath } from "../elasticpath" interface StoreState { client: EPCCClient - setClient: Dispatch> nav?: NavigationNode[] } export const StoreProviderContext = createContext(null) -// TODO give default options e.g. client and resolveCartId export const StoreProvider = ({ children, - storeContext, - resolveCartId, - client: initialClient, + initialState, + cartId, }: StoreProviderProps) => { - const [client, setClient] = useState(initialClient) + const { client } = useElasticPath() return ( - - - - {children} - - + + + {children} + ) } diff --git a/packages/react-shopper-hooks/src/store/types/store-context-types.ts b/packages/react-shopper-hooks/src/store/types/store-context-types.ts index 8a9e3614..afea61d4 100644 --- a/packages/react-shopper-hooks/src/store/types/store-context-types.ts +++ b/packages/react-shopper-hooks/src/store/types/store-context-types.ts @@ -1,30 +1,14 @@ -import { - Cart, - CartIncluded, - Moltin as EPCCClient, - ResourceIncluded, -} from "@moltin/sdk" +import type { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk" import { ReactNode } from "react" import { NavigationNode } from "@elasticpath/shopper-common" export interface StoreProviderProps { - storeContext?: StoreContext + initialState?: InitialState children: ReactNode - client: EPCCClient - resolveCartId: () => string + cartId?: string } -interface StoreContextBase { - nav: NavigationNode[] -} - -export interface StoreContextSSR extends StoreContextBase { - type: "store-context-ssr" +export interface InitialState { + nav?: NavigationNode[] cart?: ResourceIncluded } - -export interface StoreContextSSG extends StoreContextBase { - type: "store-context-ssg" -} - -export type StoreContext = StoreContextSSR | StoreContextSSG diff --git a/packages/react-shopper-hooks/src/stories/Cart.stories.tsx b/packages/react-shopper-hooks/src/stories/Cart.stories.tsx new file mode 100644 index 00000000..12a50130 --- /dev/null +++ b/packages/react-shopper-hooks/src/stories/Cart.stories.tsx @@ -0,0 +1,131 @@ +import { Meta } from "@storybook/react" +import React from "react" + +import Layout from "./components/Layout" +import { useGetCart } from "../cart/hooks/use-get-cart" +import { CartProvider, useCart } from "../cart" +import { CartItem } from "@moltin/sdk" + +const Cart = ({ showHookData, id }: { showHookData: boolean; id: string }) => { + const { data, isLoading } = useGetCart(id) // TODO add real token + return ( + +

Cart: {id}

+

{data?.id}

+
+ ) +} + +const meta: Meta = { + title: "Cart", + argTypes: { + showHookData: { + name: "Show hook data", + description: + "Whether or not story should display JSON of data returned from hook", + control: { + type: "boolean", + }, + defaultValue: true, + }, + }, + parameters: { + controls: { expanded: true }, + }, +} + +export default meta + +export const GetOne = (args: { showHookData: boolean; id: string }) => ( + +) + +GetOne.argTypes = { + id: { + control: { + type: "text", + }, + name: "cart id", + defaultValue: "f0341e8e-4962-4245-9748-aaa84615c2f6", + }, +} + +function Item({ item }: { item: CartItem }) { + const { useScopedUpdateCartItem, useScopedRemoveCartItem } = useCart() + const { mutate: mutateUpdate, isPending: isUpdateItemPending } = + useScopedUpdateCartItem() + const { mutate: mutateRemove, isPending: isRemovePending } = + useScopedRemoveCartItem() + return ( +
  • + {item.name} - {item.quantity} -{" "} + + + + {isUpdateItemPending &&

    Updating...

    } + {isRemovePending &&

    Removing...

    } +
  • + ) +} + +const CartUsingProvider = ({ + showHookData, + id, +}: { + showHookData: boolean + id: string +}) => { + const { state } = useCart() + return ( + +

    Cart: {id}

    +
      + {(state as any)?.items?.map((item: CartItem) => { + return + })} +
    +
    + ) +} + +export const CartProv = (args: { showHookData: boolean; id: string }) => ( + + + +) + +CartProv.argTypes = { + id: { + control: { + type: "text", + }, + name: "cart id", + defaultValue: "f0341e8e-4962-4245-9748-aaa84615c2f6", + }, +}