Last active
July 10, 2020 05:43
-
-
Save kidunot89/9df76e788c7f72acd86a3b98b9107ff7 to your computer and use it in GitHub Desktop.
useCartMutations - React Hook that use "@apollo-react-hooks", and "uuid" to bundle WooGraphQL cart mutations in a modular tool.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Node modules | |
import { ApolloClient } from 'apollo-client'; | |
import { HttpLink } from 'apollo-link-http'; | |
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory'; | |
import { ApolloLink } from 'apollo-link'; | |
import { onError } from 'apollo-link-error'; | |
import { get, isEmpty } from 'lodash'; | |
// Local imports | |
import { typeDefs, resolvers } from './schema'; | |
// Apollo Caching configuration | |
const cache = new InMemoryCache({ | |
dataIdFromObject: (object) => { | |
// eslint-disable-next-line | |
switch (object.__typename) { | |
case 'CartItem': | |
return object.key; | |
default: | |
return object.id || defaultDataIdFromObject(object); | |
} | |
}, | |
}); | |
// Authorization middleware | |
const middleware = new ApolloLink((operation, forward) => { | |
const token = localStorage.getItem('user-token'); | |
const session = localStorage.getItem('woo-session'); | |
const middlewareHeaders = {}; | |
middlewareHeaders.authorization = token ? `Bearer ${token}` : ''; | |
if (session) { | |
middlewareHeaders['woocommerce-session'] = session; | |
} | |
if (!isEmpty(middlewareHeaders)) { | |
operation.setContext(({ headers = {} }) => ({ | |
headers: { | |
...headers, | |
...middlewareHeaders, | |
}, | |
})); | |
} | |
return forward(operation); | |
}); | |
// Authorization afterware | |
const afterware = new ApolloLink((operation, forward) => forward(operation) | |
.map((response) => { | |
// Update session data. | |
const context = operation.getContext(); | |
const { response: { headers } } = context; | |
const session = headers.get('woocommerce-session'); | |
if (session) { | |
if (session === 'false') { | |
localStorage.removeItem('woo-session'); | |
} else if (localStorage.getItem('woo-session') !== session) { | |
localStorage.setItem('woo-session', headers.get('woocommerce-session')); | |
} | |
} | |
// Update token if changed. | |
const authToken = get(response, 'data.login.authToken'); | |
if (authToken && authToken !== localStorage.getItem('user-token')) { | |
localStorage.setItem('user-token', authToken); | |
} | |
return response; | |
})); | |
const onForbidden = onError(({ networkError }) => { | |
if (networkError && networkError.statusCode === 403) { | |
localStorage.clear(); | |
} | |
}); | |
const httpLink = new HttpLink({ uri: 'http://example.com/graphql' }); | |
const client = new ApolloClient({ | |
link: onForbidden.concat(middleware.concat(afterware.concat(httpLink))), | |
cache, | |
clientState: {}, | |
typeDefs, | |
resolvers, | |
}); | |
export default client; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { gql } from 'apollo-boost'; | |
import get from 'lodash/get'; | |
import find from 'lodash/find'; | |
import { GET_CART } from './use-cart-mutations'; | |
export const typeDefs = gql` | |
extend type Query { | |
isInCart(productId: Int!, variationId: Int): Boolean! | |
getCartItem(productId: Int!, variationId: Int): CartItem | |
} | |
`; | |
const cartItemFilter = (productId, variationId) => ({ product, variation }) => { | |
if (productId !== product.productId) { | |
return false; | |
} | |
if (variation && variationId !== variation.variationId) { | |
return false; | |
} | |
return true; | |
}; | |
export const resolvers = { | |
Query: { | |
isInCart: (_, { productId, variationId }, { cache }) => { | |
if ( ! cache.readQuery({ query: GET_CART }) ) { | |
return null; | |
} | |
const { cart } = cache.readQuery({ query: GET_CART }); | |
const items = get(cart, 'contents.nodes') || []; | |
const item = find(items, (cartItemFilter(productId, variationId))); | |
return !!item; | |
}, | |
getCartItem: (_, { productId, variationId }, { cache }) => { | |
if ( ! cache.readQuery({ query: GET_CART }) ) { | |
return null; | |
} | |
const { cart } = cache.readQuery({ query: GET_CART }); | |
const items = get(cart, 'contents.nodes') || []; | |
return find(items, (cartItemFilter(productId, variationId))); | |
}, | |
}, | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useMutation, useQuery } from '@apollo/react-hooks'; | |
import { gql } from 'apollo-boost'; | |
import v4 from 'uuid/v4'; | |
export const GET_CART = gql` | |
query getCart { | |
cart { | |
contents { | |
nodes { | |
key | |
product { | |
id | |
productId | |
name | |
description | |
type | |
onSale | |
price | |
regularPrice | |
salePrice | |
slug | |
averageRating | |
reviewCount | |
image { | |
id | |
sourceUrl | |
altText | |
} | |
galleryImages { | |
nodes { | |
id | |
sourceUrl | |
altText | |
} | |
} | |
defaultAttributes { | |
nodes { | |
id | |
attributeId | |
name | |
value | |
} | |
} | |
} | |
variation { | |
id | |
variationId | |
name | |
description | |
type | |
onSale | |
price | |
regularPrice | |
salePrice | |
image { | |
id | |
sourceUrl | |
altText | |
} | |
attributes { | |
nodes { | |
id | |
name | |
value | |
} | |
} | |
} | |
quantity | |
total | |
subtotal | |
subtotalTax | |
} | |
} | |
appliedCoupons { | |
nodes { | |
couponId | |
discountType | |
amount | |
dateExpiry | |
products { | |
nodes { | |
id | |
} | |
} | |
productCategories { | |
nodes { | |
id | |
} | |
} | |
} | |
} | |
subtotal | |
subtotalTax | |
shippingTax | |
shippingTotal | |
total | |
totalTax | |
feeTax | |
feeTotal | |
discountTax | |
discountTotal | |
} | |
} | |
`; | |
export const ADD_TO_CART = gql` | |
mutation ($input: AddToCartInput!) { | |
addToCart(input: $input) { | |
cartItem { | |
key | |
product { | |
id | |
productId | |
name | |
description | |
type | |
onSale | |
price | |
regularPrice | |
salePrice | |
slug | |
averageRating | |
reviewCount | |
image { | |
id | |
sourceUrl | |
altText | |
} | |
galleryImages { | |
nodes { | |
id | |
sourceUrl | |
altText | |
} | |
} | |
defaultAttributes { | |
nodes { | |
id | |
attributeId | |
name | |
value | |
} | |
} | |
} | |
variation { | |
id | |
variationId | |
name | |
description | |
type | |
onSale | |
price | |
regularPrice | |
salePrice | |
image { | |
id | |
sourceUrl | |
altText | |
} | |
attributes { | |
nodes { | |
id | |
attributeId | |
name | |
value | |
} | |
} | |
} | |
quantity | |
total | |
subtotal | |
subtotalTax | |
} | |
} | |
} | |
`; | |
export const UPDATE_ITEM_QUANTITIES = gql` | |
mutation ($input: UpdateItemQuantitiesInput!) { | |
updateItemQuantities(input: $input) { | |
items { | |
key | |
product { | |
id | |
productId | |
name | |
description | |
type | |
onSale | |
price | |
regularPrice | |
salePrice | |
slug | |
averageRating | |
reviewCount | |
image { | |
id | |
sourceUrl | |
altText | |
} | |
galleryImages { | |
nodes { | |
id | |
sourceUrl | |
altText | |
} | |
} | |
defaultAttributes { | |
nodes { | |
id | |
attributeId | |
name | |
value | |
} | |
} | |
} | |
variation { | |
id | |
variationId | |
name | |
description | |
type | |
onSale | |
price | |
regularPrice | |
salePrice | |
image { | |
id | |
sourceUrl | |
altText | |
} | |
attributes { | |
nodes { | |
id | |
attributeId | |
name | |
value | |
} | |
} | |
} | |
quantity | |
total | |
subtotal | |
subtotalTax | |
} | |
removed { | |
key | |
product { | |
id | |
productId | |
} | |
variation { | |
id | |
variationId | |
} | |
} | |
updated { | |
key | |
product { | |
id | |
productId | |
} | |
variation { | |
id | |
variationId | |
} | |
} | |
} | |
} | |
`; | |
export const CHECK_CART = gql` | |
query checkCart($productId: Int!, $variationId: Int) { | |
isInCart(productId: $productId, variationId: $variationId) @client | |
getCartItem(productId: $productId, variationId: $variationId) @client | |
} | |
`; | |
function useCartMutations(productId, variationId) { | |
const { data: cartData } = useQuery(CHECK_CART, { variables: { productId, variationId } }); | |
const [addToCart] = useMutation(ADD_TO_CART, { | |
update(cache, { data: { addToCart: { cartItem } } }) { | |
const { cart } = cache.readQuery({ query: GET_CART }); | |
const { contents } = cart; | |
contents.nodes.push(cartItem); | |
cache.writeQuery({ | |
query: GET_CART, | |
data: { cart: { ...cart, contents } }, | |
}); | |
}, | |
refetchQueries: ({ data }) => { | |
const { product, variation } = data.addToCart.cartItem; | |
return [{ | |
query: CHECK_CART, | |
variables: { | |
productId: product.productId, | |
variationId: variation ? variation.variationId : null, | |
}, | |
}]; | |
}, | |
}); | |
const [updateItemQuantities] = useMutation(UPDATE_ITEM_QUANTITIES, { | |
update(cache, { data: { updateItemQuantities: { items } } }) { | |
const { cart } = cache.readQuery({ query: GET_CART }); | |
const contents = { ...cart.contents, nodes: items }; | |
cache.writeQuery({ | |
query: GET_CART, | |
data: { cart: { ...cart, contents } }, | |
}); | |
}, | |
refetchQueries: ({ data }) => { | |
const { updated, removed } = data.updateItemQuantities; | |
console.log(data); | |
const mapper = ({ product, variation }) => ({ | |
query: CHECK_CART, | |
variables: { | |
productId: product.productId, | |
variationId: variation ? variation.variationId : null, | |
}, | |
}); | |
return [ | |
...updated.map(mapper), | |
...removed.map(mapper), | |
]; | |
}, | |
}); | |
return { | |
itemInCart: cartData && cartData.isInCart ? cartData.getCartItem : false, | |
addToCart: (id, quantity, vId, variation, options = {}) => addToCart({ | |
variables: { | |
input: { | |
clientMutationId: v4(), | |
productId: id, | |
quantity, | |
variationId: vId, | |
variation, | |
}, | |
}, | |
...options, | |
}), | |
updateItemQuantities: (cartData && cartData.getCartItem) | |
? (quantity, options = {}) => updateItemQuantities({ | |
variables: { | |
input: { | |
clientMutationId: v4(), | |
items: [ | |
{ key: cartData.getCartItem.key, quantity }, | |
], | |
}, | |
}, | |
...options, | |
}) | |
: (items, options) => updateItemQuantities({ | |
variables: { | |
input: { | |
clientMutationId: v4(), | |
items, | |
}, | |
}, | |
...options, | |
}), | |
}; | |
} | |
export default useCartMutations; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// variable-cart-options.jsx | |
import React, { useState, useEffect } from 'react'; | |
import { useQuery } from '@apollo/react-hooks'; | |
import Button from '@kiwicom/orbit-components/lib/Button'; | |
import ListChoice from '@kiwicom/orbit-components/lib/ListChoice'; | |
import InputField from '@kiwicom/orbit-components/lib/InputField'; | |
import Remove from '@kiwicom/orbit-components/lib/icons/Remove'; | |
import Loading from '@kiwicom/orbit-components/lib/Loading'; | |
import Stack from '@kiwicom/orbit-components/lib/Stack'; | |
import get from 'lodash/get'; | |
import gql from 'graphql-tag'; | |
import PropTypes from 'prop-types'; | |
import styled from 'styled-components'; | |
import useCartMutations from 'use-cart-mutations'; | |
const Form = styled.form` | |
display: flex; | |
flex-direction: column; | |
`; | |
const GET_VARIATIONS = gql` | |
query($id: ID!) { | |
product(id: $id) { | |
variations { | |
nodes { | |
id | |
variationId | |
name | |
stockStatus | |
price | |
attributes { | |
nodes { | |
id | |
name | |
value | |
} | |
} | |
} | |
} | |
} | |
} | |
`; | |
/** | |
* Renders cart options form for "variable" products | |
* | |
* @param {string} id Relay global ID. | |
* @param {number} productId WP Product ID. | |
* @param {boolean} soldIndividually Whether this product such be sold individually or in bulk. | |
*/ | |
const VariableCartOptions = ({ id, productId, soldIndividually }) => { | |
const [quantity, setQuantity] = useState(1); | |
const [variationId, setVariationId] = useState(null); | |
const [variationAttributes, setAttributes] = useState(null); | |
const selectVariation = (variation) => { | |
setVariationId(variation.variationId); | |
const attributes = variation.attributes.nodes.map(({ name, value }) => ({ | |
attribute: name, | |
attributeTerm: value, | |
})); | |
setAttributes(attributes); | |
}; | |
const { data, error, loading } = useQuery(GET_VARIATIONS, { | |
variables: { id }, | |
onCompleted: (results) => selectVariation(get(results, 'product.variations.nodes[0]')), | |
}); | |
const { itemInCart, addToCart, updateItemQuantities } = useCartMutations(productId, variationId); | |
useEffect(() => { | |
console.log(itemInCart); | |
if (itemInCart) { | |
setQuantity(itemInCart.quantity); | |
} | |
}, [itemInCart]); | |
const onSubmit = (e) => { | |
e.preventDefault(); | |
if (itemInCart) { | |
updateItemQuantities(quantity); | |
} else { | |
addToCart(productId, quantity, variationId, variationAttributes); | |
} | |
}; | |
const removeItem = () => updateItemQuantities(0); | |
if (loading || variationId === null) { | |
return <Loading type="boxLoader" />; | |
} | |
if (error) { | |
return <div>{`Error! ${error.message}`}</div>; | |
} | |
const variations = data.product.variations.nodes; | |
return ( | |
<Form onSubmit={onSubmit}> | |
{variations.map((variation) => ( | |
<ListChoice | |
key={variation.id} | |
title={variation.name} | |
description={variation.price || undefined} | |
selectable={variation.stockStatus === 'IN_STOCK'} | |
selected={variationId === variation.variationId} | |
onClick={() => selectVariation(variation)} | |
/> | |
))} | |
{variations.length && ( | |
<> | |
<Stack flex={['0 0 auto', '1 1 70%', '1 0 20%']}> | |
{!soldIndividually && ( | |
<InputField | |
type="number" | |
value={quantity} | |
placeholder="Quantity" | |
onChange={(e) => setQuantity(e.target.value)} | |
/> | |
)} | |
{itemInCart ? ( | |
<> | |
<Button type="primary" submit>Update Quantity/Button> | |
<Button icon={<Remove />} type="critical" onClick={removeItem} /> | |
</> | |
) : ( | |
<Button type="primary" submit>Add To Cart</Button> | |
)} | |
</Stack> | |
</> | |
)} | |
</Form> | |
); | |
}; | |
VariableCartOptions.propTypes = { | |
id: PropTypes.string.isRequired, | |
productId: PropTypes.number.isRequired, | |
soldIndividually: PropTypes.bool, | |
}; | |
VariableCartOptions.defaultProps = { | |
soldIndividually: false, | |
}; | |
export default VariableCartOptions; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment