Cart Data Store: Use createReduxStore and fix type warnings (#52219)
* Use createReduxStore instead of deprecated registerStore * Move thunks to correct file * Fix type definitions * Add changefile(s) from automation for the following project(s): woocommerce-blocks * changelog * reduce diff size * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Remove todo --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
parent
91eeb179e5
commit
084297a1a9
|
@ -2,92 +2,71 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import type {
|
import type {
|
||||||
Cart,
|
|
||||||
CartResponse,
|
|
||||||
CartResponseItem,
|
|
||||||
ExtensionCartUpdateArgs,
|
|
||||||
BillingAddressShippingAddress,
|
|
||||||
ApiErrorResponse,
|
ApiErrorResponse,
|
||||||
CartShippingPackageShippingRate,
|
Cart,
|
||||||
CartShippingRate,
|
CartResponseItem,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
import { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
||||||
import {
|
|
||||||
triggerAddedToCartEvent,
|
|
||||||
triggerAddingToCartEvent,
|
|
||||||
} from '@woocommerce/base-utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ACTION_TYPES as types } from './action-types';
|
import { ACTION_TYPES as types } from './action-types';
|
||||||
import { apiFetchWithHeaders } from '../shared-controls';
|
|
||||||
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
|
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
|
||||||
import { CartDispatchFromMap, CartSelectFromMap } from './index';
|
|
||||||
import type { Thunks } from './thunks';
|
import type { Thunks } from './thunks';
|
||||||
|
|
||||||
// Thunks are functions that can be dispatched, similar to actions creators
|
// Thunks are functions that can be dispatched, similar to actions creators
|
||||||
// @todo Many of the functions that return promises in this file need to be moved to thunks.ts.
|
|
||||||
export * from './thunks';
|
export * from './thunks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An action creator that dispatches the plain action responsible for setting the cart data in the store.
|
* An action creator that dispatches the plain action responsible for setting the cart data in the store.
|
||||||
*
|
|
||||||
* @param cart the parsed cart object. (Parsed into camelCase).
|
|
||||||
*/
|
*/
|
||||||
export const setCartData = ( cart: Cart ): { type: string; response: Cart } => {
|
export function setCartData( cart: Cart ) {
|
||||||
return {
|
return {
|
||||||
type: types.SET_CART_DATA,
|
type: types.SET_CART_DATA,
|
||||||
response: cart,
|
response: cart,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An action creator that dispatches the plain action responsible for setting the cart error data in the store.
|
* An action creator that dispatches the plain action responsible for setting the cart error data in the store.
|
||||||
*
|
|
||||||
* @param error the parsed error object (Parsed into camelCase).
|
|
||||||
*/
|
*/
|
||||||
export const setErrorData = (
|
export function setErrorData( error: ApiErrorResponse | null ) {
|
||||||
error: ApiErrorResponse | null
|
|
||||||
): { type: string; response: ApiErrorResponse | null } => {
|
|
||||||
return {
|
return {
|
||||||
type: types.SET_ERROR_DATA,
|
type: types.SET_ERROR_DATA,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object used to track when a coupon is applying.
|
* Returns an action object used to track when a coupon is applying.
|
||||||
*
|
|
||||||
* @param {string} [couponCode] Coupon being added.
|
|
||||||
*/
|
*/
|
||||||
export const receiveApplyingCoupon = ( couponCode: string ) =>
|
export function receiveApplyingCoupon( couponCode: string ) {
|
||||||
( {
|
return {
|
||||||
type: types.APPLYING_COUPON,
|
type: types.APPLYING_COUPON,
|
||||||
couponCode,
|
couponCode,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object used to track when a coupon is removing.
|
* Returns an action object used to track when a coupon is removing.
|
||||||
*
|
|
||||||
* @param {string} [couponCode] Coupon being removed..
|
|
||||||
*/
|
*/
|
||||||
export const receiveRemovingCoupon = ( couponCode: string ) =>
|
export function receiveRemovingCoupon( couponCode: string ) {
|
||||||
( {
|
return {
|
||||||
type: types.REMOVING_COUPON,
|
type: types.REMOVING_COUPON,
|
||||||
couponCode,
|
couponCode,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object for updating a single cart item in the store.
|
* Returns an action object for updating a single cart item in the store.
|
||||||
*
|
|
||||||
* @param {CartResponseItem} [response=null] A cart item API response.
|
|
||||||
*/
|
*/
|
||||||
export const receiveCartItem = ( response: CartResponseItem | null = null ) =>
|
export function receiveCartItem( response: CartResponseItem | null = null ) {
|
||||||
( {
|
return {
|
||||||
type: types.RECEIVE_CART_ITEM,
|
type: types.RECEIVE_CART_ITEM,
|
||||||
cartItem: response,
|
cartItem: response,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object to indicate if the specified cart item quantity is
|
* Returns an action object to indicate if the specified cart item quantity is
|
||||||
|
@ -97,15 +76,16 @@ export const receiveCartItem = ( response: CartResponseItem | null = null ) =>
|
||||||
* @param {boolean} [isPendingQuantity=true] Flag for update state; true if API
|
* @param {boolean} [isPendingQuantity=true] Flag for update state; true if API
|
||||||
* request is pending.
|
* request is pending.
|
||||||
*/
|
*/
|
||||||
export const itemIsPendingQuantity = (
|
export function itemIsPendingQuantity(
|
||||||
cartItemKey: string,
|
cartItemKey: string,
|
||||||
isPendingQuantity = true
|
isPendingQuantity = true
|
||||||
) =>
|
) {
|
||||||
( {
|
return {
|
||||||
type: types.ITEM_PENDING_QUANTITY,
|
type: types.ITEM_PENDING_QUANTITY,
|
||||||
cartItemKey,
|
cartItemKey,
|
||||||
isPendingQuantity,
|
isPendingQuantity,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object to remove a cart item from the store.
|
* Returns an action object to remove a cart item from the store.
|
||||||
|
@ -114,15 +94,16 @@ export const itemIsPendingQuantity = (
|
||||||
* @param {boolean} [isPendingDelete=true] Flag for update state; true if API
|
* @param {boolean} [isPendingDelete=true] Flag for update state; true if API
|
||||||
* request is pending.
|
* request is pending.
|
||||||
*/
|
*/
|
||||||
export const itemIsPendingDelete = (
|
export function itemIsPendingDelete(
|
||||||
cartItemKey: string,
|
cartItemKey: string,
|
||||||
isPendingDelete = true
|
isPendingDelete = true
|
||||||
) =>
|
) {
|
||||||
( {
|
return {
|
||||||
type: types.RECEIVE_REMOVED_ITEM,
|
type: types.RECEIVE_REMOVED_ITEM,
|
||||||
cartItemKey,
|
cartItemKey,
|
||||||
isPendingDelete,
|
isPendingDelete,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object to mark the cart data in the store as stale.
|
* Returns an action object to mark the cart data in the store as stale.
|
||||||
|
@ -131,21 +112,23 @@ export const itemIsPendingDelete = (
|
||||||
* lastCartUpdate timestamp is newer than the
|
* lastCartUpdate timestamp is newer than the
|
||||||
* one in wcSettings.
|
* one in wcSettings.
|
||||||
*/
|
*/
|
||||||
export const setIsCartDataStale = ( isCartDataStale = true ) =>
|
export function setIsCartDataStale( isCartDataStale = true ) {
|
||||||
( {
|
return {
|
||||||
type: types.SET_IS_CART_DATA_STALE,
|
type: types.SET_IS_CART_DATA_STALE,
|
||||||
isCartDataStale,
|
isCartDataStale,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object used to track when customer data is being updated
|
* Returns an action object used to track when customer data is being updated
|
||||||
* (billing and/or shipping).
|
* (billing and/or shipping).
|
||||||
*/
|
*/
|
||||||
export const updatingCustomerData = ( isResolving: boolean ) =>
|
export function updatingCustomerData( isResolving: boolean ) {
|
||||||
( {
|
return {
|
||||||
type: types.UPDATING_CUSTOMER_DATA,
|
type: types.UPDATING_CUSTOMER_DATA,
|
||||||
isResolving,
|
isResolving,
|
||||||
} as const );
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an action object used to track whether the shipping rate is being
|
* Returns an action object used to track whether the shipping rate is being
|
||||||
|
@ -153,346 +136,41 @@ export const updatingCustomerData = ( isResolving: boolean ) =>
|
||||||
*
|
*
|
||||||
* @param {boolean} isResolving True if shipping rate is being selected.
|
* @param {boolean} isResolving True if shipping rate is being selected.
|
||||||
*/
|
*/
|
||||||
export const shippingRatesBeingSelected = ( isResolving: boolean ) =>
|
export function shippingRatesBeingSelected( isResolving: boolean ) {
|
||||||
( {
|
return {
|
||||||
type: types.UPDATING_SELECTED_SHIPPING_RATE,
|
type: types.UPDATING_SELECTED_SHIPPING_RATE,
|
||||||
isResolving,
|
isResolving,
|
||||||
} as const );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POSTs to the /cart/extensions endpoint with the data supplied by the extension.
|
|
||||||
*
|
|
||||||
* @param {Object} args The data to be posted to the endpoint
|
|
||||||
*/
|
|
||||||
export const applyExtensionCartUpdate =
|
|
||||||
( args: ExtensionCartUpdateArgs ) =>
|
|
||||||
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
|
||||||
try {
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: '/wc/store/v1/cart/extensions',
|
|
||||||
method: 'POST',
|
|
||||||
data: { namespace: args.namespace, data: args.data },
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a coupon code and either invalidates caches, or receives an error if
|
|
||||||
* the coupon cannot be applied.
|
|
||||||
*
|
|
||||||
* @param {string} couponCode The coupon code to apply to the cart.
|
|
||||||
* @throws Will throw an error if there is an API problem.
|
|
||||||
*/
|
|
||||||
export const applyCoupon =
|
|
||||||
( couponCode: string ) =>
|
|
||||||
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
|
||||||
try {
|
|
||||||
dispatch.receiveApplyingCoupon( couponCode );
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: '/wc/store/v1/cart/apply-coupon',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
code: couponCode,
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
} finally {
|
|
||||||
dispatch.receiveApplyingCoupon( '' );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a coupon code and either invalidates caches, or receives an error if
|
|
||||||
* the coupon cannot be removed.
|
|
||||||
*
|
|
||||||
* @param {string} couponCode The coupon code to remove from the cart.
|
|
||||||
* @throws Will throw an error if there is an API problem.
|
|
||||||
*/
|
|
||||||
export const removeCoupon =
|
|
||||||
( couponCode: string ) =>
|
|
||||||
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
|
||||||
try {
|
|
||||||
dispatch.receiveRemovingCoupon( couponCode );
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: '/wc/store/v1/cart/remove-coupon',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
code: couponCode,
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
} finally {
|
|
||||||
dispatch.receiveRemovingCoupon( '' );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an item to the cart:
|
|
||||||
* - Calls API to add item.
|
|
||||||
* - If successful, yields action to add item from store.
|
|
||||||
* - If error, yields action to store error.
|
|
||||||
*
|
|
||||||
* @param {number} productId Product ID to add to cart.
|
|
||||||
* @param {number} [quantity=1] Number of product ID being added to cart.
|
|
||||||
* @throws Will throw an error if there is an API problem.
|
|
||||||
*/
|
|
||||||
export const addItemToCart =
|
|
||||||
( productId: number, quantity = 1 ) =>
|
|
||||||
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
|
||||||
try {
|
|
||||||
triggerAddingToCartEvent();
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: `/wc/store/v1/cart/add-item`,
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
id: productId,
|
|
||||||
quantity,
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
triggerAddedToCartEvent( { preserveCartData: true } );
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes specified item from the cart:
|
|
||||||
* - Calls API to remove item.
|
|
||||||
* - If successful, yields action to remove item from store.
|
|
||||||
* - If error, yields action to store error.
|
|
||||||
* - Sets cart item as pending while API request is in progress.
|
|
||||||
*
|
|
||||||
* @param {string} cartItemKey Cart item being updated.
|
|
||||||
*/
|
|
||||||
export const removeItemFromCart =
|
|
||||||
( cartItemKey: string ) =>
|
|
||||||
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
|
||||||
try {
|
|
||||||
dispatch.itemIsPendingDelete( cartItemKey );
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: `/wc/store/v1/cart/remove-item`,
|
|
||||||
data: {
|
|
||||||
key: cartItemKey,
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
} finally {
|
|
||||||
dispatch.itemIsPendingDelete( cartItemKey, false );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persists a quantity change the for specified cart item:
|
|
||||||
* - Calls API to set quantity.
|
|
||||||
* - If successful, yields action to update store.
|
|
||||||
* - If error, yields action to store error.
|
|
||||||
*
|
|
||||||
* @param {string} cartItemKey Cart item being updated.
|
|
||||||
* @param {number} quantity Specified (new) quantity.
|
|
||||||
*/
|
|
||||||
export const changeCartItemQuantity =
|
|
||||||
(
|
|
||||||
cartItemKey: string,
|
|
||||||
quantity: number
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unclear how to represent multiple different yields as type
|
|
||||||
) =>
|
|
||||||
async ( {
|
|
||||||
dispatch,
|
|
||||||
select,
|
|
||||||
}: {
|
|
||||||
dispatch: CartDispatchFromMap;
|
|
||||||
select: CartSelectFromMap;
|
|
||||||
} ) => {
|
|
||||||
const cartItem = select.getCartItem( cartItemKey );
|
|
||||||
if ( cartItem?.quantity === quantity ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
dispatch.itemIsPendingQuantity( cartItemKey );
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: '/wc/store/v1/cart/update-item',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
key: cartItemKey,
|
|
||||||
quantity,
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
} finally {
|
|
||||||
dispatch.itemIsPendingQuantity( cartItemKey, false );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Facilitates aborting fetch requests.
|
|
||||||
let abortController = null as AbortController | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects a shipping rate.
|
|
||||||
*
|
|
||||||
* @param {string} rateId The id of the rate being selected.
|
|
||||||
* @param {number | string} [packageId] The key of the packages that we will select within.
|
|
||||||
*/
|
|
||||||
export const selectShippingRate =
|
|
||||||
( rateId: string, packageId: number | null = null ) =>
|
|
||||||
async ( {
|
|
||||||
dispatch,
|
|
||||||
select,
|
|
||||||
}: {
|
|
||||||
dispatch: CartDispatchFromMap;
|
|
||||||
select: CartSelectFromMap;
|
|
||||||
} ) => {
|
|
||||||
const selectedShippingRate = select
|
|
||||||
.getShippingRates()
|
|
||||||
.find(
|
|
||||||
( shippingPackage: CartShippingRate ) =>
|
|
||||||
shippingPackage.package_id === packageId
|
|
||||||
)
|
|
||||||
?.shipping_rates.find(
|
|
||||||
( rate: CartShippingPackageShippingRate ) =>
|
|
||||||
rate.selected === true
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( selectedShippingRate?.rate_id === rateId ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
dispatch.shippingRatesBeingSelected( true );
|
|
||||||
if ( abortController ) {
|
|
||||||
abortController.abort();
|
|
||||||
}
|
|
||||||
abortController =
|
|
||||||
typeof AbortController === 'undefined'
|
|
||||||
? undefined
|
|
||||||
: new AbortController();
|
|
||||||
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: `/wc/store/v1/cart/select-shipping-rate`,
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
package_id: packageId,
|
|
||||||
rate_id: rateId,
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
signal: abortController?.signal || null,
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Remove shipping and billing address from the response, so we don't overwrite what the shopper is
|
|
||||||
// entering in the form if rates suddenly appear mid-edit.
|
|
||||||
const {
|
|
||||||
shipping_address: shippingAddress,
|
|
||||||
billing_address: billingAddress,
|
|
||||||
...rest
|
|
||||||
} = response;
|
|
||||||
|
|
||||||
dispatch.receiveCart( rest );
|
|
||||||
dispatch.shippingRatesBeingSelected( false );
|
|
||||||
|
|
||||||
return response as CartResponse;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
dispatch.shippingRatesBeingSelected( false );
|
|
||||||
return Promise.reject( error );
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets billing address locally, as opposed to updateCustomerData which sends it to the server.
|
* Sets billing address locally, as opposed to updateCustomerData which sends it to the server.
|
||||||
*/
|
*/
|
||||||
export const setBillingAddress = (
|
export function setBillingAddress( billingAddress: Partial< BillingAddress > ) {
|
||||||
billingAddress: Partial< BillingAddress >
|
return { type: types.SET_BILLING_ADDRESS, billingAddress };
|
||||||
) => ( { type: types.SET_BILLING_ADDRESS, billingAddress } as const );
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets shipping address locally, as opposed to updateCustomerData which sends it to the server.
|
* Sets shipping address locally, as opposed to updateCustomerData which sends it to the server.
|
||||||
*/
|
*/
|
||||||
export const setShippingAddress = (
|
export function setShippingAddress(
|
||||||
shippingAddress: Partial< ShippingAddress >
|
shippingAddress: Partial< ShippingAddress >
|
||||||
) => ( { type: types.SET_SHIPPING_ADDRESS, shippingAddress } as const );
|
) {
|
||||||
|
return { type: types.SET_SHIPPING_ADDRESS, shippingAddress };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
export type Actions =
|
||||||
* Updates the shipping and/or billing address for the customer and returns an updated cart.
|
|
||||||
*/
|
|
||||||
export const updateCustomerData =
|
|
||||||
(
|
|
||||||
// Address data to be updated; can contain both billing_address and shipping_address.
|
|
||||||
customerData: Partial< BillingAddressShippingAddress >,
|
|
||||||
// If the address is being edited, we don't update the customer data in the store from the response.
|
|
||||||
editing = true
|
|
||||||
) =>
|
|
||||||
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
|
||||||
try {
|
|
||||||
dispatch.updatingCustomerData( true );
|
|
||||||
const { response } = await apiFetchWithHeaders( {
|
|
||||||
path: '/wc/store/v1/cart/update-customer',
|
|
||||||
method: 'POST',
|
|
||||||
data: customerData,
|
|
||||||
cache: 'no-store',
|
|
||||||
} );
|
|
||||||
if ( editing ) {
|
|
||||||
dispatch.receiveCartContents( response );
|
|
||||||
} else {
|
|
||||||
dispatch.receiveCart( response );
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
} catch ( error ) {
|
|
||||||
dispatch.receiveError( error );
|
|
||||||
return Promise.reject( error );
|
|
||||||
} finally {
|
|
||||||
dispatch.updatingCustomerData( false );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type Actions =
|
|
||||||
| typeof addItemToCart
|
|
||||||
| typeof applyCoupon
|
|
||||||
| typeof changeCartItemQuantity
|
|
||||||
| typeof itemIsPendingDelete
|
| typeof itemIsPendingDelete
|
||||||
| typeof itemIsPendingQuantity
|
| typeof itemIsPendingQuantity
|
||||||
| typeof receiveApplyingCoupon
|
| typeof receiveApplyingCoupon
|
||||||
| typeof receiveCartItem
|
| typeof receiveCartItem
|
||||||
| typeof receiveRemovingCoupon
|
| typeof receiveRemovingCoupon
|
||||||
| typeof removeCoupon
|
|
||||||
| typeof removeItemFromCart
|
|
||||||
| typeof selectShippingRate
|
|
||||||
| typeof setBillingAddress
|
| typeof setBillingAddress
|
||||||
| typeof setCartData
|
| typeof setCartData
|
||||||
| typeof setErrorData
|
| typeof setErrorData
|
||||||
| typeof setIsCartDataStale
|
| typeof setIsCartDataStale
|
||||||
| typeof setShippingAddress
|
| typeof setShippingAddress
|
||||||
| typeof shippingRatesBeingSelected
|
| typeof shippingRatesBeingSelected
|
||||||
| typeof updateCustomerData
|
|
||||||
| typeof updatingCustomerData;
|
| typeof updatingCustomerData;
|
||||||
|
|
||||||
export type CartAction = ReturnOrGeneratorYieldUnion< Actions | Thunks >;
|
export type CartAction = ReturnOrGeneratorYieldUnion< Actions | Thunks >;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { registerStore } from '@wordpress/data';
|
import { register, subscribe, createReduxStore } from '@wordpress/data';
|
||||||
import { controls as dataControls } from '@wordpress/data-controls';
|
import { controls as dataControls } from '@wordpress/data-controls';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +11,7 @@ import { STORE_KEY } from './constants';
|
||||||
import * as selectors from './selectors';
|
import * as selectors from './selectors';
|
||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
import * as resolvers from './resolvers';
|
import * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducers';
|
import reducer from './reducers';
|
||||||
import type { SelectFromMap, DispatchFromMap } from '../mapped-types';
|
import type { SelectFromMap, DispatchFromMap } from '../mapped-types';
|
||||||
import { pushChanges, flushChanges } from './push-changes';
|
import { pushChanges, flushChanges } from './push-changes';
|
||||||
import {
|
import {
|
||||||
|
@ -20,20 +20,19 @@ import {
|
||||||
} from './update-payment-methods';
|
} from './update-payment-methods';
|
||||||
import { ResolveSelectFromMap } from '../mapped-types';
|
import { ResolveSelectFromMap } from '../mapped-types';
|
||||||
|
|
||||||
// Please update from deprecated "registerStore" to "createReduxStore" when this PR is merged:
|
const store = createReduxStore( STORE_KEY, {
|
||||||
// https://github.com/WordPress/gutenberg/pull/45513
|
|
||||||
const registeredStore = registerStore< State >( STORE_KEY, {
|
|
||||||
reducer,
|
reducer,
|
||||||
actions,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
actions: actions as any,
|
||||||
controls: dataControls,
|
controls: dataControls,
|
||||||
selectors,
|
selectors,
|
||||||
resolvers,
|
resolvers,
|
||||||
__experimentalUseThunks: true,
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
register( store );
|
||||||
|
|
||||||
// Pushes changes whenever the store is updated.
|
// Pushes changes whenever the store is updated.
|
||||||
registeredStore.subscribe( pushChanges );
|
subscribe( pushChanges, store );
|
||||||
|
|
||||||
// This will skip the debounce and immediately push changes to the server when a field is blurred.
|
// This will skip the debounce and immediately push changes to the server when a field is blurred.
|
||||||
document.body.addEventListener( 'focusout', ( event: FocusEvent ) => {
|
document.body.addEventListener( 'focusout', ( event: FocusEvent ) => {
|
||||||
|
@ -49,24 +48,24 @@ document.body.addEventListener( 'focusout', ( event: FocusEvent ) => {
|
||||||
// First we will run the updatePaymentMethods function without any debounce to ensure payment methods are ready as soon
|
// First we will run the updatePaymentMethods function without any debounce to ensure payment methods are ready as soon
|
||||||
// as the cart is loaded. After that, we will unsubscribe this function and instead run the
|
// as the cart is loaded. After that, we will unsubscribe this function and instead run the
|
||||||
// debouncedUpdatePaymentMethods function on subsequent cart updates.
|
// debouncedUpdatePaymentMethods function on subsequent cart updates.
|
||||||
const unsubscribeUpdatePaymentMethods = registeredStore.subscribe( async () => {
|
const unsubscribeUpdatePaymentMethods = subscribe( async () => {
|
||||||
const didActionDispatch = await updatePaymentMethods();
|
const didActionDispatch = await updatePaymentMethods();
|
||||||
if ( didActionDispatch ) {
|
if ( didActionDispatch ) {
|
||||||
// The function we're currently in will unsubscribe itself. When we reach this line, this will be the last time
|
// The function we're currently in will unsubscribe itself. When we reach this line, this will be the last time
|
||||||
// this function is called.
|
// this function is called.
|
||||||
unsubscribeUpdatePaymentMethods();
|
unsubscribeUpdatePaymentMethods();
|
||||||
// Resubscribe, but with the debounced version of updatePaymentMethods.
|
// Resubscribe, but with the debounced version of updatePaymentMethods.
|
||||||
registeredStore.subscribe( debouncedUpdatePaymentMethods );
|
subscribe( debouncedUpdatePaymentMethods, store );
|
||||||
}
|
}
|
||||||
} );
|
}, store );
|
||||||
|
|
||||||
export const CART_STORE_KEY = STORE_KEY;
|
export const CART_STORE_KEY = STORE_KEY;
|
||||||
|
|
||||||
declare module '@wordpress/data' {
|
declare module '@wordpress/data' {
|
||||||
function dispatch(
|
function dispatch(
|
||||||
key: typeof CART_STORE_KEY
|
key: typeof STORE_KEY
|
||||||
): DispatchFromMap< typeof actions >;
|
): DispatchFromMap< typeof actions >;
|
||||||
function select( key: typeof CART_STORE_KEY ): SelectFromMap<
|
function select( key: typeof STORE_KEY ): SelectFromMap<
|
||||||
typeof selectors
|
typeof selectors
|
||||||
> & {
|
> & {
|
||||||
hasFinishedResolution: ( selector: string ) => boolean;
|
hasFinishedResolution: ( selector: string ) => boolean;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import type { CartItem } from '@woocommerce/types';
|
|
||||||
import type { Reducer } from 'redux';
|
import type { Reducer } from 'redux';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,46 +9,14 @@ import type { Reducer } from 'redux';
|
||||||
import { ACTION_TYPES as types } from './action-types';
|
import { ACTION_TYPES as types } from './action-types';
|
||||||
import { defaultCartState, CartState } from './default-state';
|
import { defaultCartState, CartState } from './default-state';
|
||||||
import { EMPTY_CART_ERRORS } from '../constants';
|
import { EMPTY_CART_ERRORS } from '../constants';
|
||||||
import type { CartAction } from './actions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sub-reducer for cart items array.
|
|
||||||
*
|
|
||||||
* @param {Array<CartItem>} state cartData.items state slice.
|
|
||||||
* @param {CartAction} action Action object.
|
|
||||||
*/
|
|
||||||
const cartItemsReducer = (
|
|
||||||
state: Array< CartItem > = [],
|
|
||||||
action: Partial< CartAction >
|
|
||||||
) => {
|
|
||||||
switch ( action.type ) {
|
|
||||||
case types.RECEIVE_CART_ITEM:
|
|
||||||
// Replace specified cart element with the new data from server.
|
|
||||||
return state.map( ( cartItem ) => {
|
|
||||||
if ( cartItem.key === action.cartItem?.key ) {
|
|
||||||
return action.cartItem;
|
|
||||||
}
|
|
||||||
return cartItem;
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reducer for receiving items related to the cart.
|
* Reducer for receiving items related to the cart.
|
||||||
*
|
|
||||||
* @param {CartState} state The current state in the store.
|
|
||||||
* @param {CartAction} action Action object.
|
|
||||||
*
|
|
||||||
* @return {CartState} New or existing state.
|
|
||||||
*/
|
*/
|
||||||
const reducer: Reducer< CartState > = (
|
const reducer: Reducer< CartState > = ( state = defaultCartState, action ) => {
|
||||||
state = defaultCartState,
|
|
||||||
action: Partial< CartAction >
|
|
||||||
) => {
|
|
||||||
switch ( action.type ) {
|
switch ( action.type ) {
|
||||||
case types.SET_ERROR_DATA:
|
case types.SET_ERROR_DATA:
|
||||||
if ( action.error ) {
|
if ( 'error' in action && action.error ) {
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
errors: [ action.error ],
|
errors: [ action.error ],
|
||||||
|
@ -142,14 +109,18 @@ const reducer: Reducer< CartState > = (
|
||||||
cartItemsPendingDelete: keysPendingDelete,
|
cartItemsPendingDelete: keysPendingDelete,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
// Delegate to cartItemsReducer.
|
|
||||||
case types.RECEIVE_CART_ITEM:
|
case types.RECEIVE_CART_ITEM:
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
errors: EMPTY_CART_ERRORS,
|
errors: EMPTY_CART_ERRORS,
|
||||||
cartData: {
|
cartData: {
|
||||||
...state.cartData,
|
...state.cartData,
|
||||||
items: cartItemsReducer( state.cartData.items, action ),
|
items: state.cartData.items.map( ( cartItem ) => {
|
||||||
|
if ( cartItem.key === action.cartItem?.key ) {
|
||||||
|
return action.cartItem;
|
||||||
|
}
|
||||||
|
return cartItem;
|
||||||
|
} ),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -6,8 +6,16 @@ import {
|
||||||
CartResponse,
|
CartResponse,
|
||||||
ApiErrorResponse,
|
ApiErrorResponse,
|
||||||
isApiErrorResponse,
|
isApiErrorResponse,
|
||||||
|
ExtensionCartUpdateArgs,
|
||||||
|
CartShippingPackageShippingRate,
|
||||||
|
CartShippingRate,
|
||||||
|
BillingAddressShippingAddress,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import { camelCaseKeys } from '@woocommerce/base-utils';
|
import {
|
||||||
|
camelCaseKeys,
|
||||||
|
triggerAddedToCartEvent,
|
||||||
|
triggerAddingToCartEvent,
|
||||||
|
} from '@woocommerce/base-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -15,12 +23,11 @@ import { camelCaseKeys } from '@woocommerce/base-utils';
|
||||||
import { notifyQuantityChanges } from './notify-quantity-changes';
|
import { notifyQuantityChanges } from './notify-quantity-changes';
|
||||||
import { notifyCartErrors } from './notify-errors';
|
import { notifyCartErrors } from './notify-errors';
|
||||||
import { CartDispatchFromMap, CartSelectFromMap } from './index';
|
import { CartDispatchFromMap, CartSelectFromMap } from './index';
|
||||||
|
import { apiFetchWithHeaders } from '../shared-controls';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A thunk used in updating the store with the cart items retrieved from a request. This also notifies the shopper
|
* A thunk used in updating the store with the cart items retrieved from a request. This also notifies the shopper
|
||||||
* of any unexpected quantity changes occurred.
|
* of any unexpected quantity changes occurred.
|
||||||
*
|
|
||||||
* @param {CartResponse} response
|
|
||||||
*/
|
*/
|
||||||
export const receiveCart =
|
export const receiveCart =
|
||||||
( response: Partial< CartResponse > ) =>
|
( response: Partial< CartResponse > ) =>
|
||||||
|
@ -75,7 +82,332 @@ export const receiveError =
|
||||||
dispatch.setErrorData( response );
|
dispatch.setErrorData( response );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POSTs to the /cart/extensions endpoint with the data supplied by the extension.
|
||||||
|
*
|
||||||
|
* @param {Object} args The data to be posted to the endpoint
|
||||||
|
*/
|
||||||
|
export const applyExtensionCartUpdate =
|
||||||
|
( args: ExtensionCartUpdateArgs ) =>
|
||||||
|
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||||
|
try {
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: '/wc/store/v1/cart/extensions',
|
||||||
|
method: 'POST',
|
||||||
|
data: { namespace: args.namespace, data: args.data },
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a coupon code and either invalidates caches, or receives an error if
|
||||||
|
* the coupon cannot be applied.
|
||||||
|
*
|
||||||
|
* @param {string} couponCode The coupon code to apply to the cart.
|
||||||
|
* @throws Will throw an error if there is an API problem.
|
||||||
|
*/
|
||||||
|
export const applyCoupon =
|
||||||
|
( couponCode: string ) =>
|
||||||
|
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||||
|
try {
|
||||||
|
dispatch.receiveApplyingCoupon( couponCode );
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: '/wc/store/v1/cart/apply-coupon',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
code: couponCode,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
} finally {
|
||||||
|
dispatch.receiveApplyingCoupon( '' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a coupon code and either invalidates caches, or receives an error if
|
||||||
|
* the coupon cannot be removed.
|
||||||
|
*
|
||||||
|
* @param {string} couponCode The coupon code to remove from the cart.
|
||||||
|
* @throws Will throw an error if there is an API problem.
|
||||||
|
*/
|
||||||
|
export const removeCoupon =
|
||||||
|
( couponCode: string ) =>
|
||||||
|
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||||
|
try {
|
||||||
|
dispatch.receiveRemovingCoupon( couponCode );
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: '/wc/store/v1/cart/remove-coupon',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
code: couponCode,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
} finally {
|
||||||
|
dispatch.receiveRemovingCoupon( '' );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to the cart:
|
||||||
|
* - Calls API to add item.
|
||||||
|
* - If successful, yields action to add item from store.
|
||||||
|
* - If error, yields action to store error.
|
||||||
|
*
|
||||||
|
* @param {number} productId Product ID to add to cart.
|
||||||
|
* @param {number} [quantity=1] Number of product ID being added to cart.
|
||||||
|
* @throws Will throw an error if there is an API problem.
|
||||||
|
*/
|
||||||
|
export const addItemToCart =
|
||||||
|
( productId: number, quantity = 1 ) =>
|
||||||
|
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||||
|
try {
|
||||||
|
triggerAddingToCartEvent();
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: `/wc/store/v1/cart/add-item`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
quantity,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
triggerAddedToCartEvent( { preserveCartData: true } );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes specified item from the cart:
|
||||||
|
* - Calls API to remove item.
|
||||||
|
* - If successful, yields action to remove item from store.
|
||||||
|
* - If error, yields action to store error.
|
||||||
|
* - Sets cart item as pending while API request is in progress.
|
||||||
|
*
|
||||||
|
* @param {string} cartItemKey Cart item being updated.
|
||||||
|
*/
|
||||||
|
export const removeItemFromCart =
|
||||||
|
( cartItemKey: string ) =>
|
||||||
|
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||||
|
try {
|
||||||
|
dispatch.itemIsPendingDelete( cartItemKey );
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: `/wc/store/v1/cart/remove-item`,
|
||||||
|
data: {
|
||||||
|
key: cartItemKey,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
} finally {
|
||||||
|
dispatch.itemIsPendingDelete( cartItemKey, false );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a quantity change the for specified cart item:
|
||||||
|
* - Calls API to set quantity.
|
||||||
|
* - If successful, yields action to update store.
|
||||||
|
* - If error, yields action to store error.
|
||||||
|
*
|
||||||
|
* @param {string} cartItemKey Cart item being updated.
|
||||||
|
* @param {number} quantity Specified (new) quantity.
|
||||||
|
*/
|
||||||
|
export const changeCartItemQuantity =
|
||||||
|
(
|
||||||
|
cartItemKey: string,
|
||||||
|
quantity: number
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unclear how to represent multiple different yields as type
|
||||||
|
) =>
|
||||||
|
async ( {
|
||||||
|
dispatch,
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
dispatch: CartDispatchFromMap;
|
||||||
|
select: CartSelectFromMap;
|
||||||
|
} ) => {
|
||||||
|
const cartItem = select.getCartItem( cartItemKey );
|
||||||
|
if ( cartItem?.quantity === quantity ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dispatch.itemIsPendingQuantity( cartItemKey );
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: '/wc/store/v1/cart/update-item',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
key: cartItemKey,
|
||||||
|
quantity,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
} finally {
|
||||||
|
dispatch.itemIsPendingQuantity( cartItemKey, false );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Facilitates aborting fetch requests.
|
||||||
|
let abortController: AbortController | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a shipping rate.
|
||||||
|
*
|
||||||
|
* @param {string} rateId The id of the rate being selected.
|
||||||
|
* @param {number | string} [packageId] The key of the packages that we will select within.
|
||||||
|
*/
|
||||||
|
export const selectShippingRate =
|
||||||
|
( rateId: string, packageId: number | null = null ) =>
|
||||||
|
async ( {
|
||||||
|
dispatch,
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
dispatch: CartDispatchFromMap;
|
||||||
|
select: CartSelectFromMap;
|
||||||
|
} ) => {
|
||||||
|
const selectedShippingRate = select
|
||||||
|
.getShippingRates()
|
||||||
|
.find(
|
||||||
|
( shippingPackage: CartShippingRate ) =>
|
||||||
|
shippingPackage.package_id === packageId
|
||||||
|
)
|
||||||
|
?.shipping_rates.find(
|
||||||
|
( rate: CartShippingPackageShippingRate ) =>
|
||||||
|
rate.selected === true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( selectedShippingRate?.rate_id === rateId ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch.shippingRatesBeingSelected( true );
|
||||||
|
if ( abortController ) {
|
||||||
|
abortController.abort();
|
||||||
|
}
|
||||||
|
abortController =
|
||||||
|
typeof AbortController === 'undefined'
|
||||||
|
? null
|
||||||
|
: new AbortController();
|
||||||
|
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: `/wc/store/v1/cart/select-shipping-rate`,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
package_id: packageId,
|
||||||
|
rate_id: rateId,
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
signal: abortController?.signal || null,
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Remove shipping and billing address from the response, so we don't overwrite what the shopper is
|
||||||
|
// entering in the form if rates suddenly appear mid-edit.
|
||||||
|
const {
|
||||||
|
shipping_address: shippingAddress,
|
||||||
|
billing_address: billingAddress,
|
||||||
|
...rest
|
||||||
|
} = response;
|
||||||
|
|
||||||
|
dispatch.receiveCart( rest );
|
||||||
|
dispatch.shippingRatesBeingSelected( false );
|
||||||
|
|
||||||
|
return response as CartResponse;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
dispatch.shippingRatesBeingSelected( false );
|
||||||
|
return Promise.reject( error );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the shipping and/or billing address for the customer and returns an updated cart.
|
||||||
|
*/
|
||||||
|
export const updateCustomerData =
|
||||||
|
(
|
||||||
|
// Address data to be updated; can contain both billing_address and shipping_address.
|
||||||
|
customerData: Partial< BillingAddressShippingAddress >,
|
||||||
|
// If the address is being edited, we don't update the customer data in the store from the response.
|
||||||
|
editing = true
|
||||||
|
) =>
|
||||||
|
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
|
||||||
|
try {
|
||||||
|
dispatch.updatingCustomerData( true );
|
||||||
|
const { response } = await apiFetchWithHeaders< {
|
||||||
|
response: CartResponse;
|
||||||
|
} >( {
|
||||||
|
path: '/wc/store/v1/cart/update-customer',
|
||||||
|
method: 'POST',
|
||||||
|
data: customerData,
|
||||||
|
cache: 'no-store',
|
||||||
|
} );
|
||||||
|
if ( editing ) {
|
||||||
|
dispatch.receiveCartContents( response );
|
||||||
|
} else {
|
||||||
|
dispatch.receiveCart( response );
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
dispatch.receiveError( isApiErrorResponse( error ) ? error : null );
|
||||||
|
return Promise.reject( error );
|
||||||
|
} finally {
|
||||||
|
dispatch.updatingCustomerData( false );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type Thunks =
|
export type Thunks =
|
||||||
| typeof receiveCart
|
| typeof receiveCart
|
||||||
| typeof receiveCartContents
|
| typeof receiveCartContents
|
||||||
| typeof receiveError;
|
| typeof receiveError
|
||||||
|
| typeof applyExtensionCartUpdate
|
||||||
|
| typeof applyCoupon
|
||||||
|
| typeof removeCoupon
|
||||||
|
| typeof addItemToCart
|
||||||
|
| typeof removeItemFromCart
|
||||||
|
| typeof changeCartItemQuantity
|
||||||
|
| typeof selectShippingRate
|
||||||
|
| typeof updateCustomerData;
|
||||||
|
|
|
@ -125,19 +125,23 @@ const doApiFetchWithHeaders = ( options: APIFetchOptions ) =>
|
||||||
...options,
|
...options,
|
||||||
parse: false,
|
parse: false,
|
||||||
} )
|
} )
|
||||||
.then( ( fetchResponse ) => {
|
.then( ( fetchResponse: unknown ) => {
|
||||||
fetchResponse
|
if ( fetchResponse instanceof Response ) {
|
||||||
.json()
|
fetchResponse
|
||||||
.then( ( response ) => {
|
.json()
|
||||||
resolve( {
|
.then( ( response: unknown ) => {
|
||||||
response,
|
resolve( {
|
||||||
headers: fetchResponse.headers,
|
response,
|
||||||
|
headers: fetchResponse.headers,
|
||||||
|
} );
|
||||||
|
setNonceOnFetch( fetchResponse.headers );
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
reject( invalidJsonError );
|
||||||
} );
|
} );
|
||||||
setNonceOnFetch( fetchResponse.headers );
|
} else {
|
||||||
} )
|
reject( invalidJsonError );
|
||||||
.catch( () => {
|
}
|
||||||
reject( invalidJsonError );
|
|
||||||
} );
|
|
||||||
} )
|
} )
|
||||||
.catch( ( errorResponse ) => {
|
.catch( ( errorResponse ) => {
|
||||||
if ( errorResponse.name !== 'AbortError' ) {
|
if ( errorResponse.name !== 'AbortError' ) {
|
||||||
|
@ -159,7 +163,7 @@ const doApiFetchWithHeaders = ( options: APIFetchOptions ) =>
|
||||||
} );
|
} );
|
||||||
} else {
|
} else {
|
||||||
batchFetch( options )
|
batchFetch( options )
|
||||||
.then( ( response: ApiResponse ) => {
|
.then( ( response: ApiResponse< unknown > ) => {
|
||||||
assertResponseIsValid( response );
|
assertResponseIsValid( response );
|
||||||
|
|
||||||
if ( response.status >= 200 && response.status < 300 ) {
|
if ( response.status >= 200 && response.status < 300 ) {
|
||||||
|
@ -173,7 +177,7 @@ const doApiFetchWithHeaders = ( options: APIFetchOptions ) =>
|
||||||
// Status code indicates error.
|
// Status code indicates error.
|
||||||
throw response;
|
throw response;
|
||||||
} )
|
} )
|
||||||
.catch( ( errorResponse: ApiResponse ) => {
|
.catch( ( errorResponse: ApiResponse< unknown > ) => {
|
||||||
if ( errorResponse.headers ) {
|
if ( errorResponse.headers ) {
|
||||||
setNonceOnFetch( errorResponse.headers );
|
setNonceOnFetch( errorResponse.headers );
|
||||||
}
|
}
|
||||||
|
@ -192,8 +196,10 @@ const doApiFetchWithHeaders = ( options: APIFetchOptions ) =>
|
||||||
*
|
*
|
||||||
* @param {APIFetchOptions} options The options for the API request.
|
* @param {APIFetchOptions} options The options for the API request.
|
||||||
*/
|
*/
|
||||||
export const apiFetchWithHeaders = ( options: APIFetchOptions ) => {
|
export const apiFetchWithHeaders = < T = unknown >(
|
||||||
return doApiFetchWithHeaders( options );
|
options: APIFetchOptions
|
||||||
|
): Promise< T > => {
|
||||||
|
return doApiFetchWithHeaders( options ) as Promise< T >;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
Comment: Refactored cart data store
|
||||||
|
|
Loading…
Reference in New Issue