Files

755 lines
18 KiB
Markdown
Raw Permalink Normal View History

<!--
title: "Using Cart Data with WooGraphQL"
description: "Learn how to retrieve and utilize cart data in your headless WooCommerce application with WPGraphQL for WooCommerce and WPGraphQL, enabling seamless shopping experiences for your customers."
keywords: "WooGraphQL, WPGraphQL, WooCommerce, GraphQL, cart data, headless, shopping experience"
author: "Geoff Taylor"
-->
# Using Cart Data
2023-07-31 15:28:39 -04:00
In this section, we will create a "/cart" page that displays a table of the items in the cart. We will use the `UserSessionProvider` created in the last section to pull the cart data using `useSession()`. The table will have four columns: `Product`, `Price`, `Quantity`, and `Total`. We will also create a cart totals table that displays the shipping totals, applied coupons, and the final total. Let's start by implementing the changes to the `useCartMutations` hook also created in the previous section.
## Prerequisites
- Basic knowledge of React and React Router.
- Familiarity with GraphQL and WPGraphQL.
- A setup WPGraphQL/WPGraphQL for WooCommerce backend.
2023-07-31 15:28:39 -04:00
- Read previous sections on [Routing By URI](routing-by-uri.md), [Using Product Data](using-product-data.md), and [Handling User Session and Using Cart Mutations](handing-user-session-and-using-cart-mutations).
## Step 0: Create `graphql.js` file
First, create a `graphql.js` file to store all the GraphQL queries and mutations:
```javascript
import { gql } from '@apollo/client';
export const CustomerContent = gql`
fragment CustomerContent on Customer {
id
sessionToken
shipping {
postcode
state
city
country
}
}
`;
export const ProductContentSlice = gql`
fragment ProductContentSlice on Product {
id
databaseId
name
slug
type
image {
id
sourceUrl(size: WOOCOMMERCE_THUMBNAIL)
altText
}
... on SimpleProduct {
price
regularPrice
soldIndividually
}
... on VariableProduct {
price
regularPrice
soldIndividually
}
}
`;
export const ProductVariationContentSlice = gql`
fragment ProductVariationContentSlice on ProductVariation {
id
databaseId
name
slug
image {
id
sourceUrl(size: WOOCOMMERCE_THUMBNAIL)
altText
}
price
regularPrice
}
`;
export const ProductContentFull = gql`
fragment ProductContentFull on Product {
id
databaseId
slug
name
type
description
shortDescription(format: RAW)
image {
id
sourceUrl
altText
}
galleryImages {
nodes {
id
sourceUrl(size: WOOCOMMERCE_THUMBNAIL)
altText
}
}
productTags(first: 20) {
nodes {
id
slug
name
}
}
attributes {
nodes {
id
attributeId
... on LocalProductAttribute {
name
options
variation
}
... on GlobalProductAttribute {
name
options
variation
}
}
}
... on SimpleProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
stockStatus
stockQuantity
soldIndividually
}
... on VariableProduct {
onSale
price
rawPrice: price(format: RAW)
regularPrice
salePrice
stockStatus
stockQuantity
soldIndividually
variations(first: 50) {
nodes {
id
databaseId
name
price
rawPrice: price(format: RAW)
regularPrice
salePrice
onSale
attributes {
nodes {
name
label
value
}
}
}
}
}
}
`;
export const VariationContent = gql`
fragment VariationContent on ProductVariation {
id
name
slug
price
regularPrice
salePrice
stockStatus
stockQuantity
onSale
image {
id
sourceUrl
altText
}
}
`;
export const CartItemContent = gql`
fragment CartItemContent on CartItem {
key
product {
node {
...ProductContentSlice
}
}
variation {
node {
...ProductVariationContentSlice
}
}
quantity
total
subtotal
subtotalTax
extraData {
key
value
}
}
`;
export const CartContent = gql`
fragment CartContent on Cart {
contents(first: 100) {
itemCount
nodes {
...CartItemContent
}
}
appliedCoupons {
code
discountAmount
discountTax
}
needsShippingAddress
availableShippingMethods {
packageDetails
supportsShippingCalculator
rates {
id
instanceId
methodId
label
cost
}
}
subtotal
subtotalTax
shippingTax
shippingTotal
total
totalTax
feeTax
feeTotal
discountTax
discountTotal
}
`;
export const GetProduct = gql`
query GetProduct($id: ID!, $idType: ProductIdTypeEnum) {
product(id: $id, idType: $idType) {
...ProductContentFull
}
}
`;
export const GetProductVariation = gql`
query GetProductVariation($id: ID!) {
productVariation(id: $id, idType: DATABASE_ID) {
...VariationContent
}
}
`;
export const GetCart = gql`
query GetCart($customerId: Int) {
cart {
...CartContent
}
customer(customerId: $customerId) {
...CustomerContent
}
}
`;
export const AddToCart = gql`
mutation AddToCart($productId: Int!, $variationId: Int, $quantity: Int, $extraData: String) {
addToCart(
input: {productId: $productId, variationId: $variationId, quantity: $quantity, extraData: $extraData}
) {
cart {
...CartContent
}
cartItem {
...CartItemContent
}
}
}
`;
export const UpdateCartItemQuantities = gql`
mutation UpdateCartItemQuantities($items: [CartItemQuantityInput]) {
updateItemQuantities(input: {items: $items}) {
cart {
...CartContent
}
items {
...CartItemContent
}
}
}
`;
export const RemoveItemsFromCart = gql`
mutation RemoveItemsFromCart($keys: [ID], $all: Boolean) {
removeItemsFromCart(input: {keys: $keys, all: $all}) {
cart {
...CartContent
}
cartItems {
...CartItemContent
}
}
}
`;
export const ApplyCouponToCart = gql`
mutation ApplyCouponToCart($code: String!) {
applyCoupon(input: {code: $code}) {
cart {
...CartContent
}
}
}
`;
export const RemoveCouponFromCart = gql`
mutation RemoveCouponFromCart($code: String!) {
removeCoupons(input: {codes: [$code]}) {
cart {
...CartContent
}
}
}
`;
export const RemoveCouponsFromCart = gql`
mutation RemoveCouponsFromCart($codes: [String!]) {
removeCoupons(input: {codes: $codes}) {
cart {
...CartContent
}
}
}
`;
export const SetShippingLocale = gql`
mutation SetShippingLocale($zip: String!, $state: String, $city: String, $country: CountriesEnum) {
updateCustomer(
input: {shipping: {postcode: $zip, country: $country, state: $state, city: $city}}
) {
customer {
...CustomerContent
}
}
}
`;
export const SetShippingMethod = gql`
mutation SetShippingMethod($shippingMethod: String!) {
updateShippingMethod(input: {shippingMethods: [$shippingMethod]}) {
cart {
...CartContent
}
}
}
`;
```
## Step 1: Update useCartMutations.js
Update the `useCartMutations` hook as follows:
```javascript
import { useMutation } from '@apollo/client';
import { useSession } from "./SessionProvider";
import {
AddToCart,
UpdateCartItemQuantities,
RemoveItemsFromCart,
ApplyCouponToCart,
RemoveCouponFromCart,
SetShippingLocale,
SetShippingMethod,
} from './graphql';
const useCartMutations = (
productId,
variationId,
extraData,
) => {
// ... rest of useCartMutations code.
const removeCartItemHelper = () => {
const item = findInCart(productId, variationId, extraData);
if (!item) {
throw new Error('Failed to find item in cart.');
}
removeCartItem({ variables: { keys: [item.key] } });
};
const updateQuantityHelper = (quantity) => {
const item = findInCart(productId, variationId, extraData);
if (!item) {
throw new Error('Failed to find item in cart.');
}
updateQuantity({ variables: { items: [{ key: item.key, quantity }] } });
};
const addToCartHelper = (pId, quantity, vId, e) => {
addToCart({
variables: {
productId: pId,
quantity,
variationId: vId,
extraData: e,
},
});
};
const store = {
adding,
updating,
removing,
quantityFound,
mutate,
addToCart: addToCartHelper,
removeCartItem: removeCartItemHelper,
updateQuantity: updateQuantityHelper,
};
return store;
};
```
This new code adds more helper functions like `removeCartItemHelper`, `updateQuantityHelper`, `addToCartHelper`, and updates the store to include these functions. With these helpers, we get access to the `add`, `update`, and `remove` cart mutations without the need to provide product details to the hook.
And now let's implement the `useOtherCartMutations` hook:
```javascript
export const useOtherCartMutations = () => {
const {
setCart,
setCustomer,
} = useSession();
const [applyCouponToCart, { loading: applyingCoupon }] = useMutation({
mutation: ApplyCouponToCart,
onCompleted({ applyCoupon: data }) {
if (data?.cart) {
setCart(data.cart as Cart);
}
},
});
const [removeCouponFromCart, { loading: removingCoupon }] = useMutation({
mutation: RemoveCouponFromCart,
onCompleted({ removeCoupons: data }) {
if (data?.cart) {
setCart(data.cart as Cart);
}
},
});
const [setShippingLocale, { loading: savingShippingLocale }] = useMutation({
mutation: SetShippingLocale,
onCompleted({ updateCustomer: data }) {
if (data?.customer) {
setCustomer(data.customer);
}
},
});
const [setShippingMethod, { loading: savingShippingMethod }] = useMutation({
mutation: SetShippingMethod,
onCompleted({ updateShippingMethod: data }) {
if (data?.cart) {
setCart(data.cart as Cart);
}
},
});
const applyCouponHelper = (code) => applyCouponToCart({ variables: { code } });
const removeCouponHelper = (code) => removeCouponFromCart({ variables: { code } });
const setShippingLocaleHelper = (input) => setShippingLocale({
variables: {
...input,
},
});
const setShippingMethodHelper = (shippingMethod) => setShippingMethod({
variables: { shippingMethod },
});
const store = {
applyingCoupon,
removingCoupon,
savingShippingInfo: savingShippingLocale || savingShippingMethod,
applyCoupon: applyCouponHelper,
removeCoupon: removeCouponHelper,
setShippingLocale: setShippingLocaleHelper,
setShippingMethod: setShippingMethodHelper,
};
return store;
};
```
2023-07-31 15:28:39 -04:00
This hook provides the helper callbacks for the other cart mutations. These mutations are the ones that effect the cart and not the cart items, at least not directly, like `applyCoupon`.
## Step 2: Create the `/cart` page
After making the necessary updates to `useCartMutations` and `useOtherCartMutations`, we can now create the `/cart` page. First, import the necessary components and hooks.
```javascript
import React, { useState } from 'react';
import { useSession } from './SessionProvider';
import { useCartMutations, useOtherCartMutations } from './useCartMutations';
import { ShippingInfo } from './ShippingInfo';
import { ApplyCouponForm } from './ApplyCouponForm';
```
2023-07-31 15:28:39 -04:00
You should see two imports that don't exist yet. Let's create them. `ShippingInfo.js` and `ApplyCouponForm.js` are components that will be used in the `CartPage` component to handle two particular actions:
First the `ShippingLocaleForm.js`.
```jsx
import React, { useState } from 'react';
import { useOtherCartMutations } from './useOtherCartMutations';
const ShippingInfo = () => {
const [country, setCountry] = useState('');
const [postalCode, setPostalCode] = useState('');
const { cart } = useSession();
const {
updateShippingLocale,
setShippingMethod,
savingShippingInfo,
savingShippingMethod,
} = useOtherCartMutations();
const handleSubmit = async (e) => {
e.preventDefault();
await updateShippingLocale({ country, postalCode });
};
const availableShippingRates = (cart?.availableShippingMethods || [])
.reduce(
(rates, nextPackage) => {
rates.push(...(nextPackage?.rates || []));
return rates;
},
[],
);
if (cart.needsShipping && !cart.needsShippingAddress) {
return (
<div>
<h4>Shipping</h4>
{availableShippingRates.map((shippingRate) => (
const { cost, id, label } = shippingRate;
<div key={id}>
<input
type="radio"
name="shipping-methods"
value={id}
disabled={savingShippingMethod}
onChange={(event) => setShippingMethod(event.target.value)}
/>
<label>
{`${label}: `}
<strong>{`$${cost}`}</strong>
</label>
</div>
))}
<p>Shipping Tax: {cart.shippingTax}</p>
<p>Shipping Total: {cart.shippingTotal}</p>
</div>
);
}
if (cart.needsShipping) {
return (
<form onSubmit={handleSubmit}>
<h4>Shipping Locale</h4>
<div>
<label htmlFor="country">Country:</label>
<input
type="text"
id="country"
name="country"
value={country}
onChange={(e) => setCountry(e.target.value)}
/>
</div>
<div>
<label htmlFor="postalCode">Postal Code:</label>
<input
type="text"
id="postalCode"
name="postalCode"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
/>
</div>
<button disabled={savingShippingInfo} type="submit">Update Shipping Locale</button>
</form>
);
}
return null;
};
export default ShippingInfo;
```
2023-07-31 15:28:39 -04:00
This component works by confirming the session shipping requirements and status before returning the proper output. If shipping is needed and a shipping address is set for the customer, the shipping rates are displayed for selection. If shipping is needed and no address is set, then a shipping address form is displayed to set the customer shipping address. If no shipping is needed, `null` is returned.
Simple enough, now the `ApplyCoupon.js`
```jsx
import React, { useState } from 'react';
import { useOtherCartMutations } from './useOtherCartMutations';
const ApplyCoupon = () => {
const [code, setCode] = useState('');
const { applyCoupon, removeCoupon } = useOtherCartMutations();
const handleSubmit = async (e) => {
e.preventDefault();
applyCoupon(code);
};
return (
<form onSubmit={handleSubmit}>
<label>
Apply Coupon:
<input type="text" value={code} onChange={(e) => setCode(e.target.value)} />
</label>
<button type="submit">Apply</button>
</form>
);
};
export default ApplyCouponForm;
```
Here we're providing a form to apply a coupon code, which calls the `applyCoupon` function from the `useOtherCartMutations` hook.
Now, onto the `CartPage` component:
```jsx
function CartPage () {
const { cart } = useSession();
const { applyCoupon } = useOtherCartMutations();
if (!cart) {
return <div>Loading...</div>;
}
const cartItems = cart.contents.nodes;
return (
<div>
<h2>Cart</h2>
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{cartItems.map((item) => {
const { key, product, variation, quantity, subtotal, total } = item;
const { node: productNode } = product;
const { node: variationNode } = variation || {};
const {
id: productId,
image,
name,
} = productNode;
const { id: variationId } = variationNode || {};
const cartMutations = useCartMutations(productId, variationId);
return (
<tr key={key}>
<td>
<button onClick={() => cartMutations.removeCartItem()}>Remove</button>
<img src={image.sourceUrl} alt={image.altText} />
<span>{name}</span>
</td>
<td>{subtotal}</td>
<td>
<input
type="number"
value={quantity}
onChange={(e) => cartMutations.updateQuantity(parseInt(e.target.value))}
/>
</td>
<td>{total}</td>
</tr>
);
})}
</tbody>
<tfoot>
<tr>
<td colSpan={4}>
<ApplyCoupon />
</td>
</tr>
</tfoot>
</table>
<div>
<h3>Cart Totals</h3>
<ShippingInfo />
<p>Subtotal: {cart.subtotal}</p>
{cart.appliedCoupons.map(({ code, discountAmount }) => (
<div key={code}>
<p>
Coupon: {code} - Discount: {discountAmount}
<span onClick={() => removeCoupon(code)} style={{ fontWeight: 'bold', color: 'red' }}>
X
</span>
</p>
</div>
))}
<p>Total: {cart.total}</p>
</div>
</div>
);
};
export default CartPage;
```
In the `CartPage` component, we first fetch the `cart` from the `SessionProvider`. If the cart is not available, we show a loading message. Once the cart is loaded, we display the cart items in a table format, allowing users to remove items or update the quantity.
2023-07-31 15:28:39 -04:00
Lastly, we display the cart's subtotal, applied coupons with their respective discounts and removal buttons, and follow that up with the cart's total.
Now, you can use the `CartPage` component in your app, allowing users to interact with the cart, apply coupons, and manage shipping options.
## Conclusion
2023-07-31 15:28:39 -04:00
With this you're essentially ready to develop a complete application. In the next couple sections we'll be exploring taking the user through checkout.