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

Commit

Permalink
Allow observers to set billingAddress by returning billingData (#…
Browse files Browse the repository at this point in the history
…8163)

* Allow observers to set billingAddress by returning billingData

This is required since we didn't correctly deprecate billingData when we changed the name to billingAddress

* Add link to original PR

* Set billingAddress when observer errors

* Rename  shippingData to shippingAddress

* Add isBillingAddress and isShippingAddress type guards

* Add tests for new type guards

* Only set billing and shipping if they are valid objects

* Add tests for __internalEmitPaymentProcessingEvent thunk

* Update deprecated version

* Return promise from this function to aid with testability

* Add tests for shippingAddress and paymentMethodData

* Ensure correct value is used to set shipping address

* Move test data out of tests to aid with reusability

* Improve success callback name

* Add mocked __internalSetPaymentMethodData to correct object

It was in registry, but should be in dispatch as the action is on the same store as the thunk. Registry is used for actions on other stores.

* Add test for failed observers

* Add test for mixed observers

* Add comments explaining the destructure & deprecation
  • Loading branch information
opr authored Jan 23, 2023
1 parent c19d4a1 commit 8c772e6
Show file tree
Hide file tree
Showing 4 changed files with 390 additions and 15 deletions.
224 changes: 224 additions & 0 deletions assets/js/data/payment/test/thunks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/**
* External dependencies
*/
import * as wpDataFunctions from '@wordpress/data';
import { EventObserversType } from '@woocommerce/base-context';

/**
* Internal dependencies
*/
import { PAYMENT_STORE_KEY } from '../index';
import { __internalEmitPaymentProcessingEvent } from '../thunks';

/**
* If an observer returns billingAddress, shippingAddress, or paymentData, then the values of these
* should be updated in the data stores.
*/
const testShippingAddress = {
first_name: 'test',
last_name: 'test',
company: 'test',
address_1: 'test',
address_2: 'test',
city: 'test',
state: 'test',
postcode: 'test',
country: 'test',
phone: 'test',
};
const testBillingAddress = {
...testShippingAddress,
email: 'test@test.com',
};
const testPaymentMethodData = {
payment_method: 'test',
};

describe( 'wc/store/payment thunks', () => {
const testPaymentProcessingCallback = jest.fn();
const testPaymentProcessingCallback2 = jest.fn();
const currentObservers: EventObserversType = {
payment_processing: new Map(),
};
currentObservers.payment_processing.set( 'test', {
callback: testPaymentProcessingCallback,
priority: 10,
} );
currentObservers.payment_processing.set( 'test2', {
callback: testPaymentProcessingCallback2,
priority: 10,
} );

describe( '__internalEmitPaymentProcessingEvent', () => {
beforeEach( () => {
jest.resetAllMocks();
} );
it( 'calls all registered observers', async () => {
const {
__internalEmitPaymentProcessingEvent:
__internalEmitPaymentProcessingEventFromStore,
} = wpDataFunctions.dispatch( PAYMENT_STORE_KEY );
await __internalEmitPaymentProcessingEventFromStore(
currentObservers,
jest.fn()
);
expect( testPaymentProcessingCallback ).toHaveBeenCalled();
expect( testPaymentProcessingCallback2 ).toHaveBeenCalled();
} );

it( 'sets metadata if successful observers return it', async () => {
const testSuccessCallbackWithMetadata = jest.fn().mockReturnValue( {
type: 'success',
meta: {
billingAddress: testBillingAddress,
shippingAddress: testShippingAddress,
paymentMethodData: testPaymentMethodData,
},
} );

currentObservers.payment_processing.set( 'test3', {
callback: testSuccessCallbackWithMetadata,
priority: 10,
} );

const setBillingAddressMock = jest.fn();
const setShippingAddressMock = jest.fn();
const setPaymentMethodDataMock = jest.fn();
const registryMock = {
dispatch: jest.fn().mockImplementation( ( store: string ) => {
return {
...wpDataFunctions.dispatch( store ),
setBillingAddress: setBillingAddressMock,
setShippingAddress: setShippingAddressMock,
};
} ),
};

// Await here because the function returned by the __internalEmitPaymentProcessingEvent action creator
// (a thunk) returns a Promise.
await __internalEmitPaymentProcessingEvent(
currentObservers,
jest.fn()
)( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - it would be too much work to mock the entire registry, so we only mock dispatch on it,
// which is all we need to test this thunk.
registry: registryMock,
dispatch: {
...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ),
__internalSetPaymentMethodData: setPaymentMethodDataMock,
},
} );

expect( setBillingAddressMock ).toHaveBeenCalledWith(
testBillingAddress
);
expect( setShippingAddressMock ).toHaveBeenCalledWith(
testShippingAddress
);
expect( setPaymentMethodDataMock ).toHaveBeenCalledWith(
testPaymentMethodData
);
} );
it( 'sets metadata if failed observers return it', async () => {
const testFailingCallbackWithMetadata = jest.fn().mockReturnValue( {
type: 'failure',
meta: {
billingAddress: testBillingAddress,
paymentMethodData: testPaymentMethodData,
},
} );

currentObservers.payment_processing.set( 'test4', {
callback: testFailingCallbackWithMetadata,
priority: 10,
} );

const setBillingAddressMock = jest.fn();
const setPaymentMethodDataMock = jest.fn();
const registryMock = {
dispatch: jest.fn().mockImplementation( ( store: string ) => {
return {
...wpDataFunctions.dispatch( store ),
setBillingAddress: setBillingAddressMock,
};
} ),
};

// Await here because the function returned by the __internalEmitPaymentProcessingEvent action creator
// (a thunk) returns a Promise.
await __internalEmitPaymentProcessingEvent(
currentObservers,
jest.fn()
)( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - it would be too much work to mock the entire registry, so we only mock dispatch on it,
// which is all we need to test this thunk.
registry: registryMock,
dispatch: {
...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ),
__internalSetPaymentMethodData: setPaymentMethodDataMock,
},
} );

expect( setBillingAddressMock ).toHaveBeenCalledWith(
testBillingAddress
);
expect( setPaymentMethodDataMock ).toHaveBeenCalledWith(
testPaymentMethodData
);
} );
it( 'sets payment status to error if one observer is successful, but another errors', async () => {
const testErrorCallbackWithMetadata = jest
.fn()
.mockImplementation( () => {
return {
type: 'error',
};
} );

const testSuccessCallback = jest.fn().mockReturnValue( {
type: 'success',
} );

currentObservers.payment_processing.set( 'test5', {
callback: testErrorCallbackWithMetadata,
priority: 10,
} );
currentObservers.payment_processing.set( 'test6', {
callback: testSuccessCallback,
priority: 9,
} );

const setPaymentErrorMock = jest.fn();
const setPaymentSuccessMock = jest.fn();
const registryMock = {
dispatch: jest
.fn()
.mockImplementation( wpDataFunctions.dispatch ),
};

// Await here because the function returned by the __internalEmitPaymentProcessingEvent action creator
// (a thunk) returns a Promise.
await __internalEmitPaymentProcessingEvent(
currentObservers,
jest.fn()
)( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - it would be too much work to mock the entire registry, so we only mock dispatch on it,
// which is all we need to test this thunk.
registry: registryMock,
dispatch: {
...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ),
__internalSetPaymentError: setPaymentErrorMock,
__internalSetPaymentSuccess: setPaymentSuccessMock,
},
} );

// The observer throwing will cause this.
//expect( console ).toHaveErroredWith( new Error( 'test error' ) );
expect( setPaymentErrorMock ).toHaveBeenCalled();
expect( setPaymentSuccessMock ).not.toHaveBeenCalled();
} );
} );
} );
75 changes: 60 additions & 15 deletions assets/js/data/payment/thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
* External dependencies
*/
import { store as noticesStore } from '@wordpress/notices';
import deprecated from '@wordpress/deprecated';
import type { BillingAddress, ShippingAddress } from '@woocommerce/settings';

/**
* Internal dependencies
*/

import {
emitEventWithAbort,
isErrorResponse,
Expand All @@ -17,6 +18,10 @@ import {
import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit';
import type { emitProcessingEventType } from './types';
import { CART_STORE_KEY } from '../cart';
import {
isBillingAddress,
isShippingAddress,
} from '../../types/type-guards/address';

export const __internalSetExpressPaymentError = ( message?: string ) => {
return ( { registry } ) => {
Expand Down Expand Up @@ -47,12 +52,15 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
const { createErrorNotice, removeNotice } =
registry.dispatch( 'core/notices' );
removeNotice( 'wc-payment-error', noticeContexts.PAYMENTS );
emitEventWithAbort(
return emitEventWithAbort(
currentObserver,
EMIT_TYPES.PAYMENT_PROCESSING,
{}
).then( ( observerResponses ) => {
let successResponse, errorResponse;
let successResponse,
errorResponse,
billingAddress: BillingAddress | undefined,
shippingAddress: ShippingAddress | undefined;
observerResponses.forEach( ( response ) => {
if ( isSuccessResponse( response ) ) {
// the last observer response always "wins" for success.
Expand All @@ -64,25 +72,64 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
) {
errorResponse = response;
}
// Extensions may return shippingData, shippingAddress, billingData, and billingAddress in the response,
// so we need to check for all. If we detect either shippingData or billingData we need to show a
// deprecated warning for it, but also apply the changes to the wc/store/cart store.
const {
billingAddress: billingAddressFromResponse,

// Deprecated, but keeping it for now, for compatibility with extensions returning it.
billingData: billingDataFromResponse,
shippingAddress: shippingAddressFromResponse,

// Deprecated, but keeping it for now, for compatibility with extensions returning it.
shippingData: shippingDataFromResponse,
} = response?.meta || {};

billingAddress = billingAddressFromResponse;
shippingAddress = shippingAddressFromResponse;

if ( billingDataFromResponse ) {
// Set this here so that old extensions still using billingData can set the billingAddress.
billingAddress = billingDataFromResponse;
deprecated(
'returning billingData from an onPaymentProcessing observer in WooCommerce Blocks',
{
version: '9.5.0',
alternative: 'billingAddress',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/6369',
}
);
}

if ( shippingDataFromResponse ) {
// Set this here so that old extensions still using shippingData can set the shippingAddress.
shippingAddress = shippingDataFromResponse;
deprecated(
'returning shippingData from an onPaymentProcessing observer in WooCommerce Blocks',
{
version: '9.5.0',
alternative: 'shippingAddress',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8163',
}
);
}
} );

const { setBillingAddress, setShippingAddress } =
registry.dispatch( CART_STORE_KEY );

if ( successResponse && ! errorResponse ) {
const { paymentMethodData, billingAddress, shippingData } =
successResponse?.meta || {};
const { paymentMethodData } = successResponse?.meta || {};

if ( billingAddress ) {
if ( billingAddress && isBillingAddress( billingAddress ) ) {
setBillingAddress( billingAddress );
}
if (
typeof shippingData !== undefined &&
shippingData?.address
typeof shippingAddress !== 'undefined' &&
isShippingAddress( shippingAddress )
) {
setShippingAddress(
shippingData.address as Record< string, unknown >
);
setShippingAddress( shippingAddress );
}
dispatch.__internalSetPaymentMethodData( paymentMethodData );
dispatch.__internalSetPaymentSuccess();
Expand All @@ -97,10 +144,8 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
} );
}

const { paymentMethodData, billingAddress } =
errorResponse?.meta || {};

if ( billingAddress ) {
const { paymentMethodData } = errorResponse?.meta || {};
if ( billingAddress && isBillingAddress( billingAddress ) ) {
setBillingAddress( billingAddress );
}
dispatch.__internalSetPaymentFailed();
Expand Down
28 changes: 28 additions & 0 deletions assets/js/types/type-guards/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import type { BillingAddress, ShippingAddress } from '@woocommerce/settings';
import { objectHasProp } from '@woocommerce/types';

export const isShippingAddress = (
address: unknown
): address is ShippingAddress => {
const keys = [
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'country',
'phone',
];
return keys.every( ( key ) => objectHasProp( address, key ) );
};
export const isBillingAddress = (
address: unknown
): address is BillingAddress => {
return isShippingAddress( address ) && objectHasProp( address, 'email' );
};
Loading

0 comments on commit 8c772e6

Please sign in to comment.