Implement Cart API for cart & wishlish

This commit is contained in:
cond0r 2021-08-10 14:41:56 +03:00
parent 1d79007171
commit bc15597be9
35 changed files with 704 additions and 202 deletions

View File

@ -37,6 +37,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
setLoading(false)
} catch (err) {
setLoading(false)
console.error(err)
}
}

View File

@ -31,8 +31,8 @@ const WishlistButton: FC<Props> = ({
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<Props> = ({
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()
}

View File

@ -56,6 +56,7 @@ const WishlistCard: FC<Props> = ({ product }) => {
setLoading(false)
} catch (err) {
setLoading(false)
console.error(err)
}
}

View File

@ -29,7 +29,9 @@ export const handler: MutationHook<AddItemHook> = {
return data
},
useHook: ({ fetch }) => () => {
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(

View File

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

View File

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

View File

@ -29,13 +29,19 @@ export const handler: MutationHook<AddItemHook> = {
})
}
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
@ -43,18 +49,14 @@ export const handler: MutationHook<AddItemHook> = {
...options,
variables: {
cartId,
lineItems: [
{
variantId: item.variantId,
quantity: item.quantity ?? 1,
},
],
lines,
},
})
throwUserErrors(cartLinesAdd?.userErrors)
return normalizeCart(cartLinesAdd?.cart)
}
},
useHook:
({ fetch }) =>

View File

@ -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<typeof handler>
@ -12,7 +19,15 @@ export const handler: SWRHook<GetCartHook> = {
query: getCartQuery,
},
async fetcher({ input: { cartId }, options, fetch }) {
return cartId ? await fetch(options) : null
if (cartId) {
const { cart } = await fetch<QueryRoot, GetCartQueryVariables>({
...options,
variables: { cartId },
})
setCheckoutUrlCookie(cart?.checkoutUrl)
return normalizeCart(cart)
}
return null
},
useHook:
({ useData }) =>
@ -25,7 +40,7 @@ export const handler: SWRHook<GetCartHook> = {
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
return (response.data?.lineItems?.length ?? 0) <= 0
},
enumerable: true,
},

View File

@ -40,7 +40,7 @@ export const handler = {
CartLinesRemoveMutationVariables
>({
...options,
variables: { cartId: getCartId(), lineItemIds: [itemId] },
variables: { cartId: getCartId(), lineIds: [itemId] },
})
throwUserErrors(data.cartLinesRemove?.userErrors)

View File

@ -54,7 +54,7 @@ export const handler = {
>({
...options,
variables: {
cartItems: getCartId(),
cartId: getCartId(),
lines: [
{
id: itemId,

View File

@ -1,6 +1,8 @@
{
"provider": "shopify",
"features": {
"wishlist": false
"wishlist": true,
"customerAuth": true,
"search": true
}
}

View File

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

View File

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

View File

@ -974,7 +974,7 @@ export type CheckoutCreatePayload = {
checkout?: Maybe<Checkout>
/** The list of errors that occurred from executing the mutation. */
checkoutUserErrors: Array<CheckoutUserError>
/** The checkout queue token. */
/** The checkout queue token. Available only to selected stores. */
queueToken?: Maybe<Scalars['String']>
/**
* 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<CartInput>
}>
export type CartCreateMutation = { __typename?: 'Mutation' } & {
cartCreate?: Maybe<
@ -6217,6 +6219,62 @@ export type CustomerCreateMutation = { __typename?: 'Mutation' } & {
>
}
export type WishlistCreateMutationVariables = Exact<{
input?: Maybe<CartInput>
}>
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> | 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']> | 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<CartLine, 'id'> & {
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<Shop, 'name'>
}
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<Attribute, 'key' | 'value'>
>
buyerIdentity: { __typename?: 'CartBuyerIdentity' } & Pick<
CartBuyerIdentity,
'email'
> & { customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id'>> }
}
export type GetWishlistQueryVariables = Exact<{
cartId: Scalars['ID']
}>
export type GetWishlistQuery = { __typename?: 'QueryRoot' } & {
cart?: Maybe<{ __typename?: 'Cart' } & WishlistDetailsFragment>
}

View File

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

View File

@ -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: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>
): Promise<CartDetailsFragment> => {
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
lines?: Array<CartLineInput> | CartLineInput
): Promise<CartDetailsFragment | null | undefined> => {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
lines?: Array<CartLineInput> | CartLineInput
): Promise<CartDetailsFragment | null | undefined> => {
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

View File

@ -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'
import {
getWishlistId,
normalizeCart,
normalizeWishlist,
throwUserErrors,
wishlistCreate,
wishlistLineItemAddMutation,
} from '../utils'
import { CartLinesAddMutation, CartLinesAddMutationVariables } from '../schema'
import useWishlist from './use-wishlist'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
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 useEmptyHook
return useCallback(
async function addItem(item) {
const data = await fetch({ input: { item } })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}
export default emptyHook

View File

@ -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<typeof handler>
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<RemoveItemHook> = {
fetchOptions: {
query: wishlistLinesRemoveMutation,
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
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

View File

@ -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<typeof handler>
const defaultOpts = {}
export const handler: SWRHook<GetWishlistHook> = {
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<QueryRoot, GetCartQueryVariables>({
...options,
variables: { cartId: wishListId },
})
return normalizeWishlist(cart)
}
]
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
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 function extendHook(
customFetcher: typeof fetcher,
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
swrOptions?: any
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)