diff --git a/components/product/ProductSidebar/ProductSidebar.tsx b/components/product/ProductSidebar/ProductSidebar.tsx index fd1ef1e0a..f3dd8b87e 100644 --- a/components/product/ProductSidebar/ProductSidebar.tsx +++ b/components/product/ProductSidebar/ProductSidebar.tsx @@ -37,6 +37,7 @@ const ProductSidebar: FC = ({ product, className }) => { setLoading(false) } catch (err) { setLoading(false) + console.error(err) } } diff --git a/components/wishlist/WishlistButton/WishlistButton.tsx b/components/wishlist/WishlistButton/WishlistButton.tsx index a48eac170..4278eb2c3 100644 --- a/components/wishlist/WishlistButton/WishlistButton.tsx +++ b/components/wishlist/WishlistButton/WishlistButton.tsx @@ -31,8 +31,8 @@ const WishlistButton: FC = ({ const itemInWishlist = data?.items?.find( // @ts-ignore Wishlist is not always enabled (item) => - item.product_id === Number(productId) && - (item.variant_id as any) === Number(variant.id) + String(item.product_id) === String(productId) && + String(item.variant_id) === String(variant?.id) ) const handleWishlistChange = async (e: any) => { @@ -41,7 +41,7 @@ const WishlistButton: FC = ({ if (loading) return // A login is required before adding an item to the wishlist - if (!customer) { + if (!customer && process.env.COMMERCE_PROVIDER !== 'shopify') { setModalView('LOGIN_VIEW') return openModal() } diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index dfc1165c2..7e8719fdf 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -56,6 +56,7 @@ const WishlistCard: FC = ({ product }) => { setLoading(false) } catch (err) { setLoading(false) + console.error(err) } } diff --git a/framework/bigcommerce/cart/use-add-item.tsx b/framework/bigcommerce/cart/use-add-item.tsx index 1ac6ac6f8..bdad1862c 100644 --- a/framework/bigcommerce/cart/use-add-item.tsx +++ b/framework/bigcommerce/cart/use-add-item.tsx @@ -29,16 +29,18 @@ export const handler: MutationHook = { return data }, - useHook: ({ fetch }) => () => { - const { mutate } = useCart() + useHook: + ({ fetch }) => + () => { + const { mutate } = useCart() - return useCallback( - async function addItem(input) { - const data = await fetch({ input }) - await mutate(data, false) - return data - }, - [fetch, mutate] - ) - }, + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + await mutate(data, false) + return data + }, + [fetch, mutate] + ) + }, } diff --git a/framework/shopify/api/endpoints/checkout/checkout.ts b/framework/shopify/api/endpoints/checkout/checkout.ts index 0a9e83b68..3e9cf2b05 100644 --- a/framework/shopify/api/endpoints/checkout/checkout.ts +++ b/framework/shopify/api/endpoints/checkout/checkout.ts @@ -1,35 +1,15 @@ -import { - SHOPIFY_CHECKOUT_ID_COOKIE, - SHOPIFY_CHECKOUT_URL_COOKIE, - SHOPIFY_CUSTOMER_TOKEN_COOKIE, -} from '../../../const' -import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout' +import { SHOPIFY_CART_URL_COOKIE } from '../../../const' import type { CheckoutEndpoint } from '.' const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ req, res, - config, }) => { const { cookies } = req - const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE] - const customerCookie = cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE] + const cartUrl = cookies[SHOPIFY_CART_URL_COOKIE] - if (customerCookie) { - try { - await config.fetch(associateCustomerWithCheckoutMutation, { - variables: { - cartId: cookies[SHOPIFY_CHECKOUT_ID_COOKIE], - customerAccessToken: cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE], - }, - }) - } catch (error) { - console.error(error) - } - } - - if (checkoutUrl) { - res.redirect(checkoutUrl) + if (cartUrl) { + res.redirect(cartUrl) } else { res.redirect('/cart') } diff --git a/framework/shopify/api/index.ts b/framework/shopify/api/index.ts index 28c7d34b3..74fdb6987 100644 --- a/framework/shopify/api/index.ts +++ b/framework/shopify/api/index.ts @@ -8,7 +8,7 @@ import { API_URL, API_TOKEN, SHOPIFY_CUSTOMER_TOKEN_COOKIE, - SHOPIFY_CHECKOUT_ID_COOKIE, + SHOPIFY_CART_ID_COOKIE, } from '../const' import fetchGraphqlApi from './utils/fetch-graphql-api' @@ -34,7 +34,7 @@ const config: ShopifyConfig = { commerceUrl: API_URL, apiToken: API_TOKEN, customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE, - cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE, + cartCookie: SHOPIFY_CART_ID_COOKIE, cartCookieMaxAge: ONE_DAY * 30, fetch: fetchGraphqlApi, } diff --git a/framework/shopify/cart/use-add-item.tsx b/framework/shopify/cart/use-add-item.tsx index 204c98ce8..8cc91f70b 100644 --- a/framework/shopify/cart/use-add-item.tsx +++ b/framework/shopify/cart/use-add-item.tsx @@ -29,32 +29,34 @@ export const handler: MutationHook = { }) } + const lines = [ + { + merchandiseId: item.variantId, + quantity: item.quantity ?? 1, + }, + ] + let cartId = getCartId() if (!cartId) { - const { id } = await cartCreate(fetch) - cartId = id + const cart = await cartCreate(fetch, lines) + return normalizeCart(cart) + } else { + const { cartLinesAdd } = await fetch< + CartLinesAddMutation, + CartLinesAddMutationVariables + >({ + ...options, + variables: { + cartId, + lines, + }, + }) + + throwUserErrors(cartLinesAdd?.userErrors) + + return normalizeCart(cartLinesAdd?.cart) } - - const { cartLinesAdd } = await fetch< - CartLinesAddMutation, - CartLinesAddMutationVariables - >({ - ...options, - variables: { - cartId, - lineItems: [ - { - variantId: item.variantId, - quantity: item.quantity ?? 1, - }, - ], - }, - }) - - throwUserErrors(cartLinesAdd?.userErrors) - - return normalizeCart(cartLinesAdd?.cart) }, useHook: ({ fetch }) => diff --git a/framework/shopify/cart/use-cart.tsx b/framework/shopify/cart/use-cart.tsx index d2d814ea6..56c4da811 100644 --- a/framework/shopify/cart/use-cart.tsx +++ b/framework/shopify/cart/use-cart.tsx @@ -4,6 +4,13 @@ import useCommerceCart, { UseCart } from '@commerce/cart/use-cart' import { SWRHook } from '@commerce/utils/types' import getCartQuery from '../utils/queries/get-cart-query' import { GetCartHook } from '../types/cart' +import { + GetCartQuery, + GetCartQueryVariables, + QueryRoot, +} from '@framework/schema' +import { normalizeCart } from '@framework/utils' +import setCheckoutUrlCookie from '@framework/utils/set-checkout-url-cookie' export default useCommerceCart as UseCart @@ -12,7 +19,15 @@ export const handler: SWRHook = { query: getCartQuery, }, async fetcher({ input: { cartId }, options, fetch }) { - return cartId ? await fetch(options) : null + if (cartId) { + const { cart } = await fetch({ + ...options, + variables: { cartId }, + }) + setCheckoutUrlCookie(cart?.checkoutUrl) + return normalizeCart(cart) + } + return null }, useHook: ({ useData }) => @@ -25,7 +40,7 @@ export const handler: SWRHook = { Object.create(response, { isEmpty: { get() { - return (response.data?.lineItems.length ?? 0) <= 0 + return (response.data?.lineItems?.length ?? 0) <= 0 }, enumerable: true, }, diff --git a/framework/shopify/cart/use-remove-item.tsx b/framework/shopify/cart/use-remove-item.tsx index 8e295f873..e7e3c87a5 100644 --- a/framework/shopify/cart/use-remove-item.tsx +++ b/framework/shopify/cart/use-remove-item.tsx @@ -40,7 +40,7 @@ export const handler = { CartLinesRemoveMutationVariables >({ ...options, - variables: { cartId: getCartId(), lineItemIds: [itemId] }, + variables: { cartId: getCartId(), lineIds: [itemId] }, }) throwUserErrors(data.cartLinesRemove?.userErrors) diff --git a/framework/shopify/cart/use-update-item.tsx b/framework/shopify/cart/use-update-item.tsx index 3a7649eeb..d02f679e8 100644 --- a/framework/shopify/cart/use-update-item.tsx +++ b/framework/shopify/cart/use-update-item.tsx @@ -54,7 +54,7 @@ export const handler = { >({ ...options, variables: { - cartItems: getCartId(), + cartId: getCartId(), lines: [ { id: itemId, diff --git a/framework/shopify/commerce.config.json b/framework/shopify/commerce.config.json index b30ab39d9..f84484e09 100644 --- a/framework/shopify/commerce.config.json +++ b/framework/shopify/commerce.config.json @@ -1,6 +1,8 @@ { "provider": "shopify", "features": { - "wishlist": false + "wishlist": true, + "customerAuth": true, + "search": true } } diff --git a/framework/shopify/const.ts b/framework/shopify/const.ts index 07de5a8aa..9d6aaea15 100644 --- a/framework/shopify/const.ts +++ b/framework/shopify/const.ts @@ -1,7 +1,3 @@ -export const SHOPIFY_CHECKOUT_ID_COOKIE = 'shopify_checkoutId' - -export const SHOPIFY_CHECKOUT_URL_COOKIE = 'shopify_checkoutUrl' - export const SHOPIFY_CUSTOMER_TOKEN_COOKIE = 'shopify_customerToken' export const STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN @@ -13,3 +9,7 @@ export const API_URL = `https://${STORE_DOMAIN}/api/unstable/graphql.json` export const API_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN export const SHOPIFY_CART_ID_COOKIE = 'shopify_cartId' + +export const SHOPIFY_CART_URL_COOKIE = 'shopify_cartUrl' + +export const SHOPIFY_WHISLIST_ID_COOKIE = 'shopify_wishlistId' diff --git a/framework/shopify/provider.ts b/framework/shopify/provider.ts index bfa102ac8..c4715fa15 100644 --- a/framework/shopify/provider.ts +++ b/framework/shopify/provider.ts @@ -5,6 +5,10 @@ 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' @@ -19,6 +23,11 @@ export const shopifyProvider = { cartCookie: SHOPIFY_CART_ID_COOKIE, fetcher, cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + wishlist: { + useWishlist, + useAddItem: useWishlistAddItem, + useRemoveItem: useWishlistRemoveItem, + }, customer: { useCustomer }, products: { useSearch }, auth: { useLogin, useLogout, useSignup }, diff --git a/framework/shopify/schema.d.ts b/framework/shopify/schema.d.ts index f8a929652..b60141d63 100644 --- a/framework/shopify/schema.d.ts +++ b/framework/shopify/schema.d.ts @@ -974,7 +974,7 @@ export type CheckoutCreatePayload = { checkout?: Maybe /** The list of errors that occurred from executing the mutation. */ checkoutUserErrors: Array - /** The checkout queue token. */ + /** The checkout queue token. Available only to selected stores. */ queueToken?: Maybe /** * The list of errors that occurred from executing the mutation. @@ -6036,7 +6036,9 @@ export type AssociateCustomerWithCheckoutMutation = { > } -export type CartCreateMutationVariables = Exact<{ [key: string]: never }> +export type CartCreateMutationVariables = Exact<{ + input?: Maybe +}> export type CartCreateMutation = { __typename?: 'Mutation' } & { cartCreate?: Maybe< @@ -6217,6 +6219,62 @@ export type CustomerCreateMutation = { __typename?: 'Mutation' } & { > } +export type WishlistCreateMutationVariables = Exact<{ + input?: Maybe +}> + +export type WishlistCreateMutation = { __typename?: 'Mutation' } & { + cartCreate?: Maybe< + { __typename?: 'CartCreatePayload' } & { + cart?: Maybe<{ __typename?: 'Cart' } & WishlistDetailsFragment> + userErrors: Array< + { __typename?: 'CartUserError' } & Pick< + CartUserError, + 'code' | 'field' | 'message' + > + > + } + > +} + +export type WishlistLinesAddMutationVariables = Exact<{ + lines: Array | CartLineInput + cartId: Scalars['ID'] +}> + +export type WishlistLinesAddMutation = { __typename?: 'Mutation' } & { + cartLinesAdd?: Maybe< + { __typename?: 'CartLinesAddPayload' } & { + cart?: Maybe<{ __typename?: 'Cart' } & CartDetailsFragment> + userErrors: Array< + { __typename?: 'CartUserError' } & Pick< + CartUserError, + 'code' | 'field' | 'message' + > + > + } + > +} + +export type WishlistLinesRemoveMutationVariables = Exact<{ + cartId: Scalars['ID'] + lineIds: Array | Scalars['ID'] +}> + +export type WishlistLinesRemoveMutation = { __typename?: 'Mutation' } & { + cartLinesRemove?: Maybe< + { __typename?: 'CartLinesRemovePayload' } & { + cart?: Maybe<{ __typename?: 'Cart' } & CartDetailsFragment> + userErrors: Array< + { __typename?: 'CartUserError' } & Pick< + CartUserError, + 'code' | 'field' | 'message' + > + > + } + > +} + export type GetSiteCollectionsQueryVariables = Exact<{ first: Scalars['Int'] }> @@ -6335,16 +6393,46 @@ export type GetAllProductsQuery = { __typename?: 'QueryRoot' } & { export type CartDetailsFragment = { __typename?: 'Cart' } & Pick< Cart, - 'id' | 'createdAt' | 'updatedAt' + 'id' | 'checkoutUrl' | 'createdAt' | 'updatedAt' > & { lines: { __typename?: 'CartLineConnection' } & { edges: Array< { __typename?: 'CartLineEdge' } & { - node: { __typename?: 'CartLine' } & Pick & { + node: { __typename?: 'CartLine' } & Pick< + CartLine, + 'id' | 'quantity' + > & { merchandise: { __typename?: 'ProductVariant' } & Pick< ProductVariant, - 'id' - > + 'id' | 'sku' | 'title' + > & { + selectedOptions: Array< + { __typename?: 'SelectedOption' } & Pick< + SelectedOption, + 'name' | 'value' + > + > + image?: Maybe< + { __typename?: 'Image' } & Pick< + Image, + 'originalSrc' | 'altText' | 'width' | 'height' + > + > + priceV2: { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + compareAtPriceV2?: Maybe< + { __typename?: 'MoneyV2' } & Pick< + MoneyV2, + 'amount' | 'currencyCode' + > + > + product: { __typename?: 'Product' } & Pick< + Product, + 'title' | 'handle' + > + } } } > @@ -6379,31 +6467,7 @@ export type GetCartQueryVariables = Exact<{ }> export type GetCartQuery = { __typename?: 'QueryRoot' } & { - node?: Maybe< - | { __typename?: 'AppliedGiftCard' } - | { __typename?: 'Article' } - | { __typename?: 'Blog' } - | ({ __typename?: 'Cart' } & CartDetailsFragment) - | { __typename?: 'CartLine' } - | { __typename?: 'Checkout' } - | { __typename?: 'CheckoutLineItem' } - | { __typename?: 'Collection' } - | { __typename?: 'Comment' } - | { __typename?: 'ExternalVideo' } - | { __typename?: 'Location' } - | { __typename?: 'MailingAddress' } - | { __typename?: 'MediaImage' } - | { __typename?: 'Metafield' } - | { __typename?: 'Model3d' } - | { __typename?: 'Order' } - | { __typename?: 'Page' } - | { __typename?: 'Payment' } - | { __typename?: 'Product' } - | { __typename?: 'ProductOption' } - | { __typename?: 'ProductVariant' } - | { __typename?: 'ShopPolicy' } - | { __typename?: 'Video' } - > + cart?: Maybe<{ __typename?: 'Cart' } & CartDetailsFragment> } export type GetProductsFromCollectionQueryVariables = Exact<{ @@ -6596,3 +6660,50 @@ export type GetSiteInfoQueryVariables = Exact<{ [key: string]: never }> export type GetSiteInfoQuery = { __typename?: 'QueryRoot' } & { shop: { __typename?: 'Shop' } & Pick } + +export type WishlistDetailsFragment = { __typename?: 'Cart' } & Pick< + Cart, + 'id' | 'createdAt' | 'updatedAt' +> & { + lines: { __typename?: 'CartLineConnection' } & { + edges: Array< + { __typename?: 'CartLineEdge' } & { + node: { __typename?: 'CartLine' } & Pick< + CartLine, + 'id' | 'quantity' + > & { + merchandise: { __typename?: 'ProductVariant' } & Pick< + ProductVariant, + 'id' | 'sku' | 'title' + > & { + image?: Maybe< + { __typename?: 'Image' } & Pick< + Image, + 'originalSrc' | 'altText' | 'width' | 'height' + > + > + product: { __typename?: 'Product' } & Pick< + Product, + 'title' | 'handle' + > + } + } + } + > + } + attributes: Array< + { __typename?: 'Attribute' } & Pick + > + buyerIdentity: { __typename?: 'CartBuyerIdentity' } & Pick< + CartBuyerIdentity, + 'email' + > & { customer?: Maybe<{ __typename?: 'Customer' } & Pick> } + } + +export type GetWishlistQueryVariables = Exact<{ + cartId: Scalars['ID'] +}> + +export type GetWishlistQuery = { __typename?: 'QueryRoot' } & { + cart?: Maybe<{ __typename?: 'Cart' } & WishlistDetailsFragment> +} diff --git a/framework/shopify/schema.graphql b/framework/shopify/schema.graphql index 811779674..c9a8465b3 100644 --- a/framework/shopify/schema.graphql +++ b/framework/shopify/schema.graphql @@ -1806,7 +1806,7 @@ type CheckoutCreatePayload { checkoutUserErrors: [CheckoutUserError!]! """ - The checkout queue token. + The checkout queue token. Available only to selected stores. """ queueToken: String @@ -7496,7 +7496,7 @@ type Mutation { input: CheckoutCreateInput! """ - The checkout queue token. + The checkout queue token. Available only to selected stores. """ queueToken: String ): CheckoutCreatePayload diff --git a/framework/shopify/utils/cart-create.ts b/framework/shopify/utils/cart-create.ts index 40ac633cd..8b689cf44 100644 --- a/framework/shopify/utils/cart-create.ts +++ b/framework/shopify/utils/cart-create.ts @@ -1,42 +1,47 @@ import Cookies from 'js-cookie' - import { SHOPIFY_CART_ID_COOKIE, SHOPIFY_COOKIE_EXPIRE } from '../const' - import cartCreateMutation from './mutations/cart-create' import { CartCreateMutation, CartCreateMutationVariables, CartDetailsFragment, + CartLineInput, } from '../schema' + import { FetcherOptions } from '@commerce/utils/types' -import { CommerceError } from '@commerce/utils/errors' +import throwUserErrors from './throw-user-errors' +import setCheckoutUrlCookie from './set-checkout-url-cookie' export const cartCreate = async ( - fetch: (options: FetcherOptions) => Promise -): Promise => { + fetch: (options: FetcherOptions) => Promise, + lines?: Array | CartLineInput +): Promise => { const { cartCreate } = await fetch< CartCreateMutation, CartCreateMutationVariables >({ query: cartCreateMutation, + variables: { + input: { + lines, + }, + }, }) const cart = cartCreate?.cart + throwUserErrors(cartCreate?.userErrors) + if (cart?.id) { const options = { expires: SHOPIFY_COOKIE_EXPIRE, } Cookies.set(SHOPIFY_CART_ID_COOKIE, cart.id, options) - } else { - throw new CommerceError({ - errors: cartCreate?.userErrors?.map((e) => ({ - message: e.message, - })) ?? [{ message: 'Could not create cart' }], - }) } + setCheckoutUrlCookie(cart?.checkoutUrl) + return cart } diff --git a/framework/shopify/utils/get-wisthlist-id.ts b/framework/shopify/utils/get-wisthlist-id.ts new file mode 100644 index 000000000..165c86913 --- /dev/null +++ b/framework/shopify/utils/get-wisthlist-id.ts @@ -0,0 +1,8 @@ +import Cookies from 'js-cookie' +import { SHOPIFY_WHISLIST_ID_COOKIE } from '../const' + +const getWishlistId = (id?: string) => { + return id || Cookies.get(SHOPIFY_WHISLIST_ID_COOKIE) +} + +export default getWishlistId diff --git a/framework/shopify/utils/handle-fetch-response.ts b/framework/shopify/utils/handle-fetch-response.ts index 91d362d7d..2aece3e1f 100644 --- a/framework/shopify/utils/handle-fetch-response.ts +++ b/framework/shopify/utils/handle-fetch-response.ts @@ -7,6 +7,9 @@ export function getError(errors: any[] | null, status: number) { export async function getAsyncError(res: Response) { const data = await res.json() + + console.log(data) + return getError(data.errors, res.status) } diff --git a/framework/shopify/utils/index.ts b/framework/shopify/utils/index.ts index 1a879f4fd..bad865470 100644 --- a/framework/shopify/utils/index.ts +++ b/framework/shopify/utils/index.ts @@ -5,9 +5,11 @@ export { default as getBrands } from './get-brands' export { default as getCategories } from './get-categories' export { default as getCartId } from './get-cart-id' export { default as cartCreate } from './cart-create' +export { default as wishlistCreate } from './wishlist-create' export { default as handleLogin, handleAutomaticLogin } from './handle-login' export { default as handleAccountActivation } from './handle-account-activation' export { default as throwUserErrors } from './throw-user-errors' +export { default as getWishlistId } from './get-wisthlist-id' export * from './queries' export * from './mutations' diff --git a/framework/shopify/utils/mutations/cart-create.ts b/framework/shopify/utils/mutations/cart-create.ts index 000fde8b1..36a743908 100644 --- a/framework/shopify/utils/mutations/cart-create.ts +++ b/framework/shopify/utils/mutations/cart-create.ts @@ -1,10 +1,10 @@ -import { cartDetailsFragment } from '../queries/get-cart-query' +import { wishlistDetailsFragment } from '../queries/get-wishlist-query' const cartCreateMutation = /* GraphQL */ ` - mutation cartCreate { - cartCreate { + mutation cartCreate($input: CartInput = {}) { + cartCreate(input: $input) { cart { - id + ...wishlistDetails } userErrors { code @@ -13,6 +13,6 @@ const cartCreateMutation = /* GraphQL */ ` } } } - ${cartDetailsFragment} + ${wishlistDetailsFragment} ` export default cartCreateMutation diff --git a/framework/shopify/utils/mutations/cart-line-item-remove.ts b/framework/shopify/utils/mutations/cart-line-item-remove.ts index 861819733..53b9fab74 100644 --- a/framework/shopify/utils/mutations/cart-line-item-remove.ts +++ b/framework/shopify/utils/mutations/cart-line-item-remove.ts @@ -1,6 +1,6 @@ import { cartDetailsFragment } from '../queries/get-cart-query' -const cartLinesAddMutation = /* GraphQL */ ` +const cartLinesRemoveMutation = /* GraphQL */ ` mutation cartLinesRemove($cartId: ID!, $lineIds: [ID!]!) { cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { cart { @@ -15,4 +15,4 @@ const cartLinesAddMutation = /* GraphQL */ ` } ${cartDetailsFragment} ` -export default cartLinesAddMutation +export default cartLinesRemoveMutation diff --git a/framework/shopify/utils/mutations/index.ts b/framework/shopify/utils/mutations/index.ts index fafd1c52a..aac729742 100644 --- a/framework/shopify/utils/mutations/index.ts +++ b/framework/shopify/utils/mutations/index.ts @@ -1,8 +1,11 @@ export { default as customerCreateMutation } from './customer-create' export { default as cartCreateMutation } from './cart-create' +export { default as wishlistCreateMutation } from './wishlist-create' export { default as cartLineItemAddMutation } from './cart-line-item-add' export { default as cartLineItemUpdateMutation } from './cart-line-item-update' export { default as cartLineItemRemoveMutation } from './cart-line-item-remove' +export { default as wishlistLineItemAddMutation } from './wishlist-line-item-add' +export { default as wishlistLineItemRemoveMutation } from './wishlist-line-item-remove' export { default as customerAccessTokenCreateMutation } from './customer-access-token-create' export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete' export { default as customerActivateMutation } from './customer-activate' diff --git a/framework/shopify/utils/mutations/wishlist-create.ts b/framework/shopify/utils/mutations/wishlist-create.ts new file mode 100644 index 000000000..5a4941c55 --- /dev/null +++ b/framework/shopify/utils/mutations/wishlist-create.ts @@ -0,0 +1,18 @@ +import { wishlistDetailsFragment } from '../queries/get-wishlist-query' + +const wishlistCreateMutation = /* GraphQL */ ` + mutation wishlistCreate($input: CartInput = {}) { + cartCreate(input: $input) { + cart { + ...wishlistDetails + } + userErrors { + code + field + message + } + } + } + ${wishlistDetailsFragment} +` +export default wishlistCreateMutation diff --git a/framework/shopify/utils/mutations/wishlist-line-item-add.ts b/framework/shopify/utils/mutations/wishlist-line-item-add.ts new file mode 100644 index 000000000..606b2897e --- /dev/null +++ b/framework/shopify/utils/mutations/wishlist-line-item-add.ts @@ -0,0 +1,18 @@ +import { wishlistDetailsFragment } from '../queries/get-wishlist-query' + +const wishlistLinesAddMutation = /* GraphQL */ ` + mutation wishlistLinesAdd($lines: [CartLineInput!]!, $cartId: ID!) { + cartLinesAdd(lines: $lines, cartId: $cartId) { + cart { + ...wishlistDetails + } + userErrors { + code + field + message + } + } + } + ${wishlistDetailsFragment} +` +export default wishlistLinesAddMutation diff --git a/framework/shopify/utils/mutations/wishlist-line-item-remove.ts b/framework/shopify/utils/mutations/wishlist-line-item-remove.ts new file mode 100644 index 000000000..424efe8c5 --- /dev/null +++ b/framework/shopify/utils/mutations/wishlist-line-item-remove.ts @@ -0,0 +1,18 @@ +import { wishlistDetailsFragment } from '../queries/get-wishlist-query' + +const wishlistLinesRemoveMutation = /* GraphQL */ ` + mutation wishlistLinesRemove($cartId: ID!, $lineIds: [ID!]!) { + cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { + cart { + ...wishlistDetails + } + userErrors { + code + field + message + } + } + } + ${wishlistDetailsFragment} +` +export default wishlistLinesRemoveMutation diff --git a/framework/shopify/utils/normalize.ts b/framework/shopify/utils/normalize.ts index d466c3eb6..75638d86f 100644 --- a/framework/shopify/utils/normalize.ts +++ b/framework/shopify/utils/normalize.ts @@ -15,8 +15,11 @@ import { CartDetailsFragment, ProductVariantConnection, } from '../schema' + import { colorMap } from '@lib/colors' + import { CommerceError } from '@commerce/utils/errors' +import type { Wishlist } from '@commerce/types/wishlist' const money = ({ amount, currencyCode }: MoneyV2) => { return { @@ -134,7 +137,6 @@ export function normalizeCart( if (!cart) { throw new CommerceError({ message: 'Missing cart details' }) } - return { id: cart.id, customerId: cart.buyerIdentity?.customer?.id, @@ -143,11 +145,11 @@ export function normalizeCart( currency: { code: cart.estimatedCost?.totalAmount?.currencyCode, }, - taxesIncluded: !!cart.estimatedCost?.totalTaxAmount, + taxesIncluded: !!cart.estimatedCost?.totalTaxAmount?.amount, lineItems: cart.lines?.edges?.map(normalizeLineItem) ?? [], - lineItemsSubtotalPrice: +cart.estimatedCost?.totalAmount, - subtotalPrice: +cart.estimatedCost?.subtotalAmount, - totalPrice: +cart.estimatedCost?.totalAmount, + lineItemsSubtotalPrice: +cart.estimatedCost?.subtotalAmount?.amount, + subtotalPrice: +cart.estimatedCost?.subtotalAmount?.amount, + totalPrice: +cart.estimatedCost?.totalAmount?.amount, discounts: [], } } @@ -159,27 +161,58 @@ function normalizeLineItem({ }): LineItem { return { id, - variantId: String(variant?.id), - productId: String(variant?.id), - name: `${variant?.title}`, + variantId: variant?.id, + productId: variant?.id, + name: variant?.product?.title || variant?.title, quantity: quantity ?? 0, variant: { - id: String(variant?.id), + id: variant?.id, sku: variant?.sku ?? '', name: variant?.title!, image: { url: variant?.image?.originalSrc || '/product-img-placeholder.svg', }, requiresShipping: variant?.requiresShipping ?? false, - price: variant?.priceV2?.amount, + price: +variant?.priceV2?.amount, listPrice: variant?.compareAtPriceV2?.amount, }, - path: String(variant?.product?.handle), + path: variant?.product?.handle, discounts: [], options: variant?.title == 'Default Title' ? [] : variant?.selectedOptions, } } +export function normalizeWishlist( + cart: CartDetailsFragment | undefined | null +): Wishlist { + if (!cart) { + throw new CommerceError({ message: 'Missing cart details' }) + } + return { + items: + cart.lines?.edges?.map(({ node: { id, merchandise: variant } }: any) => ({ + id, + product_id: variant?.product?.id, + variant_id: variant?.id, + product: { + name: variant?.product?.title, + path: '/' + variant?.product?.handle, + description: variant?.product?.description, + images: [ + { + url: + variant?.image?.originalSrc || '/product-img-placeholder.svg', + }, + ], + variants: variant?.id ? [{ id: variant?.id }] : [], + amount: +variant?.priceV2?.amount, + baseAmount: +variant?.compareAtPriceV2?.amount, + currencyCode: variant?.priceV2?.currencyCode, + }, + })) ?? [], + } +} + export const normalizePage = ( { title: name, handle, ...page }: ShopifyPage, locale: string = 'en-US' diff --git a/framework/shopify/utils/queries/get-all-products-query.ts b/framework/shopify/utils/queries/get-all-products-query.ts index 179cf9812..767e30b1c 100644 --- a/framework/shopify/utils/queries/get-all-products-query.ts +++ b/framework/shopify/utils/queries/get-all-products-query.ts @@ -16,11 +16,30 @@ export const productConnectionFragment = /* GraphQL */ ` currencyCode } } - images(first: 1) { - pageInfo { - hasNextPage - hasPreviousPage + variants(first: 1) { + edges { + node { + id + title + sku + availableForSale + requiresShipping + selectedOptions { + name + value + } + priceV2 { + amount + currencyCode + } + compareAtPriceV2 { + amount + currencyCode + } + } } + } + images(first: 1) { edges { node { originalSrc diff --git a/framework/shopify/utils/queries/get-cart-query.ts b/framework/shopify/utils/queries/get-cart-query.ts index ff608eb9e..f56d441ba 100644 --- a/framework/shopify/utils/queries/get-cart-query.ts +++ b/framework/shopify/utils/queries/get-cart-query.ts @@ -8,9 +8,35 @@ export const cartDetailsFragment = /* GraphQL */ ` edges { node { id + quantity merchandise { ... on ProductVariant { id + id + sku + title + selectedOptions { + name + value + } + image { + originalSrc + altText + width + height + } + priceV2 { + amount + currencyCode + } + compareAtPriceV2 { + amount + currencyCode + } + product { + title + handle + } } } } @@ -49,7 +75,7 @@ export const cartDetailsFragment = /* GraphQL */ ` const getCartQuery = /* GraphQL */ ` query getCart($cartId: ID!) { - node(id: $cartId) { + cart(id: $cartId) { ...cartDetails } } diff --git a/framework/shopify/utils/queries/get-wishlist-query.ts b/framework/shopify/utils/queries/get-wishlist-query.ts new file mode 100644 index 000000000..d11dfcb8e --- /dev/null +++ b/framework/shopify/utils/queries/get-wishlist-query.ts @@ -0,0 +1,57 @@ +export const wishlistDetailsFragment = /* GraphQL */ ` + fragment wishlistDetails on Cart { + id + createdAt + updatedAt + lines(first: 10) { + edges { + node { + id + quantity + merchandise { + ... on ProductVariant { + id + id + sku + title + image { + originalSrc + altText + width + height + } + priceV2 { + amount + currencyCode + } + compareAtPriceV2 { + amount + currencyCode + } + product { + id + title + description + handle + } + } + } + } + } + } + attributes { + key + value + } + } +` + +const getWishlistQuery = /* GraphQL */ ` + query getWishlist($cartId: ID!) { + cart(id: $cartId) { + ...wishlistDetails + } + } + ${wishlistDetailsFragment} +` +export default getWishlistQuery diff --git a/framework/shopify/utils/queries/index.ts b/framework/shopify/utils/queries/index.ts index 7181c94ce..b14f01e77 100644 --- a/framework/shopify/utils/queries/index.ts +++ b/framework/shopify/utils/queries/index.ts @@ -5,6 +5,7 @@ export { default as getAllProductsPathtsQuery } from './get-all-products-paths-q export { default as getAllProductVendors } from './get-all-product-vendors-query' export { default as getCollectionProductsQuery } from './get-collection-products-query' export { default as getCartQuery } from './get-cart-query' +export { default as getWishlistQuery } from './get-wishlist-query' export { default as getAllPagesQuery } from './get-all-pages-query' export { default as getPageQuery } from './get-page-query' export { default as getCustomerQuery } from './get-customer-query' diff --git a/framework/shopify/utils/set-checkout-url-cookie.ts b/framework/shopify/utils/set-checkout-url-cookie.ts new file mode 100644 index 000000000..c999d854a --- /dev/null +++ b/framework/shopify/utils/set-checkout-url-cookie.ts @@ -0,0 +1,16 @@ +import Cookies from 'js-cookie' + +import { SHOPIFY_CART_URL_COOKIE, SHOPIFY_COOKIE_EXPIRE } from '../const' + +export const setCheckoutUrlCookie = (checkoutUrl: string) => { + if (checkoutUrl) { + const oldCookie = Cookies.get(SHOPIFY_CART_URL_COOKIE) + if (oldCookie !== checkoutUrl) { + Cookies.set(SHOPIFY_CART_URL_COOKIE, checkoutUrl, { + expires: SHOPIFY_COOKIE_EXPIRE, + }) + } + } +} + +export default setCheckoutUrlCookie diff --git a/framework/shopify/utils/wishlist-create.ts b/framework/shopify/utils/wishlist-create.ts new file mode 100644 index 000000000..0ec1a7c42 --- /dev/null +++ b/framework/shopify/utils/wishlist-create.ts @@ -0,0 +1,45 @@ +import Cookies from 'js-cookie' +import { SHOPIFY_COOKIE_EXPIRE, SHOPIFY_WHISLIST_ID_COOKIE } from '../const' +import wishlistCreateMutation from './mutations/wishlist-create' +import { FetcherOptions } from '@commerce/utils/types' + +import { + CartCreateMutation, + CartCreateMutationVariables, + CartDetailsFragment, + CartLineInput, +} from '../schema' + +import throwUserErrors from './throw-user-errors' + +export const wishlistCreate = async ( + fetch: (options: FetcherOptions) => Promise, + lines?: Array | CartLineInput +): Promise => { + const { cartCreate } = await fetch< + CartCreateMutation, + CartCreateMutationVariables + >({ + query: wishlistCreateMutation, + variables: { + input: { + lines, + }, + }, + }) + + const wishlist = cartCreate?.cart + + throwUserErrors(cartCreate?.userErrors) + + if (wishlist?.id) { + const options = { + expires: SHOPIFY_COOKIE_EXPIRE, + } + Cookies.set(SHOPIFY_WHISLIST_ID_COOKIE, wishlist.id, options) + } + + return wishlist +} + +export default wishlistCreate diff --git a/framework/shopify/wishlist/use-add-item.tsx b/framework/shopify/wishlist/use-add-item.tsx index 75f067c3a..ebb03b685 100644 --- a/framework/shopify/wishlist/use-add-item.tsx +++ b/framework/shopify/wishlist/use-add-item.tsx @@ -1,13 +1,69 @@ import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item' -export function emptyHook() { - const useEmptyHook = async (options = {}) => { - return useCallback(async function () { - return Promise.resolve() - }, []) - } +import type { AddItemHook } from '../types/wishlist' - return useEmptyHook +import { + getWishlistId, + normalizeCart, + normalizeWishlist, + throwUserErrors, + wishlistCreate, + wishlistLineItemAddMutation, +} from '../utils' + +import { CartLinesAddMutation, CartLinesAddMutationVariables } from '../schema' + +import useWishlist from './use-wishlist' +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + query: wishlistLineItemAddMutation, + }, + async fetcher({ input, options, fetch }) { + const lines = [ + { + merchandiseId: String(input.item.variantId || input.item.productId), + quantity: 1, + }, + ] + + let wishlistId = getWishlistId() + + if (!wishlistId) { + const cart = await wishlistCreate(fetch, lines) + return normalizeCart(cart) + } else { + const { cartLinesAdd } = await fetch< + CartLinesAddMutation, + CartLinesAddMutationVariables + >({ + ...options, + variables: { + cartId: wishlistId, + lines, + }, + }) + + throwUserErrors(cartLinesAdd?.userErrors) + + return normalizeWishlist(cartLinesAdd?.cart) + } + }, + useHook: + ({ fetch }) => + () => { + const { mutate } = useWishlist() + + return useCallback( + async function addItem(item) { + const data = await fetch({ input: { item } }) + await mutate(data, false) + return data + }, + [fetch, mutate] + ) + }, } - -export default emptyHook diff --git a/framework/shopify/wishlist/use-remove-item.tsx b/framework/shopify/wishlist/use-remove-item.tsx index a2d3a8a05..d82c0abea 100644 --- a/framework/shopify/wishlist/use-remove-item.tsx +++ b/framework/shopify/wishlist/use-remove-item.tsx @@ -1,17 +1,62 @@ import { useCallback } from 'react' +import type { HookFetcherContext, MutationHook } from '@commerce/utils/types' -type Options = { - includeProducts?: boolean +import useRemoveItem, { + UseRemoveItem, +} from '@commerce/wishlist/use-remove-item' +import type { RemoveItemHook } from '../types/wishlist' + +export default useRemoveItem as UseRemoveItem + +import { + getCartId, + getWishlistId, + normalizeWishlist, + throwUserErrors, +} from '../utils' + +import { + CartLinesRemoveMutation, + CartLinesRemoveMutationVariables, +} from '../schema' + +import useWishlist from './use-wishlist' +import wishlistLinesRemoveMutation from '../utils/mutations/wishlist-line-item-remove' + +export const handler: MutationHook = { + fetchOptions: { + query: wishlistLinesRemoveMutation, + }, + async fetcher({ + input: { itemId }, + options, + fetch, + }: HookFetcherContext) { + const { cartLinesRemove } = await fetch< + CartLinesRemoveMutation, + CartLinesRemoveMutationVariables + >({ + ...options, + variables: { cartId: getWishlistId(), lineIds: [itemId] }, + }) + + throwUserErrors(cartLinesRemove?.userErrors) + + return normalizeWishlist(cartLinesRemove?.cart) + }, + + useHook: + ({ fetch }) => + ({ wishlist } = {}) => { + const { revalidate } = useWishlist(wishlist) + + return useCallback( + async function removeItem(input) { + const data = await fetch({ input: { itemId: String(input.id) } }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, } - -export function emptyHook(options?: Options) { - const useEmptyHook = async ({ id }: { id: string | number }) => { - return useCallback(async function () { - return Promise.resolve() - }, []) - } - - return useEmptyHook -} - -export default emptyHook diff --git a/framework/shopify/wishlist/use-wishlist.tsx b/framework/shopify/wishlist/use-wishlist.tsx index d2ce9db5b..71c7c5b91 100644 --- a/framework/shopify/wishlist/use-wishlist.tsx +++ b/framework/shopify/wishlist/use-wishlist.tsx @@ -1,46 +1,52 @@ -// TODO: replace this hook and other wishlist hooks with a handler, or remove them if -// Shopify doesn't have a wishlist +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' +import type { GetWishlistHook } from '../types/wishlist' +import { getWishlistId, normalizeWishlist, getWishlistQuery } from '../utils' +import { GetCartQueryVariables, QueryRoot } from '../schema' -import { HookFetcher } from '@commerce/utils/types' -import { Product } from '../schema' +export default useWishlist as UseWishlist -const defaultOpts = {} +export const handler: SWRHook = { + fetchOptions: { + query: getWishlistQuery, + }, + async fetcher({ input: _input, options, fetch }) { + const wishListId = getWishlistId() -export type Wishlist = { - items: [ - { - product_id: number - variant_id: number - id: number - product: Product + if (wishListId) { + const { cart } = await fetch({ + ...options, + variables: { cartId: wishListId }, + }) + + return normalizeWishlist(cart) } - ] + + return null + }, + useHook: + ({ useData }) => + (input) => { + const response = useData({ + input: [], + swrOptions: { + revalidateOnFocus: false, + ...input?.swrOptions, + }, + }) + + return useMemo( + () => + Object.create(response, { + isEmpty: { + get() { + return (response.data?.items?.length || 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, } - -export interface UseWishlistOptions { - includeProducts?: boolean -} - -export interface UseWishlistInput extends UseWishlistOptions { - customerId?: number -} - -export const fetcher: HookFetcher = () => { - return null -} - -export function extendHook( - customFetcher: typeof fetcher, - // swrOptions?: SwrOptions - swrOptions?: any -) { - const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => { - return { data: null } - } - - useWishlist.extend = extendHook - - return useWishlist -} - -export default extendHook(fetcher)