From 073a220b59c0a0a0ff3da8a343d91acf4fb9a42b Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Tue, 11 May 2021 09:36:56 -0700 Subject: [PATCH] Adding Slotfill extension components for remote payments (https://github.com/woocommerce/woocommerce-admin/pull/6932) --- .../activity-panel/reviews/test/index.js | 18 +- .../navigation/components/Item/index.js | 2 +- .../components/PaymentConnect.js | 177 +++++++++++------- .../components/PaymentMethod.js | 52 +++-- .../packages/components/src/index.js | 3 + .../components/src/settings-form/README.md | 44 +++++ .../components/src/settings-form/index.js | 96 ++++++++++ .../src/settings-form/stories/index.js | 98 ++++++++++ .../components/src/settings-form/style.scss | 13 ++ .../src/settings-form/types/index.js | 4 + .../settings-form/types/setting-checkbox.js | 17 ++ .../settings-form/types/setting-password.js | 8 + .../src/settings-form/types/setting-select.js | 38 ++++ .../src/settings-form/types/setting-text.js | 18 ++ .../packages/components/src/style.scss | 1 + .../src/woo-remote-payment-settings/README.md | 32 ++++ .../src/woo-remote-payment-settings/index.js | 17 ++ .../src/woo-remote-payment/README.md | 31 +++ .../src/woo-remote-payment/index.js | 14 ++ plugins/woocommerce-admin/readme.txt | 2 +- .../woocommerce-admin/tests/js/util/index.js | 23 +++ 21 files changed, 621 insertions(+), 87 deletions(-) create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/stories/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/style.scss create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-checkbox.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-password.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-select.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-text.js create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment/index.js create mode 100644 plugins/woocommerce-admin/tests/js/util/index.js diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js index b3559a832a4..d75c3a15cc0 100644 --- a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js +++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js @@ -7,6 +7,7 @@ import { render, screen, fireEvent } from '@testing-library/react'; * Internal dependencies */ import { ReviewsPanel } from '../'; +import { getByTextWithMarkup } from '../../../../../tests/js/util'; const REVIEW = { id: 10, @@ -47,13 +48,6 @@ const REVIEW = { }, }; -jest.mock( '@woocommerce/components', () => ( { - ...jest.requireActual( '@woocommerce/components' ), - Link: ( { children } ) => { - return <>{ children }>; - }, -} ) ); - jest.mock( '../checkmark-circle-icon', () => jest.fn().mockImplementation( () => '[checkmark-circle-icon]' ) ); @@ -66,6 +60,7 @@ describe( 'ReviewsPanel', () => { isError={ false } isRequesting={ false } reviews={ [] } + createNotice={ () => {} } /> ); expect( screen.queryByRole( 'section' ) ).toBeNull(); @@ -78,9 +73,11 @@ describe( 'ReviewsPanel', () => { isError={ false } isRequesting={ false } reviews={ [ REVIEW ] } + createNotice={ () => {} } /> ); - expect( screen.getByText( 'Reviewer reviewed Cap' ) ).not.toBeNull(); + + expect( getByTextWithMarkup( 'Reviewer reviewed Cap' ) ).not.toBeNull(); } ); it( 'should render checkmark circle icon in the review title, if review is verfied owner', () => { @@ -90,6 +87,7 @@ describe( 'ReviewsPanel', () => { isError={ false } isRequesting={ false } reviews={ [ { ...REVIEW, verified: true } ] } + createNotice={ () => {} } /> ); const header = screen.getByRole( 'heading', { level: 3 } ); @@ -104,6 +102,7 @@ describe( 'ReviewsPanel', () => { isError={ false } isRequesting={ false } reviews={ [ REVIEW ] } + createNotice={ () => {} } /> ); expect( screen.queryByText( 'Approve' ) ).toBeInTheDocument(); @@ -122,6 +121,7 @@ describe( 'ReviewsPanel', () => { isRequesting={ false } reviews={ [ REVIEW ] } updateReview={ clickHandler } + createNotice={ () => {} } /> ); fireEvent.click( screen.getByText( 'Approve' ) ); @@ -141,6 +141,7 @@ describe( 'ReviewsPanel', () => { isRequesting={ false } reviews={ [ REVIEW ] } updateReview={ clickHandler } + createNotice={ () => {} } /> ); fireEvent.click( screen.getByText( 'Mark as spam' ) ); @@ -160,6 +161,7 @@ describe( 'ReviewsPanel', () => { isRequesting={ false } reviews={ [ REVIEW ] } deleteReview={ clickHandler } + createNotice={ () => {} } /> ); fireEvent.click( screen.getByText( 'Delete' ) ); diff --git a/plugins/woocommerce-admin/client/navigation/components/Item/index.js b/plugins/woocommerce-admin/client/navigation/components/Item/index.js index fccc608f7a5..ff47b180221 100644 --- a/plugins/woocommerce-admin/client/navigation/components/Item/index.js +++ b/plugins/woocommerce-admin/client/navigation/components/Item/index.js @@ -7,7 +7,7 @@ import { WooNavigationItem } from '@woocommerce/navigation'; const Item = ( { item } ) => { const slot = useSlot( 'woocommerce_navigation_' + item.id ); - const hasFills = Boolean( slot.fills && slot.fills.length ); + const hasFills = Boolean( slot?.fills?.length ); const trackClick = ( id ) => { recordEvent( 'navigation_click', { diff --git a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js index 4d9c6fbc6df..cde2506dcba 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js +++ b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js @@ -2,20 +2,66 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; import interpolateComponents from 'interpolate-components'; import { useDispatch, useSelect } from '@wordpress/data'; -import { Form, Link, TextControl } from '@woocommerce/components'; +import { + Link, + SettingsForm, + WooRemotePaymentSettings, + Spinner, +} from '@woocommerce/components'; +import apiFetch from '@wordpress/api-fetch'; +import { useEffect, useState } from '@wordpress/element'; import { OPTIONS_STORE_NAME } from '@woocommerce/data'; +import { useSlot, Text } from '@woocommerce/experimental'; export const PaymentConnect = ( { markConfigured, method, recordConnectStartEvent, } ) => { - const { api_details_url: apiDetailsUrl, fields, key, title } = method; + const { + api_details_url: apiDetailsUrl, + fields: fieldsConfig, + key, + title, + } = method; + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); const { createNotice } = useDispatch( 'core/notices' ); + const slot = useSlot( `woocommerce_remote_payment_settings_${ key }` ); + const hasFills = Boolean( slot?.fills?.length ); + const [ state, setState ] = useState( 'loading' ); + const [ fields, setFields ] = useState( null ); + + // This transform will be obsolete when we can derive essential fields from the API + const settingsTransform = ( settings ) => { + const essentialFields = fieldsConfig.map( ( field ) => field.name ); + + return Object.values( settings ).filter( ( setting ) => + essentialFields.includes( setting.id ) + ); + }; + + // TODO: Will soon be replaced with the payments data store implemented in #6918 + useEffect( () => { + apiFetch( { + path: `/wc/v3/payment_gateways/${ key }/`, + } ) + .then( ( results ) => { + setFields( settingsTransform( results.settings ) ); + setState( 'loaded' ); + } ) + .catch( ( e ) => { + setState( 'error' ); + /* eslint-disable no-console */ + console.error( + `Error fetching information for payment gateway ${ key }`, + e.message + ); + /* eslint-enable no-console */ + } ); + }, [] ); const isOptionsRequesting = useSelect( ( select ) => { const { isOptionsUpdating } = select( OPTIONS_STORE_NAME ); @@ -23,37 +69,6 @@ export const PaymentConnect = ( { return isOptionsUpdating(); } ); - const getInitialConfigValues = () => { - if ( fields ) { - return fields.reduce( ( data, field ) => { - return { - ...data, - [ field.name ]: '', - }; - }, {} ); - } - }; - - const validate = ( values ) => { - if ( fields ) { - return fields.reduce( ( errors, field ) => { - if ( ! values[ field.name ] ) { - // Matches any word that is capitalized aside from abrevitions like ID. - const label = field.label.replace( - /([A-Z][a-z]+)/, - ( val ) => val.toLowerCase() - ); - return { - ...errors, - [ field.name ]: __( 'Please enter your ' ) + label, - }; - } - return errors; - }, {} ); - } - return {}; - }; - const updateSettings = async ( values ) => { const options = {}; @@ -87,6 +102,26 @@ export const PaymentConnect = ( { } }; + const validate = ( values ) => { + const errors = {}; + const getField = ( fieldId ) => + fields.find( ( field ) => field.id === fieldId ); + + for ( const [ valueKey, value ] of Object.entries( values ) ) { + const field = getField( valueKey ); + // Matches any word that is capitalized aside from abrevitions like ID. + const label = field.label.replace( /([A-Z][a-z]+)/g, ( val ) => + val.toLowerCase() + ); + + if ( ! value ) { + errors[ valueKey ] = `Please enter your ${ label }`; + } + } + + return errors; + }; + const helpText = interpolateComponents( { mixedString: __( 'Your API details can be obtained from your {{link/}}', @@ -103,39 +138,51 @@ export const PaymentConnect = ( { }, } ); - return ( -
+ if ( state === 'loading' ) { + return{ helpText }
+ > + ) } + > ); }; diff --git a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js index 3c5eab666b2..ec29eee5782 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js +++ b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js @@ -9,9 +9,11 @@ import { PLUGINS_STORE_NAME, pluginNames, } from '@woocommerce/data'; -import { Plugins, Stepper } from '@woocommerce/components'; +import { Plugins, Stepper, WooRemotePayment } from '@woocommerce/components'; + import { recordEvent } from '@woocommerce/tracks'; import { useSelect } from '@wordpress/data'; +import { useSlot } from '@woocommerce/experimental'; /** * Internal dependencies @@ -24,7 +26,15 @@ export const PaymentMethod = ( { method, recordConnectStartEvent, } ) => { - const { key, plugins, title } = method; + const { + key, + plugins, + title, + post_install_script: postInstallScript, + } = method; + const slot = useSlot( `woocommerce_remote_payment_${ key }` ); + const hasFills = Boolean( slot?.fills?.length ); + useEffect( () => { recordEvent( 'payments_task_stepper_view', { payment_method: key, @@ -71,6 +81,12 @@ export const PaymentMethod = ( { recordEvent( 'tasklist_payment_install_method', { plugins, } ); + + if ( postInstallScript ) { + const script = document.createElement( 'script' ); + script.src = postInstallScript; + document.body.append( script ); + } } } onError={ ( errors, response ) => createNoticesFromResponse( response ) @@ -102,19 +118,31 @@ export const PaymentMethod = ( { }; }, [ title ] ); + const DefaultStepper = ( props ) => ( +{ submitted ? JSON.stringify( submitted, null, 3 ) : 'None' }
+ > + ); +}; + +export const Basic = () =>Fill Content
} +