Skip to content
Open
18 changes: 18 additions & 0 deletions src/components/LoadingImg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Image from 'next/image';


const LoadingImg = (props) => {

return (
<span className="align-middle" {...props} >
<Image
src='/cart-spinner.gif'
width="54px"
height="54px"
alt="Carregando..."
/>
</span>
);
};

export default LoadingImg;
65 changes: 43 additions & 22 deletions src/components/checkout/CheckoutForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "../../utils/checkout";
import CheckboxField from "./form-elements/CheckboxField";
import CLEAR_CART_MUTATION from "../../mutations/clear-cart";
import ShippingCosts from './ShippingCosts';

// Use this for testing purposes, so you dont have to fill the checkout form over an over again.
// const defaultCustomerInfo = {
Expand Down Expand Up @@ -79,7 +80,11 @@ const CheckoutForm = ({countriesData}) => {
const [createdOrderData, setCreatedOrderData] = useState({});

// Get Cart Data.
const {data} = useQuery(GET_CART, {
const {
loading: loadingCart,
data,
refetch
} = useQuery(GET_CART, {
notifyOnNetworkStatusChange: true,
onCompleted: () => {
// Update cart in the localStorage.
Expand Down Expand Up @@ -108,40 +113,50 @@ const CheckoutForm = ({countriesData}) => {

const [ clearCartMutation ] = useMutation( CLEAR_CART_MUTATION );

/*
* Handle form submit.
*
* @param {Object} event Event Object.
/**
* Validate Billing and Shipping Details
*
* @return {void}
* Note:
* 1. If billing is different than shipping address, only then validate billing.
* 2. We are passing theBillingStates?.length and theShippingStates?.length, so that
* the respective states should only be mandatory, if a country has states.
*/
const handleFormSubmit = async (event) => {
event.preventDefault();
const validateFields = () => {
let isValid= true;

/**
* Validate Billing and Shipping Details
*
* Note:
* 1. If billing is different than shipping address, only then validate billing.
* 2. We are passing theBillingStates?.length and theShippingStates?.length, so that
* the respective states should only be mandatory, if a country has states.
*/
const billingValidationResult = input?.billingDifferentThanShipping ? validateAndSanitizeCheckoutForm(input?.billing, theBillingStates?.length) : {errors: null, isValid: true};
const billingValidationResult = input?.billingDifferentThanShipping ? validateAndSanitizeCheckoutForm(input?.billing, theBillingStates?.length) : { errors: null, isValid: true };
const shippingValidationResult = validateAndSanitizeCheckoutForm(input?.shipping, theShippingStates?.length);

if (!shippingValidationResult.isValid || !billingValidationResult.isValid) {
setInput({
...input,
billing: {...input.billing, errors: billingValidationResult.errors},
shipping: {...input.shipping, errors: shippingValidationResult.errors}
billing: { ...input.billing, errors: billingValidationResult.errors },
shipping: { ...input.shipping, errors: shippingValidationResult.errors }
});

isValid = false;
}

return isValid;
};

/*
* Handle form submit.
*
* @param {Object} event Event Object.
*
* @return {void}
*/
const handleFormSubmit = async (event) => {
event.preventDefault();

if( ! validateFields() ) {
return;
}

if ( 'stripe-mode' === input.paymentMethod ) {
if ('stripe-mode' === input.paymentMethod) {
const createdOrderData = await handleStripeCheckout(input, cart?.products, setRequestError, clearCartMutation, setIsStripeOrderProcessing, setCreatedOrderData);
return null;
return null;
}

const checkOutData = createCheckoutData(input);
Expand Down Expand Up @@ -257,7 +272,13 @@ const CheckoutForm = ({countriesData}) => {
{/* Order*/}
<h2 className="text-xl font-medium mb-4">Your Order</h2>
<YourOrder cart={cart}/>

<ShippingCosts
cart={cart}
refetchCart={refetch}
shippingAddress={input?.shipping}
loadingCart={loadingCart}
validateFields={validateFields}
/>
{/*Payment*/}
<PaymentModes input={input} handleOnChange={handleOnChange}/>

Expand Down
198 changes: 198 additions & 0 deletions src/components/checkout/ShippingCosts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { useState } from 'react';
import { v4 } from 'uuid';
import { useMutation, useQuery } from '@apollo/client';
import UPDATE_SHIPPING_ADDRESS from "../../mutations/update-shipping-address";
import { UPDATE_SHIPPING_METHOD } from "../../mutations/update-shipping-method";
import LoadingImg from "../LoadingImg";
import { isEmpty } from 'lodash';
import cx from 'classnames';
import { formatCurrency } from "../../functions";

const ShippingSelection = ({
cart,
refetchCart,
shippingAddress,
loadingCart,
validateFields,
}) => {

const [
shippingMethod,
setShippingMethod
] = useState(cart?.shippingMethod ?? '');

const [requestError, setRequestError] = useState(null);

const requestDefaultOptions = {
onCompleted: () => {
refetchCart('cart');
},
onError: (error) => {
if (error) {
const errorMessage = !isEmpty(error?.graphQLErrors?.[0])
? error.graphQLErrors[0]?.message
: '';
console.warn(error);
if (setRequestError) {
setRequestError(errorMessage);
}
}
}
};

// Update Customer Shipping Address for cart shipping calculations.
const [updateShippingAddress, {
data: updatedShippingData,
loading: updatingShippingAddress,
error: updateShippingAddressError
}] = useMutation(UPDATE_SHIPPING_ADDRESS, requestDefaultOptions);

// Update Shipping Method.
const [ShippingSelectionMethod, {
data: chosenShippingData,
loading: choosingShippingMethod,
error: ShippingSelectionError
}] = useMutation(UPDATE_SHIPPING_METHOD, requestDefaultOptions);

const handleCalcShippingClick = async (event) => {

setRequestError("");
if (!validateFields()) {
setRequestError('Please fill out all required shipping fields to calculate shipping costs.');
return;
}

const {
errors,
createAccount,
orderNotes,
...shipping
} = shippingAddress;

updateShippingAddress({
variables: {
input: {
clientMutationId: v4(),
shipping,
}
},
});
};

const handleShippingSelection = (event) => {
const chosenShippingMethod = event.target.value;

setShippingMethod(chosenShippingMethod);

if (chosenShippingMethod != shippingMethod) {
ShippingSelectionMethod({
variables: {
shippingMethod: {
clientMutationId: v4(),
shippingMethods: [chosenShippingMethod],
}
},
});

};
}

const isLoading = updatingShippingAddress
|| choosingShippingMethod
|| loadingCart;

return (
<div
className={cx(
'choose-shipping-wrap flex-grow',
{ 'opacity-50': isLoading }
)}
>
{cart?.needsShippingAddress &&
<>
<h2 className="mb-2 text-xl text-bold">Shipping Costs</h2>
<hr className="my-4 " />
<div className="flex flex-wrap justify-between">
<div className="flex-grow">
{
<>
<button
disabled={isLoading}
type={"button"}
onClick={handleCalcShippingClick}
className={cx(
'bg-purple-600 text-white px-5 py-3 rounded-sm w-auto xl:w-full',
{ 'opacity-50': isLoading }
)}
>
Update Shipping Costs
</button>
{isLoading &&
<LoadingImg />
}
{requestError
? <p className="my-4 text-red-600">{requestError}</p>
: <p className="my-4 text-xs opacity-75">
{
[
cart?.customer?.shipping?.address1,
cart?.customer?.shipping?.city,
cart?.customer?.shipping?.state
].filter(val => val).join(' - ')
}
</p>
}
</>
}
{cart?.customer?.shipping?.country
&& cart?.customer?.shipping?.state
&& cart?.customer?.shipping?.postcode
&& cart?.shippingMethods?.length
&& <div className='shipping-methods-wrap'>
<div className='flex'>
<h2 className="my-2 self-center text-xl text-bold">
Choose Shipping Method
</h2>
</div>
<hr className="my-2" />
{cart?.shippingMethods?.map(method => (
<div key={method.id}>
<label>
<input
type="radio"
name="chosenShippingMethod"
className="my-2"
disabled={isLoading}
value={method.id}
onChange={handleShippingSelection}
checked={shippingMethod == method.id}
/> {method.label} - {formatCurrency(method.cost)}
</label>
</div>
))}
</div>
}
</div>
</div>
<table className="mt-4 checkout-cart table table-hover w-full mb-10">
<tbody>
<tr className="bg-gray-200">
<td className="w-24" />
<td className="woo-next-checkout-total font-normal ">Shipping</td>
<td className="woo-next-checkout-total font-bold ">{formatCurrency(cart.shippingTotal)}</td>
</tr>
<tr className="bg-gray-200">
<td className="" />
<td className="woo-next-checkout-total font-normal text-xl">Total</td>
<td className="woo-next-checkout-total font-bold text-xl">{cart.total}</td>
</tr>
</tbody>
</table>
</>
}
</div>
)

};

export default ShippingSelection;
38 changes: 37 additions & 1 deletion src/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ export const getFloatVal = ( string ) => {

};

/**
* Format float value as Currency string.
*
* @param {float} num The number to be formatted.
* @return {string}
*/
export const formatCurrency = (num, currency = '$') => {
let floatValue = num;
if ('string' === typeof floatValue) {
floatValue = getFloatVal(floatValue);
}
if( ! floatValue ) {
floatValue = 0;
}
return (currency + floatValue.toFixed(2));
};

/**
* Add first product.
*
Expand Down Expand Up @@ -237,8 +254,27 @@ export const getFormattedCart = ( data ) => {
formattedCart.products.push( product );
}

formattedCart.needsShippingAddress = data?.cart?.needsShippingAddress;
formattedCart.shippingMethod = data?.cart?.chosenShippingMethods[0] ?? '';
formattedCart.shippingMethods = data?.cart?.availableShippingMethods
? data?.cart?.availableShippingMethods[0]?.rates
: [];
formattedCart.shippingTotal = formattedCart?.shippingMethods?.find(
ship => ship.id == formattedCart?.shippingMethod
)?.cost;

formattedCart.totalProductsCount = totalProductsCount;
formattedCart.totalProductsPrice = data?.cart?.total ?? '';
formattedCart.totalProductsPrice = data?.cart?.subtotal ?? '';
formattedCart.total = data?.cart?.total ?? '';

if (data?.customer) {
let customer = {
...data?.customer,
shipping: { ...data?.customer?.shipping },
billing: { ...data?.customer?.billing }
};
formattedCart.customer = customer;
}

return formattedCart;

Expand Down
Loading