Skip to content

Commit

Permalink
feat(core): update add to cart visual errors (#1674)
Browse files Browse the repository at this point in the history
  • Loading branch information
chanceaclark authored Nov 22, 2024
1 parent ffefc61 commit 512c338
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-owls-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

Uses the API responses to show better errors when adding a product to the cart.
19 changes: 15 additions & 4 deletions core/app/[locale]/(default)/compare/_actions/add-to-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';

import { addCartLineItem } from '~/client/mutations/add-cart-line-item';
import { createCart } from '~/client/mutations/create-cart';
import {
addCartLineItem,
assertAddCartLineItemErrors,
} from '~/client/mutations/add-cart-line-item';
import { assertCreateCartErrors, createCart } from '~/client/mutations/create-cart';
import { getCart } from '~/client/queries/get-cart';
import { TAGS } from '~/client/tags';

Expand All @@ -20,7 +23,7 @@ export const addToCart = async (data: FormData) => {
cart = await getCart(cartId);

if (cart) {
cart = await addCartLineItem(cart.entityId, {
const addCartLineItemResponse = await addCartLineItem(cart.entityId, {
lineItems: [
{
productEntityId,
Expand All @@ -29,6 +32,10 @@ export const addToCart = async (data: FormData) => {
],
});

assertAddCartLineItemErrors(addCartLineItemResponse);

cart = addCartLineItemResponse.data.cart.addCartLineItems?.cart;

if (!cart?.entityId) {
return { status: 'error', error: 'Failed to add product to cart.' };
}
Expand All @@ -38,7 +45,11 @@ export const addToCart = async (data: FormData) => {
return { status: 'success', data: cart };
}

cart = await createCart([{ productEntityId, quantity: 1 }]);
const createCartResponse = await createCart([{ productEntityId, quantity: 1 }]);

assertCreateCartErrors(createCartResponse);

cart = createCartResponse.data.cart.createCart?.cart;

if (!cart?.entityId) {
return { status: 'error', error: 'Failed to add product to cart.' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { FragmentOf } from 'gql.tada';
import { AlertCircle, Check } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useTransition } from 'react';
import { useId, useTransition } from 'react';
import { toast } from 'react-hot-toast';

import { AddToCartButton } from '~/components/add-to-cart-button';
Expand All @@ -17,6 +17,7 @@ import { AddToCartFragment } from './fragment';
export const AddToCart = ({ data: product }: { data: FragmentOf<typeof AddToCartFragment> }) => {
const t = useTranslations('Compare.AddToCart');
const cart = useCart();
const toastId = useId();
const [isPending, startTransition] = useTransition();

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
Expand Down Expand Up @@ -47,7 +48,7 @@ export const AddToCart = ({ data: product }: { data: FragmentOf<typeof AddToCart
</span>
</div>
),
{ icon: <Check className="text-success-secondary" /> },
{ icon: <Check className="text-success-secondary" />, id: toastId },
);

startTransition(async () => {
Expand All @@ -56,8 +57,9 @@ export const AddToCart = ({ data: product }: { data: FragmentOf<typeof AddToCart
if (result.error) {
cart.decrement(quantity);

toast.error(t('error'), {
toast.error(result.error, {
icon: <AlertCircle className="text-error-secondary" />,
id: toastId,
});
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';

import { FragmentOf, graphql } from '~/client/graphql';
import { addCartLineItem } from '~/client/mutations/add-cart-line-item';
import { createCart } from '~/client/mutations/create-cart';
import {
addCartLineItem,
assertAddCartLineItemErrors,
} from '~/client/mutations/add-cart-line-item';
import { assertCreateCartErrors, createCart } from '~/client/mutations/create-cart';
import { getCart } from '~/client/queries/get-cart';
import { TAGS } from '~/client/tags';

Expand Down Expand Up @@ -138,7 +141,7 @@ export async function handleAddToCart(
cart = await getCart(cartId);

if (cart) {
cart = await addCartLineItem(cart.entityId, {
const addCartLineItemResponse = await addCartLineItem(cart.entityId, {
lineItems: [
{
productEntityId,
Expand All @@ -148,24 +151,31 @@ export async function handleAddToCart(
],
});

assertAddCartLineItemErrors(addCartLineItemResponse);

cart = addCartLineItemResponse.data.cart.addCartLineItems?.cart;

if (!cart?.entityId) {
return { status: 'error', error: 'Failed to add product to cart.' };
throw new Error('Failed to add product to cart.');
}

revalidateTag(TAGS.cart);

return { status: 'success', data: cart };
}

// Create cart
cart = await createCart([
const createCartResponse = await createCart([
{
productEntityId,
selectedOptions,
quantity,
},
]);

assertCreateCartErrors(createCartResponse);

cart = createCartResponse.data.cart.createCart?.cart;

if (!cart?.entityId) {
return { status: 'error', error: 'Failed to add product to cart.' };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
import { FragmentOf } from 'gql.tada';
import { AlertCircle, Check, Heart, ShoppingCart } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useId } from 'react';
import { FormProvider, useFormContext } from 'react-hook-form';
import { toast } from 'react-hot-toast';

Expand Down Expand Up @@ -63,6 +64,7 @@ export const ProductForm = ({ data: product }: Props) => {
const productOptions = removeEdgesAndNodes(product.productOptions);

const cart = useCart();
const toastId = useId();
const { handleSubmit, register, ...methods } = useProductForm();

const productFormSubmit = async (data: ProductFormData) => {
Expand Down Expand Up @@ -91,14 +93,15 @@ export const ProductForm = ({ data: product }: Props) => {
</span>
</div>
),
{ icon: <Check className="text-success-secondary" /> },
{ icon: <Check className="text-success-secondary" />, id: toastId },
);

const result = await handleAddToCart(data, product);

if (result.error) {
toast.error(t('error'), {
toast.error(result.error, {
icon: <AlertCircle className="text-error-secondary" />,
id: toastId,
});

cart.decrement(quantity);
Expand Down
2 changes: 1 addition & 1 deletion core/app/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const Notifications = () => {
position="top-right"
toastOptions={{
className:
'!text-black !rounded !border !border-gray-200 !bg-white !shadow-lg !py-4 !px-6 !text-base',
'!text-black !rounded !border !border-gray-200 !bg-white !shadow-lg !py-4 !px-6 !text-base [&>svg]:!shrink-0',
}}
/>
);
Expand Down
22 changes: 19 additions & 3 deletions core/client/mutations/add-cart-line-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,28 @@ export const addCartLineItem = async (
) => {
const customerAccessToken = await getSessionCustomerAccessToken();

const response = await client.fetch({
return await client.fetch({
document: AddCartLineItemMutation,
variables: { input: { cartEntityId, data } },
customerAccessToken,
fetchOptions: { cache: 'no-store' },
});

return response.data.cart.addCartLineItems?.cart;
};

export function assertAddCartLineItemErrors(
response: Awaited<ReturnType<typeof addCartLineItem>>,
): asserts response is Awaited<ReturnType<typeof addCartLineItem>> {
if (typeof response === 'object' && 'errors' in response && Array.isArray(response.errors)) {
response.errors.forEach((error) => {
if (error.message.includes('Not enough stock:')) {
// This removes the item id from the message. It's very brittle, but it's the only
// solution to do it until our API returns a better error message.
throw new Error(
error.message.replace('Not enough stock: ', '').replace(/\(\w.+\)\s{1}/, ''),
);
}

throw new Error(error.message);
});
}
}
22 changes: 19 additions & 3 deletions core/client/mutations/create-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type LineItems = CreateCartInput['lineItems'];
export const createCart = async (cartItems: LineItems) => {
const customerAccessToken = await getSessionCustomerAccessToken();

const response = await client.fetch({
return await client.fetch({
document: CreateCartMutation,
variables: {
createCartInput: {
Expand All @@ -32,6 +32,22 @@ export const createCart = async (cartItems: LineItems) => {
customerAccessToken,
fetchOptions: { cache: 'no-store' },
});

return response.data.cart.createCart?.cart;
};

export function assertCreateCartErrors(
response: Awaited<ReturnType<typeof createCart>>,
): asserts response is Awaited<ReturnType<typeof createCart>> {
if (typeof response === 'object' && 'errors' in response && Array.isArray(response.errors)) {
response.errors.forEach((error) => {
if (error.message.includes('Not enough stock:')) {
// This removes the item id from the message. It's very brittle, but it's the only
// solution to do it until our API returns a better error message.
throw new Error(
error.message.replace('Not enough stock: ', '').replace(/\(\w.+\)\s{1}/, ''),
);
}

throw new Error(error.message);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';

import { addCartLineItem } from '~/client/mutations/add-cart-line-item';
import { createCart } from '~/client/mutations/create-cart';
import {
addCartLineItem,
assertAddCartLineItemErrors,
} from '~/client/mutations/add-cart-line-item';
import { assertCreateCartErrors, createCart } from '~/client/mutations/create-cart';
import { getCart } from '~/client/queries/get-cart';
import { TAGS } from '~/client/tags';

Expand All @@ -19,7 +22,7 @@ export const addToCart = async (data: FormData) => {
cart = await getCart(cartId);

if (cart) {
cart = await addCartLineItem(cart.entityId, {
const addCartLineItemResponse = await addCartLineItem(cart.entityId, {
lineItems: [
{
productEntityId,
Expand All @@ -28,6 +31,10 @@ export const addToCart = async (data: FormData) => {
],
});

assertAddCartLineItemErrors(addCartLineItemResponse);

cart = addCartLineItemResponse.data.cart.addCartLineItems?.cart;

if (!cart?.entityId) {
return { status: 'error', error: 'Failed to add product to cart.' };
}
Expand All @@ -37,7 +44,11 @@ export const addToCart = async (data: FormData) => {
return { status: 'success', data: cart };
}

cart = await createCart([{ productEntityId, quantity: 1 }]);
const createCartResponse = await createCart([{ productEntityId, quantity: 1 }]);

assertCreateCartErrors(createCartResponse);

cart = createCartResponse.data.cart.createCart?.cart;

if (!cart?.entityId) {
return { status: 'error', error: 'Failed to add product to cart.' };
Expand Down
8 changes: 5 additions & 3 deletions core/components/product-card/add-to-cart/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { FragmentOf } from 'gql.tada';
import { AlertCircle, Check } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useTransition } from 'react';
import { useId, useTransition } from 'react';
import { toast } from 'react-hot-toast';

import { AddToCartButton } from '~/components/add-to-cart-button';
Expand All @@ -21,6 +21,7 @@ interface Props {
export const Form = ({ data: product }: Props) => {
const t = useTranslations('Components.ProductCard.AddToCart');
const cart = useCart();
const toastId = useId();
const [isPending, startTransition] = useTransition();

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
Expand Down Expand Up @@ -51,7 +52,7 @@ export const Form = ({ data: product }: Props) => {
</span>
</div>
),
{ icon: <Check className="text-success-secondary" /> },
{ icon: <Check className="text-success-secondary" />, id: toastId },
);

startTransition(async () => {
Expand All @@ -60,8 +61,9 @@ export const Form = ({ data: product }: Props) => {
if (result.error) {
cart.decrement(quantity);

toast.error(t('error'), {
toast.error(result.error, {
icon: <AlertCircle className="text-error-secondary" />,
id: toastId,
});
}
});
Expand Down
7 changes: 2 additions & 5 deletions core/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,7 @@
}
},
"AddToCart": {
"success": "{cartItems, plural, =1 {1 Item} other {# Items}} added to <cartLink> your cart</cartLink>",
"error": "Error adding product to cart. Please try again."
"success": "{cartItems, plural, =1 {1 Item} other {# Items}} added to <cartLink> your cart</cartLink>"
}
},
"Product": {
Expand All @@ -346,7 +345,6 @@
},
"Form": {
"success": "{cartItems, plural, =1 {1 Item} other {# Items}} added to <cartLink> your cart</cartLink>",
"error": "Error adding product to cart. Please try again.",
"saveToWishlist": "Save to wishlist",
"quantityLabel": "Quantity"
},
Expand Down Expand Up @@ -461,8 +459,7 @@
"ProductCard": {
"AddToCart": {
"viewOptions": "View options",
"success": "{cartItems, plural, =1 {1 Item} other {# Items}} added to <cartLink> your cart</cartLink>",
"error": "Error adding product to cart. Please try again."
"success": "{cartItems, plural, =1 {1 Item} other {# Items}} added to <cartLink> your cart</cartLink>"
}
},
"FormFields": {
Expand Down

0 comments on commit 512c338

Please sign in to comment.