Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Cart Action Promises with success/reject handling #8272

Merged
merged 13 commits into from
Jan 30, 2023
36 changes: 15 additions & 21 deletions assets/js/base/components/cart-checkout/totals/coupon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState, useEffect, useRef } from '@wordpress/element';
import { useState } from '@wordpress/element';
import Button from '@woocommerce/base-components/button';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { withInstanceId } from '@wordpress/compose';
Expand Down Expand Up @@ -35,35 +35,29 @@ export interface TotalsCouponProps {
/**
* Submit handler
*/
onSubmit?: ( couponValue: string ) => void;
onSubmit?: ( couponValue: string ) => Promise< boolean > | undefined;
}

export const TotalsCoupon = ( {
instanceId,
isLoading = false,
onSubmit,
displayCouponForm = false,
onSubmit = () => void 0,
}: TotalsCouponProps ): JSX.Element => {
const [ couponValue, setCouponValue ] = useState( '' );
const [ isCouponFormHidden, setIsCouponFormHidden ] = useState(
! displayCouponForm
);
const currentIsLoading = useRef( false );

const validationErrorKey = 'coupon';
const textInputId = `wc-block-components-totals-coupon__input-${ instanceId }`;

const formWrapperClass = classnames(
'wc-block-components-totals-coupon__content',
{
'screen-reader-text': isCouponFormHidden,
}
);

const { validationError, validationErrorId } = useSelect( ( select ) => {
const { validationErrorId } = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
validationError: store.getValidationError( validationErrorKey ),
validationErrorId: store.getValidationErrorId( textInputId ),
};
} );
Expand All @@ -77,18 +71,18 @@ export const TotalsCoupon = ( {
e: React.MouseEvent< HTMLButtonElement, MouseEvent >
) => {
e.preventDefault();
onSubmit( couponValue );
};

useEffect( () => {
if ( currentIsLoading.current !== isLoading ) {
if ( ! isLoading && couponValue && ! validationError ) {
setCouponValue( '' );
setIsCouponFormHidden( true );
}
currentIsLoading.current = isLoading;
if ( onSubmit !== undefined ) {
onSubmit( couponValue ).then( ( result ) => {
if ( result ) {
setCouponValue( '' );
setIsCouponFormHidden( true );
}
} );
} else {
setCouponValue( '' );
setIsCouponFormHidden( true );
Comment on lines +74 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is possible confusing, why is onSubmit ever undefined?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typedef allows for undefined, so this just handles that case.

}
}, [ isLoading, couponValue, validationError ] );
};

return (
<div className="wc-block-components-totals-coupon">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ const Template: Story< TotalsCouponProps > = ( args ) => {
const onSubmit = ( code: string ) => {
args.onSubmit?.( code );
setArgs( { isLoading: true } );

setTimeout(
() => setArgs( { isLoading: false } ),
INTERACTION_TIMEOUT
);
return new Promise( ( resolve ) => {
setTimeout( () => {
setArgs( { isLoading: false } );
resolve( true );
}, INTERACTION_TIMEOUT );
} );
};

return <TotalsCoupon { ...args } onSubmit={ onSubmit } />;
Expand Down
35 changes: 26 additions & 9 deletions assets/js/base/context/hooks/cart/test/use-store-cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe( 'useStoreCart', () => {
let registry, renderer;

const receiveCartMock = () => {};
const receiveCartContentsMock = () => {};

const previewCartData = {
cartCoupons: previewCart.coupons,
Expand Down Expand Up @@ -102,8 +103,9 @@ describe( 'useStoreCart', () => {
hasCalculatedShipping: true,
extensions: {},
errors: [],
receiveCart: undefined,
paymentRequirements: [],
receiveCart: undefined,
receiveCartContents: undefined,
};
const mockCartTotals = {
currency_code: 'USD',
Expand All @@ -129,8 +131,9 @@ describe( 'useStoreCart', () => {
extensions: {},
isLoadingRates: false,
cartHasCalculatedShipping: true,
receiveCart: undefined,
paymentRequirements: [],
receiveCart: undefined,
receiveCartContents: undefined,
};

const getWrappedComponents = ( Component ) => (
Expand All @@ -140,8 +143,15 @@ describe( 'useStoreCart', () => {
);

const getTestComponent = ( options ) => () => {
const { receiveCart, ...results } = useStoreCart( options );
return <div results={ results } receiveCart={ receiveCart } />;
const { receiveCart, receiveCartContents, ...results } =
useStoreCart( options );
return (
<div
results={ results }
receiveCart={ receiveCart }
receiveCartContents={ receiveCartContents }
/>
);
};

const setUpMocks = () => {
Expand Down Expand Up @@ -190,12 +200,16 @@ describe( 'useStoreCart', () => {
);
} );

const { results, receiveCart } =
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const { receiveCart: defaultReceiveCart, ...remaining } =
defaultCartData;
const {
receiveCart: defaultReceiveCart,
receiveCartContents: defaultReceiveCartContents,
...remaining
} = defaultCartData;
expect( results ).toEqual( remaining );
expect( receiveCart ).toEqual( defaultReceiveCart );
expect( receiveCartContents ).toEqual( defaultReceiveCartContents );
} );

it( 'return store data when shouldSelect is true', () => {
Expand All @@ -209,11 +223,12 @@ describe( 'useStoreCart', () => {
);
} );

const { results, receiveCart } =
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query

expect( results ).toEqual( mockStoreCartData );
expect( receiveCart ).toBeUndefined();
expect( receiveCartContents ).toBeUndefined();
} );
} );

Expand All @@ -225,6 +240,7 @@ describe( 'useStoreCart', () => {
previewCart: {
...previewCart,
receiveCart: receiveCartMock,
receiveCartContents: receiveCartContentsMock,
},
},
} );
Expand All @@ -239,11 +255,12 @@ describe( 'useStoreCart', () => {
);
} );

const { results, receiveCart } =
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query

expect( results ).toEqual( previewCartData );
expect( receiveCart ).toEqual( receiveCartMock );
expect( receiveCartContents ).toEqual( receiveCartContentsMock );
} );
} );
} );
21 changes: 9 additions & 12 deletions assets/js/base/context/hooks/cart/use-store-cart-coupons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,12 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
[ createErrorNotice, createNotice ]
);

const { applyCoupon, removeCoupon, receiveApplyingCoupon } =
useDispatch( CART_STORE_KEY );
const { applyCoupon, removeCoupon } = useDispatch( CART_STORE_KEY );

const applyCouponWithNotices = ( couponCode: string ) => {
applyCoupon( couponCode )
.then( ( result ) => {
return applyCoupon( couponCode )
.then( () => {
if (
result === true &&
__experimentalApplyCheckoutFilter( {
filterName: 'showApplyCouponNotice',
defaultValue: true,
Expand All @@ -71,6 +69,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
}
);
}
return Promise.resolve( true );
} )
.catch( ( error ) => {
setValidationErrors( {
Expand All @@ -79,16 +78,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
hidden: false,
},
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
return Promise.resolve( false );
} );
};

const removeCouponWithNotices = ( couponCode: string ) => {
removeCoupon( couponCode )
.then( ( result ) => {
return removeCoupon( couponCode )
.then( () => {
if (
result === true &&
__experimentalApplyCheckoutFilter( {
filterName: 'showRemoveCouponNotice',
defaultValue: true,
Expand All @@ -112,14 +109,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
}
);
}
return Promise.resolve( true );
} )
.catch( ( error ) => {
createErrorNotice( error.message, {
id: 'coupon-form',
context,
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled in the thunks already.

return Promise.resolve( false );
} );
};

Expand Down
21 changes: 16 additions & 5 deletions assets/js/base/context/hooks/cart/use-store-cart-item-quantity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback, useState, useEffect } from '@wordpress/element';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import {
CART_STORE_KEY,
CHECKOUT_STORE_KEY,
processErrorResponse,
} from '@woocommerce/block-data';
import { useDebounce } from 'use-debounce';
import { usePrevious } from '@woocommerce/base-hooks';
import {
Expand Down Expand Up @@ -84,9 +88,12 @@ export const useStoreCartItemQuantity = (
);

const removeItem = useCallback( () => {
return cartItemKey
? removeItemFromCart( cartItemKey )
: Promise.resolve( false );
if ( cartItemKey ) {
return removeItemFromCart( cartItemKey ).catch( ( error ) => {
processErrorResponse( error );
} );
}
return Promise.resolve( false );
}, [ cartItemKey, removeItemFromCart ] );

// Observe debounced quantity value, fire action to update server on change.
Expand All @@ -97,7 +104,11 @@ export const useStoreCartItemQuantity = (
Number.isFinite( previousDebouncedQuantity ) &&
previousDebouncedQuantity !== debouncedQuantity
) {
changeCartItemQuantity( cartItemKey, debouncedQuantity );
changeCartItemQuantity( cartItemKey, debouncedQuantity ).catch(
( error ) => {
processErrorResponse( error );
}
);
}
}, [
cartItemKey,
Expand Down
8 changes: 7 additions & 1 deletion assets/js/base/context/hooks/cart/use-store-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const defaultCartData: StoreCart = {
cartHasCalculatedShipping: false,
paymentRequirements: EMPTY_PAYMENT_REQUIREMENTS,
receiveCart: () => undefined,
receiveCartContents: () => undefined,
extensions: EMPTY_EXTENSIONS,
};

Expand Down Expand Up @@ -174,6 +175,10 @@ export const useStoreCart = (
typeof previewCart?.receiveCart === 'function'
? previewCart.receiveCart
: () => undefined,
receiveCartContents:
typeof previewCart?.receiveCartContents === 'function'
? previewCart.receiveCartContents
: () => undefined,
};
}

Expand All @@ -185,7 +190,7 @@ export const useStoreCart = (
! store.hasFinishedResolution( 'getCartData' );

const isLoadingRates = store.isCustomerDataUpdating();
const { receiveCart } = dispatch( storeKey );
const { receiveCart, receiveCartContents } = dispatch( storeKey );
const billingAddress = decodeValues( cartData.billingAddress );
const shippingAddress = cartData.needsShipping
? decodeValues( cartData.shippingAddress )
Expand Down Expand Up @@ -232,6 +237,7 @@ export const useStoreCart = (
cartHasCalculatedShipping: cartData.hasCalculatedShipping,
paymentRequirements: cartData.paymentRequirements,
receiveCart,
receiveCartContents,
};
},
[ shouldSelect ]
Expand Down
19 changes: 7 additions & 12 deletions assets/js/base/context/hooks/shipping/use-shipping-data.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/**
* External dependencies
*/
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import {
CART_STORE_KEY as storeKey,
processErrorResponse,
} from '@woocommerce/block-data';
import { useSelect, useDispatch } from '@wordpress/data';
import { isObject } from '@woocommerce/types';
import { useEffect, useRef, useCallback } from '@wordpress/element';
import { deriveSelectedShippingRates } from '@woocommerce/base-utils';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { previewCart } from '@woocommerce/resource-previews';
import { useThrowError } from '@woocommerce/base-hooks';

/**
* Internal dependencies
Expand Down Expand Up @@ -72,12 +74,11 @@ export const useShippingData = (): ShippingData => {
} as {
selectShippingRate: (
newShippingRateId: string,
packageId?: string | number
packageId?: string | number | undefined
) => Promise< unknown >;
};

// Selects a shipping rate, fires an event, and catch any errors.
const throwError = useThrowError();
const { dispatchCheckoutEvent } = useStoreEvents();
const selectShippingRate = useCallback(
(
Expand Down Expand Up @@ -113,16 +114,10 @@ export const useShippingData = (): ShippingData => {
} );
} )
.catch( ( error ) => {
// Throw an error because an error when selecting a rate is problematic.
throwError( error );
processErrorResponse( error );
} );
},
[
dispatchSelectShippingRate,
dispatchCheckoutEvent,
throwError,
selectedRates,
]
[ dispatchSelectShippingRate, dispatchCheckoutEvent, selectedRates ]
);

return {
Expand Down
Loading