From a3f900b29808d59b6543f0f6e3d2017853e4eb8c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 2 Dec 2024 16:31:44 +0100 Subject: [PATCH 01/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20reusable?= =?UTF-8?q?=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/ReusableComponents/Icons.js | 1 + .../ReusableComponents/Icons/open-signup.js | 12 ++++++++++ .../Screens/Onboarding/StepCompleteSetup.js | 24 +++---------------- 3 files changed, 16 insertions(+), 21 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js new file mode 100644 index 000000000..3344c3ceb --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js @@ -0,0 +1 @@ +export { default as openSignup } from './Icons/open-signup'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js new file mode 100644 index 000000000..83c792f22 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const openSignup = ( + + + +); + +export default openSignup; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index 5f63f923e..d649b7c83 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -1,28 +1,10 @@ import { __ } from '@wordpress/i18n'; -import { Button, Icon } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; +import { openSignup } from '../../ReusableComponents/Icons'; const StepCompleteSetup = ( { setCompleted } ) => { - const ButtonIcon = () => ( - ( - - - - ) } - /> - ); - return (
{
+ { } value={ clientId } onChange={ setClientId } - className={ - clientId && ! isValidClientId ? 'has-error' : '' - } /> - { clientId && ! isValidClientId && ( -

- { __( - 'Please enter a valid Client ID', - 'woocommerce-paypal-payments' - ) } -

- ) } { onChange={ setClientSecret } type="password" /> -
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js new file mode 100644 index 000000000..179c8773f --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -0,0 +1,36 @@ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { openSignup } from '../../../ReusableComponents/Icons'; +import { useSandboxConnection } from '../../../../hooks/useHandleConnections'; + +const ConnectionButton = ( { + title, + isSandbox = false, + variant = 'primary', + showIcon = true, +} ) => { + const className = 'ppcp-r-connection-button'; + const { handleSandboxConnect } = useSandboxConnection(); + + const handleConnectClick = () => { + if ( isSandbox ) { + handleSandboxConnect(); + } else { + // Handle live connection logic here + console.warn( 'Live connection not implemented yet' ); + } + }; + + return ( + + ); +}; + +export default ConnectionButton; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js new file mode 100644 index 000000000..b30d5213f --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -0,0 +1,113 @@ +import { __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; + +import { CommonHooks, OnboardingHooks } from '../data'; +import { openPopup } from '../utils/window'; + +const useCommonConnectionLogic = () => { + const { setCompleted } = OnboardingHooks.useSteps(); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + const handleServerError = ( res, genericMessage ) => { + console.error( 'Connection error', res ); + createErrorNotice( res?.message ?? genericMessage ); + }; + + const handleServerSuccess = () => { + createSuccessNotice( + __( 'Connected to PayPal', 'woocommerce-paypal-payments' ) + ); + setCompleted( true ); + }; + + return { handleServerError, handleServerSuccess, createErrorNotice }; +}; + +export const useSandboxConnection = () => { + const { connectViaSandbox, isSandboxMode, setSandboxMode } = + CommonHooks.useSandbox(); + const { handleServerError, createErrorNotice } = useCommonConnectionLogic(); + + const handleSandboxConnect = async () => { + const res = await connectViaSandbox(); + + if ( ! res.success || ! res.data ) { + handleServerError( + res, + __( + 'Could not generate a Sandbox login link.', + 'woocommerce-paypal-payments' + ) + ); + return; + } + + const connectionUrl = res.data; + const popup = openPopup( connectionUrl ); + + if ( ! popup ) { + createErrorNotice( + __( + 'Popup blocked. Please allow popups for this site to connect to PayPal.', + 'woocommerce-paypal-payments' + ) + ); + } + }; + + return { + handleSandboxConnect, + isSandboxMode, + setSandboxMode, + }; +}; + +export const useManualConnection = () => { + const { + connectViaIdAndSecret, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + } = CommonHooks.useManualConnection(); + const { handleServerError, handleServerSuccess, createErrorNotice } = + useCommonConnectionLogic(); + + const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { + if ( 'function' === typeof validation ) { + try { + validation(); + } catch ( exception ) { + createErrorNotice( exception.message ); + return; + } + } + const res = await connectViaIdAndSecret(); + + if ( res.success ) { + handleServerSuccess(); + } else { + handleServerError( + res, + __( + 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', + 'woocommerce-paypal-payments' + ) + ); + } + }; + + return { + handleConnectViaIdAndSecret, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + }; +}; From a58a2c7b209d274b1762080019095bdf67396199 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 16:06:41 +0100 Subject: [PATCH 03/40] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20setCompl?= =?UTF-8?q?eted=20prop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js --- .../Components/Screens/Onboarding/Onboarding.js | 4 +--- .../Screens/Onboarding/StepCompleteSetup.js | 17 +++++------------ .../Screens/Onboarding/StepWelcome.js | 5 +++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index e59bcdeeb..225527053 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -5,8 +5,7 @@ import { getSteps, getCurrentStep } from './availableSteps'; import Navigation from './Components/Navigation'; const Onboarding = () => { - const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps(); - + const { step, setStep, flags } = OnboardingHooks.useSteps(); const Steps = getSteps( flags ); const currentStep = getCurrentStep( step, Steps ); @@ -30,7 +29,6 @@ const Onboarding = () => {
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index d649b7c83..ed2001ac2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -1,10 +1,9 @@ import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; -import { openSignup } from '../../ReusableComponents/Icons'; +import ConnectionButton from './Components/ConnectionButton'; -const StepCompleteSetup = ( { setCompleted } ) => { +const StepCompleteSetup = () => { return (
{ />
-
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index 761093b24..461d95d26 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -10,8 +10,9 @@ import AccordionSection from '../../ReusableComponents/AccordionSection'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; import { CommonHooks } from '../../../data'; -const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { +const StepWelcome = ( { setStep, currentStep } ) => { const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + return (
{ className="onboarding-advanced-options" id="advanced-options" > - +
); From 2ccc489fd6e5e5a1d27b90573523cc8ee41630f5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 16:19:16 +0100 Subject: [PATCH 04/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20code=20impro?= =?UTF-8?q?vement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components/ConnectionButton.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js index 179c8773f..8fb4b235c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -1,5 +1,7 @@ import { Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; + +import classNames from 'classnames'; + import { openSignup } from '../../../ReusableComponents/Icons'; import { useSandboxConnection } from '../../../../hooks/useHandleConnections'; @@ -9,14 +11,16 @@ const ConnectionButton = ( { variant = 'primary', showIcon = true, } ) => { - const className = 'ppcp-r-connection-button'; const { handleSandboxConnect } = useSandboxConnection(); + const className = classNames( 'ppcp-r-connection-button', { + 'sandbox-mode': isSandbox, + 'live-mode': ! isSandbox, + } ); - const handleConnectClick = () => { + const handleConnectClick = async () => { if ( isSandbox ) { - handleSandboxConnect(); + await handleSandboxConnect(); } else { - // Handle live connection logic here console.warn( 'Live connection not implemented yet' ); } }; From 8341d4ec0e0f5f5d19fd6b74b7caa795f03593b7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 16:28:28 +0100 Subject: [PATCH 05/40] =?UTF-8?q?=E2=9C=A8=20First=20draft=20of=20producti?= =?UTF-8?q?on=20login=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 1 + .../resources/js/data/common/actions.js | 17 +++++++++++ .../resources/js/data/common/constants.js | 4 +-- .../resources/js/data/common/controls.js | 28 +++++++++++++++++-- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 47de76afe..3b1e1fcf3 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -16,4 +16,5 @@ export default { DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN', + DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 619aaca5f..ad9a8f5b5 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -131,6 +131,23 @@ export const connectViaSandbox = function* () { return result; }; +/** + * Side effect. Initiates the production login ISU. + * + * @return {Action} The action. + */ +export const connectToProduction = function* () { + yield setIsBusy( true ); + + const result = yield { + type: ACTION_TYPES.DO_PRODUCTION_LOGIN, + products: [ 'EXPRESS_CHECKOUT' ], + }; + yield setIsBusy( false ); + + return result; +}; + /** * Side effect. Initiates a manual connection attempt using the provided client ID and secret. * diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index c7ea9b4c1..4ec4ad20d 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -36,11 +36,11 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual'; /** - * REST path to generate an ISU URL for the sandbox-login. + * REST path to generate an ISU URL for the PayPal-login. * * Used by: Controls * See: LoginLinkRestEndpoint.php * * @type {string} */ -export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link'; +export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 6de513e0b..6005385f9 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -12,7 +12,7 @@ import apiFetch from '@wordpress/api-fetch'; import { REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH, - REST_SANDBOX_CONNECTION_PATH, + REST_CONNECTION_URL_PATH, } from './constants'; import ACTION_TYPES from './action-types'; @@ -34,11 +34,33 @@ export const controls = { try { result = await apiFetch( { - path: REST_SANDBOX_CONNECTION_PATH, + path: REST_CONNECTION_URL_PATH, method: 'POST', data: { environment: 'sandbox', - products: [ 'EXPRESS_CHECKOUT' ], + products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT. + }, + } ); + } catch ( e ) { + result = { + success: false, + error: e, + }; + } + + return result; + }, + + async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) { + let result = null; + + try { + result = await apiFetch( { + path: REST_CONNECTION_URL_PATH, + method: 'POST', + data: { + environment: 'production', + products, }, } ); } catch ( e ) { From 907567992145e371ecb43164186e45df27cffd5b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 17:54:09 +0100 Subject: [PATCH 06/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20Redux=20act?= =?UTF-8?q?ion=20for=20consistent=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/actions.js | 7 ++++--- modules/ppcp-settings/resources/js/data/common/hooks.js | 8 ++++---- .../resources/js/hooks/useHandleConnections.js | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index ad9a8f5b5..6b1a03b4e 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -122,7 +122,7 @@ export const persist = function* () { * * @return {Action} The action. */ -export const connectViaSandbox = function* () { +export const connectToSandbox = function* () { yield setIsBusy( true ); const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; @@ -134,14 +134,15 @@ export const connectViaSandbox = function* () { /** * Side effect. Initiates the production login ISU. * + * @param {string[]} products Which products/features to display in the ISU popup. * @return {Action} The action. */ -export const connectToProduction = function* () { +export const connectToProduction = function* ( products = [] ) { yield setIsBusy( true ); const result = yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, - products: [ 'EXPRESS_CHECKOUT' ], + products, }; yield setIsBusy( false ); diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index fbe6a4842..3f9185102 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -31,7 +31,7 @@ const useHooks = () => { setManualConnectionMode, setClientId, setClientSecret, - connectViaSandbox, + connectToSandbox, connectViaIdAndSecret, } = useDispatch( STORE_NAME ); @@ -72,7 +72,7 @@ const useHooks = () => { setClientSecret: ( value ) => { return savePersistent( setClientSecret, value ); }, - connectViaSandbox, + connectToSandbox, connectViaIdAndSecret, wooSettings, }; @@ -89,9 +89,9 @@ export const useBusyState = () => { }; export const useSandbox = () => { - const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks(); + const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks(); - return { isSandboxMode, setSandboxMode, connectViaSandbox }; + return { isSandboxMode, setSandboxMode, connectToSandbox }; }; export const useManualConnection = () => { diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index b30d5213f..e9a2e30bc 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -26,12 +26,12 @@ const useCommonConnectionLogic = () => { }; export const useSandboxConnection = () => { - const { connectViaSandbox, isSandboxMode, setSandboxMode } = + const { connectToSandbox, isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); const { handleServerError, createErrorNotice } = useCommonConnectionLogic(); const handleSandboxConnect = async () => { - const res = await connectViaSandbox(); + const res = await connectToSandbox(); if ( ! res.success || ! res.data ) { handleServerError( From 74edbc7184c728d86e66517330678702724526c4 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 18:31:35 +0100 Subject: [PATCH 07/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20validatio?= =?UTF-8?q?n=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js --- .../Components/AdvancedOptionsForm.js | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 731436df5..1e2c0ac97 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -1,6 +1,8 @@ import { __, sprintf } from '@wordpress/i18n'; import { Button, TextControl } from '@wordpress/components'; -import { useRef } from '@wordpress/element'; +import { useRef, useState, useEffect } from '@wordpress/element'; + +import classNames from 'classnames'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; @@ -14,6 +16,8 @@ import { import ConnectionButton from './ConnectionButton'; const AdvancedOptionsForm = () => { + const [ clientValid, setClientValid ] = useState( false ); + const [ secretValid, setSecretValid ] = useState( false ); const { isBusy } = CommonHooks.useBusyState(); const { isSandboxMode, setSandboxMode } = useSandboxConnection(); const { @@ -29,43 +33,10 @@ const AdvancedOptionsForm = () => { const refClientId = useRef( null ); const refClientSecret = useRef( null ); - const validateManualConnectionForm = () => { - const fields = [ - { - ref: refClientId, - value: clientId, - errorMessage: __( - 'Please enter your Client ID', - 'woocommerce-paypal-payments' - ), - }, - { - ref: refClientSecret, - value: clientSecret, - errorMessage: __( - 'Please enter your Secret Key', - 'woocommerce-paypal-payments' - ), - }, - ]; - - for ( const { ref, value, errorMessage } of fields ) { - if ( value ) { - continue; - } - - ref?.current?.focus(); - throw new Error( errorMessage ); - } - - return true; - }; - - const handleManualConnect = async () => { - await handleConnectViaIdAndSecret( { - validation: validateManualConnectionForm, - } ); - }; + useEffect( () => { + setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); + setSecretValid( clientSecret && clientSecret.length > 0 ); + }, [ clientId, clientSecret ] ); const advancedUsersDescription = sprintf( // translators: %s: Link to PayPal REST application guide @@ -130,7 +101,18 @@ const AdvancedOptionsForm = () => { } value={ clientId } onChange={ setClientId } + className={ classNames( { + 'has-error': ! clientValid, + } ) } /> + { clientValid || ( +

+ { __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ) } +

+ ) } { onChange={ setClientSecret } type="password" /> - From 71347957d1709083e4c30b34a23ca3dda9e8f144 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 18:49:40 +0100 Subject: [PATCH 08/40] =?UTF-8?q?=F0=9F=9A=B8=20Restore=20previous=20error?= =?UTF-8?q?=20handling=20(additional)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 1e2c0ac97..cf3eaa7cf 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -33,6 +33,43 @@ const AdvancedOptionsForm = () => { const refClientId = useRef( null ); const refClientSecret = useRef( null ); + + const validateManualConnectionForm = () => { + const fields = [ + { + ref: refClientId, + valid: () => clientId && clientValid, + errorMessage: __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ), + }, + { + ref: refClientSecret, + valid: () => clientSecret && secretValid, + errorMessage: __( + 'Please enter your Secret Key', + 'woocommerce-paypal-payments' + ), + }, + ]; + + for ( const { ref, valid, errorMessage } of fields ) { + if ( valid() ) { + continue; + } + + ref?.current?.focus(); + throw new Error( errorMessage ); + } + }; + + const handleManualConnect = async () => { + await handleConnectViaIdAndSecret( { + validation: validateManualConnectionForm, + } ); + }; + useEffect( () => { setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); setSecretValid( clientSecret && clientSecret.length > 0 ); @@ -131,11 +168,7 @@ const AdvancedOptionsForm = () => { onChange={ setClientSecret } type="password" /> - From 3ddff169e720b5b1984ebbc14fd038daea0a3850 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 3 Dec 2024 18:53:59 +0100 Subject: [PATCH 09/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20error?= =?UTF-8?q?=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index cf3eaa7cf..9875cc9a0 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -33,28 +33,41 @@ const AdvancedOptionsForm = () => { const refClientId = useRef( null ); const refClientSecret = useRef( null ); + const errors = { + noClientId: __( + 'Please enter a Client ID', + 'woocommerce-paypal-payments' + ), + noClientSecret: __( + 'Please enter your Secret Key', + 'woocommerce-paypal-payments' + ), + invalidClientId: __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ), + }; const validateManualConnectionForm = () => { - const fields = [ + const checks = [ { ref: refClientId, - valid: () => clientId && clientValid, - errorMessage: __( - 'Please enter a valid Client ID', - 'woocommerce-paypal-payments' - ), + valid: () => clientId, + errorMessage: errors.noClientId, + }, + { + ref: refClientId, + valid: () => clientValid, + errorMessage: errors.invalidClientId, }, { ref: refClientSecret, valid: () => clientSecret && secretValid, - errorMessage: __( - 'Please enter your Secret Key', - 'woocommerce-paypal-payments' - ), + errorMessage: errors.noClientSecret, }, ]; - for ( const { ref, valid, errorMessage } of fields ) { + for ( const { ref, valid, errorMessage } of checks ) { if ( valid() ) { continue; } @@ -144,10 +157,7 @@ const AdvancedOptionsForm = () => { /> { clientValid || (

- { __( - 'Please enter a valid Client ID', - 'woocommerce-paypal-payments' - ) } + { errors.invalidClientId }

) } Date: Tue, 3 Dec 2024 18:57:34 +0100 Subject: [PATCH 10/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20label=20lo?= =?UTF-8?q?gic=20to=20new=20effect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 9875cc9a0..bad5cf953 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -18,6 +18,9 @@ import ConnectionButton from './ConnectionButton'; const AdvancedOptionsForm = () => { const [ clientValid, setClientValid ] = useState( false ); const [ secretValid, setSecretValid ] = useState( false ); + const [ clientIdLabel, setClientIdLabel ] = useState( '' ); + const [ secretKeyLabel, setSecretKeyLabel ] = useState( '' ); + const { isBusy } = CommonHooks.useBusyState(); const { isSandboxMode, setSandboxMode } = useSandboxConnection(); const { @@ -88,6 +91,19 @@ const AdvancedOptionsForm = () => { setSecretValid( clientSecret && clientSecret.length > 0 ); }, [ clientId, clientSecret ] ); + useEffect( () => { + setClientIdLabel( + isSandboxMode + ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' ) + : __( 'Live Client ID', 'woocommerce-paypal-payments' ) + ); + setSecretKeyLabel( + isSandboxMode + ? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ) + : __( 'Live Secret Key', 'woocommerce-paypal-payments' ) + ); + }, [ isSandboxMode ] ); + const advancedUsersDescription = sprintf( // translators: %s: Link to PayPal REST application guide __( @@ -138,17 +154,7 @@ const AdvancedOptionsForm = () => { { Date: Tue, 3 Dec 2024 19:00:04 +0100 Subject: [PATCH 11/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20code=20impro?= =?UTF-8?q?vements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index bad5cf953..cc6f49ae2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -15,6 +15,21 @@ import { import ConnectionButton from './ConnectionButton'; +const FORM_ERRORS = { + noClientId: __( + 'Please enter your Client ID', + 'woocommerce-paypal-payments' + ), + noClientSecret: __( + 'Please enter your Secret Key', + 'woocommerce-paypal-payments' + ), + invalidClientId: __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ), +}; + const AdvancedOptionsForm = () => { const [ clientValid, setClientValid ] = useState( false ); const [ secretValid, setSecretValid ] = useState( false ); @@ -36,37 +51,22 @@ const AdvancedOptionsForm = () => { const refClientId = useRef( null ); const refClientSecret = useRef( null ); - const errors = { - noClientId: __( - 'Please enter a Client ID', - 'woocommerce-paypal-payments' - ), - noClientSecret: __( - 'Please enter your Secret Key', - 'woocommerce-paypal-payments' - ), - invalidClientId: __( - 'Please enter a valid Client ID', - 'woocommerce-paypal-payments' - ), - }; - const validateManualConnectionForm = () => { const checks = [ { ref: refClientId, valid: () => clientId, - errorMessage: errors.noClientId, + errorMessage: FORM_ERRORS.noClientId, }, { ref: refClientId, valid: () => clientValid, - errorMessage: errors.invalidClientId, + errorMessage: FORM_ERRORS.invalidClientId, }, { ref: refClientSecret, valid: () => clientSecret && secretValid, - errorMessage: errors.noClientSecret, + errorMessage: FORM_ERRORS.noClientSecret, }, ]; @@ -80,11 +80,10 @@ const AdvancedOptionsForm = () => { } }; - const handleManualConnect = async () => { - await handleConnectViaIdAndSecret( { + const handleManualConnect = () => + handleConnectViaIdAndSecret( { validation: validateManualConnectionForm, } ); - }; useEffect( () => { setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); @@ -163,7 +162,7 @@ const AdvancedOptionsForm = () => { /> { clientValid || (

- { errors.invalidClientId } + { FORM_ERRORS.invalidClientId }

) } Date: Thu, 5 Dec 2024 14:52:10 +0100 Subject: [PATCH 12/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20translatio?= =?UTF-8?q?ns=20to=20global=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index e9a2e30bc..e9a62c250 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -5,6 +5,26 @@ import { store as noticesStore } from '@wordpress/notices'; import { CommonHooks, OnboardingHooks } from '../data'; import { openPopup } from '../utils/window'; +const MESSAGES = { + CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ), + POPUP_BLOCKED: __( + 'Popup blocked. Please allow popups for this site to connect to PayPal.', + 'woocommerce-paypal-payments' + ), + SANDBOX_ERROR: __( + 'Could not generate a Sandbox login link.', + 'woocommerce-paypal-payments' + ), + PRODUCTION_ERROR: __( + 'Could not generate a login link.', + 'woocommerce-paypal-payments' + ), + MANUAL_ERROR: __( + 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', + 'woocommerce-paypal-payments' + ), +}; + const useCommonConnectionLogic = () => { const { setCompleted } = OnboardingHooks.useSteps(); const { createSuccessNotice, createErrorNotice } = @@ -16,10 +36,8 @@ const useCommonConnectionLogic = () => { }; const handleServerSuccess = () => { - createSuccessNotice( - __( 'Connected to PayPal', 'woocommerce-paypal-payments' ) - ); - setCompleted( true ); + createSuccessNotice( MESSAGES.CONNECTED ); + return setCompleted( true ); }; return { handleServerError, handleServerSuccess, createErrorNotice }; @@ -34,13 +52,7 @@ export const useSandboxConnection = () => { const res = await connectToSandbox(); if ( ! res.success || ! res.data ) { - handleServerError( - res, - __( - 'Could not generate a Sandbox login link.', - 'woocommerce-paypal-payments' - ) - ); + handleServerError( res, MESSAGES.SANDBOX_ERROR ); return; } @@ -48,12 +60,7 @@ export const useSandboxConnection = () => { const popup = openPopup( connectionUrl ); if ( ! popup ) { - createErrorNotice( - __( - 'Popup blocked. Please allow popups for this site to connect to PayPal.', - 'woocommerce-paypal-payments' - ) - ); + createErrorNotice( MESSAGES.POPUP_BLOCKED ); } }; @@ -89,15 +96,9 @@ export const useManualConnection = () => { const res = await connectViaIdAndSecret(); if ( res.success ) { - handleServerSuccess(); + await handleServerSuccess(); } else { - handleServerError( - res, - __( - 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', - 'woocommerce-paypal-payments' - ) - ); + handleServerError( res, MESSAGES.MANUAL_ERROR ); } }; From 5a81d7378fddd871d6ce19fb940188732514f5fa Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 15:02:02 +0100 Subject: [PATCH 13/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Restructure=20intern?= =?UTF-8?q?al=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index e9a62c250..eed6f4c51 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -25,34 +25,34 @@ const MESSAGES = { ), }; -const useCommonConnectionLogic = () => { +const useConnectionBase = () => { const { setCompleted } = OnboardingHooks.useSteps(); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); - const handleServerError = ( res, genericMessage ) => { - console.error( 'Connection error', res ); - createErrorNotice( res?.message ?? genericMessage ); + return { + handleError: ( res, genericMessage ) => { + console.error( 'Connection error', res ); + createErrorNotice( res?.message ?? genericMessage ); + }, + handleSuccess: async () => { + createSuccessNotice( MESSAGES.CONNECTED ); + return setCompleted( true ); + }, + createErrorNotice, }; - - const handleServerSuccess = () => { - createSuccessNotice( MESSAGES.CONNECTED ); - return setCompleted( true ); - }; - - return { handleServerError, handleServerSuccess, createErrorNotice }; }; export const useSandboxConnection = () => { const { connectToSandbox, isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); - const { handleServerError, createErrorNotice } = useCommonConnectionLogic(); + const { handleError, createErrorNotice } = useConnectionBase(); const handleSandboxConnect = async () => { const res = await connectToSandbox(); if ( ! res.success || ! res.data ) { - handleServerError( res, MESSAGES.SANDBOX_ERROR ); + handleError( res, MESSAGES.SANDBOX_ERROR ); return; } @@ -81,8 +81,8 @@ export const useManualConnection = () => { clientSecret, setClientSecret, } = CommonHooks.useManualConnection(); - const { handleServerError, handleServerSuccess, createErrorNotice } = - useCommonConnectionLogic(); + const { handleError, handleSuccess, createErrorNotice } = + useConnectionBase(); const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { if ( 'function' === typeof validation ) { @@ -96,9 +96,9 @@ export const useManualConnection = () => { const res = await connectViaIdAndSecret(); if ( res.success ) { - await handleServerSuccess(); + await handleSuccess(); } else { - handleServerError( res, MESSAGES.MANUAL_ERROR ); + handleError( res, MESSAGES.MANUAL_ERROR ); } }; From 67822b3b11cc476a79f90d1bd00e2c6a3ee3a0e5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 15:03:03 +0100 Subject: [PATCH 14/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Restructure=20Sandbo?= =?UTF-8?q?x=20connection=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index eed6f4c51..ce4ab441c 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -25,6 +25,30 @@ const MESSAGES = { ), }; +const handlePopupOpen = ( url, onError ) => { + const popup = openPopup( url ); + if ( ! popup ) { + onError( MESSAGES.POPUP_BLOCKED ); + return false; + } + return true; +}; + +const useConnectionAttempt = ( connectFn, errorMessage ) => { + const { handleError, createErrorNotice } = useConnectionBase(); + + return async ( ...args ) => { + const res = await connectFn( ...args ); + + if ( ! res.success || ! res.data ) { + handleError( res, errorMessage ); + return false; + } + + return handlePopupOpen( res.data, createErrorNotice ); + }; +}; + const useConnectionBase = () => { const { setCompleted } = OnboardingHooks.useSteps(); const { createSuccessNotice, createErrorNotice } = @@ -46,23 +70,10 @@ const useConnectionBase = () => { export const useSandboxConnection = () => { const { connectToSandbox, isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); - const { handleError, createErrorNotice } = useConnectionBase(); - - const handleSandboxConnect = async () => { - const res = await connectToSandbox(); - - if ( ! res.success || ! res.data ) { - handleError( res, MESSAGES.SANDBOX_ERROR ); - return; - } - - const connectionUrl = res.data; - const popup = openPopup( connectionUrl ); - - if ( ! popup ) { - createErrorNotice( MESSAGES.POPUP_BLOCKED ); - } - }; + const handleSandboxConnect = useConnectionAttempt( + connectToSandbox, + MESSAGES.SANDBOX_ERROR + ); return { handleSandboxConnect, From a8f12c63fa6deac89e87c2cc7e9f2d7d9b9ba0e5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 15:04:36 +0100 Subject: [PATCH 15/40] =?UTF-8?q?=F0=9F=8E=A8=20Minor=20re-organization=20?= =?UTF-8?q?of=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index ce4ab441c..ed2fd9f7b 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -34,21 +34,6 @@ const handlePopupOpen = ( url, onError ) => { return true; }; -const useConnectionAttempt = ( connectFn, errorMessage ) => { - const { handleError, createErrorNotice } = useConnectionBase(); - - return async ( ...args ) => { - const res = await connectFn( ...args ); - - if ( ! res.success || ! res.data ) { - handleError( res, errorMessage ); - return false; - } - - return handlePopupOpen( res.data, createErrorNotice ); - }; -}; - const useConnectionBase = () => { const { setCompleted } = OnboardingHooks.useSteps(); const { createSuccessNotice, createErrorNotice } = @@ -67,6 +52,21 @@ const useConnectionBase = () => { }; }; +const useConnectionAttempt = ( connectFn, errorMessage ) => { + const { handleError, createErrorNotice } = useConnectionBase(); + + return async ( ...args ) => { + const res = await connectFn( ...args ); + + if ( ! res.success || ! res.data ) { + handleError( res, errorMessage ); + return false; + } + + return handlePopupOpen( res.data, createErrorNotice ); + }; +}; + export const useSandboxConnection = () => { const { connectToSandbox, isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); @@ -83,6 +83,8 @@ export const useSandboxConnection = () => { }; export const useManualConnection = () => { + const { handleError, handleSuccess, createErrorNotice } = + useConnectionBase(); const { connectViaIdAndSecret, isManualConnectionMode, @@ -92,8 +94,6 @@ export const useManualConnection = () => { clientSecret, setClientSecret, } = CommonHooks.useManualConnection(); - const { handleError, handleSuccess, createErrorNotice } = - useConnectionBase(); const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { if ( 'function' === typeof validation ) { @@ -104,6 +104,7 @@ export const useManualConnection = () => { return; } } + const res = await connectViaIdAndSecret(); if ( res.success ) { From 05c1978f0dd3086e09b5d50d43dcad7fe7099fe7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 15:05:00 +0100 Subject: [PATCH 16/40] =?UTF-8?q?=E2=9C=A8=20Add=20new=20hook=20for=20prod?= =?UTF-8?q?uction=20ISU=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/hooks.js | 13 ++++++++++++- .../resources/js/data/onboarding/selectors.js | 15 +++++++++++++++ .../resources/js/hooks/useHandleConnections.js | 11 +++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index 5da85634f..96bae287c 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -34,8 +34,12 @@ const useHooks = () => { setProducts, } = useDispatch( STORE_NAME ); - // Read-only flags. + // Read-only flags and derived state. const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); + const determineProducts = useSelect( + ( select ) => select( STORE_NAME ).determineProducts(), + [] + ); // Transient accessors. const isReady = useTransient( 'isReady' ); @@ -80,6 +84,7 @@ const useHooks = () => { ); return savePersistent( setProducts, validProducts ); }, + determineProducts, }; }; @@ -123,3 +128,9 @@ export const useNavigationState = () => { business, }; }; + +export const useDetermineProducts = () => { + const { determineProducts } = useHooks(); + + return determineProducts; +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index d4d57ef4d..63296d8a4 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -23,3 +23,18 @@ export const transientData = ( state ) => { export const flags = ( state ) => { return getState( state ).flags || EMPTY_OBJ; }; + +/** + * Returns the products that we use for the production login link in the last onboarding step. + * + * This selector does not return state-values, but uses the state to derive the products-array + * that should be returned. + * + * @param {{}} state + * @return {string[]} The ISU products, based on choices made in the onboarding wizard. + */ +export const determineProducts = ( state ) => { + const derivedProducts = []; + + return derivedProducts; +}; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index ed2fd9f7b..2c3cfcea7 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -82,6 +82,17 @@ export const useSandboxConnection = () => { }; }; +export const useProductionConnection = () => { + const { connectToProduction } = CommonHooks.useProduction(); + const products = OnboardingHooks.useDetermineProducts(); + const handleProductionConnect = useConnectionAttempt( + () => connectToProduction( products ), + MESSAGES.PRODUCTION_ERROR + ); + + return { handleProductionConnect }; +}; + export const useManualConnection = () => { const { handleError, handleSuccess, createErrorNotice } = useConnectionBase(); From 0e30ecde82182b331ca7df4cf2ccafa01e3fd241 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 15:06:34 +0100 Subject: [PATCH 17/40] =?UTF-8?q?=E2=9C=A8=20Add=20production=20login=20ho?= =?UTF-8?q?ok=20to=20last=20wizard=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/Onboarding/Components/ConnectionButton.js | 8 ++++++-- modules/ppcp-settings/resources/js/data/common/hooks.js | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js index 8fb4b235c..5d1bae47e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -3,7 +3,10 @@ import { Button } from '@wordpress/components'; import classNames from 'classnames'; import { openSignup } from '../../../ReusableComponents/Icons'; -import { useSandboxConnection } from '../../../../hooks/useHandleConnections'; +import { + useProductionConnection, + useSandboxConnection, +} from '../../../../hooks/useHandleConnections'; const ConnectionButton = ( { title, @@ -12,6 +15,7 @@ const ConnectionButton = ( { showIcon = true, } ) => { const { handleSandboxConnect } = useSandboxConnection(); + const { handleProductionConnect } = useProductionConnection(); const className = classNames( 'ppcp-r-connection-button', { 'sandbox-mode': isSandbox, 'live-mode': ! isSandbox, @@ -21,7 +25,7 @@ const ConnectionButton = ( { if ( isSandbox ) { await handleSandboxConnect(); } else { - console.warn( 'Live connection not implemented yet' ); + await handleProductionConnect(); } }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 3f9185102..c1fa859d9 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -32,6 +32,7 @@ const useHooks = () => { setClientId, setClientSecret, connectToSandbox, + connectToProduction, connectViaIdAndSecret, } = useDispatch( STORE_NAME ); @@ -73,6 +74,7 @@ const useHooks = () => { return savePersistent( setClientSecret, value ); }, connectToSandbox, + connectToProduction, connectViaIdAndSecret, wooSettings, }; @@ -94,6 +96,12 @@ export const useSandbox = () => { return { isSandboxMode, setSandboxMode, connectToSandbox }; }; +export const useProduction = () => { + const { connectToProduction } = useHooks(); + + return { connectToProduction }; +}; + export const useManualConnection = () => { const { isManualConnectionMode, From 1a36144095f85dd653b387b450a3bca5139adb68 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 15:06:59 +0100 Subject: [PATCH 18/40] =?UTF-8?q?=F0=9F=91=94=20Start=20to=20customize=20t?= =?UTF-8?q?he=20production=20ISU=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/PartnerReferrals.php | 2 ++ .../resources/js/data/onboarding/selectors.js | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php index c7f5ec131..1d100cdda 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php @@ -16,6 +16,8 @@ use Psr\Log\LoggerInterface; /** * Class PartnerReferrals + * + * @see https://developer.paypal.com/docs/api/partner-referrals/v2/ */ class PartnerReferrals { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index 63296d8a4..2e0953437 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -36,5 +36,37 @@ export const flags = ( state ) => { export const determineProducts = ( state ) => { const derivedProducts = []; + const { isCasualSeller, areOptionalPaymentMethodsEnabled } = + persistentData( state ); + const { canUseVaulting, canUseCardPayments } = flags( state ); + + if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) { + /** + * Branch 1: Credit Card Payments not available. + * The store uses the Express-checkout product. + */ + derivedProducts.push( 'EXPRESS_CHECKOUT' ); + } else if ( isCasualSeller ) { + /** + * Branch 2: Merchant has no business. + * The store uses the Express-checkout product. + */ + derivedProducts.push( 'EXPRESS_CHECKOUT' ); + + // TODO: Add the "BCDC" product/feature + // Requirement: "EXPRESS_CHECKOUT with BCDC" + } else { + /** + * Branch 3: Merchant is business, and can use CC payments. + * The store uses the advanced PPCP product. + */ + derivedProducts.push( 'PPCP' ); + } + + if ( canUseVaulting ) { + // TODO: Add the "Vaulting" product/feature + // Requirement: "... with Vault" + } + return derivedProducts; }; From e502f78376d8527e3ad427be4aac00f20486535b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 16:09:28 +0100 Subject: [PATCH 19/40] =?UTF-8?q?=E2=9C=A8=20Add=20automatic=20popup=20com?= =?UTF-8?q?pletion=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 2c3cfcea7..6573f2478 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -25,13 +25,32 @@ const MESSAGES = { ), }; -const handlePopupOpen = ( url, onError ) => { - const popup = openPopup( url ); - if ( ! popup ) { - onError( MESSAGES.POPUP_BLOCKED ); - return false; - } - return true; +const handlePopupWithCompletion = ( url, onError ) => { + return new Promise( ( resolve ) => { + const popup = openPopup( url ); + + if ( ! popup ) { + onError( MESSAGES.POPUP_BLOCKED ); + resolve( false ); + return; + } + + // Check popup state every 500ms + const checkPopup = setInterval( () => { + if ( popup.closed ) { + clearInterval( checkPopup ); + resolve( true ); + } + }, 500 ); + + return () => { + clearInterval( checkPopup ); + + if ( popup && ! popup.closed ) { + popup.close(); + } + }; + } ); }; const useConnectionBase = () => { @@ -46,6 +65,8 @@ const useConnectionBase = () => { }, handleSuccess: async () => { createSuccessNotice( MESSAGES.CONNECTED ); + + // TODO: Contact the plugin to confirm onboarding is completed. return setCompleted( true ); }, createErrorNotice, @@ -53,7 +74,8 @@ const useConnectionBase = () => { }; const useConnectionAttempt = ( connectFn, errorMessage ) => { - const { handleError, createErrorNotice } = useConnectionBase(); + const { handleError, createErrorNotice, handleSuccess } = + useConnectionBase(); return async ( ...args ) => { const res = await connectFn( ...args ); @@ -63,7 +85,16 @@ const useConnectionAttempt = ( connectFn, errorMessage ) => { return false; } - return handlePopupOpen( res.data, createErrorNotice ); + const popupClosed = await handlePopupWithCompletion( + res.data, + createErrorNotice + ); + + if ( popupClosed ) { + await handleSuccess(); + } + + return popupClosed; }; }; From 51db2de840b02d80c0f7c713cb09607c47083ed0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 16:22:26 +0100 Subject: [PATCH 20/40] =?UTF-8?q?=E2=9C=A8=20Add=20a=20default=20=E2=80=9C?= =?UTF-8?q?reset=E2=80=9D=20action=20to=20every=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 1 + .../resources/js/data/common/actions.js | 7 +++++++ .../resources/js/data/common/reducer.js | 13 +++++++++++++ .../resources/js/data/onboarding/reducer.js | 14 ++++++++++++-- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 3b1e1fcf3..0cfe2e758 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -10,6 +10,7 @@ export default { // Persistent data. SET_PERSISTENT: 'COMMON:SET_PERSISTENT', + RESET: 'COMMON:RESET', HYDRATE: 'COMMON:HYDRATE', // Controls - always start with "DO_". diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 6b1a03b4e..6aea05024 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -18,6 +18,13 @@ import { STORE_NAME } from './constants'; * @property {Object?} payload - Optional payload for the action. */ +/** + * Special. Resets all values in the onboarding store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { type: ACTION_TYPES.RESET } ); + /** * Persistent. Set the full onboarding details, usually during app initialization. * diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 771dfa8f5..814f4d6b3 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -44,6 +44,19 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) => setPersistent( state, action ), + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.wooSettings = { ...state.wooSettings }; + cleanState.isReady = true; + + return cleanState; + }, + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 176d4875d..65d82d2a5 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -45,8 +45,18 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => setPersistent( state, payload ), - [ ACTION_TYPES.RESET ]: ( state ) => - setPersistent( state, defaultPersistent ), + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.flags = { ...state.flags }; + cleanState.isReady = true; + + return cleanState; + }, [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); From 405c397331af31a4dc3a490edbc9802f881e626d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 16:23:09 +0100 Subject: [PATCH 21/40] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Corre?= =?UTF-8?q?ct=20debug=20method=20to=20reset=20all=20stores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/debug.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index b292d1920..6380c6d6a 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -1,4 +1,4 @@ -import { OnboardingStoreName } from './index'; +import { OnboardingStoreName, CommonStoreName } from './index'; export const addDebugTools = ( context, modules ) => { if ( ! context || ! context?.debug ) { @@ -33,9 +33,14 @@ export const addDebugTools = ( context, modules ) => { }; context.resetStore = () => { - const onboarding = wp.data.dispatch( OnboardingStoreName ); - onboarding.reset(); - onboarding.persist(); + const stores = [ OnboardingStoreName, CommonStoreName ]; + + stores.forEach( ( storeName ) => { + const store = wp.data.dispatch( storeName ); + + store.reset(); + store.persist(); + } ); }; context.startOnboarding = () => { From 9786a18eb0e2322e67510bcfcc405913af0e4bab Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 18:55:56 +0100 Subject: [PATCH 22/40] =?UTF-8?q?=E2=9C=A8=20Replace=20isBusy=20with=20new?= =?UTF-8?q?=20activity-state=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 4 ++ .../resources/js/data/common/actions.js | 37 +++++++++++++------ .../resources/js/data/common/hooks.js | 30 +++++++++++++-- .../resources/js/data/common/reducer.js | 17 ++++++++- .../resources/js/data/common/selectors.js | 5 +++ 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 0cfe2e758..ac2c6db37 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -13,6 +13,10 @@ export default { RESET: 'COMMON:RESET', HYDRATE: 'COMMON:HYDRATE', + // Activity management (advanced solution that replaces the isBusy state). + START_ACTIVITY: 'COMMON:START_ACTIVITY', + STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY', + // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 6aea05024..c6906546a 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -59,14 +59,35 @@ export const setIsSaving = ( isSaving ) => ( { } ); /** - * Transient. Changes the "manual connection is busy" flag. + * Transient (Activity): Marks the start of an async activity + * Think of it as "setIsBusy(true)" * - * @param {boolean} isBusy + * @param {string} id Internal ID/key of the action, used to stop it again. + * @param {?string} description Optional, description for logging/debugging + * @return {?Action} The action. + */ +export const startActivity = ( id, description = null ) => { + if ( ! id || 'string' !== typeof id ) { + console.warn( 'Activity ID must be a non-empty string' ); + return null; + } + + return { + type: ACTION_TYPES.START_ACTIVITY, + payload: { id, description }, + }; +}; + +/** + * Transient (Activity): Marks the end of an async activity. + * Think of it as "setIsBusy(false)" + * + * @param {string} id Internal ID/key of the action, used to stop it again. * @return {Action} The action. */ -export const setIsBusy = ( isBusy ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isBusy }, +export const stopActivity = ( id ) => ( { + type: ACTION_TYPES.STOP_ACTIVITY, + payload: { id }, } ); /** @@ -130,10 +151,8 @@ export const persist = function* () { * @return {Action} The action. */ export const connectToSandbox = function* () { - yield setIsBusy( true ); const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; - yield setIsBusy( false ); return result; }; @@ -145,13 +164,11 @@ export const connectToSandbox = function* () { * @return {Action} The action. */ export const connectToProduction = function* ( products = [] ) { - yield setIsBusy( true ); const result = yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products, }; - yield setIsBusy( false ); return result; }; @@ -165,7 +182,6 @@ export const connectViaIdAndSecret = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); - yield setIsBusy( true ); const result = yield { type: ACTION_TYPES.DO_MANUAL_CONNECTION, @@ -173,7 +189,6 @@ export const connectViaIdAndSecret = function* () { clientSecret, useSandbox, }; - yield setIsBusy( false ); return result; }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index c1fa859d9..e4442e50f 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -81,12 +81,34 @@ const useHooks = () => { }; export const useBusyState = () => { - const { setIsBusy } = useDispatch( STORE_NAME ); - const isBusy = useTransient( 'isBusy' ); + const { startActivity, stopActivity } = useDispatch( STORE_NAME ); + + // Resolved value (object), contains a list of all running actions. + const activities = useSelect( + ( select ) => select( STORE_NAME ).getActivityList(), + [] + ); + + // Derive isBusy state from activities + const isBusy = Object.keys( activities ).length > 0; + + // HOC that starts and stops an activity while the callback is executed. + const withActivity = useCallback( + async ( id, description, asyncFn ) => { + startActivity( id, description ); + try { + return await asyncFn(); + } finally { + stopActivity( id ); + } + }, + [ startActivity, stopActivity ] + ); return { - isBusy, - setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ), + withActivity, // HOC + isBusy, // Boolean. + activities, // Object. }; }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 814f4d6b3..63c231f85 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -14,7 +14,7 @@ import ACTION_TYPES from './action-types'; const defaultTransient = { isReady: false, - isBusy: false, + activities: new Map(), // Read only values, provided by the server via hydrate. wooSettings: { @@ -57,6 +57,21 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { return cleanState; }, + [ ACTION_TYPES.START_ACTIVITY ]: ( state, payload ) => { + return setTransient( state, { + activities: new Map( state.activities ).set( + payload.id, + payload.description + ), + } ); + }, + + [ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload ) => { + const newActivities = new Map( state.activities ); + newActivities.delete( payload.id ); + return setTransient( state, { activities: newActivities } ); + }, + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 7f0b3ee20..17e422b7a 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -20,6 +20,11 @@ export const transientData = ( state ) => { return transientState || EMPTY_OBJ; }; +export const getActivityList = ( state ) => { + const { activities = new Map() } = state; + return Object.fromEntries( activities ); +}; + export const wooSettings = ( state ) => { return getState( state ).wooSettings || EMPTY_OBJ; }; From 64ea8ec011de032589f42cc5c16f6967fb0385fa Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 18:56:23 +0100 Subject: [PATCH 23/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20some=20ac?= =?UTF-8?q?tion=20generators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/actions.js | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index c6906546a..abed4f2d3 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -146,31 +146,22 @@ export const persist = function* () { }; /** - * Side effect. Initiates the sandbox login ISU. + * Side effect. Fetches the ISU-login URL for a sandbox account. * * @return {Action} The action. */ export const connectToSandbox = function* () { - - const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; - - return result; + return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; }; /** - * Side effect. Initiates the production login ISU. + * Side effect. Fetches the ISU-login URL for a production account. * * @param {string[]} products Which products/features to display in the ISU popup. * @return {Action} The action. */ export const connectToProduction = function* ( products = [] ) { - - const result = yield { - type: ACTION_TYPES.DO_PRODUCTION_LOGIN, - products, - }; - - return result; + return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products }; }; /** @@ -182,13 +173,10 @@ export const connectViaIdAndSecret = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); - - const result = yield { + return yield { type: ACTION_TYPES.DO_MANUAL_CONNECTION, clientId, clientSecret, useSandbox, }; - - return result; }; From 5b26d7c5ca165ef34c96989f8f8158600d2a4f63 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 5 Dec 2024 18:56:43 +0100 Subject: [PATCH 24/40] =?UTF-8?q?=E2=9C=A8=20Use=20new=20actitiy-state=20f?= =?UTF-8?q?or=20login=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 6573f2478..d242b1de9 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -25,6 +25,12 @@ const MESSAGES = { ), }; +const ACTIVITIES = { + CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX', + CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION', + CONNECT_MANUAL: 'MANUAL_LOGIN', +}; + const handlePopupWithCompletion = ( url, onError ) => { return new Promise( ( resolve ) => { const popup = openPopup( url ); @@ -101,11 +107,20 @@ const useConnectionAttempt = ( connectFn, errorMessage ) => { export const useSandboxConnection = () => { const { connectToSandbox, isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); - const handleSandboxConnect = useConnectionAttempt( + const { withActivity } = CommonHooks.useBusyState(); + const connectionAttempt = useConnectionAttempt( connectToSandbox, MESSAGES.SANDBOX_ERROR ); + const handleSandboxConnect = async () => { + return withActivity( + ACTIVITIES.CONNECT_SANDBOX, + 'Connecting to sandbox account', + connectionAttempt + ); + }; + return { handleSandboxConnect, isSandboxMode, @@ -115,18 +130,28 @@ export const useSandboxConnection = () => { export const useProductionConnection = () => { const { connectToProduction } = CommonHooks.useProduction(); + const { withActivity } = CommonHooks.useBusyState(); const products = OnboardingHooks.useDetermineProducts(); - const handleProductionConnect = useConnectionAttempt( + const connectionAttempt = useConnectionAttempt( () => connectToProduction( products ), MESSAGES.PRODUCTION_ERROR ); + const handleProductionConnect = async () => { + return withActivity( + ACTIVITIES.CONNECT_PRODUCTION, + 'Connecting to production account', + connectionAttempt + ); + }; + return { handleProductionConnect }; }; export const useManualConnection = () => { const { handleError, handleSuccess, createErrorNotice } = useConnectionBase(); + const { withActivity } = CommonHooks.useBusyState(); const { connectViaIdAndSecret, isManualConnectionMode, @@ -138,22 +163,30 @@ export const useManualConnection = () => { } = CommonHooks.useManualConnection(); const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { - if ( 'function' === typeof validation ) { - try { - validation(); - } catch ( exception ) { - createErrorNotice( exception.message ); - return; + return withActivity( + ACTIVITIES.CONNECT_MANUAL, + 'Connecting manually via Client ID and Secret', + async () => { + if ( 'function' === typeof validation ) { + try { + validation(); + } catch ( exception ) { + createErrorNotice( exception.message ); + return; + } + } + + const res = await connectViaIdAndSecret(); + + if ( res.success ) { + await handleSuccess(); + } else { + handleError( res, MESSAGES.MANUAL_ERROR ); + } + + return res.success; } - } - - const res = await connectViaIdAndSecret(); - - if ( res.success ) { - await handleSuccess(); - } else { - handleError( res, MESSAGES.MANUAL_ERROR ); - } + ); }; return { From 3174bc158fe86852dcc92ab44cd8ecc36b09406b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 14:37:21 +0100 Subject: [PATCH 25/40] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Minor=20performance?= =?UTF-8?q?=20tweaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index cc6f49ae2..bb2d58209 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -1,6 +1,12 @@ import { __, sprintf } from '@wordpress/i18n'; import { Button, TextControl } from '@wordpress/components'; -import { useRef, useState, useEffect } from '@wordpress/element'; +import { + useRef, + useState, + useEffect, + useMemo, + useCallback, +} from '@wordpress/element'; import classNames from 'classnames'; @@ -33,8 +39,6 @@ const FORM_ERRORS = { const AdvancedOptionsForm = () => { const [ clientValid, setClientValid ] = useState( false ); const [ secretValid, setSecretValid ] = useState( false ); - const [ clientIdLabel, setClientIdLabel ] = useState( '' ); - const [ secretKeyLabel, setSecretKeyLabel ] = useState( '' ); const { isBusy } = CommonHooks.useBusyState(); const { isSandboxMode, setSandboxMode } = useSandboxConnection(); @@ -51,7 +55,7 @@ const AdvancedOptionsForm = () => { const refClientId = useRef( null ); const refClientSecret = useRef( null ); - const validateManualConnectionForm = () => { + const validateManualConnectionForm = useCallback( () => { const checks = [ { ref: refClientId, @@ -78,30 +82,36 @@ const AdvancedOptionsForm = () => { ref?.current?.focus(); throw new Error( errorMessage ); } - }; + }, [ clientId, clientSecret, clientValid, secretValid ] ); - const handleManualConnect = () => - handleConnectViaIdAndSecret( { - validation: validateManualConnectionForm, - } ); + const handleManualConnect = useCallback( + () => + handleConnectViaIdAndSecret( { + validation: validateManualConnectionForm, + } ), + [ validateManualConnectionForm ] + ); useEffect( () => { setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); setSecretValid( clientSecret && clientSecret.length > 0 ); }, [ clientId, clientSecret ] ); - useEffect( () => { - setClientIdLabel( + const clientIdLabel = useMemo( + () => isSandboxMode ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' ) - : __( 'Live Client ID', 'woocommerce-paypal-payments' ) - ); - setSecretKeyLabel( + : __( 'Live Client ID', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); + + const secretKeyLabel = useMemo( + () => isSandboxMode ? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ) - : __( 'Live Secret Key', 'woocommerce-paypal-payments' ) - ); - }, [ isSandboxMode ] ); + : __( 'Live Secret Key', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); const advancedUsersDescription = sprintf( // translators: %s: Link to PayPal REST application guide From 4f3c4e6f3df17e35e07376f285b13666a329c27c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 14:37:48 +0100 Subject: [PATCH 26/40] =?UTF-8?q?=E2=9C=A8=20Disable=20the=20Connect-butto?= =?UTF-8?q?n=20during=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/Onboarding/Components/ConnectionButton.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js index 5d1bae47e..0a0ac5bfb 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -2,6 +2,7 @@ import { Button } from '@wordpress/components'; import classNames from 'classnames'; +import { CommonHooks } from '../../../../data'; import { openSignup } from '../../../ReusableComponents/Icons'; import { useProductionConnection, @@ -14,11 +15,13 @@ const ConnectionButton = ( { variant = 'primary', showIcon = true, } ) => { + const { isBusy } = CommonHooks.useBusyState(); const { handleSandboxConnect } = useSandboxConnection(); const { handleProductionConnect } = useProductionConnection(); const className = classNames( 'ppcp-r-connection-button', { 'sandbox-mode': isSandbox, 'live-mode': ! isSandbox, + 'ppcp--is-loading': isBusy, } ); const handleConnectClick = async () => { @@ -35,6 +38,7 @@ const ConnectionButton = ( { variant={ variant } icon={ showIcon ? openSignup : null } onClick={ handleConnectClick } + disabled={ isBusy } > { title } From 4e9d588058715894a180bf45d8c790729d6cab27 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 15:49:37 +0100 Subject: [PATCH 27/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rearrange=20code,=20?= =?UTF-8?q?minor=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/hooks.js | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index e4442e50f..de56976e5 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -80,38 +80,6 @@ const useHooks = () => { }; }; -export const useBusyState = () => { - const { startActivity, stopActivity } = useDispatch( STORE_NAME ); - - // Resolved value (object), contains a list of all running actions. - const activities = useSelect( - ( select ) => select( STORE_NAME ).getActivityList(), - [] - ); - - // Derive isBusy state from activities - const isBusy = Object.keys( activities ).length > 0; - - // HOC that starts and stops an activity while the callback is executed. - const withActivity = useCallback( - async ( id, description, asyncFn ) => { - startActivity( id, description ); - try { - return await asyncFn(); - } finally { - stopActivity( id ); - } - }, - [ startActivity, stopActivity ] - ); - - return { - withActivity, // HOC - isBusy, // Boolean. - activities, // Object. - }; -}; - export const useSandbox = () => { const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks(); @@ -148,5 +116,40 @@ export const useManualConnection = () => { export const useWooSettings = () => { const { wooSettings } = useHooks(); + return wooSettings; }; + +// -- Not using the `useHooks()` data provider -- + +export const useBusyState = () => { + const { startActivity, stopActivity } = useDispatch( STORE_NAME ); + + // Resolved value (object), contains a list of all running actions. + const activities = useSelect( + ( select ) => select( STORE_NAME ).getActivityList(), + [] + ); + + // Derive isBusy state from activities + const isBusy = Object.keys( activities ).length > 0; + + // HOC that starts and stops an activity while the callback is executed. + const withActivity = useCallback( + async ( id, description, asyncFn ) => { + startActivity( id, description ); + try { + return await asyncFn(); + } finally { + stopActivity( id ); + } + }, + [ startActivity, stopActivity ] + ); + + return { + withActivity, // HOC + isBusy, // Boolean. + activities, // Object. + }; +}; From 0502c25ddf684a1d56465838da355c376a0cc6a6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 15:50:58 +0100 Subject: [PATCH 28/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20callback=20?= =?UTF-8?q?methods,=20minor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/hooks/useHandleConnections.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index d242b1de9..10d164cea 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -65,11 +65,11 @@ const useConnectionBase = () => { useDispatch( noticesStore ); return { - handleError: ( res, genericMessage ) => { + handleFailed: ( res, genericMessage ) => { console.error( 'Connection error', res ); createErrorNotice( res?.message ?? genericMessage ); }, - handleSuccess: async () => { + handleCompleted: async () => { createSuccessNotice( MESSAGES.CONNECTED ); // TODO: Contact the plugin to confirm onboarding is completed. @@ -80,14 +80,14 @@ const useConnectionBase = () => { }; const useConnectionAttempt = ( connectFn, errorMessage ) => { - const { handleError, createErrorNotice, handleSuccess } = + const { handleFailed, createErrorNotice, handleCompleted } = useConnectionBase(); return async ( ...args ) => { const res = await connectFn( ...args ); if ( ! res.success || ! res.data ) { - handleError( res, errorMessage ); + handleFailed( res, errorMessage ); return false; } @@ -97,7 +97,7 @@ const useConnectionAttempt = ( connectFn, errorMessage ) => { ); if ( popupClosed ) { - await handleSuccess(); + await handleCompleted(); } return popupClosed; @@ -149,7 +149,7 @@ export const useProductionConnection = () => { }; export const useManualConnection = () => { - const { handleError, handleSuccess, createErrorNotice } = + const { handleFailed, handleCompleted, createErrorNotice } = useConnectionBase(); const { withActivity } = CommonHooks.useBusyState(); const { @@ -179,9 +179,9 @@ export const useManualConnection = () => { const res = await connectViaIdAndSecret(); if ( res.success ) { - await handleSuccess(); + await handleCompleted(); } else { - handleError( res, MESSAGES.MANUAL_ERROR ); + handleFailed( res, MESSAGES.MANUAL_ERROR ); } return res.success; From a76b3471bc4896d69b34789192352dbc9adf114e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:04:54 +0100 Subject: [PATCH 29/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20REST=20contro?= =?UTF-8?q?ller=20more=20reusable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/CommonRestEndpoint.php | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 721c07e11..62ee44ff7 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -123,17 +123,9 @@ class CommonRestEndpoint extends RestEndpoint { $this->field_map ); - $js_woo_settings = $this->sanitize_for_javascript( - $this->settings->get_woo_settings(), - $this->woo_settings_map - ); + $extra_data = $this->add_woo_settings( array() ); - return $this->return_success( - $js_data, - array( - 'wooSettings' => $js_woo_settings, - ) - ); + return $this->return_success( $js_data, $extra_data ); } /** @@ -154,4 +146,21 @@ class CommonRestEndpoint extends RestEndpoint { return $this->get_details(); } + + /** + * Appends the "wooSettings" attribute to the extra_data collection to + * provide WooCommerce store details, like the store country and currency. + * + * @param array $extra_data Initial extra_data collection. + * + * @return array Updated extra_data collection. + */ + protected function add_woo_settings( array $extra_data ) : array { + $extra_data['wooSettings'] = $this->sanitize_for_javascript( + $this->settings->get_woo_settings(), + $this->woo_settings_map + ); + + return $extra_data; + } } From 04629051234e457946bc4d492706ab12333f7f30 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:06:27 +0100 Subject: [PATCH 30/40] =?UTF-8?q?=E2=9C=A8=20Add=20merchant-connection=20d?= =?UTF-8?q?ata=20to=20CommonSettings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/src/Data/CommonSettings.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index b377b66aa..1894255ff 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -59,6 +59,12 @@ class CommonSettings extends AbstractDataModel { 'use_manual_connection' => false, 'client_id' => '', 'client_secret' => '', + + // Details about connected merchant account. + 'merchant_connected' => false, + 'sandbox_merchant' => false, + 'merchant_id' => '', + 'merchant_email' => '', ); } @@ -144,4 +150,58 @@ class CommonSettings extends AbstractDataModel { public function get_woo_settings() : array { return $this->woo_settings; } + + /** + * Setter to update details of the connected merchant account. + * + * Those details cannot be changed individually. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id The merchant ID. + * @param string $merchant_email The merchant's email. + * + * @return void + */ + public function set_merchant_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->data['sandbox_merchant'] = $is_sandbox; + $this->data['merchant_id'] = sanitize_text_field( $merchant_id ); + $this->data['merchant_email'] = sanitize_email( $merchant_email ); + $this->data['merchant_connected'] = true; + } + + /** + * Whether the currently connected merchant is a sandbox account. + * + * @return bool + */ + public function is_sandbox_merchant() : bool { + return $this->data['sandbox_merchant']; + } + + /** + * Whether the merchant successfully logged into their PayPal account. + * + * @return bool + */ + public function is_merchant_connected() : bool { + return $this->data['merchant_connected'] && $this->data['merchant_id'] && $this->data['merchant_email']; + } + + /** + * Gets the currently connected merchant ID. + * + * @return string + */ + public function get_merchant_id() : string { + return $this->data['merchant_id']; + } + + /** + * Gets the currently connected merchant's email. + * + * @return string + */ + public function get_merchant_email() : string { + return $this->data['merchant_email']; + } } From c97464d7e25cbfe831bcfc56d43af67e9dc518ad Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:07:44 +0100 Subject: [PATCH 31/40] =?UTF-8?q?=E2=9C=A8=20Add=20merchant=20details=20to?= =?UTF-8?q?=20=E2=80=9Ccommon=E2=80=9D=20hydration=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/constants.js | 2 +- .../resources/js/data/common/reducer.js | 24 ++++++++--- .../resources/js/data/common/selectors.js | 3 +- .../src/Endpoint/CommonRestEndpoint.php | 40 ++++++++++++++++++- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 4ec4ad20d..60d8512fa 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -8,7 +8,7 @@ export const STORE_NAME = 'wc/paypal/common'; /** - * REST path to hydrate data of this module by loading data from the WP DB.. + * REST path to hydrate data of this module by loading data from the WP DB. * * Used by resolvers. * diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 63c231f85..cca47ac6f 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -17,6 +17,13 @@ const defaultTransient = { activities: new Map(), // Read only values, provided by the server via hydrate. + merchant: { + isConnected: false, + isSandbox: false, + id: '', + email: '', + }, + wooSettings: { storeCountry: '', storeCurrency: '', @@ -75,12 +82,17 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); - if ( payload.wooSettings ) { - newState.wooSettings = { - ...newState.wooSettings, - ...payload.wooSettings, - }; - } + // Populate read-only properties. + [ 'wooSettings', 'merchant' ].forEach( ( key ) => { + if ( ! payload[ key ] ) { + return; + } + + newState[ key ] = Object.freeze( { + ...newState[ key ], + ...payload[ key ], + } ); + } ); return newState; }, diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 17e422b7a..30d513784 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -16,7 +16,8 @@ export const persistentData = ( state ) => { }; export const transientData = ( state ) => { - const { data, wooSettings, ...transientState } = getState( state ); + const { data, merchant, wooSettings, ...transientState } = + getState( state ); return transientState || EMPTY_OBJ; }; diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 62ee44ff7..b6fe29d4a 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -61,7 +61,27 @@ class CommonRestEndpoint extends RestEndpoint { ); /** - * Map the internal flags to JS names. + * Map merchant details to JS names. + * + * @var array + */ + private array $merchant_info_map = array( + 'merchant_connected' => array( + 'js_name' => 'isConnected', + ), + 'sandbox_merchant' => array( + 'js_name' => 'isSandbox', + ), + 'merchant_id' => array( + 'js_name' => 'id', + ), + 'merchant_email' => array( + 'js_name' => 'email', + ), + ); + + /** + * Map woo-settings to JS names. * * @var array */ @@ -124,6 +144,7 @@ class CommonRestEndpoint extends RestEndpoint { ); $extra_data = $this->add_woo_settings( array() ); + $extra_data = $this->add_merchant_info( $extra_data ); return $this->return_success( $js_data, $extra_data ); } @@ -147,6 +168,23 @@ class CommonRestEndpoint extends RestEndpoint { return $this->get_details(); } + /** + * Appends the "merchant" attribute to the extra_data collection, which + * contains details about the merchant's PayPal account, like the merchant ID. + * + * @param array $extra_data Initial extra_data collection. + * + * @return array Updated extra_data collection. + */ + protected function add_merchant_info( array $extra_data ) : array { + $extra_data['merchant'] = $this->sanitize_for_javascript( + $this->settings->to_array(), + $this->merchant_info_map + ); + + return $extra_data; + } + /** * Appends the "wooSettings" attribute to the extra_data collection to * provide WooCommerce store details, like the store country and currency. From b4d1596fd1747ab91483740e7069d653f72b0b08 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:10:33 +0100 Subject: [PATCH 32/40] =?UTF-8?q?=E2=9C=A8=20New=20action=20to=20refresh?= =?UTF-8?q?=20merchant=20data=20from=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 1 + .../resources/js/data/common/actions.js | 9 +++++++ .../resources/js/data/common/constants.js | 9 +++++++ .../resources/js/data/common/controls.js | 22 +++++++++++++++++ .../resources/js/data/common/reducer.js | 5 ++++ .../src/Endpoint/CommonRestEndpoint.php | 24 +++++++++++++++++++ 6 files changed, 70 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index ac2c6db37..34e831508 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -22,4 +22,5 @@ export default { DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN', DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN', + DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index abed4f2d3..7dd13206e 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -180,3 +180,12 @@ export const connectViaIdAndSecret = function* () { useSandbox, }; }; + +/** + * Side effect. Clears and refreshes the merchant data via a REST request. + * + * @return {Action} The action. + */ +export const refreshMerchantData = function* () { + return yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT }; +}; diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 60d8512fa..9499ef069 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -16,6 +16,15 @@ export const STORE_NAME = 'wc/paypal/common'; */ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common'; +/** + * REST path to fetch merchant details from the WordPress DB. + * + * Used by controls. + * + * @type {string} + */ +export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant'; + /** * REST path to persist data of this module to the WP DB. * diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 6005385f9..7845f335f 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -7,12 +7,15 @@ * @file */ +import { dispatch } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; import { + STORE_NAME, REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH, REST_CONNECTION_URL_PATH, + REST_HYDRATE_MERCHANT_PATH, } from './constants'; import ACTION_TYPES from './action-types'; @@ -99,4 +102,23 @@ export const controls = { return result; }, + + async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() { + let result = null; + + try { + result = await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } ); + + if ( result.success && result.merchant ) { + await dispatch( STORE_NAME ).hydrate( result ); + } + } catch ( e ) { + result = { + success: false, + error: e, + }; + } + + return result; + }, }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index cca47ac6f..5e94a2fa4 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -79,6 +79,11 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { return setTransient( state, { activities: newActivities } ); }, + [ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( { + ...state, + merchant: Object.freeze( { ...defaultTransient.merchant } ), + } ), + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index b6fe29d4a..3c0131759 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -130,6 +130,18 @@ class CommonRestEndpoint extends RestEndpoint { ), ) ); + + register_rest_route( + $this->namespace, + "/$this->rest_base/merchant", + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_merchant_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); } /** @@ -168,6 +180,18 @@ class CommonRestEndpoint extends RestEndpoint { return $this->get_details(); } + /** + * Returns only the (read-only) merchant details from the DB. + * + * @return WP_REST_Response Merchant details. + */ + public function get_merchant_details() : WP_REST_Response { + $js_data = array(); // No persistent data. + $extra_data = $this->add_merchant_info( array() ); + + return $this->return_success( $js_data, $extra_data ); + } + /** * Appends the "merchant" attribute to the extra_data collection, which * contains details about the merchant's PayPal account, like the merchant ID. From 82b364a0acd2678dc1359048c9c77bca859e129d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:11:17 +0100 Subject: [PATCH 33/40] =?UTF-8?q?=E2=9C=A8=20Add=20=E2=80=9CCommon?= =?UTF-8?q?=E2=80=9D=20hook=20to=20access=20merchant=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/hooks.js | 26 +++++++++++++++++++ .../resources/js/data/common/selectors.js | 4 +++ 2 files changed, 30 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index de56976e5..8eaaa3924 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -45,6 +45,10 @@ const useHooks = () => { const isSandboxMode = usePersistent( 'useSandbox' ); const isManualConnectionMode = usePersistent( 'useManualConnection' ); + const merchant = useSelect( + ( select ) => select( STORE_NAME ).merchant(), + [] + ); const wooSettings = useSelect( ( select ) => select( STORE_NAME ).wooSettings(), [] @@ -76,6 +80,7 @@ const useHooks = () => { connectToSandbox, connectToProduction, connectViaIdAndSecret, + merchant, wooSettings, }; }; @@ -120,6 +125,27 @@ export const useWooSettings = () => { return wooSettings; }; +export const useMerchantInfo = () => { + const { merchant } = useHooks(); + const { refreshMerchantData } = useDispatch( STORE_NAME ); + + const verifyLoginStatus = useCallback( async () => { + const result = await refreshMerchantData(); + + if ( ! result.success ) { + throw new Error( result?.message || result?.error?.message ); + } + + // Verify if the server state is "connected" and we have a merchant ID. + return merchant?.isConnected && merchant?.id; + }, [ refreshMerchantData, merchant ] ); + + return { + merchant, // Merchant details + verifyLoginStatus, // Callback + }; +}; + // -- Not using the `useHooks()` data provider -- export const useBusyState = () => { diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 30d513784..fde5d8c9e 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -26,6 +26,10 @@ export const getActivityList = ( state ) => { return Object.fromEntries( activities ); }; +export const merchant = ( state ) => { + return getState( state ).merchant || EMPTY_OBJ; +}; + export const wooSettings = ( state ) => { return getState( state ).wooSettings || EMPTY_OBJ; }; From 2b2d5585b1a51dc7fda28ac361ab98ab7c1a9581 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:22:32 +0100 Subject: [PATCH 34/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor:=20Freeze=20re?= =?UTF-8?q?ad-only=20state=20objects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/reducer.js | 16 ++++++++-------- .../resources/js/data/onboarding/reducer.js | 17 ++++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 5e94a2fa4..7d3f5697f 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -12,30 +12,30 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, activities: new Map(), // Read only values, provided by the server via hydrate. - merchant: { + merchant: Object.freeze( { isConnected: false, isSandbox: false, id: '', email: '', - }, + } ), - wooSettings: { + wooSettings: Object.freeze( { storeCountry: '', storeCurrency: '', - }, -}; + } ), +} ); -const defaultPersistent = { +const defaultPersistent = Object.freeze( { useSandbox: false, useManualConnection: false, clientId: '', clientSecret: '', -}; +} ); // Reducer logic. diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index d886a07e8..9dedefc09 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -12,24 +12,24 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, // Read only values, provided by the server. - flags: { + flags: Object.freeze( { canUseCasualSelling: false, canUseVaulting: false, canUseCardPayments: false, - }, -}; + } ), +} ); -const defaultPersistent = { +const defaultPersistent = Object.freeze( { completed: false, step: 0, isCasualSeller: null, // null value will uncheck both options in the UI. areOptionalPaymentMethodsEnabled: null, products: [], -}; +} ); // Reducer logic. @@ -63,7 +63,10 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, { // Flags are not updated by `setPersistent()`. if ( payload.flags ) { - newState.flags = { ...newState.flags, ...payload.flags }; + newState.flags = Object.freeze( { + ...newState.flags, + ...payload.flags, + } ); } return newState; From 620360681cdaa9a067f86c12b6c1819717f53160 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 6 Dec 2024 19:23:22 +0100 Subject: [PATCH 35/40] =?UTF-8?q?=E2=9C=A8=20Integrate=20merchant=20checks?= =?UTF-8?q?=20into=20connections-hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 10d164cea..d34e74f42 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -23,6 +23,10 @@ const MESSAGES = { 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', 'woocommerce-paypal-payments' ), + LOGIN_FAILED: __( + 'Login was not successful. Please try again.', + 'woocommerce-paypal-payments' + ), }; const ACTIVITIES = { @@ -63,6 +67,7 @@ const useConnectionBase = () => { const { setCompleted } = OnboardingHooks.useSteps(); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); + const { verifyLoginStatus } = CommonHooks.useMerchantInfo(); return { handleFailed: ( res, genericMessage ) => { @@ -70,10 +75,18 @@ const useConnectionBase = () => { createErrorNotice( res?.message ?? genericMessage ); }, handleCompleted: async () => { - createSuccessNotice( MESSAGES.CONNECTED ); + try { + const loginSuccessful = await verifyLoginStatus(); - // TODO: Contact the plugin to confirm onboarding is completed. - return setCompleted( true ); + if ( loginSuccessful ) { + createSuccessNotice( MESSAGES.CONNECTED ); + await setCompleted( true ); + } else { + createErrorNotice( MESSAGES.LOGIN_FAILED ); + } + } catch ( error ) { + createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED ); + } }, createErrorNotice, }; From 1826b95c08afcbf7f07a381ca029a90167a6eb7c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Dec 2024 18:16:13 +0100 Subject: [PATCH 36/40] =?UTF-8?q?=E2=9C=A8=20Introduce=20new=20BusyStateWr?= =?UTF-8?q?apper=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reusable-components/_busy-state.scss | 10 ++ .../_settings-toggle-block.scss | 6 - .../ppcp-settings/resources/css/style.scss | 5 +- .../ReusableComponents/BusyStateWrapper.js | 57 ++++++++ .../ReusableComponents/SettingsToggleBlock.js | 13 +- .../Components/AdvancedOptionsForm.js | 131 ++++++++++-------- .../Onboarding/Components/ConnectionButton.js | 22 +-- .../Onboarding/Components/Navigation.js | 12 +- .../Screens/Onboarding/StepWelcome.js | 23 +-- 9 files changed, 176 insertions(+), 103 deletions(-) create mode 100644 modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss new file mode 100644 index 000000000..4254320aa --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss @@ -0,0 +1,10 @@ +.ppcp-r-busy-wrapper { + position: relative; + + &.ppcp--is-loading { + pointer-events: none; + user-select: none; + + --spinner-overlay-color: #fff4; + } +} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss index e5157a862..af4d264ad 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss @@ -31,10 +31,4 @@ &__toggled-content { margin-top: 24px; } - - &.ppcp--is-loading { - pointer-events: none; - - --spinner-overlay-color: #fff4; - } } diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 7ad7aa7a4..70e9b8971 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -3,10 +3,11 @@ #ppcp-settings-container { @import './global'; - @import './components/reusable-components/onboarding-header'; + @import './components/reusable-components/busy-state'; @import './components/reusable-components/button'; - @import './components/reusable-components/settings-toggle-block'; @import './components/reusable-components/separator'; + @import './components/reusable-components/onboarding-header'; + @import './components/reusable-components/settings-toggle-block'; @import './components/reusable-components/payment-method-icons'; @import "./components/reusable-components/payment-method-item"; @import './components/reusable-components/settings-wrapper'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js new file mode 100644 index 000000000..79799bce2 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js @@ -0,0 +1,57 @@ +import { + Children, + isValidElement, + cloneElement, + useMemo, +} from '@wordpress/element'; +import classNames from 'classnames'; + +import { CommonHooks } from '../../data'; +import SpinnerOverlay from './SpinnerOverlay'; + +/** + * Wraps interactive child elements and modifies their behavior based on the global `isBusy` state. + * Allows custom processing of child props via the `onBusy` callback. + * + * @param {Object} props - Component properties. + * @param {Children} props.children - Child components to wrap. + * @param {boolean} props.enabled - Enables or disables the busy-state logic. + * @param {string} props.className - Additional class names for the wrapper. + * @param {Function} props.onBusy - Callback to process child props when busy. + */ +const BusyStateWrapper = ( { + children, + enabled = true, + className = '', + onBusy = () => ( { disabled: true } ), +} ) => { + const { isBusy } = CommonHooks.useBusyState(); + + const markAsBusy = isBusy && enabled; + + const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, { + 'ppcp--is-loading': markAsBusy, + } ); + + const memoizedChildren = useMemo( + () => + Children.map( children, ( child ) => + isValidElement( child ) + ? cloneElement( + child, + markAsBusy ? onBusy( child.props ) : {} + ) + : child + ), + [ children, markAsBusy, onBusy ] + ); + + return ( +
+ { markAsBusy && } + { memoizedChildren } +
+ ); +}; + +export default BusyStateWrapper; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js index d8dda1cfb..4a7cf1a20 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js @@ -1,23 +1,17 @@ import { ToggleControl } from '@wordpress/components'; import { useRef } from '@wordpress/element'; -import SpinnerOverlay from './SpinnerOverlay'; - const SettingsToggleBlock = ( { isToggled, setToggled, - isLoading = false, + disabled = false, ...props } ) => { const toggleRef = useRef( null ); const blockClasses = [ 'ppcp-r-toggle-block' ]; - if ( isLoading ) { - blockClasses.push( 'ppcp--is-loading' ); - } - const handleLabelClick = () => { - if ( ! toggleRef.current || isLoading ) { + if ( ! toggleRef.current || disabled ) { return; } @@ -52,13 +46,12 @@ const SettingsToggleBlock = ( { ref={ toggleRef } checked={ isToggled } onChange={ ( newState ) => setToggled( newState ) } - disabled={ isLoading } + disabled={ disabled } />
{ props.children && isToggled && (
- { isLoading && } { props.children }
) } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index bb2d58209..9b29815f7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -20,6 +20,7 @@ import { } from '../../../../hooks/useHandleConnections'; import ConnectionButton from './ConnectionButton'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; const FORM_ERRORS = { noClientId: __( @@ -89,7 +90,7 @@ const AdvancedOptionsForm = () => { handleConnectViaIdAndSecret( { validation: validateManualConnectionForm, } ), - [ validateManualConnectionForm ] + [ handleConnectViaIdAndSecret, validateManualConnectionForm ] ); useEffect( () => { @@ -124,69 +125,79 @@ const AdvancedOptionsForm = () => { return ( <> - - + - + description={ __( + 'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.', + 'woocommerce-paypal-payments' + ) } + isToggled={ !! isSandboxMode } + setToggled={ setSandboxMode } + > + + + - ( { + disabled: true, + label: props.label + ' ...', + } ) } > - - { clientValid || ( -

- { FORM_ERRORS.invalidClientId } -

- ) } - - -
+ + + { clientValid || ( +

+ { FORM_ERRORS.invalidClientId } +

+ ) } + + +
+ ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js index 0a0ac5bfb..284943e18 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -8,6 +8,7 @@ import { useProductionConnection, useSandboxConnection, } from '../../../../hooks/useHandleConnections'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; const ConnectionButton = ( { title, @@ -15,13 +16,11 @@ const ConnectionButton = ( { variant = 'primary', showIcon = true, } ) => { - const { isBusy } = CommonHooks.useBusyState(); const { handleSandboxConnect } = useSandboxConnection(); const { handleProductionConnect } = useProductionConnection(); const className = classNames( 'ppcp-r-connection-button', { 'sandbox-mode': isSandbox, 'live-mode': ! isSandbox, - 'ppcp--is-loading': isBusy, } ); const handleConnectClick = async () => { @@ -33,15 +32,16 @@ const ConnectionButton = ( { }; return ( - + + + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js index 82bfdb656..a30cb4ce2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { OnboardingHooks } from '../../../../data'; import useIsScrolled from '../../../../hooks/useIsScrolled'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { const { title, isFirst, percentage, showNext, canProceed } = stepDetails; @@ -20,7 +21,10 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { return (
-
+ -
+ { ! isFirst && NextButton( { showNext, isDisabled, onNext, onExit } ) } @@ -42,7 +46,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { return ( -
+ @@ -55,7 +59,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { { __( 'Continue', 'woocommerce-paypal-payments' ) } ) } -
+ ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index 461d95d26..f8abf9ea5 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -9,6 +9,7 @@ import AccordionSection from '../../ReusableComponents/AccordionSection'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; import { CommonHooks } from '../../../data'; +import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper'; const StepWelcome = ( { setStep, currentStep } ) => { const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); @@ -34,16 +35,18 @@ const StepWelcome = ( { setStep, currentStep } ) => { 'woocommerce-paypal-payments' ) }

- + + +
Date: Mon, 9 Dec 2024 18:48:56 +0100 Subject: [PATCH 37/40] =?UTF-8?q?=F0=9F=92=84=20Fix=20multiple=20spinners?= =?UTF-8?q?=20in=20nested=20Busy-wrappers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reusable-components/_spinner-overlay.scss | 1 + .../ReusableComponents/BusyStateWrapper.js | 37 ++++++++++++------- .../Onboarding/Components/Navigation.js | 6 ++- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss index 2f32b118f..8f5e136e9 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss @@ -12,5 +12,6 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); + margin: 0; } } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js index 79799bce2..959b71bfe 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js @@ -3,34 +3,43 @@ import { isValidElement, cloneElement, useMemo, + createContext, + useContext, } from '@wordpress/element'; import classNames from 'classnames'; import { CommonHooks } from '../../data'; import SpinnerOverlay from './SpinnerOverlay'; +// Create context to track the busy state across nested wrappers +const BusyContext = createContext( false ); + /** * Wraps interactive child elements and modifies their behavior based on the global `isBusy` state. * Allows custom processing of child props via the `onBusy` callback. * - * @param {Object} props - Component properties. - * @param {Children} props.children - Child components to wrap. - * @param {boolean} props.enabled - Enables or disables the busy-state logic. - * @param {string} props.className - Additional class names for the wrapper. - * @param {Function} props.onBusy - Callback to process child props when busy. + * @param {Object} props - Component properties. + * @param {Children} props.children - Child components to wrap. + * @param {boolean} props.enabled - Enables or disables the busy-state logic. + * @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state. + * @param {string} props.className - Additional class names for the wrapper. + * @param {Function} props.onBusy - Callback to process child props when busy. */ const BusyStateWrapper = ( { children, enabled = true, + busySpinner = true, className = '', onBusy = () => ( { disabled: true } ), } ) => { const { isBusy } = CommonHooks.useBusyState(); + const hasBusyParent = useContext( BusyContext ); - const markAsBusy = isBusy && enabled; + const isBusyComponent = isBusy && enabled; + const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent; const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, { - 'ppcp--is-loading': markAsBusy, + 'ppcp--is-loading': isBusyComponent, } ); const memoizedChildren = useMemo( @@ -39,18 +48,20 @@ const BusyStateWrapper = ( { isValidElement( child ) ? cloneElement( child, - markAsBusy ? onBusy( child.props ) : {} + isBusyComponent ? onBusy( child.props ) : {} ) : child ), - [ children, markAsBusy, onBusy ] + [ children, isBusyComponent, onBusy ] ); return ( -
- { markAsBusy && } - { memoizedChildren } -
+ +
+ { showSpinner && } + { memoizedChildren } +
+
); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js index a30cb4ce2..3c12e1206 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js @@ -23,6 +23,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
From 36de426260b703b9c005ca9e64971109c90fedab Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Dec 2024 19:24:34 +0100 Subject: [PATCH 38/40] =?UTF-8?q?=F0=9F=92=84=20Add=20a=20real=20loading?= =?UTF-8?q?=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/css/components/_app.scss | 22 +++++++++++++ .../ppcp-settings/resources/css/style.scss | 1 + .../ReusableComponents/SpinnerOverlay.js | 7 +++- .../js/Components/Screens/Settings.js | 33 ++++++++++++++----- 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 modules/ppcp-settings/resources/css/components/_app.scss diff --git a/modules/ppcp-settings/resources/css/components/_app.scss b/modules/ppcp-settings/resources/css/components/_app.scss new file mode 100644 index 000000000..7e69cbada --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/_app.scss @@ -0,0 +1,22 @@ +/** + * Global app-level styles + */ + +.ppcp-r-app.loading { + height: 400px; + width: 400px; + position: absolute; + left: 50%; + transform: translate(-50%, 0); + text-align: center; + + .ppcp-r-spinner-overlay { + display: flex; + flex-direction: column; + justify-content: center; + } + + .ppcp-r-spinner-overlay__message { + transform: translate(0, 32px) + } +} diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 70e9b8971..56fe55a62 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -23,6 +23,7 @@ @import './components/screens/onboarding'; @import './components/screens/settings'; @import './components/screens/overview/tab-styling'; + @import './components/app'; } @import './components/reusable-components/payment-method-modal'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js index dec732a3e..b4165b5ba 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js @@ -1,8 +1,13 @@ import { Spinner } from '@wordpress/components'; -const SpinnerOverlay = () => { +const SpinnerOverlay = ( { message = '' } ) => { return (
+ { message && ( + + { message } + + ) }
); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index e0634343c..009d1d46b 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -1,20 +1,37 @@ +import { useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; + import { OnboardingHooks } from '../../data'; +import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay'; + import Onboarding from './Onboarding/Onboarding'; import SettingsScreen from './SettingsScreen'; const Settings = () => { const onboardingProgress = OnboardingHooks.useSteps(); - if ( ! onboardingProgress.isReady ) { - // TODO: Use better loading state indicator. - return
Loading...
; - } + const wrapperClass = classNames( 'ppcp-r-app', { + loading: ! onboardingProgress.isReady, + } ); - if ( ! onboardingProgress.completed ) { - return ; - } + const Content = useMemo( () => { + if ( ! onboardingProgress.isReady ) { + return ( + + ); + } - return ; + if ( ! onboardingProgress.completed ) { + return ; + } + + return ; + }, [ onboardingProgress ] ); + + return
{ Content }
; }; export default Settings; From 3d49241c5ec81aeb2e042b96d8aaccefeb62f982 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 10 Dec 2024 13:58:00 +0100 Subject: [PATCH 39/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Major=20button-styli?= =?UTF-8?q?ng=20refactoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/css/_variables.scss | 17 +++ .../reusable-components/_button.scss | 108 +++++++++++++----- .../screens/onboarding/_step-welcome.scss | 6 - .../Components/AdvancedOptionsForm.js | 7 +- .../Onboarding/Components/ConnectionButton.js | 5 +- 5 files changed, 107 insertions(+), 36 deletions(-) diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss index 73b656f3a..10f427ea9 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -10,6 +10,7 @@ $color-gray-500: #BBBBBB; $color-gray-400: #CCCCCC; $color-gray-300: #EBEBEB; $color-gray-200: #E0E0E0; +$color-gray-100: #F0F0F0; $color-gray: #646970; $color-text-tertiary: #505050; $color-text-text: #070707; @@ -27,6 +28,8 @@ $max-width-settings: 938px; $card-vertical-gap: 48px; +/* define custom theming options */ + :root { --ppcp-color-app-bg: #{$color-white}; } @@ -37,4 +40,18 @@ $card-vertical-gap: 48px; --max-width-onboarding-content: #{$max-width-onboarding-content}; --max-container-width: var(--max-width-settings); + + --color-black: #{$color-black}; + --color-white: #{$color-white}; + --color-blueberry: #{$color-blueberry}; + --color-gray-900: #{$color-gray-900}; + --color-gray-800: #{$color-gray-800}; + --color-gray-700: #{$color-gray-700}; + --color-gray-600: #{$color-gray-600}; + --color-gray-500: #{$color-gray-500}; + --color-gray-400: #{$color-gray-400}; + --color-gray-300: #{$color-gray-300}; + --color-gray-200: #{$color-gray-200}; + --color-gray-100: #{$color-gray-100}; + --color-gradient-dark: #{$color-gradient-dark}; } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 4174e6a23..558ccaaf2 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -1,48 +1,102 @@ +%button-style-default { + background-color: var(--button-background); + color: var(--button-color); + box-shadow: inset 0 0 0 1px var(--button-border-color); +} + +%button-style-hover { + background-color: var(--button-hover-background); + color: var(--button-hover-color); + box-shadow: inset 0 0 0 1px var(--button-hover-border-color); +} + +%button-style-disabled { + background-color: var(--button-disabled-background); + color: var(--button-disabled-color); + box-shadow: inset 0 0 0 1px var(--button-disabled-border-color); +} + +%button-shape-pill { + border-radius: 50px; + padding: 15px 32px; + height: auto; +} + button.components-button, a.components-button { - &.is-primary, &.is-secondary { - &:not(:disabled) { - background-color: $color-black; - } + /* default theme */ + --button-color: var(--color-gray-900); + --button-background: transparent; + --button-border-color: transparent; - &:disabled { - color: $color-gray-700; - } + --button-hover-color: var(--button-color); + --button-hover-background: var(--button-background); + --button-hover-border-color: var(--button-border-color); - border-radius: 50px; - padding: 15px 32px; - height: auto; + --button-disabled-color: var(--color-gray-500); + --button-disabled-background: transparent; + --button-disabled-border-color: transparent; + + /* style the button template */ + + &:not(:disabled) { + @extend %button-style-default; + } + + &:hover { + @extend %button-style-hover; + } + + &:disabled { + @extend %button-style-disabled; + } + + /* + ---------------------------------------------- + Customize variants using the theming variables + */ + + &.is-primary, + &.is-secondary { + @extend %button-shape-pill; } &.is-primary { @include font(14, 18, 900); - &:not(:disabled) { - background-color: $color-blueberry; - color: $color-white; - } + --button-color: #{$color-white}; + --button-background: #{$color-blueberry}; + + --button-disabled-color: #{$color-gray-100}; + --button-disabled-background: #{$color-gray-500}; } - &.is-secondary:not(:disabled) { - border-color: $color-blueberry; - background-color: $color-white; - color: $color-blueberry; + &.is-secondary { + --button-color: #{$color-blueberry}; + --button-background: #{$color-white}; + --button-border-color: #{$color-blueberry}; - &:hover { - background-color: $color-white; - background: none; - } + --button-disabled-color: #{$color-gray-600}; + --button-disabled-background: #{$color-gray-100}; + --button-disabled-border-color: #{$color-gray-400}; } &.is-tertiary { - color: $color-blueberry; - - &:hover { - color: $color-gradient-dark; - } + --button-color: #{$color-blueberry}; + --button-hover-color: #{$color-gradient-dark}; &:focus:not(:disabled) { border: none; box-shadow: none; } } + + &.small-button { + @include small-button; + } +} + +.ppcp--is-loading { + button.components-button, a.components-button { + @extend %button-style-disabled; + } } diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss index 47af9c99a..450251b6f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss @@ -20,12 +20,6 @@ margin: 0 0 24px 0; } - .ppcp-r-toggle-block__toggled-content > button{ - @include small-button; - color: $color-white; - border: none; - } - .client-id-error { color: #cc1818; margin: -16px 0 24px; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 9b29815f7..6aabd15fd 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -145,6 +145,7 @@ const AdvancedOptionsForm = () => { ) } showIcon={ false } variant="secondary" + className="small-button" isSandbox={ true /* This button always connects to sandbox */ } @@ -190,7 +191,11 @@ const AdvancedOptionsForm = () => { onChange={ setClientSecret } type="password" /> -