Skip to content

Instantly share code, notes, and snippets.

@kidunot89
Last active July 10, 2020 05:43
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kidunot89/9df76e788c7f72acd86a3b98b9107ff7 to your computer and use it in GitHub Desktop.
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.
// 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;
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)));
},
},
};
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;
// 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