Skip to content

Commit

Permalink
refactore(core): checkout summary (#711)
Browse files Browse the repository at this point in the history
* refactore(core): checkout summary

* fix: use reducer in shipping info

* fix: messaging

* fix: use Promise.all
  • Loading branch information
jorgemoya committed Mar 28, 2024
1 parent b1a2939 commit 0ec2269
Show file tree
Hide file tree
Showing 20 changed files with 761 additions and 847 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-walls-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

Use checkout field from GQL to populate checkout summary.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use server';

import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';
import { z } from 'zod';

import { selectCheckoutShippingOption } from '~/client/mutations/select-checkout-shipping-option';
Expand All @@ -26,27 +25,7 @@ export const submitShippingCosts = async (
shippingOptionEntityId: parsedData.shippingOption,
});

const selectedShippingOption =
shippingCost && shippingCost.shippingConsignments
? shippingCost.shippingConsignments[0]?.selectedShippingOption?.description
: '';

const shippingCosts = {
shippingCostTotal: shippingCost?.shippingCostTotal?.value ?? 0,
handlingCostTotal: shippingCost?.handlingCostTotal?.value ?? 0,
selectedShippingOption,
};

cookies().set({
name: 'shippingCosts',
value: JSON.stringify(shippingCosts),
httpOnly: true,
sameSite: 'lax',
secure: true,
path: '/',
});

revalidateTag('cart');
revalidateTag('checkout');

return { status: 'success', data: shippingCost };
} catch (e: unknown) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use server';

import { revalidateTag } from 'next/cache';
import { z } from 'zod';

import { addCheckoutShippingInfo } from '~/client/mutations/add-checkout-shipping-info';
import { updateCheckoutShippingInfo } from '~/client/mutations/update-checkout-shipping-info';
import { addCheckoutShippingConsignments } from '~/client/mutations/add-checkout-shipping-consignments';
import { updateCheckoutShippingConsignment } from '~/client/mutations/update-checkout-shipping-consigment';

const ShippingInfoSchema = z.object({
country: z.string(),
Expand All @@ -14,10 +15,10 @@ const ShippingInfoSchema = z.object({

export const submitShippingInfo = async (
formData: FormData,
cartData: {
cartId: string;
checkoutData: {
checkoutId: string;
shippingId: string | null;
cartItems: Array<{ quantity: number; lineItemEntityId: string }>;
lineItems: Array<{ quantity: number; lineItemEntityId: string }>;
},
) => {
try {
Expand All @@ -27,31 +28,33 @@ export const submitShippingInfo = async (
city: formData.get('city'),
zipcode: formData.get('zip'),
});
const { cartId, cartItems, shippingId } = cartData;
const { checkoutId, lineItems, shippingId } = checkoutData;

let result;

if (shippingId) {
result = await updateCheckoutShippingInfo({
cartId,
result = await updateCheckoutShippingConsignment({
checkoutId,
shippingId,
cartItems,
lineItems,
countryCode: parsedData.country.split('-')[0] ?? '',
stateOrProvince: parsedData.state,
city: parsedData.city,
postalCode: parsedData.zipcode,
});
} else {
result = await addCheckoutShippingInfo({
cartId,
cartItems,
result = await addCheckoutShippingConsignments({
checkoutId,
lineItems,
countryCode: parsedData.country.split('-')[0] ?? '',
stateOrProvince: parsedData.state,
city: parsedData.city,
postalCode: parsedData.zipcode,
});
}

revalidateTag('checkout');

return { status: 'success', data: result };
} catch (e: unknown) {
if (e instanceof Error || e instanceof z.ZodError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,5 @@ export async function updateProductQuantity({
throw new Error('Failed to change product quantity in Cart');
}

// reset shipping estimation on update product quantity
cookies().set({
name: 'shippingCosts',
value: JSON.stringify({
shippingCostTotal: 0,
handlingCostTotal: 0,
selectedShippingOption: '',
}),
httpOnly: true,
sameSite: 'lax',
secure: true,
path: '/',
});

revalidatePath('/cart');
}
152 changes: 31 additions & 121 deletions apps/core/app/[locale]/(default)/cart/_components/checkout-summary.tsx
Original file line number Diff line number Diff line change
@@ -1,146 +1,56 @@
'use client';
import { AlertCircle } from 'lucide-react';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { toast } from 'react-hot-toast';

import { useTranslations } from 'next-intl';
import { createContext, Dispatch, SetStateAction, useState } from 'react';

import { getCart } from '~/client/queries/get-cart';
import { ExistingResultType } from '~/client/util';
import { getCheckout } from '~/client/queries/get-checkout';

import { getShippingCountries } from '../_actions/get-shipping-countries';

import { ShippingEstimator } from './shipping-estimator';

export const createCurrencyFormatter = (currencyCode: string) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currencyCode,
});
export const CheckoutSummary = async ({ cartId, locale }: { cartId: string; locale: string }) => {
const t = await getTranslations({ locale, namespace: 'Cart.CheckoutSummary' });
const messages = await getMessages({ locale });

type CartSummary = ExistingResultType<typeof getCart>;
export type CheckoutSummary = CartSummary & {
shippingCostTotal: {
currencyCode: string;
value: number;
};
handlingCostTotal: {
currencyCode: string;
value: number;
};
consignmentEntityId: string;
};
export interface ShippingCosts {
shippingCostTotal: number;
handlingCostTotal: number;
selectedShippingOption: string;
}
type ShippingCountries = ExistingResultType<typeof getShippingCountries>;
const [checkout, shippingCountries] = await Promise.all([
getCheckout(cartId),
getShippingCountries(),
]);

export const CheckoutContext = createContext<{
availableShippingCountries: ShippingCountries;
checkoutEntityId: string;
consignmentEntityId: string | null;
shippingCosts: ShippingCosts | null;
currencyCode: string;
isShippingMethodSelected: boolean;
setIsShippingMethodSelected: (newIsShippingMethodSelected: boolean) => void;
updateCheckoutSummary: Dispatch<SetStateAction<CheckoutSummary>>;
}>({
availableShippingCountries: [],
checkoutEntityId: '',
consignmentEntityId: '',
currencyCode: '',
isShippingMethodSelected: false,
setIsShippingMethodSelected: () => undefined,
shippingCosts: null,
updateCheckoutSummary: () => undefined,
});
if (!checkout) {
toast.error(t('errorMessage'), {
icon: <AlertCircle className="text-error-secondary" />,
});

export const CheckoutSummary = ({
cart,
shippingCountries,
shippingCosts,
}: {
cart: NonNullable<CartSummary>;
shippingCountries: ShippingCountries;
shippingCosts: ShippingCosts | null;
}) => {
const t = useTranslations('Cart.CheckoutSummary');
const [isShippingMethodSelected, setIsShippingMethodSelected] = useState(false);
const [checkoutSummary, updateCheckoutSummary] = useState<CheckoutSummary>({
...cart,
shippingCostTotal: {
currencyCode: cart.currencyCode,
value: shippingCosts?.shippingCostTotal ?? 0,
},
handlingCostTotal: {
currencyCode: cart.currencyCode,
value: shippingCosts?.handlingCostTotal ?? 0,
},
consignmentEntityId: '',
});
return null;
}

const currencyFormatter = createCurrencyFormatter(checkoutSummary.currencyCode);
const extractCartlineItemsData = ({
entityId,
productEntityId,
quantity,
variantEntityId,
}: (typeof cart.lineItems.physicalItems)[number]) => ({
lineItemEntityId: entityId,
productEntityId,
quantity,
variantEntityId,
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: checkout.cart?.currencyCode,
});

return (
<CheckoutContext.Provider
value={{
availableShippingCountries: shippingCountries,
checkoutEntityId: checkoutSummary.entityId,
consignmentEntityId: checkoutSummary.consignmentEntityId,
currencyCode: checkoutSummary.currencyCode,
isShippingMethodSelected,
setIsShippingMethodSelected,
shippingCosts,
updateCheckoutSummary,
}}
>
<>
<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="text-base font-semibold">{t('subTotal')}</span>
<span className="text-base">
{currencyFormatter.format(cart.totalExtendedListPrice.value)}
</span>
<span className="font-semibold">{t('subTotal')}</span>
<span>{currencyFormatter.format(checkout.subtotal?.value || 0)}</span>
</div>

<ShippingEstimator
shippingItems={checkoutSummary.lineItems.physicalItems.reduce<
Array<{ quantity: number; lineItemEntityId: string }>
>((items, product) => {
const { lineItemEntityId, quantity } = extractCartlineItemsData(product);

items.push({ quantity, lineItemEntityId });

return items;
}, [])}
/>
<NextIntlClientProvider locale={locale} messages={{ Cart: messages.Cart ?? {} }}>
<ShippingEstimator checkout={checkout} shippingCountries={shippingCountries} />
</NextIntlClientProvider>

<div className="flex justify-between border-t border-t-gray-200 py-4">
<span className="text-base font-semibold">{t('discounts')}</span>
<span className="text-base">
-{currencyFormatter.format(checkoutSummary.discountedAmount.value)}
</span>
<span className="font-semibold">{t('discounts')}</span>
<span>-{currencyFormatter.format(checkout.cart?.discountedAmount.value || 0)}</span>
</div>

<div className="flex justify-between border-t border-t-gray-200 py-4 text-xl font-bold lg:text-2xl">
{t('grandTotal')}
<span>
{currencyFormatter.format(
checkoutSummary.amount.value +
checkoutSummary.shippingCostTotal.value +
checkoutSummary.handlingCostTotal.value,
)}
</span>
<span>{currencyFormatter.format(checkout.grandTotal?.value || 0)}</span>
</div>
</CheckoutContext.Provider>
</>
);
};
Loading

0 comments on commit 0ec2269

Please sign in to comment.