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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ A form component to handle form state and provide input helper props.
const initialValues = { firstName: '' };
<Form
onSubmitCallback={ ( values ) => {} }
onSubmit={ ( values ) => {} }
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
`errors` | Object | `{}` | Object of all initial errors to store in state
`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
`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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export type PaymentGateway = {
export type PluginsState = {
paymentGateways: PaymentGateway[];
requesting: Record< string, boolean >;
isUpdating: boolean;
errors: Record< string, RestApiError >;
};
@ -37,11 +37,6 @@ export type RestApiError = {
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.

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: Experimental task list import to the experimental package. #6950
- 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 ==
- Tweak: Store profiler - Changed MailPoet's title and description #6990