rebase from live enviornment
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
export const ACTION_TYPES = {
|
||||
SET_IDLE: 'SET_IDLE',
|
||||
SET_REDIRECT_URL: 'SET_REDIRECT_URL',
|
||||
SET_COMPLETE: 'SET_CHECKOUT_COMPLETE',
|
||||
SET_BEFORE_PROCESSING: 'SET_BEFORE_PROCESSING',
|
||||
SET_AFTER_PROCESSING: 'SET_AFTER_PROCESSING',
|
||||
SET_PROCESSING: 'SET_CHECKOUT_IS_PROCESSING',
|
||||
SET_HAS_ERROR: 'SET_CHECKOUT_HAS_ERROR',
|
||||
SET_CUSTOMER_ID: 'SET_CHECKOUT_CUSTOMER_ID',
|
||||
SET_ORDER_NOTES: 'SET_CHECKOUT_ORDER_NOTES',
|
||||
INCREMENT_CALCULATING: 'INCREMENT_CALCULATING',
|
||||
DECREMENT_CALCULATING: 'DECREMENT_CALCULATING',
|
||||
SET_USE_SHIPPING_AS_BILLING: 'SET_USE_SHIPPING_AS_BILLING',
|
||||
SET_SHOULD_CREATE_ACCOUNT: 'SET_SHOULD_CREATE_ACCOUNT',
|
||||
SET_PREFERS_COLLECTION: 'SET_PREFERS_COLLECTION',
|
||||
SET_EXTENSION_DATA: 'SET_EXTENSION_DATA',
|
||||
SET_IS_CART: 'SET_IS_CART',
|
||||
} as const;
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES as types } from './action-types';
|
||||
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
|
||||
|
||||
// `Thunks are functions that can be dispatched, similar to actions creators
|
||||
export * from './thunks';
|
||||
|
||||
/**
|
||||
* Set the checkout status to `idle`
|
||||
*/
|
||||
export const __internalSetIdle = () => ( {
|
||||
type: types.SET_IDLE,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the checkout status to `before_processing`
|
||||
*/
|
||||
export const __internalSetBeforeProcessing = () => ( {
|
||||
type: types.SET_BEFORE_PROCESSING,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the checkout status to `processing`
|
||||
*/
|
||||
export const __internalSetProcessing = () => ( {
|
||||
type: types.SET_PROCESSING,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the checkout status to `after_processing`
|
||||
*/
|
||||
export const __internalSetAfterProcessing = () => ( {
|
||||
type: types.SET_AFTER_PROCESSING,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the checkout status to `complete`
|
||||
*/
|
||||
export const __internalSetComplete = (
|
||||
data: Record< string, unknown > = {}
|
||||
) => ( {
|
||||
type: types.SET_COMPLETE,
|
||||
data,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the url to redirect to after checkout completes`
|
||||
*
|
||||
* @param redirectUrl the url to redirect to
|
||||
*/
|
||||
export const __internalSetRedirectUrl = ( redirectUrl: string ) => ( {
|
||||
type: types.SET_REDIRECT_URL,
|
||||
redirectUrl,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set whether the checkout has an error or not
|
||||
*
|
||||
* @param hasError Wether the checkout has an error or not
|
||||
*/
|
||||
export const __internalSetHasError = ( hasError = true ) => ( {
|
||||
type: types.SET_HAS_ERROR,
|
||||
hasError,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Used when any of the totals, taxes, shipping, etc need to be calculated, the `calculatingCount` will be increased
|
||||
* A `calculatingCount` of 0 means nothing is being updated.
|
||||
*/
|
||||
export const __internalIncrementCalculating = () => ( {
|
||||
type: types.INCREMENT_CALCULATING,
|
||||
} );
|
||||
|
||||
/**
|
||||
* When any of the totals, taxes, shipping, etc are done beign calculated, the `calculatingCount` will be decreased
|
||||
* A `calculatingCount` of 0 means nothing is being updated.
|
||||
*/
|
||||
export const __internalDecrementCalculating = () => ( {
|
||||
type: types.DECREMENT_CALCULATING,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the customer id
|
||||
*
|
||||
* @param customerId ID of the customer who is checking out.
|
||||
*/
|
||||
export const __internalSetCustomerId = ( customerId: number ) => ( {
|
||||
type: types.SET_CUSTOMER_ID,
|
||||
customerId,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Whether to use the shipping address as the billing address
|
||||
*
|
||||
* @param useShippingAsBilling True if shipping address should be the same as billing, false otherwise
|
||||
*/
|
||||
export const __internalSetUseShippingAsBilling = (
|
||||
useShippingAsBilling: boolean
|
||||
) => ( {
|
||||
type: types.SET_USE_SHIPPING_AS_BILLING,
|
||||
useShippingAsBilling,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Whether an account should be created for the user while checking out
|
||||
*
|
||||
* @param shouldCreateAccount True if an account should be created, false otherwise
|
||||
*/
|
||||
export const __internalSetShouldCreateAccount = (
|
||||
shouldCreateAccount: boolean
|
||||
) => ( {
|
||||
type: types.SET_SHOULD_CREATE_ACCOUNT,
|
||||
shouldCreateAccount,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set the notes for the order
|
||||
*
|
||||
* @param orderNotes String that represents a note for the order
|
||||
*/
|
||||
export const __internalSetOrderNotes = ( orderNotes: string ) => ( {
|
||||
type: types.SET_ORDER_NOTES,
|
||||
orderNotes,
|
||||
} );
|
||||
|
||||
export const setPrefersCollection = ( prefersCollection: boolean ) => ( {
|
||||
type: types.SET_PREFERS_COLLECTION,
|
||||
prefersCollection,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Registers additional data under an extension namespace.
|
||||
*/
|
||||
export const __internalSetExtensionData = (
|
||||
// The namespace for the extension. Defaults to 'default'. Must be unique to prevent conflicts.
|
||||
namespace: string,
|
||||
// Data to register under the namespace.
|
||||
extensionData: Record< string, unknown >,
|
||||
// If true, all data under the current extension namespace is replaced. If false, data is appended.
|
||||
replace = false
|
||||
) => ( {
|
||||
type: types.SET_EXTENSION_DATA,
|
||||
extensionData,
|
||||
namespace,
|
||||
replace,
|
||||
} );
|
||||
|
||||
export type CheckoutAction =
|
||||
| ReturnOrGeneratorYieldUnion<
|
||||
| typeof __internalSetIdle
|
||||
| typeof __internalSetComplete
|
||||
| typeof __internalSetProcessing
|
||||
| typeof __internalSetBeforeProcessing
|
||||
| typeof __internalSetAfterProcessing
|
||||
| typeof __internalSetRedirectUrl
|
||||
| typeof __internalSetHasError
|
||||
| typeof __internalIncrementCalculating
|
||||
| typeof __internalDecrementCalculating
|
||||
| typeof __internalSetCustomerId
|
||||
| typeof __internalSetUseShippingAsBilling
|
||||
| typeof __internalSetShouldCreateAccount
|
||||
| typeof __internalSetOrderNotes
|
||||
| typeof setPrefersCollection
|
||||
| typeof __internalSetExtensionData
|
||||
>
|
||||
| Record< string, never >;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
BillingAddress,
|
||||
getSetting,
|
||||
ShippingAddress,
|
||||
} from '@woocommerce/settings';
|
||||
|
||||
import { CheckoutResponseSuccess } from '@woocommerce/types';
|
||||
|
||||
export const STORE_KEY = 'wc/store/checkout';
|
||||
|
||||
export enum STATUS {
|
||||
// When checkout state has changed but there is no activity happening.
|
||||
IDLE = 'idle',
|
||||
// After the AFTER_PROCESSING event emitters have completed. This status triggers the checkout redirect.
|
||||
COMPLETE = 'complete',
|
||||
// This is the state before checkout processing begins after the checkout button has been pressed/submitted.
|
||||
BEFORE_PROCESSING = 'before_processing',
|
||||
// After BEFORE_PROCESSING status emitters have finished successfully. Payment processing is started on this checkout status.
|
||||
PROCESSING = 'processing',
|
||||
// After server side checkout processing is completed this status is set
|
||||
AFTER_PROCESSING = 'after_processing',
|
||||
}
|
||||
|
||||
const preloadedCheckoutData = getSetting(
|
||||
'checkoutData',
|
||||
{}
|
||||
) as Partial< CheckoutResponseSuccess >;
|
||||
|
||||
export const checkoutData = {
|
||||
order_id: 0,
|
||||
customer_id: 0,
|
||||
billing_address: {} as BillingAddress,
|
||||
shipping_address: {} as ShippingAddress,
|
||||
...( preloadedCheckoutData || {} ),
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isSameAddress } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STATUS, checkoutData } from './constants';
|
||||
|
||||
export type CheckoutState = {
|
||||
// Status of the checkout
|
||||
status: STATUS;
|
||||
// If any of the totals, taxes, shipping, etc need to be calculated, the count will be increased here
|
||||
calculatingCount: number;
|
||||
// True when the checkout is in an error state. Whatever caused the error (validation/payment method) will likely have triggered a notice.
|
||||
hasError: boolean;
|
||||
// This is the url that checkout will redirect to when it's ready.
|
||||
redirectUrl: string;
|
||||
// This is the ID for the draft order if one exists.
|
||||
orderId: number;
|
||||
// Order notes introduced by the user in the checkout form.
|
||||
orderNotes: string;
|
||||
// This is the ID of the customer the draft order belongs to.
|
||||
customerId: number;
|
||||
// Should the billing form be hidden and inherit the shipping address?
|
||||
useShippingAsBilling: boolean;
|
||||
// Should a user account be created?
|
||||
shouldCreateAccount: boolean;
|
||||
// If customer wants to checkout with a local pickup option.
|
||||
prefersCollection?: boolean | undefined;
|
||||
// Custom checkout data passed to the store API on processing.
|
||||
extensionData: Record< string, Record< string, unknown > >;
|
||||
};
|
||||
|
||||
export const defaultState: CheckoutState = {
|
||||
redirectUrl: '',
|
||||
status: STATUS.PRISTINE,
|
||||
hasError: false,
|
||||
orderId: checkoutData.order_id,
|
||||
customerId: checkoutData.customer_id,
|
||||
calculatingCount: 0,
|
||||
orderNotes: '',
|
||||
useShippingAsBilling: isSameAddress(
|
||||
checkoutData.billing_address,
|
||||
checkoutData.shipping_address
|
||||
),
|
||||
shouldCreateAccount: false,
|
||||
prefersCollection: undefined,
|
||||
extensionData: {},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './constants';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
import reducer from './reducers';
|
||||
import { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
||||
|
||||
export const config = {
|
||||
reducer,
|
||||
selectors,
|
||||
actions,
|
||||
__experimentalUseThunks: true,
|
||||
};
|
||||
|
||||
const store = createReduxStore( STORE_KEY, config );
|
||||
register( store );
|
||||
|
||||
export const CHECKOUT_STORE_KEY = STORE_KEY;
|
||||
declare module '@wordpress/data' {
|
||||
function dispatch(
|
||||
key: typeof CHECKOUT_STORE_KEY
|
||||
): DispatchFromMap< typeof actions >;
|
||||
function select( key: typeof CHECKOUT_STORE_KEY ): SelectFromMap<
|
||||
typeof selectors
|
||||
> & {
|
||||
hasFinishedResolution: ( selector: string ) => boolean;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES as types } from './action-types';
|
||||
import { STATUS } from './constants';
|
||||
import { defaultState } from './default-state';
|
||||
import { CheckoutAction } from './actions';
|
||||
|
||||
const reducer = ( state = defaultState, action: CheckoutAction ) => {
|
||||
let newState = state;
|
||||
switch ( action.type ) {
|
||||
case types.SET_IDLE:
|
||||
newState =
|
||||
state.status !== STATUS.IDLE
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.IDLE,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
|
||||
case types.SET_REDIRECT_URL:
|
||||
newState =
|
||||
action.redirectUrl !== undefined &&
|
||||
action.redirectUrl !== state.redirectUrl
|
||||
? {
|
||||
...state,
|
||||
redirectUrl: action.redirectUrl,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
|
||||
case types.SET_COMPLETE:
|
||||
newState = {
|
||||
...state,
|
||||
status: STATUS.COMPLETE,
|
||||
redirectUrl:
|
||||
typeof action.data?.redirectUrl === 'string'
|
||||
? action.data.redirectUrl
|
||||
: state.redirectUrl,
|
||||
};
|
||||
break;
|
||||
case types.SET_PROCESSING:
|
||||
newState = {
|
||||
...state,
|
||||
status: STATUS.PROCESSING,
|
||||
hasError: false,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.SET_BEFORE_PROCESSING:
|
||||
newState = {
|
||||
...state,
|
||||
status: STATUS.BEFORE_PROCESSING,
|
||||
hasError: false,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.SET_AFTER_PROCESSING:
|
||||
newState = {
|
||||
...state,
|
||||
status: STATUS.AFTER_PROCESSING,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.SET_HAS_ERROR:
|
||||
newState = {
|
||||
...state,
|
||||
hasError: action.hasError,
|
||||
status:
|
||||
state.status === STATUS.PROCESSING ||
|
||||
state.status === STATUS.BEFORE_PROCESSING
|
||||
? STATUS.IDLE
|
||||
: state.status,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.INCREMENT_CALCULATING:
|
||||
newState = {
|
||||
...state,
|
||||
calculatingCount: state.calculatingCount + 1,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.DECREMENT_CALCULATING:
|
||||
newState = {
|
||||
...state,
|
||||
calculatingCount: Math.max( 0, state.calculatingCount - 1 ),
|
||||
};
|
||||
break;
|
||||
|
||||
case types.SET_CUSTOMER_ID:
|
||||
if ( action.customerId !== undefined ) {
|
||||
newState = {
|
||||
...state,
|
||||
customerId: action.customerId,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case types.SET_USE_SHIPPING_AS_BILLING:
|
||||
if (
|
||||
action.useShippingAsBilling !== undefined &&
|
||||
action.useShippingAsBilling !== state.useShippingAsBilling
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
useShippingAsBilling: action.useShippingAsBilling,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case types.SET_SHOULD_CREATE_ACCOUNT:
|
||||
if (
|
||||
action.shouldCreateAccount !== undefined &&
|
||||
action.shouldCreateAccount !== state.shouldCreateAccount
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
shouldCreateAccount: action.shouldCreateAccount,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case types.SET_PREFERS_COLLECTION:
|
||||
if (
|
||||
action.prefersCollection !== undefined &&
|
||||
action.prefersCollection !== state.prefersCollection
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
prefersCollection: action.prefersCollection,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case types.SET_ORDER_NOTES:
|
||||
if (
|
||||
action.orderNotes !== undefined &&
|
||||
state.orderNotes !== action.orderNotes
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
orderNotes: action.orderNotes,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case types.SET_EXTENSION_DATA:
|
||||
if (
|
||||
action.extensionData !== undefined &&
|
||||
action.namespace !== undefined
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
extensionData: {
|
||||
...state.extensionData,
|
||||
[ action.namespace ]: action.replace
|
||||
? action.extensionData
|
||||
: {
|
||||
...state.extensionData[ action.namespace ],
|
||||
...action.extensionData,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { select } from '@wordpress/data';
|
||||
import { hasCollectableRate } from '@woocommerce/base-utils';
|
||||
import { isString, objectHasProp } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STATUS } from './constants';
|
||||
import { CheckoutState } from './default-state';
|
||||
import { STORE_KEY as cartStoreKey } from '../cart/constants';
|
||||
|
||||
export const getCustomerId = ( state: CheckoutState ) => {
|
||||
return state.customerId;
|
||||
};
|
||||
|
||||
export const getOrderId = ( state: CheckoutState ) => {
|
||||
return state.orderId;
|
||||
};
|
||||
|
||||
export const getOrderNotes = ( state: CheckoutState ) => {
|
||||
return state.orderNotes;
|
||||
};
|
||||
|
||||
export const getRedirectUrl = ( state: CheckoutState ) => {
|
||||
return state.redirectUrl;
|
||||
};
|
||||
|
||||
export const getUseShippingAsBilling = ( state: CheckoutState ) => {
|
||||
return state.useShippingAsBilling;
|
||||
};
|
||||
|
||||
export const getExtensionData = ( state: CheckoutState ) => {
|
||||
return state.extensionData;
|
||||
};
|
||||
|
||||
export const getShouldCreateAccount = ( state: CheckoutState ) => {
|
||||
return state.shouldCreateAccount;
|
||||
};
|
||||
|
||||
export const getCheckoutStatus = ( state: CheckoutState ) => {
|
||||
return state.status;
|
||||
};
|
||||
|
||||
export const hasError = ( state: CheckoutState ) => {
|
||||
return state.hasError;
|
||||
};
|
||||
|
||||
export const hasOrder = ( state: CheckoutState ) => {
|
||||
return !! state.orderId;
|
||||
};
|
||||
|
||||
export const isComplete = ( state: CheckoutState ) => {
|
||||
return state.status === STATUS.COMPLETE;
|
||||
};
|
||||
|
||||
export const isIdle = ( state: CheckoutState ) => {
|
||||
return state.status === STATUS.IDLE;
|
||||
};
|
||||
|
||||
export const isBeforeProcessing = ( state: CheckoutState ) => {
|
||||
return state.status === STATUS.BEFORE_PROCESSING;
|
||||
};
|
||||
|
||||
export const isAfterProcessing = ( state: CheckoutState ) => {
|
||||
return state.status === STATUS.AFTER_PROCESSING;
|
||||
};
|
||||
|
||||
export const isProcessing = ( state: CheckoutState ) => {
|
||||
return state.status === STATUS.PROCESSING;
|
||||
};
|
||||
|
||||
export const isCalculating = ( state: CheckoutState ) => {
|
||||
return state.calculatingCount > 0;
|
||||
};
|
||||
|
||||
export const prefersCollection = ( state: CheckoutState ) => {
|
||||
if ( typeof state.prefersCollection === 'undefined' ) {
|
||||
const shippingRates = select( cartStoreKey ).getShippingRates();
|
||||
if ( ! shippingRates || ! shippingRates.length ) {
|
||||
return false;
|
||||
}
|
||||
const selectedRate = shippingRates[ 0 ].shipping_rates.find(
|
||||
( rate ) => rate.selected
|
||||
);
|
||||
|
||||
if (
|
||||
objectHasProp( selectedRate, 'method_id' ) &&
|
||||
isString( selectedRate.method_id )
|
||||
) {
|
||||
return hasCollectableRate( selectedRate?.method_id );
|
||||
}
|
||||
}
|
||||
return state.prefersCollection;
|
||||
};
|
||||
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import reducer from '../reducers';
|
||||
import { defaultState } from '../default-state';
|
||||
import { STATUS } from '../constants';
|
||||
import * as actions from '../actions';
|
||||
|
||||
describe.only( 'Checkout Store Reducer', () => {
|
||||
it( 'should return the initial state', () => {
|
||||
expect( reducer( undefined, {} ) ).toEqual( defaultState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_IDLE', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
status: STATUS.IDLE,
|
||||
};
|
||||
|
||||
expect( reducer( defaultState, actions.__internalSetIdle() ) ).toEqual(
|
||||
expectedState
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should handle SET_REDIRECT_URL', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
redirectUrl: 'https://example.com',
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(
|
||||
defaultState,
|
||||
actions.__internalSetRedirectUrl( 'https://example.com' )
|
||||
)
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_COMPLETE', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
status: STATUS.COMPLETE,
|
||||
redirectUrl: 'https://example.com',
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(
|
||||
defaultState,
|
||||
actions.__internalSetComplete( {
|
||||
redirectUrl: 'https://example.com',
|
||||
} )
|
||||
)
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_PROCESSING', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
status: STATUS.PROCESSING,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( defaultState, actions.__internalSetProcessing() )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_HAS_ERROR when status is PROCESSING', () => {
|
||||
const initialState = { ...defaultState, status: STATUS.PROCESSING };
|
||||
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
hasError: true,
|
||||
status: STATUS.IDLE,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( initialState, actions.__internalSetHasError( true ) )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_HAS_ERROR when status is BEFORE_PROCESSING', () => {
|
||||
const initialState = {
|
||||
...defaultState,
|
||||
status: STATUS.BEFORE_PROCESSING,
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
hasError: true,
|
||||
status: STATUS.IDLE,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( initialState, actions.__internalSetHasError( true ) )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_HAS_ERROR when status is anything else', () => {
|
||||
const initialState = {
|
||||
...defaultState,
|
||||
status: STATUS.AFTER_PROCESSING,
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
hasError: false,
|
||||
status: STATUS.AFTER_PROCESSING,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( initialState, actions.__internalSetHasError( false ) )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_BEFORE_PROCESSING', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
status: STATUS.BEFORE_PROCESSING,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( defaultState, actions.__internalSetBeforeProcessing() )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_AFTER_PROCESSING', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
status: STATUS.AFTER_PROCESSING,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( defaultState, actions.__internalSetAfterProcessing() )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle INCREMENT_CALCULATING', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
calculatingCount: 1,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( defaultState, actions.__internalIncrementCalculating() )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle DECREMENT_CALCULATING', () => {
|
||||
const initialState = {
|
||||
...defaultState,
|
||||
calculatingCount: 1,
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
calculatingCount: 0,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( initialState, actions.__internalDecrementCalculating() )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_CUSTOMER_ID', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
customerId: 1,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( defaultState, actions.__internalSetCustomerId( 1 ) )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_USE_SHIPPING_AS_BILLING', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
useShippingAsBilling: false,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(
|
||||
defaultState,
|
||||
actions.__internalSetUseShippingAsBilling( false )
|
||||
)
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_SHOULD_CREATE_ACCOUNT', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
shouldCreateAccount: true,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(
|
||||
defaultState,
|
||||
actions.__internalSetShouldCreateAccount( true )
|
||||
)
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
it( 'should handle SET_ORDER_NOTES', () => {
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
orderNotes: 'test',
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer( defaultState, actions.__internalSetOrderNotes( 'test' ) )
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
|
||||
describe( 'should handle SET_EXTENSION_DATA', () => {
|
||||
it( 'should set data under a namespace', () => {
|
||||
const mockExtensionData = {
|
||||
extensionNamespace: {
|
||||
testKey: 'test-value',
|
||||
testKey2: 'test-value-2',
|
||||
},
|
||||
};
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
extensionData: mockExtensionData,
|
||||
};
|
||||
expect(
|
||||
reducer(
|
||||
defaultState,
|
||||
actions.__internalSetExtensionData(
|
||||
'extensionNamespace',
|
||||
mockExtensionData.extensionNamespace
|
||||
)
|
||||
)
|
||||
).toEqual( expectedState );
|
||||
} );
|
||||
it( 'should append data under a namespace', () => {
|
||||
const mockExtensionData = {
|
||||
extensionNamespace: {
|
||||
testKey: 'test-value',
|
||||
testKey2: 'test-value-2',
|
||||
},
|
||||
};
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
extensionData: mockExtensionData,
|
||||
};
|
||||
const firstState = reducer(
|
||||
defaultState,
|
||||
actions.__internalSetExtensionData( 'extensionNamespace', {
|
||||
testKey: 'test-value',
|
||||
} )
|
||||
);
|
||||
const secondState = reducer(
|
||||
firstState,
|
||||
actions.__internalSetExtensionData( 'extensionNamespace', {
|
||||
testKey2: 'test-value-2',
|
||||
} )
|
||||
);
|
||||
expect( secondState ).toEqual( expectedState );
|
||||
} );
|
||||
it( 'support replacing data under a namespace', () => {
|
||||
const mockExtensionData = {
|
||||
extensionNamespace: {
|
||||
testKey: 'test-value',
|
||||
},
|
||||
};
|
||||
const expectedState = {
|
||||
...defaultState,
|
||||
extensionData: mockExtensionData,
|
||||
};
|
||||
const firstState = reducer(
|
||||
defaultState,
|
||||
actions.__internalSetExtensionData( 'extensionNamespace', {
|
||||
testKeyOld: 'test-value',
|
||||
} )
|
||||
);
|
||||
const secondState = reducer(
|
||||
firstState,
|
||||
actions.__internalSetExtensionData(
|
||||
'extensionNamespace',
|
||||
{ testKey: 'test-value' },
|
||||
true
|
||||
)
|
||||
);
|
||||
expect( secondState ).toEqual( expectedState );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { CheckoutResponse } from '@woocommerce/types';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { dispatch as wpDispatch, select as wpSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY as PAYMENT_STORE_KEY } from '../payment/constants';
|
||||
import { removeNoticesByStatus } from '../../utils/notices';
|
||||
import {
|
||||
getPaymentResultFromCheckoutResponse,
|
||||
runCheckoutFailObservers,
|
||||
runCheckoutSuccessObservers,
|
||||
} from './utils';
|
||||
import {
|
||||
EVENTS,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
} from '../../base/context/providers/cart-checkout/checkout-events/event-emit';
|
||||
import type {
|
||||
emitValidateEventType,
|
||||
emitAfterProcessingEventsType,
|
||||
} from './types';
|
||||
import type { DispatchFromMap } from '../mapped-types';
|
||||
import * as actions from './actions';
|
||||
|
||||
/**
|
||||
* Based on the result of the payment, update the redirect url,
|
||||
* set the payment processing response in the checkout data store
|
||||
* and change the status to AFTER_PROCESSING
|
||||
*/
|
||||
export const __internalProcessCheckoutResponse = (
|
||||
response: CheckoutResponse
|
||||
) => {
|
||||
return ( {
|
||||
dispatch,
|
||||
}: {
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
} ) => {
|
||||
const paymentResult = getPaymentResultFromCheckoutResponse( response );
|
||||
dispatch.__internalSetRedirectUrl( paymentResult?.redirectUrl || '' );
|
||||
// The local `dispatch` here is bound to the actions of the data store. We need to use the global dispatch here
|
||||
// to dispatch an action on a different store.
|
||||
wpDispatch( PAYMENT_STORE_KEY ).__internalSetPaymentResult(
|
||||
paymentResult
|
||||
);
|
||||
dispatch.__internalSetAfterProcessing();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit the CHECKOUT_VALIDATION event and process all
|
||||
* registered observers
|
||||
*/
|
||||
export const __internalEmitValidateEvent: emitValidateEventType = ( {
|
||||
observers,
|
||||
setValidationErrors, // TODO: Fix this type after we move to validation store
|
||||
} ) => {
|
||||
return ( { dispatch, registry } ) => {
|
||||
const { createErrorNotice } = registry.dispatch( noticesStore );
|
||||
removeNoticesByStatus( 'error' );
|
||||
emitEvent( observers, EVENTS.CHECKOUT_VALIDATION, {} ).then(
|
||||
( response ) => {
|
||||
if ( response !== true ) {
|
||||
if ( Array.isArray( response ) ) {
|
||||
response.forEach(
|
||||
( {
|
||||
errorMessage,
|
||||
validationErrors,
|
||||
context = 'wc/checkout',
|
||||
} ) => {
|
||||
createErrorNotice( errorMessage, { context } );
|
||||
setValidationErrors( validationErrors );
|
||||
}
|
||||
);
|
||||
}
|
||||
dispatch.__internalSetIdle();
|
||||
dispatch.__internalSetHasError();
|
||||
} else {
|
||||
dispatch.__internalSetProcessing();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit the CHECKOUT_FAIL if the checkout contains an error,
|
||||
* or the CHECKOUT_SUCCESS if not. Set checkout errors according
|
||||
* to the observer responses
|
||||
*/
|
||||
export const __internalEmitAfterProcessingEvents: emitAfterProcessingEventsType =
|
||||
( { observers, notices } ) => {
|
||||
return ( { select, dispatch, registry } ) => {
|
||||
const { createErrorNotice } = registry.dispatch( noticesStore );
|
||||
const data = {
|
||||
redirectUrl: select.getRedirectUrl(),
|
||||
orderId: select.getOrderId(),
|
||||
customerId: select.getCustomerId(),
|
||||
orderNotes: select.getOrderNotes(),
|
||||
processingResponse:
|
||||
wpSelect( PAYMENT_STORE_KEY ).getPaymentResult(),
|
||||
};
|
||||
if ( select.hasError() ) {
|
||||
// allow payment methods or other things to customize the error
|
||||
// with a fallback if nothing customizes it.
|
||||
emitEventWithAbort(
|
||||
observers,
|
||||
EVENTS.CHECKOUT_FAIL,
|
||||
data
|
||||
).then( ( observerResponses ) => {
|
||||
runCheckoutFailObservers( {
|
||||
observerResponses,
|
||||
notices,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
data,
|
||||
} );
|
||||
} );
|
||||
} else {
|
||||
emitEventWithAbort(
|
||||
observers,
|
||||
EVENTS.CHECKOUT_SUCCESS,
|
||||
data
|
||||
).then( ( observerResponses: unknown[] ) => {
|
||||
runCheckoutSuccessObservers( {
|
||||
observerResponses,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
} );
|
||||
} );
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Notice } from '@wordpress/notices/';
|
||||
import { DataRegistry } from '@wordpress/data';
|
||||
import { FieldValidationStatus } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { EventObserversType } from '../../base/context/event-emit/types';
|
||||
import type { CheckoutState } from './default-state';
|
||||
import type { PaymentState } from '../payment/default-state';
|
||||
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
|
||||
export type CheckoutAfterProcessingWithErrorEventData = {
|
||||
redirectUrl: CheckoutState[ 'redirectUrl' ];
|
||||
orderId: CheckoutState[ 'orderId' ];
|
||||
customerId: CheckoutState[ 'customerId' ];
|
||||
orderNotes: CheckoutState[ 'orderNotes' ];
|
||||
processingResponse: PaymentState[ 'paymentResult' ];
|
||||
};
|
||||
export type CheckoutAndPaymentNotices = {
|
||||
checkoutNotices: Notice[];
|
||||
paymentNotices: Notice[];
|
||||
expressPaymentNotices: Notice[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for emitAfterProcessingEventsType() thunk
|
||||
*/
|
||||
export type emitAfterProcessingEventsType = ( {
|
||||
observers,
|
||||
notices,
|
||||
}: {
|
||||
observers: EventObserversType;
|
||||
notices: CheckoutAndPaymentNotices;
|
||||
} ) => ( {
|
||||
select,
|
||||
dispatch,
|
||||
registry,
|
||||
}: {
|
||||
select: SelectFromMap< typeof selectors >;
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
registry: DataRegistry;
|
||||
} ) => void;
|
||||
|
||||
/**
|
||||
* Type for emitValidateEventType() thunk
|
||||
*/
|
||||
export type emitValidateEventType = ( {
|
||||
observers,
|
||||
setValidationErrors,
|
||||
}: {
|
||||
observers: EventObserversType;
|
||||
setValidationErrors: (
|
||||
errors: Record< string, FieldValidationStatus >
|
||||
) => void;
|
||||
} ) => ( {
|
||||
dispatch,
|
||||
registry,
|
||||
}: {
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
registry: DataRegistry;
|
||||
} ) => void;
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isString, isObject } from '@woocommerce/types';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import type { PaymentResult, CheckoutResponse } from '@woocommerce/types';
|
||||
import type { createErrorNotice as originalCreateErrorNotice } from '@wordpress/notices/store/actions';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
isSuccessResponse,
|
||||
shouldRetry,
|
||||
} from '../../base/context/event-emit';
|
||||
import {
|
||||
CheckoutAndPaymentNotices,
|
||||
CheckoutAfterProcessingWithErrorEventData,
|
||||
} from './types';
|
||||
import { DispatchFromMap } from '../mapped-types';
|
||||
import * as actions from './actions';
|
||||
|
||||
/**
|
||||
* Based on the given observers, create Error Notices where necessary
|
||||
* and return the error response of the last registered observer
|
||||
*/
|
||||
export const handleErrorResponse = ( {
|
||||
observerResponses,
|
||||
createErrorNotice,
|
||||
}: {
|
||||
observerResponses: unknown[];
|
||||
createErrorNotice: typeof originalCreateErrorNotice;
|
||||
} ) => {
|
||||
let errorResponse = null;
|
||||
observerResponses.forEach( ( response ) => {
|
||||
if ( isErrorResponse( response ) || isFailResponse( response ) ) {
|
||||
if ( response.message && isString( response.message ) ) {
|
||||
const errorOptions =
|
||||
response.messageContext &&
|
||||
isString( response.messageContext )
|
||||
? // The `as string` is OK here because of the type guard above.
|
||||
{
|
||||
context: response.messageContext as string,
|
||||
}
|
||||
: undefined;
|
||||
errorResponse = response;
|
||||
createErrorNotice( response.message, errorOptions );
|
||||
}
|
||||
}
|
||||
} );
|
||||
return errorResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* This functions runs after the CHECKOUT_FAIL event has been triggered and
|
||||
* all observers have been processed. It sets any Error Notices and the status of the Checkout
|
||||
* based on the observer responses
|
||||
*/
|
||||
export const runCheckoutFailObservers = ( {
|
||||
observerResponses,
|
||||
notices,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
data,
|
||||
}: {
|
||||
observerResponses: unknown[];
|
||||
notices: CheckoutAndPaymentNotices;
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
data: CheckoutAfterProcessingWithErrorEventData;
|
||||
createErrorNotice: typeof originalCreateErrorNotice;
|
||||
} ) => {
|
||||
const errorResponse = handleErrorResponse( {
|
||||
observerResponses,
|
||||
createErrorNotice,
|
||||
} );
|
||||
|
||||
if ( errorResponse !== null ) {
|
||||
// irrecoverable error so set complete
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
dispatch.__internalSetComplete( errorResponse );
|
||||
} else {
|
||||
dispatch.__internalSetIdle();
|
||||
}
|
||||
} else {
|
||||
const hasErrorNotices =
|
||||
notices.checkoutNotices.some(
|
||||
( notice: { status: string } ) => notice.status === 'error'
|
||||
) ||
|
||||
notices.expressPaymentNotices.some(
|
||||
( notice: { status: string } ) => notice.status === 'error'
|
||||
) ||
|
||||
notices.paymentNotices.some(
|
||||
( notice: { status: string } ) => notice.status === 'error'
|
||||
);
|
||||
if ( ! hasErrorNotices ) {
|
||||
// no error handling in place by anything so let's fall
|
||||
// back to default
|
||||
const message =
|
||||
data.processingResponse?.message ||
|
||||
__(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
createErrorNotice( message, {
|
||||
id: 'checkout',
|
||||
context: 'wc/checkout',
|
||||
} );
|
||||
}
|
||||
|
||||
dispatch.__internalSetIdle();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This functions runs after the CHECKOUT_SUCCESS event has been triggered and
|
||||
* all observers have been processed. It sets any Error Notices and the status of the Checkout
|
||||
* based on the observer responses
|
||||
*/
|
||||
export const runCheckoutSuccessObservers = ( {
|
||||
observerResponses,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
}: {
|
||||
observerResponses: unknown[];
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
createErrorNotice: typeof originalCreateErrorNotice;
|
||||
} ) => {
|
||||
let successResponse = null as null | Record< string, unknown >;
|
||||
let errorResponse = null as null | Record< string, unknown >;
|
||||
|
||||
observerResponses.forEach( ( response ) => {
|
||||
if ( isSuccessResponse( response ) ) {
|
||||
// the last observer response always "wins" for success.
|
||||
successResponse = response;
|
||||
}
|
||||
|
||||
if ( isErrorResponse( response ) || isFailResponse( response ) ) {
|
||||
errorResponse = response;
|
||||
}
|
||||
} );
|
||||
|
||||
if ( successResponse && ! errorResponse ) {
|
||||
dispatch.__internalSetComplete( successResponse );
|
||||
} else if ( isObject( errorResponse ) ) {
|
||||
if ( errorResponse.message && isString( errorResponse.message ) ) {
|
||||
const errorOptions =
|
||||
errorResponse.messageContext &&
|
||||
isString( errorResponse.messageContext )
|
||||
? {
|
||||
context: errorResponse.messageContext,
|
||||
}
|
||||
: undefined;
|
||||
createErrorNotice( errorResponse.message, errorOptions );
|
||||
}
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
dispatch.__internalSetComplete( errorResponse );
|
||||
} else {
|
||||
// this will set an error which will end up
|
||||
// triggering the onCheckoutFail emitter.
|
||||
// and then setting checkout to IDLE state.
|
||||
dispatch.__internalSetHasError( true );
|
||||
}
|
||||
} else {
|
||||
// nothing hooked in had any response type so let's just consider successful.
|
||||
dispatch.__internalSetComplete();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares the payment_result data from the server checkout endpoint response.
|
||||
*/
|
||||
export const getPaymentResultFromCheckoutResponse = (
|
||||
response: CheckoutResponse
|
||||
): PaymentResult => {
|
||||
const paymentResult = {
|
||||
message: '',
|
||||
paymentStatus: 'not set',
|
||||
redirectUrl: '',
|
||||
paymentDetails: {},
|
||||
} as PaymentResult;
|
||||
|
||||
// payment_result is present in successful responses.
|
||||
if ( 'payment_result' in response ) {
|
||||
paymentResult.paymentStatus = response.payment_result.payment_status;
|
||||
paymentResult.redirectUrl = response.payment_result.redirect_url;
|
||||
|
||||
if (
|
||||
response.payment_result.hasOwnProperty( 'payment_details' ) &&
|
||||
Array.isArray( response.payment_result.payment_details )
|
||||
) {
|
||||
response.payment_result.payment_details.forEach(
|
||||
( { key, value }: { key: string; value: string } ) => {
|
||||
paymentResult.paymentDetails[ key ] =
|
||||
decodeEntities( value );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// message is present in error responses.
|
||||
if ( 'message' in response ) {
|
||||
paymentResult.message = decodeEntities( response.message );
|
||||
}
|
||||
|
||||
// If there was an error code but no message, set a default message.
|
||||
if (
|
||||
! paymentResult.message &&
|
||||
'data' in response &&
|
||||
'status' in response.data &&
|
||||
response.data.status > 299
|
||||
) {
|
||||
paymentResult.message = __(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
}
|
||||
|
||||
return paymentResult;
|
||||
};
|
||||
Reference in New Issue
Block a user