Add a new store to interact with WC Payments REST APIs (https://github.com/woocommerce/woocommerce-admin/pull/6918)

* Add a new store to interact with WC Payments REST APIs

* Convert to Typescript

* Refactor payments store

* Fixed type on GET_PAYMENT_GATEWAYS_SUCCESS action name

* Added SettingDefinition

* Added PaymentSelectors type

* Updated Array<type> to string[]

* Update action name in test

* Move stub.ts out of test directory

* Set type for the test stub and change order type to number | ""

* Rename action type name

* Add changelog

* Follow _REQUEST _SUCCESS _ERROR action naming

* Add a new action and reducer for getPaymentGateway

* Change store key

* Move to packages/data

* Export store name
This commit is contained in:
Moon 2021-05-24 10:29:44 -07:00 committed by GitHub
parent ce02b58b23
commit 9613c04d31
12 changed files with 649 additions and 0 deletions

View File

@ -56,6 +56,8 @@ export { getLeaderboard, searchItemsByString } from './items/utils';
export { NAVIGATION_STORE_NAME } from './navigation';
export { withNavigationHydration } from './navigation/with-navigation-hydration';
export { PAYMENT_GATEWAYS_STORE_NAME } from './payment-gateways';
export {
getFilterQuery,
getSummaryNumbers,

View File

@ -0,0 +1,13 @@
export enum ACTION_TYPES {
GET_PAYMENT_GATEWAYS_REQUEST = 'GET_PAYMENT_GATEWAYS_REQUEST',
GET_PAYMENT_GATEWAYS_SUCCESS = 'GET_PAYMENT_GATEWAYS_SUCCESS',
GET_PAYMENT_GATEWAYS_ERROR = 'GET_PAYMENT_GATEWAYS_ERROR',
UPDATE_PAYMENT_GATEWAY_REQUEST = 'UPDATE_PAYMENT_GATEWAY_REQUEST',
UPDATE_PAYMENT_GATEWAY_SUCCESS = 'UPDATE_PAYMENT_GATEWAY_SUCCESS',
UPDATE_PAYMENT_GATEWAY_ERROR = 'UPDATE_PAYMENT_GATEWAY_ERROR',
GET_PAYMENT_GATEWAY_REQUEST = 'GET_PAYMENT_GATEWAY_REQUEST',
GET_PAYMENT_GATEWAY_SUCCESS = 'GET_PAYMENT_GATEWAY_SUCCESS',
GET_PAYMENT_GATEWAY_ERROR = 'GET_PAYMENT_GATEWAY_ERROR',
}

View File

@ -0,0 +1,141 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { ACTION_TYPES } from './action-types';
import { API_NAMESPACE } from './constants';
import { PaymentGateway, RestApiError } from './types';
export function getPaymentGatewaysRequest(): {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_REQUEST;
} {
return {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_REQUEST,
};
}
export function getPaymentGatewaysSuccess(
paymentGateways: PaymentGateway[]
): {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_SUCCESS;
paymentGateways: PaymentGateway[];
} {
return {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_SUCCESS,
paymentGateways,
};
}
export function getPaymentGatewaysError(
error: RestApiError
): {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR;
error: RestApiError;
} {
return {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR,
error,
};
}
export function getPaymentGatewayRequest(): {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_REQUEST;
} {
return {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_REQUEST,
};
}
export function getPaymentGatewayError(
error: RestApiError
): {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR;
error: RestApiError;
} {
return {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR,
error,
};
}
export function getPaymentGatewaySuccess(
paymentGateway: PaymentGateway
): {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_SUCCESS;
paymentGateway: PaymentGateway;
} {
return {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_SUCCESS,
paymentGateway,
};
}
export function updatePaymentGatewaySuccess(
paymentGateway: PaymentGateway
): {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS;
paymentGateway: PaymentGateway;
} {
return {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS,
paymentGateway,
};
}
export function updatePaymentGatewayRequest(): {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST;
} {
return {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST,
};
}
export function updatePaymentGatewayError(
error: RestApiError
): {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR;
error: RestApiError;
} {
return {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR,
error,
};
}
export function* updatePaymentGateway(
id: string,
data: Partial< PaymentGateway >
) {
try {
yield updatePaymentGatewayRequest();
const response: PaymentGateway = yield apiFetch( {
method: 'PUT',
path: API_NAMESPACE + '/payment_gateways/' + id,
body: JSON.stringify( data ),
} );
if ( response && response.id === id ) {
// Update the already loaded payment gateway list with the new data
yield updatePaymentGatewaySuccess( response );
return response;
}
} catch ( e ) {
yield updatePaymentGatewayError( e );
}
}
export type Actions =
| ReturnType< typeof updatePaymentGateway >
| ReturnType< typeof updatePaymentGatewayRequest >
| ReturnType< typeof updatePaymentGatewaySuccess >
| ReturnType< typeof getPaymentGatewaysRequest >
| ReturnType< typeof getPaymentGatewaysSuccess >
| ReturnType< typeof getPaymentGatewaysError >
| ReturnType< typeof getPaymentGatewayRequest >
| ReturnType< typeof getPaymentGatewaySuccess >
| ReturnType< typeof getPaymentGatewayError >
| ReturnType< typeof updatePaymentGatewayRequest >
| ReturnType< typeof updatePaymentGatewayError >;

View File

@ -0,0 +1,2 @@
export const STORE_KEY = 'wc/payment-gateways';
export const API_NAMESPACE = 'wc/v3';

View File

@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import * as actions from './actions';
import * as resolvers from './resolvers';
import * as selectors from './selectors';
import reducer from './reducer';
import { STORE_KEY } from './constants';
export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY;
registerStore( STORE_KEY, {
actions,
selectors,
resolvers,
controls,
reducer,
} );

View File

@ -0,0 +1,139 @@
/**
* Internal dependencies
*/
import { ACTION_TYPES } from './action-types';
import { PluginsState, SelectorKeysWithActions, PaymentGateway } from './types';
import { Actions } from './actions';
function updatePaymentGatewayList(
state: PluginsState,
paymentGateway: PaymentGateway,
selector: SelectorKeysWithActions
): PluginsState {
const targetIndex = state.paymentGateways.findIndex(
( gateway ) => gateway.id === paymentGateway.id
);
if ( targetIndex === -1 ) {
return {
...state,
paymentGateways: [ ...state.paymentGateways, paymentGateway ],
requesting: {
...state.requesting,
[ selector ]: false,
},
};
}
return {
...state,
paymentGateways: [
...state.paymentGateways.slice( 0, targetIndex ),
paymentGateway,
...state.paymentGateways.slice( targetIndex + 1 ),
],
requesting: {
...state.requesting,
[ selector ]: false,
},
};
}
const reducer = (
state: PluginsState = {
paymentGateways: [],
requesting: {},
errors: {},
},
payload?: Actions
): PluginsState => {
if ( payload && 'type' in payload ) {
switch ( payload.type ) {
case ACTION_TYPES.GET_PAYMENT_GATEWAYS_REQUEST:
return {
...state,
requesting: {
...state.requesting,
getPaymentGateways: true,
},
};
case ACTION_TYPES.GET_PAYMENT_GATEWAY_REQUEST:
return {
...state,
requesting: {
...state.requesting,
getPaymentGateway: true,
},
};
case ACTION_TYPES.GET_PAYMENT_GATEWAYS_SUCCESS:
return {
...state,
paymentGateways: payload.paymentGateways,
requesting: {
...state.requesting,
getPaymentGateways: false,
},
};
case ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR:
return {
...state,
errors: {
...state.errors,
getPaymentGateways: payload.error,
},
requesting: {
...state.requesting,
getPaymentGateways: false,
},
};
case ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR:
return {
...state,
errors: {
...state.errors,
getPaymentGateway: payload.error,
},
requesting: {
...state.requesting,
getPaymentGateway: false,
},
};
case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST:
return {
...state,
requesting: {
...state.requesting,
updatePaymentGateway: true,
},
};
case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS:
return updatePaymentGatewayList(
state,
payload.paymentGateway,
'updatePaymentGateway'
);
case ACTION_TYPES.GET_PAYMENT_GATEWAY_SUCCESS:
return updatePaymentGatewayList(
state,
payload.paymentGateway,
'getPaymentGateway'
);
case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR:
return {
...state,
errors: {
...state.errors,
updatePaymentGateway: payload.error,
},
requesting: {
...state.requesting,
updatePaymentGateway: false,
},
};
}
}
return state;
};
export default reducer;

View File

@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import {
getPaymentGatewaysSuccess,
getPaymentGatewaySuccess,
getPaymentGatewaysError,
getPaymentGatewayError,
getPaymentGatewayRequest,
getPaymentGatewaysRequest,
} from './actions';
import { API_NAMESPACE } from './constants';
import { PaymentGateway } from './types';
export function* getPaymentGateways() {
yield getPaymentGatewaysRequest();
try {
const response: Array< PaymentGateway > = yield apiFetch( {
path: API_NAMESPACE + '/payment_gateways',
} );
yield getPaymentGatewaysSuccess( response );
} catch ( e ) {
yield getPaymentGatewaysError( e );
}
}
export function* getPaymentGateway( id: string ) {
yield getPaymentGatewayRequest();
try {
const response: PaymentGateway = yield apiFetch( {
path: API_NAMESPACE + '/payment_gateways/' + id,
} );
if ( response && response.id ) {
yield getPaymentGatewaySuccess( response );
return response;
}
} catch ( e ) {
yield getPaymentGatewayError( e );
}
}

View File

@ -0,0 +1,39 @@
/**
* Internal dependencies
*/
import {
PaymentGateway,
PluginsState,
WPDataSelector,
WPDataSelectors,
} from './types';
export function getPaymentGateway(
state: PluginsState,
id: string
): PaymentGateway | undefined {
return state.paymentGateways.find(
( paymentGateway ) => paymentGateway.id === id
);
}
export function getPaymentGateways(
state: PluginsState
): Array< PaymentGateway > {
return state.paymentGateways;
}
export function isPaymentGatewayRequesting(
state: PluginsState,
selector: string
): boolean {
return state.requesting[ selector ] || false;
}
export type PaymentSelectors = {
getPaymentGateway: WPDataSelector< typeof getPaymentGateway >;
getPaymentGateways: WPDataSelector< typeof getPaymentGateways >;
isPaymentGatewayRequesting: WPDataSelector<
typeof isPaymentGatewayRequesting
>;
} & WPDataSelectors;

View File

@ -0,0 +1,55 @@
/**
* Internal dependencies
*/
import { PaymentGateway } from '../types';
export const paymentGatewaysStub: PaymentGateway[] = [
{
id: 'bacs',
title: 'direct bank',
description: 'description',
order: '',
enabled: false,
method_title: 'Direct bank transfer',
method_description: 'method description',
method_supports: [ 'products' ],
settings: {
title: {
id: 'title',
label: 'Title',
description:
'This controls the title which the user sees during checkout.',
type: 'text',
value: 'direct bank',
default: 'Direct bank transfer',
tip:
'This controls the title which the user sees during checkout.',
placeholder: '',
},
},
},
{
id: 'test',
title: 'test',
description: 'test',
order: 0,
enabled: false,
method_title: 'test',
method_description: 'method description',
method_supports: [ 'products' ],
settings: {
title: {
id: 'title',
label: 'Title',
description:
'This controls the title which the user sees during checkout.',
type: 'text',
value: 'direct bank',
default: 'Direct bank transfer',
tip:
'This controls the title which the user sees during checkout.',
placeholder: '',
},
},
},
];

View File

@ -0,0 +1,119 @@
/**
* @jest-environment node
*/
/**
* Internal dependencies
*/
import reducer from '../reducer';
import { ACTION_TYPES } from '../action-types';
import { PluginsState } from '../types';
import { paymentGatewaysStub } from '../test-helpers/stub';
const defaultState: PluginsState = {
paymentGateways: [],
requesting: {},
errors: {},
};
const restApiError = {
code: 'error code',
data: {
status: 400,
},
message: 'error message',
};
describe( 'plugins reducer', () => {
it( 'should return a default state', () => {
const state = reducer( undefined );
expect( state ).toEqual( defaultState );
expect( state ).not.toBe( defaultState );
} );
it( 'should handle GET_PAYMENT_GATEWAY_REQUEST', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_REQUEST,
} );
expect( state.requesting.getPaymentGateway ).toBe( true );
} );
it( 'should handle GET_PAYMENT_GATEWAYS_REQUEST', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_REQUEST,
} );
expect( state.requesting.getPaymentGateways ).toBe( true );
} );
it( 'should handle UPDATE_PAYMENT_GATEWAY_REQUEST', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST,
} );
expect( state.requesting.updatePaymentGateway ).toBe( true );
} );
it( 'should handle GET_PAYMENT_GATEWAYS_ERROR', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR,
error: restApiError,
} );
expect( state.errors.getPaymentGateways ).toBe( restApiError );
expect( state.requesting.getPaymentGateways ).toBe( false );
} );
it( 'should handle GET_PAYMENT_GATEWAY_ERROR', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR,
error: restApiError,
} );
expect( state.errors.getPaymentGateway ).toBe( restApiError );
expect( state.requesting.getPaymentGateway ).toBe( false );
} );
it( 'should handle UPDATE_PAYMENT_GATEWAY_ERROR', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR,
error: restApiError,
} );
expect( state.errors.updatePaymentGateway ).toBe( restApiError );
expect( state.requesting.updatePaymentGateway ).toBe( false );
} );
it( 'should handle GET_PAYMENT_GATEWAYS_SUCCESS', () => {
const state = reducer( defaultState, {
type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_SUCCESS,
paymentGateways: paymentGatewaysStub,
} );
expect( state.paymentGateways ).toHaveLength( 2 );
expect( state.paymentGateways ).toBe( paymentGatewaysStub );
} );
it( 'should replace an existing payment gateway on UPDATE_PAYMENT_GATEWAY_SUCCESS', () => {
const updatedPaymentGateway = {
...paymentGatewaysStub[ 1 ],
description: 'update test',
};
const state = reducer(
{
...defaultState,
paymentGateways: paymentGatewaysStub,
},
{
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS,
paymentGateway: updatedPaymentGateway,
}
);
expect( state.paymentGateways[ 1 ].id ).toBe(
paymentGatewaysStub[ 1 ].id
);
expect( state.paymentGateways[ 1 ].description ).toBe( 'update test' );
} );
} );

View File

@ -0,0 +1,65 @@
export type SettingDefinition = {
default: string;
description: string;
id: string;
label: string;
placeholder: string;
tip: string;
type: string;
value: string;
};
export type PaymentGateway = {
id: string;
title: string;
description: string;
order: number | '';
enabled: boolean;
method_title: string;
method_description: string;
method_supports: string[];
settings: Record< string, SettingDefinition >;
};
export type PluginsState = {
paymentGateways: PaymentGateway[];
requesting: Record< string, boolean >;
errors: Record< string, RestApiError >;
};
interface RestApiErrorData {
status?: number;
}
export type RestApiError = {
code: string;
data: RestApiErrorData;
message: string;
};
export type SelectorKeysWithActions =
| 'getPaymentGateways'
| 'getPaymentGateway'
| 'updatePaymentGateway';
// Type for the basic selectors built into @wordpress/data, note these
// types define the interface for the public selectors, so state is not an
// argument.
export type WPDataSelectors = {
hasStartedResolution: ( selector: string, args?: string[] ) => boolean;
hasFinishedResolution: ( selector: string, args?: string[] ) => boolean;
isResolving: ( selector: string, args: string[] ) => boolean;
};
export type WPDataActions = {
startResolution: ( selector: string, args?: string[] ) => void;
finishResolution: ( selector: string, args?: string[] ) => void;
};
// Omitting state from selector parameter
export type WPDataSelector< T > = T extends (
state: infer S,
...args: infer A
) => infer R
? ( ...args: A ) => R
: T;

View File

@ -88,6 +88,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
- Add: Get post install scripts from gateway and enqueue in client #6967
- Add: Free extension list powered by remote config #6952
- Add: Add PayPal to fallback payment gateways #7001
- Add: Add a data store for WC Payments REST APIs #6918
- Dev: Update package-lock to fix versioning of local packages. #6843
- Dev: Use rule processing for remote payment methods #6830
- Dev: Update E2E jest config, so it correctly creates screenshots on failure. #6858