* Hook up payment gateway data store

* Fix deprecated onSubmitCallback in dynamic form

* Throw catchable errors in data store

* Provide a way to get errors from the data store

* Hook up payment connection update with data store

* Remove redundant requesting state on selectors

* Add changelog entry

* Handle PR feedback

* Fix linting errors
This commit is contained in:
Joshua T Flowers 2021-05-26 14:31:30 -04:00 committed by GitHub
parent 71d34c4c21
commit dc175824c9
10 changed files with 113 additions and 177 deletions

View File

@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { DynamicForm, WooRemotePaymentForm } from '@woocommerce/components'; import { DynamicForm, WooRemotePaymentForm } from '@woocommerce/components';
import { OPTIONS_STORE_NAME } from '@woocommerce/data'; import { PAYMENT_GATEWAYS_STORE_NAME } from '@woocommerce/data';
import { useSlot } from '@woocommerce/experimental'; import { useSlot } from '@woocommerce/experimental';
/** /**
@ -19,7 +19,7 @@ export const PaymentConnect = ( {
recordConnectStartEvent, recordConnectStartEvent,
} ) => { } ) => {
const { const {
key, id,
oauth_connection_url: oAuthConnectionUrl, oauth_connection_url: oAuthConnectionUrl,
setup_help_text: setupHelpText, setup_help_text: setupHelpText,
required_settings_keys: settingKeys, required_settings_keys: settingKeys,
@ -28,9 +28,9 @@ export const PaymentConnect = ( {
title, title,
} = paymentGateway; } = paymentGateway;
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { createNotice } = useDispatch( 'core/notices' ); const { createNotice } = useDispatch( 'core/notices' );
const slot = useSlot( `woocommerce_remote_payment_form_${ key }` ); const { updatePaymentGateway } = useDispatch( PAYMENT_GATEWAYS_STORE_NAME );
const slot = useSlot( `woocommerce_remote_payment_form_${ id }` );
const hasFills = Boolean( slot?.fills?.length ); const hasFills = Boolean( slot?.fills?.length );
const fields = settingKeys const fields = settingKeys
? settingKeys ? settingKeys
@ -38,37 +38,34 @@ export const PaymentConnect = ( {
.filter( Boolean ) .filter( Boolean )
: []; : [];
const isOptionsRequesting = useSelect( ( select ) => { const { isUpdating } = useSelect( ( select ) => {
const { isOptionsUpdating } = select( OPTIONS_STORE_NAME ); const { isPaymentGatewayUpdating } = select(
PAYMENT_GATEWAYS_STORE_NAME
);
return isOptionsUpdating(); return {
isUpdating: isPaymentGatewayUpdating(),
};
} ); } );
const updateSettings = async ( values ) => { const handleSubmit = ( values ) => {
recordConnectStartEvent( key ); recordConnectStartEvent( id );
const options = {}; updatePaymentGateway( id, {
enabled: true,
fields.forEach( ( field ) => { settings: values,
const optionName = field.option || field.name; } )
options[ optionName ] = values[ field.name ]; .then( ( result ) => {
} ); if ( result && result.id === id ) {
markConfigured( id );
if ( ! Object.keys( options ).length ) {
return;
}
const update = await updateOptions( {
...options,
} );
if ( update.success ) {
markConfigured( key );
createNotice( createNotice(
'success', 'success',
title + __( ' connected successfully', 'woocommerce-admin' ) title +
__( ' connected successfully', 'woocommerce-admin' )
); );
} else { }
} )
.catch( () => {
createNotice( createNotice(
'error', 'error',
__( __(
@ -76,7 +73,7 @@ export const PaymentConnect = ( {
'woocommerce-admin' 'woocommerce-admin'
) )
); );
} } );
}; };
const validate = ( values ) => { const validate = ( values ) => {
@ -105,8 +102,8 @@ export const PaymentConnect = ( {
const DefaultForm = ( props ) => ( const DefaultForm = ( props ) => (
<DynamicForm <DynamicForm
fields={ fields } fields={ fields }
isBusy={ isOptionsRequesting } isBusy={ isUpdating }
onSubmit={ updateSettings } onSubmit={ handleSubmit }
submitLabel={ __( 'Proceed', 'woocommerce-admin' ) } submitLabel={ __( 'Proceed', 'woocommerce-admin' ) }
validate={ validate } validate={ validate }
{ ...props } { ...props }
@ -118,11 +115,11 @@ export const PaymentConnect = ( {
<WooRemotePaymentForm.Slot <WooRemotePaymentForm.Slot
fillProps={ { fillProps={ {
defaultForm: DefaultForm, defaultForm: DefaultForm,
defaultSubmit: updateSettings, defaultSubmit: handleSubmit,
defaultFields: fields, defaultFields: fields,
markConfigured: () => markConfigured( key ), markConfigured: () => markConfigured( id ),
} } } }
id={ key } id={ id }
/> />
); );
} }

View File

@ -2,11 +2,11 @@
* External dependencies * External dependencies
*/ */
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { Card, CardBody } from '@wordpress/components'; import { Card, CardBody } from '@wordpress/components';
import { enqueueScript } from '@woocommerce/wc-admin-settings'; import { enqueueScript } from '@woocommerce/wc-admin-settings';
import { import {
OPTIONS_STORE_NAME, OPTIONS_STORE_NAME,
PAYMENT_GATEWAYS_STORE_NAME,
PLUGINS_STORE_NAME, PLUGINS_STORE_NAME,
pluginNames, pluginNames,
} from '@woocommerce/data'; } from '@woocommerce/data';
@ -30,10 +30,7 @@ export const PaymentMethod = ( {
const { key, plugins, title } = method; const { key, plugins, title } = method;
const slot = useSlot( `woocommerce_remote_payment_${ key }` ); const slot = useSlot( `woocommerce_remote_payment_${ key }` );
const hasFills = Boolean( slot?.fills?.length ); const hasFills = Boolean( slot?.fills?.length );
const [ isFetchingPaymentGateway, setIsFetchingPaymentGateway ] = useState( const [ isPluginLoaded, setIsPluginLoaded ] = useState( false );
false
);
const [ paymentGateway, setPaymentGateway ] = useState( null );
useEffect( () => { useEffect( () => {
recordEvent( 'payments_task_stepper_view', { recordEvent( 'payments_task_stepper_view', {
@ -41,52 +38,51 @@ export const PaymentMethod = ( {
} ); } );
}, [] ); }, [] );
const { activePlugins } = useSelect( ( select ) => { const {
const { getActivePlugins } = select( PLUGINS_STORE_NAME ); isOptionUpdating,
isPaymentGatewayResolving,
return { needsPluginInstall,
activePlugins: getActivePlugins(), paymentGateway,
}; } = useSelect( ( select ) => {
} );
const isOptionsRequesting = useSelect( ( select ) => {
const { isOptionsUpdating } = select( OPTIONS_STORE_NAME ); const { isOptionsUpdating } = select( OPTIONS_STORE_NAME );
const { getPaymentGateway, isResolving } = select(
return isOptionsUpdating(); PAYMENT_GATEWAYS_STORE_NAME
} ); );
const activePlugins = select( PLUGINS_STORE_NAME ).getActivePlugins();
const pluginsToInstall = plugins.filter( const pluginsToInstall = plugins.filter(
( m ) => ! activePlugins.includes( m ) ( m ) => ! activePlugins.includes( m )
); );
return {
isOptionUpdating: isOptionsUpdating(),
isPaymentGatewayResolving: isResolving( 'getPaymentGateway', [
key,
] ),
paymentGateway: ! pluginsToInstall.length
? getPaymentGateway( key )
: null,
needsPluginInstall: !! pluginsToInstall.length,
};
} );
useEffect( () => { useEffect( () => {
if ( if ( ! paymentGateway ) {
pluginsToInstall.length ||
paymentGateway ||
isFetchingPaymentGateway
) {
return; return;
} }
fetchGateway();
}, [ pluginsToInstall ] );
// @todo This should updated to use the data store in https://github.com/woocommerce/woocommerce-admin/pull/6918 const { post_install_scripts: postInstallScripts } = paymentGateway;
const fetchGateway = () => { if ( postInstallScripts && postInstallScripts.length ) {
setIsFetchingPaymentGateway( true );
apiFetch( {
path: 'wc/v3/payment_gateways/' + key,
} ).then( async ( results ) => {
const { post_install_scripts: postInstallScripts } = results;
if ( postInstallScripts ) {
const scriptPromises = postInstallScripts.map( ( script ) => const scriptPromises = postInstallScripts.map( ( script ) =>
enqueueScript( script ) enqueueScript( script )
); );
await Promise.all( scriptPromises ); Promise.all( scriptPromises ).then( () => {
} setIsPluginLoaded( true );
setPaymentGateway( results );
setIsFetchingPaymentGateway( false );
} ); } );
}; return;
}
setIsPluginLoaded( true );
}, [ paymentGateway ] );
const pluginNamesString = plugins const pluginNamesString = plugins
.map( ( pluginSlug ) => pluginNames[ pluginSlug ] ) .map( ( pluginSlug ) => pluginNames[ pluginSlug ] )
@ -119,10 +115,10 @@ export const PaymentMethod = ( {
pluginSlugs={ plugins } pluginSlugs={ plugins }
/> />
), ),
isComplete: ! pluginsToInstall.length, isComplete: ! needsPluginInstall,
} }
: null; : null;
}, [ pluginsToInstall.length ] ); }, [ needsPluginInstall ] );
const connectStep = { const connectStep = {
key: 'connect', key: 'connect',
@ -143,8 +139,9 @@ export const PaymentMethod = ( {
const stepperPending = const stepperPending =
! installStep.isComplete || ! installStep.isComplete ||
isOptionsRequesting || isOptionUpdating ||
isFetchingPaymentGateway; isPaymentGatewayResolving ||
! isPluginLoaded;
const DefaultStepper = useCallback( const DefaultStepper = useCallback(
( props ) => ( ( props ) => (

View File

@ -65,8 +65,8 @@ export const DynamicForm: React.FC< DynamicFormProps > = ( {
return ( return (
<Form <Form
initialValues={ getInitialConfigValues() } initialValues={ getInitialConfigValues() }
onChangeCallback={ onChange } onChange={ onChange }
onSubmitCallback={ onSubmit } onSubmit={ onSubmit }
validate={ validate } validate={ validate }
> >
{ ( { { ( {

View File

@ -9,7 +9,7 @@ A form component to handle form state and provide input helper props.
const initialValues = { firstName: '' }; const initialValues = { firstName: '' };
<Form <Form
onSubmitCallback={ ( values ) => {} } onSubmit={ ( values ) => {} }
initialValues={ initialValues } initialValues={ initialValues }
> >
{ ( { { ( {
@ -42,7 +42,7 @@ Name | Type | Default | Description
`children` | * | `null` | A renderable component in which to pass this component's state and helpers. Generally a number of input or other form elements `children` | * | `null` | A renderable component in which to pass this component's state and helpers. Generally a number of input or other form elements
`errors` | Object | `{}` | Object of all initial errors to store in state `errors` | Object | `{}` | Object of all initial errors to store in state
`initialValues` | Object | `{}` | Object key:value pair list of all initial field values `initialValues` | Object | `{}` | Object key:value pair list of all initial field values
`onSubmitCallback` | Function | `noop` | Function to call when a form is submitted with valid fields `onSubmit` | Function | `noop` | Function to call when a form is submitted with valid fields
`validate` | Function | `noop` | A function that is passed a list of all values and should return an `errors` object with error response `validate` | Function | `noop` | A function that is passed a list of all values and should return an `errors` object with error response
`touched` | Object | `{}` | This prop helps determine whether or not a field has received focus `touched` | Object | `{}` | This prop helps determine whether or not a field has received focus
`onChange` | Function | `null` | A function that receives the value of the input; called when selected items change, whether added, edited, or removed `onChange` | Function | `null` | A function that receives the value of the input; called when selected items change, whether added, edited, or removed

View File

@ -124,6 +124,7 @@ export function* updatePaymentGateway(
} }
} catch ( e ) { } catch ( e ) {
yield updatePaymentGatewayError( e ); yield updatePaymentGatewayError( e );
throw e;
} }
} }

View File

@ -2,13 +2,12 @@
* Internal dependencies * Internal dependencies
*/ */
import { ACTION_TYPES } from './action-types'; import { ACTION_TYPES } from './action-types';
import { PluginsState, SelectorKeysWithActions, PaymentGateway } from './types'; import { PluginsState, PaymentGateway } from './types';
import { Actions } from './actions'; import { Actions } from './actions';
function updatePaymentGatewayList( function updatePaymentGatewayList(
state: PluginsState, state: PluginsState,
paymentGateway: PaymentGateway, paymentGateway: PaymentGateway
selector: SelectorKeysWithActions
): PluginsState { ): PluginsState {
const targetIndex = state.paymentGateways.findIndex( const targetIndex = state.paymentGateways.findIndex(
( gateway ) => gateway.id === paymentGateway.id ( gateway ) => gateway.id === paymentGateway.id
@ -18,10 +17,7 @@ function updatePaymentGatewayList(
return { return {
...state, ...state,
paymentGateways: [ ...state.paymentGateways, paymentGateway ], paymentGateways: [ ...state.paymentGateways, paymentGateway ],
requesting: { isUpdating: false,
...state.requesting,
[ selector ]: false,
},
}; };
} }
@ -32,17 +28,14 @@ function updatePaymentGatewayList(
paymentGateway, paymentGateway,
...state.paymentGateways.slice( targetIndex + 1 ), ...state.paymentGateways.slice( targetIndex + 1 ),
], ],
requesting: { isUpdating: false,
...state.requesting,
[ selector ]: false,
},
}; };
} }
const reducer = ( const reducer = (
state: PluginsState = { state: PluginsState = {
paymentGateways: [], paymentGateways: [],
requesting: {}, isUpdating: false,
errors: {}, errors: {},
}, },
payload?: Actions payload?: Actions
@ -50,29 +43,12 @@ const reducer = (
if ( payload && 'type' in payload ) { if ( payload && 'type' in payload ) {
switch ( payload.type ) { switch ( payload.type ) {
case ACTION_TYPES.GET_PAYMENT_GATEWAYS_REQUEST: case ACTION_TYPES.GET_PAYMENT_GATEWAYS_REQUEST:
return {
...state,
requesting: {
...state.requesting,
getPaymentGateways: true,
},
};
case ACTION_TYPES.GET_PAYMENT_GATEWAY_REQUEST: case ACTION_TYPES.GET_PAYMENT_GATEWAY_REQUEST:
return { return state;
...state,
requesting: {
...state.requesting,
getPaymentGateway: true,
},
};
case ACTION_TYPES.GET_PAYMENT_GATEWAYS_SUCCESS: case ACTION_TYPES.GET_PAYMENT_GATEWAYS_SUCCESS:
return { return {
...state, ...state,
paymentGateways: payload.paymentGateways, paymentGateways: payload.paymentGateways,
requesting: {
...state.requesting,
getPaymentGateways: false,
},
}; };
case ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR: case ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR:
return { return {
@ -81,10 +57,6 @@ const reducer = (
...state.errors, ...state.errors,
getPaymentGateways: payload.error, getPaymentGateways: payload.error,
}, },
requesting: {
...state.requesting,
getPaymentGateways: false,
},
}; };
case ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR: case ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR:
return { return {
@ -93,30 +65,21 @@ const reducer = (
...state.errors, ...state.errors,
getPaymentGateway: payload.error, getPaymentGateway: payload.error,
}, },
requesting: {
...state.requesting,
getPaymentGateway: false,
},
}; };
case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST: case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST:
return { return {
...state, ...state,
requesting: { isUpdating: true,
...state.requesting,
updatePaymentGateway: true,
},
}; };
case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS: case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS:
return updatePaymentGatewayList( return updatePaymentGatewayList(
state, state,
payload.paymentGateway, payload.paymentGateway
'updatePaymentGateway'
); );
case ACTION_TYPES.GET_PAYMENT_GATEWAY_SUCCESS: case ACTION_TYPES.GET_PAYMENT_GATEWAY_SUCCESS:
return updatePaymentGatewayList( return updatePaymentGatewayList(
state, state,
payload.paymentGateway, payload.paymentGateway
'getPaymentGateway'
); );
case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR: case ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR:
@ -126,10 +89,7 @@ const reducer = (
...state.errors, ...state.errors,
updatePaymentGateway: payload.error, updatePaymentGateway: payload.error,
}, },
requesting: { isUpdating: false,
...state.requesting,
updatePaymentGateway: false,
},
}; };
} }
} }

View File

@ -4,6 +4,7 @@
import { import {
PaymentGateway, PaymentGateway,
PluginsState, PluginsState,
RestApiError,
WPDataSelector, WPDataSelector,
WPDataSelectors, WPDataSelectors,
} from './types'; } from './types';
@ -23,17 +24,19 @@ export function getPaymentGateways(
return state.paymentGateways; return state.paymentGateways;
} }
export function isPaymentGatewayRequesting( export function getPaymentGatewayError(
state: PluginsState, state: PluginsState,
selector: string selector: string
): boolean { ): RestApiError | null {
return state.requesting[ selector ] || false; return state.errors[ selector ] || null;
}
export function isPaymentGatewayUpdating( state: PluginsState ): boolean {
return state.isUpdating || false;
} }
export type PaymentSelectors = { export type PaymentSelectors = {
getPaymentGateway: WPDataSelector< typeof getPaymentGateway >; getPaymentGateway: WPDataSelector< typeof getPaymentGateway >;
getPaymentGateways: WPDataSelector< typeof getPaymentGateways >; getPaymentGateways: WPDataSelector< typeof getPaymentGateways >;
isPaymentGatewayRequesting: WPDataSelector< isPaymentGatewayUpdating: WPDataSelector< typeof isPaymentGatewayUpdating >;
typeof isPaymentGatewayRequesting
>;
} & WPDataSelectors; } & WPDataSelectors;

View File

@ -12,7 +12,7 @@ import { paymentGatewaysStub } from '../test-helpers/stub';
const defaultState: PluginsState = { const defaultState: PluginsState = {
paymentGateways: [], paymentGateways: [],
requesting: {}, isUpdating: false,
errors: {}, errors: {},
}; };
@ -31,28 +31,12 @@ describe( 'plugins reducer', () => {
expect( state ).not.toBe( 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', () => { it( 'should handle UPDATE_PAYMENT_GATEWAY_REQUEST', () => {
const state = reducer( defaultState, { const state = reducer( defaultState, {
type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST, type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_REQUEST,
} ); } );
expect( state.requesting.updatePaymentGateway ).toBe( true ); expect( state.isUpdating ).toBe( true );
} ); } );
it( 'should handle GET_PAYMENT_GATEWAYS_ERROR', () => { it( 'should handle GET_PAYMENT_GATEWAYS_ERROR', () => {
@ -62,7 +46,6 @@ describe( 'plugins reducer', () => {
} ); } );
expect( state.errors.getPaymentGateways ).toBe( restApiError ); expect( state.errors.getPaymentGateways ).toBe( restApiError );
expect( state.requesting.getPaymentGateways ).toBe( false );
} ); } );
it( 'should handle GET_PAYMENT_GATEWAY_ERROR', () => { it( 'should handle GET_PAYMENT_GATEWAY_ERROR', () => {
@ -72,7 +55,6 @@ describe( 'plugins reducer', () => {
} ); } );
expect( state.errors.getPaymentGateway ).toBe( restApiError ); expect( state.errors.getPaymentGateway ).toBe( restApiError );
expect( state.requesting.getPaymentGateway ).toBe( false );
} ); } );
it( 'should handle UPDATE_PAYMENT_GATEWAY_ERROR', () => { it( 'should handle UPDATE_PAYMENT_GATEWAY_ERROR', () => {
@ -82,7 +64,7 @@ describe( 'plugins reducer', () => {
} ); } );
expect( state.errors.updatePaymentGateway ).toBe( restApiError ); expect( state.errors.updatePaymentGateway ).toBe( restApiError );
expect( state.requesting.updatePaymentGateway ).toBe( false ); expect( state.isUpdating ).toBe( false );
} ); } );
it( 'should handle GET_PAYMENT_GATEWAYS_SUCCESS', () => { it( 'should handle GET_PAYMENT_GATEWAYS_SUCCESS', () => {

View File

@ -23,7 +23,7 @@ export type PaymentGateway = {
export type PluginsState = { export type PluginsState = {
paymentGateways: PaymentGateway[]; paymentGateways: PaymentGateway[];
requesting: Record< string, boolean >; isUpdating: boolean;
errors: Record< string, RestApiError >; errors: Record< string, RestApiError >;
}; };
@ -37,11 +37,6 @@ export type RestApiError = {
message: string; message: string;
}; };
export type SelectorKeysWithActions =
| 'getPaymentGateways'
| 'getPaymentGateway'
| 'updatePaymentGateway';
// Type for the basic selectors built into @wordpress/data, note these // Type for the basic selectors built into @wordpress/data, note these
// types define the interface for the public selectors, so state is not an // types define the interface for the public selectors, so state is not an
// argument. // argument.

View File

@ -125,6 +125,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
- Update: Task list component with new Experimental Task list. #6849 - Update: Task list component with new Experimental Task list. #6849
- Update: Experimental task list import to the experimental package. #6950 - Update: Experimental task list import to the experimental package. #6950
- Update: Redirect to WC Home after setting up a payment method #6891 - Update: Redirect to WC Home after setting up a payment method #6891
- Update: Hook up payments gateway data store #7038
== 2.3.1 5/24/2021 == == 2.3.1 5/24/2021 ==
- Tweak: Store profiler - Changed MailPoet's title and description #6990 - Tweak: Store profiler - Changed MailPoet's title and description #6990