rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -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;

View File

@@ -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 >;

View File

@@ -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 || {} ),
};

View File

@@ -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: {},
};

View File

@@ -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;
};
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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 );
} );
} );
} );

View File

@@ -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,
} );
} );
}
};
};

View File

@@ -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;

View File

@@ -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;
};