@@ -66,7 +66,7 @@ const AcdcFlow = ( {
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
- 'Offer installment payment options and get paid upfront - at no extra cost to you.
',
+ 'Offer installment payment options and get paid upfront.
',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/us/business/paypal-business-fees'
@@ -123,7 +123,7 @@ const AcdcFlow = ( {
);
}
- if ( isPayLater && storeCountry === 'uk' ) {
+ if ( isPayLater && storeCountry === 'UK' ) {
return (
@@ -256,7 +256,7 @@ const AcdcFlow = ( {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
- 'Offer installment payment options and get paid upfront - at no extra cost to you.
Learn more ',
+ 'Offer installment payment options and get paid upfront.
Learn more ',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js
index 6c984cfc1..99abfc4f2 100644
--- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js
+++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js
@@ -6,7 +6,7 @@ import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
- if ( isPayLater && storeCountry === 'us' ) {
+ if ( isPayLater && storeCountry === 'US' ) {
return (
@@ -60,7 +60,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
- 'Offer installment payment options and get paid upfront - at no extra cost to you.
Learn more ',
+ 'Offer installment payment options and get paid upfront.
Learn more ',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@@ -158,7 +158,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
- 'Offer installment payment options and get paid upfront - at no extra cost to you.
Learn more ',
+ 'Offer installment payment options and get paid upfront.
Learn more ',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js
index b3b60fee1..eabb5b3db 100644
--- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js
+++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js
@@ -1,7 +1,8 @@
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import AcdcFlow from './AcdcFlow';
import BcdcFlow from './BcdcFlow';
-import { Button } from '@wordpress/components';
+import { countryPriceInfo } from '../../../utils/countryPriceInfo';
+import { pricesBasedDescription } from './pricesBasedDescription';
const WelcomeDocs = ( {
useAcdc,
@@ -10,15 +11,6 @@ const WelcomeDocs = ( {
storeCountry,
storeCurrency,
} ) => {
- const pricesBasedDescription = sprintf(
- // translators: %s: Link to PayPal REST application guide
- __(
- '
1 Prices based on domestic transactions as of October 25th, 2024.
Click here for full pricing details.',
- 'woocommerce-paypal-payments'
- ),
- 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
- );
-
return (
@@ -41,10 +33,14 @@ const WelcomeDocs = ( {
storeCurrency={ storeCurrency }
/>
) }
-
+ { storeCountry in countryPriceInfo && (
+
+ ) }
);
};
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/pricesBasedDescription.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/pricesBasedDescription.js
new file mode 100644
index 000000000..c4d3eb983
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/pricesBasedDescription.js
@@ -0,0 +1,10 @@
+import { __, sprintf } from '@wordpress/i18n';
+
+export const pricesBasedDescription = sprintf(
+ // translators: %s: Link to PayPal REST application guide
+ __(
+ '
1 Prices based on domestic transactions as of October 25th, 2024.
Click here for full pricing details.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
+);
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 e7891955b..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
@@ -1,96 +1,118 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
-import { useRef, useMemo } from '@wordpress/element';
-import { useDispatch } from '@wordpress/data';
-import { store as noticesStore } from '@wordpress/notices';
+import {
+ useRef,
+ useState,
+ useEffect,
+ useMemo,
+ useCallback,
+} from '@wordpress/element';
+
+import classNames from 'classnames';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import Separator from '../../../ReusableComponents/Separator';
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
import { CommonHooks } from '../../../../data';
-import { openPopup } from '../../../../utils/window';
+import {
+ useSandboxConnection,
+ useManualConnection,
+} from '../../../../hooks/useHandleConnections';
+
+import ConnectionButton from './ConnectionButton';
+import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
+
+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 );
-const AdvancedOptionsForm = ( { setCompleted } ) => {
const { isBusy } = CommonHooks.useBusyState();
- const { isSandboxMode, setSandboxMode, connectViaSandbox } =
- CommonHooks.useSandbox();
+ const { isSandboxMode, setSandboxMode } = useSandboxConnection();
const {
+ handleConnectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
- connectViaIdAndSecret,
- } = CommonHooks.useManualConnection();
+ } = useManualConnection();
- const { createSuccessNotice, createErrorNotice } =
- useDispatch( noticesStore );
const refClientId = useRef( null );
const refClientSecret = useRef( null );
- const isValidClientId = useMemo( () => {
- return /^A[\w-]{79}$/.test( clientId );
- }, [ clientId ] );
+ const validateManualConnectionForm = useCallback( () => {
+ const checks = [
+ {
+ ref: refClientId,
+ valid: () => clientId,
+ errorMessage: FORM_ERRORS.noClientId,
+ },
+ {
+ ref: refClientId,
+ valid: () => clientValid,
+ errorMessage: FORM_ERRORS.invalidClientId,
+ },
+ {
+ ref: refClientSecret,
+ valid: () => clientSecret && secretValid,
+ errorMessage: FORM_ERRORS.noClientSecret,
+ },
+ ];
- const isFormValid = useMemo( () => {
- return isValidClientId && clientId && clientSecret;
- }, [ isValidClientId, clientId, clientSecret ] );
+ for ( const { ref, valid, errorMessage } of checks ) {
+ if ( valid() ) {
+ continue;
+ }
- const handleServerError = ( res, genericMessage ) => {
- console.error( 'Connection error', res );
- createErrorNotice( res?.message ?? genericMessage );
- };
-
- const handleServerSuccess = () => {
- createSuccessNotice(
- __( 'Connected to PayPal', 'woocommerce-paypal-payments' )
- );
- setCompleted( true );
- };
-
- 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;
+ ref?.current?.focus();
+ throw new Error( errorMessage );
}
+ }, [ clientId, clientSecret, clientValid, secretValid ] );
- const connectionUrl = res.data;
- const popup = openPopup( connectionUrl );
+ const handleManualConnect = useCallback(
+ () =>
+ handleConnectViaIdAndSecret( {
+ validation: validateManualConnectionForm,
+ } ),
+ [ handleConnectViaIdAndSecret, validateManualConnectionForm ]
+ );
- if ( ! popup ) {
- createErrorNotice(
- __(
- 'Popup blocked. Please allow popups for this site to connect to PayPal.',
- 'woocommerce-paypal-payments'
- )
- );
- }
- };
+ useEffect( () => {
+ setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) );
+ setSecretValid( clientSecret && clientSecret.length > 0 );
+ }, [ clientId, clientSecret ] );
- const handleManualConnect = async () => {
- const res = await connectViaIdAndSecret();
+ const clientIdLabel = useMemo(
+ () =>
+ isSandboxMode
+ ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' )
+ : __( 'Live Client ID', 'woocommerce-paypal-payments' ),
+ [ isSandboxMode ]
+ );
- 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'
- )
- );
- }
- };
+ const secretKeyLabel = useMemo(
+ () =>
+ 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
@@ -103,88 +125,84 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
return (
<>
-
-
- { __( 'Connect Account', 'woocommerce-paypal-payments' ) }
-
-
-
-
-
- { clientId && ! isValidClientId && (
-
- { __(
- 'Please enter a valid Client ID',
+
+
+
- ) }
-
-
+
+
+
+ ( {
+ disabled: true,
+ label: props.label + ' ...',
+ } ) }
+ >
+
- { __( 'Connect Account', 'woocommerce-paypal-payments' ) }
-
-
+
+ { clientValid || (
+
+ { FORM_ERRORS.invalidClientId }
+
+ ) }
+
+
+ { __(
+ 'Connect Account',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+
>
);
};
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..ad6a7dcef
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js
@@ -0,0 +1,49 @@
+import { Button } from '@wordpress/components';
+
+import classNames from 'classnames';
+
+import { CommonHooks } from '../../../../data';
+import { openSignup } from '../../../ReusableComponents/Icons';
+import {
+ useProductionConnection,
+ useSandboxConnection,
+} from '../../../../hooks/useHandleConnections';
+import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
+
+const ConnectionButton = ( {
+ title,
+ isSandbox = false,
+ variant = 'primary',
+ showIcon = true,
+ className = '',
+} ) => {
+ const { handleSandboxConnect } = useSandboxConnection();
+ const { handleProductionConnect } = useProductionConnection();
+ const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
+ 'sandbox-mode': isSandbox,
+ 'live-mode': ! isSandbox,
+ } );
+
+ const handleConnectClick = async () => {
+ if ( isSandbox ) {
+ await handleSandboxConnect();
+ } else {
+ await handleProductionConnect();
+ }
+ };
+
+ return (
+
+
+ { title }
+
+
+ );
+};
+
+export default ConnectionButton;
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 5a1da25cb..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
@@ -1,130 +1,81 @@
-import { Button } from '@wordpress/components';
+import { Button, Icon } from '@wordpress/components';
+import { chevronLeft } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
+import classNames from 'classnames';
+
import { OnboardingHooks } from '../../../../data';
-import data from '../../../../utils/data';
+import useIsScrolled from '../../../../hooks/useIsScrolled';
+import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
-const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => {
- const isLastStep = () => currentStep + 1 === stepperOrder.length;
- const isFistStep = () => currentStep === 0;
- const navigateBy = ( stepDirection ) => {
- let newStep = currentStep + stepDirection;
+const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
+ const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
+ const { isScrolled } = useIsScrolled();
- if ( isNaN( newStep ) || newStep < 0 ) {
- console.warn( 'Invalid next step:', newStep );
- newStep = 0;
- }
-
- if ( newStep >= stepperOrder.length ) {
- setCompleted( true );
- } else {
- setStep( newStep );
- }
- };
-
- const { products } = OnboardingHooks.useProducts();
- const { isCasualSeller } = OnboardingHooks.useBusiness();
-
- let navigationTitle = '';
- let disabled = false;
-
- switch ( currentStep ) {
- case 1:
- navigationTitle = __(
- 'Set up store type',
- 'woocommerce-paypal-payments'
- );
- disabled = isCasualSeller === null;
- break;
- case 2:
- navigationTitle = __(
- 'Select product types',
- 'woocommerce-paypal-payments'
- );
- disabled = products.length < 1;
- break;
- case 3:
- navigationTitle = __(
- 'Choose checkout options',
- 'woocommerce-paypal-payments'
- );
- case 4:
- navigationTitle = __(
- 'Connect your PayPal account',
- 'woocommerce-paypal-payments'
- );
- break;
- default:
- navigationTitle = __(
- 'PayPal Payments',
- 'woocommerce-paypal-payments'
- );
- }
+ const state = OnboardingHooks.useNavigationState();
+ const isDisabled = ! canProceed( state );
+ const className = classNames( 'ppcp-r-navigation-container', {
+ 'is-scrolled': isScrolled,
+ } );
return (
-
+
-
-
{ data().getImage( 'icon-arrow-left.svg' ) }
- { ! isFistStep() ? (
-
navigateBy( -1 ) }
- >
- { navigationTitle }
-
- ) : (
-
- { navigationTitle }
-
- ) }
-
- { ! isFistStep() && (
-
- ) }
-
+
+
+
+
+ { title }
+
+
+
+ { ! isFirst &&
+ NextButton( { showNext, isDisabled, onNext, onExit } ) }
+
);
};
+const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
+ return (
+
+
+ { __( 'Save and exit', 'woocommerce-paypal-payments' ) }
+
+ { showNext && (
+
+ { __( 'Continue', 'woocommerce-paypal-payments' ) }
+
+ ) }
+
+ );
+};
+
+const ProgressBar = ( { percent } ) => {
+ percent = Math.min( Math.max( percent, 0 ), 100 );
+
+ return (
+
+ );
+};
+
export default Navigation;
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 30cd52ffe..225527053 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js
@@ -1,40 +1,35 @@
import Container from '../../ReusableComponents/Container';
import { OnboardingHooks } from '../../../data';
-import { getSteps } from './availableSteps';
+
+import { getSteps, getCurrentStep } from './availableSteps';
import Navigation from './Components/Navigation';
-const getCurrentStep = ( requestedStep, steps ) => {
- const isValidStep = ( step ) =>
- typeof step === 'number' &&
- Number.isInteger( step ) &&
- step >= 0 &&
- step < steps.length;
-
- const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0;
- return steps[ safeCurrentStep ];
-};
-
const Onboarding = () => {
- const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
- const steps = getSteps( flags );
+ const { step, setStep, flags } = OnboardingHooks.useSteps();
+ const Steps = getSteps( flags );
+ const currentStep = getCurrentStep( step, Steps );
- const CurrentStepComponent = getCurrentStep( step, steps );
+ const handleNext = () => setStep( currentStep.nextStep );
+ const handlePrev = () => setStep( currentStep.prevStep );
+ const handleExit = () => {
+ window.location.href = window.ppcpSettings.wcPaymentsTabUrl;
+ };
return (
<>
+
-
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js
index a223686ff..dd9a1dcd5 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js
@@ -10,9 +10,8 @@ const BUSINESS_RADIO_GROUP_NAME = 'business';
const StepBusiness = ( {} ) => {
const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness();
- const handleSellerTypeChange = ( value ) => {
+ const handleSellerTypeChange = ( value ) =>
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value );
- };
const getCurrentValue = () => {
if ( isCasualSeller === null ) {
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..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,28 +1,9 @@
import { __ } from '@wordpress/i18n';
-import { Button, Icon } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
+import ConnectionButton from './Components/ConnectionButton';
-const StepCompleteSetup = ( { setCompleted } ) => {
- const ButtonIcon = () => (
-
(
-
-
-
- ) }
- />
- );
-
+const StepCompleteSetup = () => {
return (
{
/>
- {
- setCompleted( true );
- } }
- >
- { __(
+
+ />
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js
index a9d2f6b9e..e94f176f7 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js
@@ -1,10 +1,12 @@
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox';
-import { OnboardingHooks } from '../../../data';
+import { CommonHooks, OnboardingHooks } from '../../../data';
import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
+import { pricesBasedDescription } from '../../ReusableComponents/WelcomeDocs/pricesBasedDescription';
+import { countryPriceInfo } from '../../../utils/countryPriceInfo';
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
@@ -13,14 +15,8 @@ const StepPaymentMethods = ( {} ) => {
areOptionalPaymentMethodsEnabled,
setAreOptionalPaymentMethodsEnabled,
} = OnboardingHooks.useOptionalPaymentMethods();
- const pricesBasedDescription = sprintf(
- // translators: %s: Link to PayPal REST application guide
- __(
- '1 Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.',
- 'woocommerce-paypal-payments'
- ),
- 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
- );
+
+ const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
return (
@@ -42,8 +38,8 @@ const StepPaymentMethods = ( {} ) => {
useAcdc={ true }
isFastlane={ true }
isPayLater={ true }
- storeCountry={ 'us' }
- storeCurrency={ 'usd' }
+ storeCountry={ storeCountry }
+ storeCurrency={ storeCurrency }
/>
}
name={ OPM_RADIO_GROUP_NAME }
@@ -64,12 +60,14 @@ const StepPaymentMethods = ( {} ) => {
type="radio"
>
-
+ { storeCountry in countryPriceInfo && (
+
+ ) }
);
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js
index cbd642327..ee99f4acf 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js
@@ -9,6 +9,7 @@ const PRODUCTS_CHECKBOX_GROUP_NAME = 'products';
const StepProducts = () => {
const { products, setProducts } = OnboardingHooks.useProducts();
+ const { canUseSubscriptions } = OnboardingHooks.useFlags();
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 c94c84935..f8abf9ea5 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
@@ -8,8 +8,12 @@ import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs';
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();
-const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
return (
{
'woocommerce-paypal-payments'
) }
- setStep( currentStep + 1 ) }
- >
- { __(
- 'Activate PayPal Payments',
- 'woocommerce-paypal-payments'
- ) }
-
+
+ setStep( currentStep + 1 ) }
+ >
+ { __(
+ 'Activate PayPal Payments',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
{
className="onboarding-advanced-options"
id="advanced-options"
>
-
+
);
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js
index 7e8ea1556..e14e66231 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js
@@ -1,21 +1,86 @@
+import { __ } from '@wordpress/i18n';
+
import StepWelcome from './StepWelcome';
import StepBusiness from './StepBusiness';
import StepProducts from './StepProducts';
import StepPaymentMethods from './StepPaymentMethods';
import StepCompleteSetup from './StepCompleteSetup';
+/**
+ * List of all onboarding screens that are available.
+ *
+ * The screens are displayed in the order in which they appear in this array
+ *
+ * @type {[{id, StepComponent, title}]}
+ */
+const ALL_STEPS = [
+ {
+ id: 'welcome',
+ title: __( 'PayPal Payments', 'woocommerce-paypal-payments' ),
+ StepComponent: StepWelcome,
+ canProceed: () => true,
+ },
+ {
+ id: 'business',
+ title: __( 'Set up store type', 'woocommerce-paypal-payments' ),
+ StepComponent: StepBusiness,
+ canProceed: ( { business } ) => business.isCasualSeller !== null,
+ },
+ {
+ id: 'products',
+ title: __( 'Select product types', 'woocommerce-paypal-payments' ),
+ StepComponent: StepProducts,
+ canProceed: ( { products } ) => products.products.length > 0,
+ },
+ {
+ id: 'methods',
+ title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ),
+ StepComponent: StepPaymentMethods,
+ canProceed: () => true,
+ },
+ {
+ id: 'complete',
+ title: __(
+ 'Connect your PayPal account',
+ 'woocommerce-paypal-payments'
+ ),
+ StepComponent: StepCompleteSetup,
+ canProceed: () => true,
+ },
+];
+
export const getSteps = ( flags ) => {
- const allSteps = [
- StepWelcome,
- StepBusiness,
- StepProducts,
- StepPaymentMethods,
- StepCompleteSetup,
- ];
+ const steps = flags.canUseCasualSelling
+ ? ALL_STEPS
+ : ALL_STEPS.filter( ( step ) => step.id !== 'business' );
- if ( ! flags.canUseCasualSelling ) {
- return allSteps.filter( ( step ) => step !== StepBusiness );
- }
+ const totalStepsCount = steps.length;
- return allSteps;
+ return steps.map( ( step, index ) => ( {
+ ...step,
+ isFirst: index === 0,
+ isLast: index === totalStepsCount - 1,
+ showNext: index < totalStepsCount - 1,
+ percentage: 100 * ( index / ( totalStepsCount - 1 ) ),
+ nextStep: index < totalStepsCount - 1 ? index + 1 : index,
+ prevStep: index > 0 ? index - 1 : 0,
+ } ) );
+};
+
+/**
+ * Returns the screen-details of the current step, based on the numeric step-index.
+ *
+ * @param {number} requestedStep Index of the screen to display.
+ * @param {[]} steps List of all available steps (see `getSteps()`)
+ * @return {{id, StepComponent, title}} The requested screen details, or the first welcome screen.
+ */
+export const getCurrentStep = ( requestedStep, steps ) => {
+ const isValidStep = ( step ) =>
+ typeof step === 'number' &&
+ Number.isInteger( step ) &&
+ step >= 0 &&
+ step < steps.length;
+
+ const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0;
+ return steps[ safeCurrentStep ];
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js
index 4bac75c9a..fe3e64218 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js
@@ -1,18 +1,11 @@
-import SettingsCard from '../../ReusableComponents/SettingsCard';
import { __ } from '@wordpress/i18n';
-import {
- PayPalCheckbox,
-} from '../../ReusableComponents/Fields';
import { useState } from '@wordpress/element';
-import data from '../../../utils/data';
import { Button } from '@wordpress/components';
-import TitleBadge, {
- TITLE_BADGE_NEGATIVE,
- TITLE_BADGE_POSITIVE,
-} from '../../ReusableComponents/TitleBadge';
-import ConnectionInfo, {
- connectionStatusDataDefault,
-} from '../../ReusableComponents/ConnectionInfo';
+import SettingsCard from '../../ReusableComponents/SettingsCard';
+import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock';
+import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock';
+import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
+import data from '../../../utils/data';
const TabOverview = () => {
const [ todos, setTodos ] = useState( [] );
@@ -32,198 +25,52 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
>
-
- { todosData.map( ( todo ) => (
-
- ) ) }
-
+
) }
+
-
-
- { featuresDefault.map( ( feature ) => {
- return (
-
- );
- } ) }
-
-
- );
-};
-
-const ConnectionStatus = ( { connectionData } ) => {
- return (
-
-
-
-
- { __( 'Connection', 'woocommerce-paypal-payments' ) }
-
- { connectionData.connectionStatus ? (
-
- ) : (
-
- ) }
-
-
-
- { __(
- 'PayPal Account Details',
- 'woocommerce-paypal-payments'
- ) }
-
-
-
- { connectionData.connectionStatus && (
-
- ) }
-
- );
-};
-
-const FeaturesRefresh = () => {
- return (
-
-
-
- { __( 'Features', 'woocommerce-paypal-payments' ) }
-
-
- { __(
- 'After making changes to your PayPal account, click Refresh to update your store features.',
- 'woocommerce-paypal-payments'
- ) }
-
-
-
- { data().getImage( 'icon-refresh.svg' ) }
- { __( 'Refresh', 'woocommerce-paypal-payments' ) }
-
-
- );
-};
-
-const TodoItem = ( props ) => {
- return (
-
-
-
- removeTodo(
- props.value,
- props.todosData,
- props.changeTodos
- )
- }
- >
- { data().getImage( 'icon-close.svg' ) }
-
-
- );
-};
-
-const FeatureItem = ( { feature } ) => {
- const printNotes = () => {
- if ( ! feature?.notes ) {
- return null;
- }
-
- if ( Array.isArray( feature.notes ) && feature.notes.length === 0 ) {
- return null;
- }
-
- return (
- <>
-
-
-
- { feature.notes.map( ( note, index ) => {
- return { note } ;
- } ) }
-
- >
- );
- };
-
- return (
-
-
- { feature.title }
- { feature?.featureStatus && (
-
- ) }
-
-
- { feature.description }
- { printNotes() }
-
-
- { feature.buttons.map( ( button ) => {
- return (
-
- { button.text }
+ className="ppcp-r-tab-overview-features"
+ title={ __( 'Features', 'woocommerce-paypal-payments' ) }
+ description={
+
+
{ __( 'Enable additional features…' ) }
+
{ __( 'Click Refresh…' ) }
+
+ { data().getImage( 'icon-refresh.svg' ) }
+ { __( 'Refresh', 'woocommerce-paypal-payments' ) }
- );
- } ) }
-
+
+ }
+ contentItems={ featuresDefault.map( ( feature ) => (
+
+ ) ) }
+ />
);
};
-const removeTodo = ( todoValue, todosData, changeTodos ) => {
- changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) );
-};
-
const todosDataDefault = [
{
value: 'paypal_later_messaging',
@@ -269,12 +116,12 @@ const featuresDefault = [
),
buttons: [
{
- type: 'primary',
+ type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
- type: 'secondary',
+ type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
@@ -293,12 +140,12 @@ const featuresDefault = [
),
buttons: [
{
- type: 'primary',
+ type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
- type: 'secondary',
+ type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
@@ -316,12 +163,12 @@ const featuresDefault = [
),
buttons: [
{
- type: 'primary',
+ type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
url: '#',
},
{
- type: 'secondary',
+ type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
@@ -337,12 +184,12 @@ const featuresDefault = [
featureStatus: true,
buttons: [
{
- type: 'primary',
+ type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
- type: 'secondary',
+ type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
@@ -360,7 +207,7 @@ const featuresDefault = [
),
buttons: [
{
- type: 'primary',
+ type: 'secondary',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
@@ -368,7 +215,7 @@ const featuresDefault = [
url: '#',
},
{
- type: 'secondary',
+ type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
@@ -383,16 +230,17 @@ const featuresDefault = [
),
buttons: [
{
- type: 'primary',
+ type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
- type: 'secondary',
+ type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
];
+
export default TabOverview;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js
index 453a34426..c1576da10 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js
@@ -1,23 +1,34 @@
-import SettingsCard from '../../ReusableComponents/SettingsCard';
import { __ } from '@wordpress/i18n';
-import PaymentMethodItem from '../../ReusableComponents/PaymentMethodItem';
+import { useMemo } from '@wordpress/element';
+
+import SettingsCard from '../../ReusableComponents/SettingsCard';
+import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock';
+import { CommonHooks } from '../../../data';
import ModalPayPal from './Modals/ModalPayPal';
import ModalFastlane from './Modals/ModalFastlane';
import ModalAcdc from './Modals/ModalAcdc';
const TabPaymentMethods = () => {
- const renderPaymentMethods = ( data ) => {
- return (
-
- { data.map( ( paymentMethod ) => (
-
- ) ) }
-
- );
- };
+ const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
+
+ const filteredPaymentMethods = useMemo( () => {
+ const contextProps = { storeCountry, storeCurrency };
+
+ return {
+ payPalCheckout: filterPaymentMethods(
+ paymentMethodsPayPalCheckout,
+ contextProps
+ ),
+ onlineCardPayments: filterPaymentMethods(
+ paymentMethodsOnlineCardPayments,
+ contextProps
+ ),
+ alternative: filterPaymentMethods(
+ paymentMethodsAlternative,
+ contextProps
+ ),
+ };
+ }, [ storeCountry, storeCurrency ] );
return (
@@ -28,8 +39,11 @@ const TabPaymentMethods = () => {
'woocommerce-paypal-payments'
) }
icon="icon-checkout-standard.svg"
+ contentContainer={ false }
>
- { renderPaymentMethods( paymentMethodsPayPalCheckoutDefault ) }
+
{
'woocommerce-paypal-payments'
) }
icon="icon-checkout-online-methods.svg"
+ contentContainer={ false }
>
- { renderPaymentMethods(
- paymentMethodsOnlineCardPaymentsDefault
- ) }
+
{
'woocommerce-paypal-payments'
) }
icon="icon-checkout-alternative-methods.svg"
+ contentContainer={ false }
>
- { renderPaymentMethods( paymentMethodsAlternativeDefault ) }
+
);
};
-const paymentMethodsPayPalCheckoutDefault = [
+function filterPaymentMethods( paymentMethods, contextProps ) {
+ return paymentMethods.filter( ( method ) =>
+ typeof method.condition === 'function'
+ ? method.condition( contextProps )
+ : true
+ );
+}
+
+const paymentMethodsPayPalCheckout = [
{
id: 'paypal',
title: __( 'PayPal', 'woocommerce-paypal-payments' ),
@@ -106,7 +132,7 @@ const paymentMethodsPayPalCheckoutDefault = [
},
];
-const paymentMethodsOnlineCardPaymentsDefault = [
+const paymentMethodsOnlineCardPayments = [
{
id: 'advanced_credit_and_debit_card_payments',
title: __(
@@ -124,7 +150,7 @@ const paymentMethodsOnlineCardPaymentsDefault = [
id: 'fastlane',
title: __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ),
description: __(
- 'Tap into the scale and trust of PayPal’s customer network to recognize shoppers and make guest checkout more seamless than ever.',
+ "Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.",
'woocommerce-paypal-payments'
),
icon: 'payment-method-fastlane',
@@ -150,7 +176,7 @@ const paymentMethodsOnlineCardPaymentsDefault = [
},
];
-const paymentMethodsAlternativeDefault = [
+const paymentMethodsAlternative = [
{
id: 'bancontact',
title: __( 'Bancontact', 'woocommerce-paypal-payments' ),
@@ -173,7 +199,7 @@ const paymentMethodsAlternativeDefault = [
id: 'eps',
title: __( 'eps', 'woocommerce-paypal-payments' ),
description: __(
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum porttitor massa ex, eget luctus lacus iaculis at.',
+ 'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-eps',
@@ -182,11 +208,69 @@ const paymentMethodsAlternativeDefault = [
id: 'blik',
title: __( 'BLIK', 'woocommerce-paypal-payments' ),
description: __(
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum porttitor massa ex, eget luctus lacus iaculis at.',
+ 'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-blik',
},
+ {
+ id: 'mybank',
+ title: __( 'MyBank', 'woocommerce-paypal-payments' ),
+ description: __(
+ 'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.',
+ 'woocommerce-paypal-payments'
+ ),
+ icon: 'payment-method-mybank',
+ },
+ {
+ id: 'przelewy24',
+ title: __( 'Przelewy24', 'woocommerce-paypal-payments' ),
+ description: __(
+ 'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.',
+ 'woocommerce-paypal-payments'
+ ),
+ icon: 'payment-method-przelewy24',
+ },
+ {
+ id: 'trustly',
+ title: __( 'Trustly', 'woocommerce-paypal-payments' ),
+ description: __(
+ 'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.',
+ 'woocommerce-paypal-payments'
+ ),
+ icon: 'payment-method-trustly',
+ },
+ {
+ id: 'multibanco',
+ title: __( 'Multibanco', 'woocommerce-paypal-payments' ),
+ description: __(
+ 'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.',
+ 'woocommerce-paypal-payments'
+ ),
+ icon: 'payment-method-multibanco',
+ },
+ {
+ id: 'pui',
+ title: __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ),
+ description: __(
+ 'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.',
+ 'woocommerce-paypal-payments'
+ ),
+ icon: 'payment-method-ratepay',
+ condition: ( { storeCountry, storeCurrency } ) =>
+ storeCountry === 'DE' && storeCurrency === 'EUR',
+ },
+ {
+ id: 'oxxo',
+ title: __( 'OXXO', 'woocommerce-paypal-payments' ),
+ description: __(
+ 'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 800–925–0304',
+ 'woocommerce-paypal-payments'
+ ),
+ icon: 'payment-method-oxxo',
+ condition: ( { storeCountry, storeCurrency } ) =>
+ storeCountry === 'MX' && storeCurrency === 'MXN',
+ },
];
export default TabPaymentMethods;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js
index 5505fac8d..1b471fe1e 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js
@@ -1,4 +1,5 @@
import { useState } from '@wordpress/element';
+import ConnectionStatus from './TabSettingsElements/ConnectionStatus';
import CommonSettings from './TabSettingsElements/CommonSettings';
import ExpertSettings from './TabSettingsElements/ExpertSettings';
@@ -30,6 +31,7 @@ const TabSettings = () => {
return (
<>
+
{
return (
-
+ components={ [
+ () => (
+ <>
+
+
+ { __(
+ 'Order Intent',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+ { __(
+ 'Choose between immediate capture or authorization-only, with manual capture in the Order section.',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+ >
+ ),
+ () => (
+ <>
+
-
-
+
+ >
+ ),
+ ] }
+ />
);
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js
index 84bea84c8..a377f0217 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js
@@ -1,49 +1,8 @@
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_TYPE_SELECT,
- SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT,
-} from '../../../../ReusableComponents/SettingsBlock';
import { __ } from '@wordpress/i18n';
-
-const OtherSettings = ( { settings, updateFormValue } ) => {
- return (
-
-
-
- );
-};
+import {
+ AccordionSettingsBlock,
+ SelectSettingsBlock,
+} from '../../../../ReusableComponents/SettingsBlocks';
const creditCardExamples = [
{ value: '', label: __( 'Select', 'woocommerce-paypal-payments' ) },
@@ -63,4 +22,38 @@ const creditCardExamples = [
},
];
+const OtherSettings = ( { settings, updateFormValue } ) => {
+ return (
+
+
+
+ );
+};
+
export default OtherSettings;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js
index 7b01ea203..f8d68881e 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js
@@ -1,33 +1,28 @@
import { __ } from '@wordpress/i18n';
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_TYPE_EMPTY,
- SETTINGS_BLOCK_TYPE_INPUT,
- SETTINGS_BLOCK_TYPE_SELECT,
- SETTINGS_BLOCK_TYPE_TOGGLE,
- SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT,
-} from '../../../../ReusableComponents/SettingsBlock';
-import { PayPalRdbWithContent } from '../../../../ReusableComponents/Fields';
+import {
+ AccordionSettingsBlock,
+ RadioSettingsBlock,
+ ToggleSettingsBlock,
+ InputSettingsBlock,
+ SelectSettingsBlock,
+} from '../../../../ReusableComponents/SettingsBlocks';
const PaypalSettings = ( { updateFormValue, settings } ) => {
return (
-
- {
'Due to differences in how WooCommerce and PayPal calculates taxes, some transactions may fail due to a rounding error. This settings determines the fallback behavior.',
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
- actionProps={ {
- type: SETTINGS_BLOCK_TYPE_EMPTY,
- } }
- >
-
-
- updateFormValue(
- 'subtotalMismatchFallback',
- newValue
- )
- }
- label={ __(
+ options={ [
+ {
+ id: 'add_a_correction',
+ value: 'add_a_correction',
+ label: __(
'Add a correction',
'woocommerce-paypal-payments'
- ) }
- description={ __(
+ ),
+ description: __(
'Adds an additional line item with the missing amount.',
'woocommerce-paypal-payments'
- ) }
- />
-
- updateFormValue(
- 'subtotalMismatchFallback',
- newValue
- )
- }
- label={ __(
+ ),
+ },
+ {
+ id: 'do_not_send_line_items',
+ value: 'do_not_send_line_items',
+ label: __(
'Do not send line items',
'woocommerce-paypal-payments'
- ) }
- description={ __(
- 'Resubmit the transaction without line item details',
+ ),
+ description: __(
+ 'Resubmit the transaction without line item details.',
'woocommerce-paypal-payments'
- ) }
- />
-
-
+ ),
+ },
+ ] }
+ actionProps={ {
+ name: 'paypal_settings_mismatch',
+ key: 'subtotalMismatchFallback',
+ currentValue: settings.subtotalMismatchFallback,
+ callback: updateFormValue,
+ } }
+ />
- {
'If enabled, PayPal will not allow buyers to use funding sources that take additional time to complete, such as eChecks.',
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
actionProps={ {
- type: SETTINGS_BLOCK_TYPE_TOGGLE,
value: settings.savePaypalAndVenmo,
callback: updateFormValue,
key: 'savePaypalAndVenmo',
} }
/>
- {
'woocommerce-paypal-payments'
),
} }
+ order={ [ 'title', 'description', 'action' ] }
/>
- {
'woocommerce-paypal-payments'
),
} }
+ order={ [ 'title', 'description', 'action' ] }
/>
- {
'Determine which experience a buyer sees when they click the PayPal button.',
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
- actionProps={ {
- type: SETTINGS_BLOCK_TYPE_EMPTY,
- } }
- >
-
-
- updateFormValue( 'paypalLandingPage', newValue )
- }
- label={ __(
+ options={ [
+ {
+ id: 'no_preference',
+ value: 'no_reference',
+ label: __(
'No preference',
'woocommerce-paypal-payments'
- ) }
- description={ __(
+ ),
+ description: __(
'Shows the buyer the PayPal login for a recognized PayPal buyer.',
'woocommerce-paypal-payments'
- ) }
- />
-
- updateFormValue( 'paypalLandingPage', newValue )
- }
- label={ __(
+ ),
+ },
+ {
+ id: 'login_page',
+ value: 'login_page',
+ label: __(
'Login page',
'woocommerce-paypal-payments'
- ) }
- description={ __(
+ ),
+ description: __(
'Always show the buyer the PayPal login screen.',
'woocommerce-paypal-payments'
- ) }
- />
-
- updateFormValue( 'paypalLandingPage', newValue )
- }
- label={ __(
+ ),
+ },
+ {
+ id: 'guest_checkout_page',
+ value: 'guest_checkout_page',
+ label: __(
'Guest checkout page',
'woocommerce-paypal-payments'
- ) }
- description={ __(
+ ),
+ description: __(
'Always show the buyer the guest checkout fields first.',
'woocommerce-paypal-payments'
- ) }
- />
-
-
-
+
+ {
'woocommerce-paypal-payments'
),
} }
+ order={ [ 'title', 'description', 'action' ] }
/>
-
+
);
};
@@ -235,4 +200,5 @@ const languagesExample = [
{ value: 'es', label: 'Spanish' },
{ value: 'it', label: 'Italian' },
];
+
export default PaypalSettings;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js
index f47711098..93a4a7d0d 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js
@@ -1,43 +1,40 @@
import { __, sprintf } from '@wordpress/i18n';
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_STYLING_TYPE_TERTIARY,
- SETTINGS_BLOCK_TYPE_EMPTY,
- SETTINGS_BLOCK_TYPE_TOGGLE,
- SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT,
-} from '../../../../ReusableComponents/SettingsBlock';
+import { Button } from '@wordpress/components';
+import {
+ AccordionSettingsBlock,
+ ButtonSettingsBlock,
+ RadioSettingsBlock,
+ ToggleSettingsBlock,
+ InputSettingsBlock,
+} from '../../../../ReusableComponents/SettingsBlocks';
import TitleBadge, {
TITLE_BADGE_POSITIVE,
} from '../../../../ReusableComponents/TitleBadge';
import ConnectionInfo, {
connectionStatusDataDefault,
} from '../../../../ReusableComponents/ConnectionInfo';
-import { Button, TextControl } from '@wordpress/components';
-import { PayPalRdbWithContent } from '../../../../ReusableComponents/Fields';
const Sandbox = ( { settings, updateFormValue } ) => {
const className = settings.sandboxConnected
? 'ppcp-r-settings-block--sandbox-connected'
: 'ppcp-r-settings-block--sandbox-disconnected';
+
return (
- Note : No real payments/money movement occur in Sandbox mode. Do not ship orders made in this mode.",
+ "Test your site in PayPal's Sandbox environment.",
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY }
actionProps={ {
- type: SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT,
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
} }
>
{ settings.sandboxConnected && (
- {
) }
/>
}
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
- actionProps={ {
- type: SETTINGS_BLOCK_TYPE_EMPTY,
- callback: updateFormValue,
- key: 'sandboxAccountCredentials',
- value: settings.sandboxAccountCredentials,
- } }
>
- {
) }
-
+
) }
{ ! settings.sandboxConnected && (
- {
'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.',
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
- actionProps={ {
- type: SETTINGS_BLOCK_TYPE_EMPTY,
- callback: updateFormValue,
- key: 'sandboxAccountCredentials',
- value: settings.sandboxAccountCredentials,
- } }
- >
-
-
- updateFormValue( 'sandboxMode', newValue )
- }
- label={ __(
+ options={ [
+ {
+ id: 'sandbox_mode',
+ value: 'sandbox_mode',
+ label: __(
'Sandbox Mode',
'woocommerce-paypal-payments'
- ) }
- description={ __(
+ ),
+ 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'
- ) }
- >
-
- updateFormValue( 'sandboxConnected', true )
- }
- >
- { __(
- 'Connect Sandbox Account',
- 'woocommerce-paypal-payments'
- ) }
-
-
-
- updateFormValue( 'sandboxMode', newValue )
- }
- label={ __(
+ ),
+ additionalContent: (
+
+ updateFormValue(
+ 'sandboxConnected',
+ true
+ )
+ }
+ >
+ { __(
+ 'Connect Sandbox Account',
+ 'woocommerce-paypal-payments'
+ ) }
+
+ ),
+ },
+ {
+ id: 'manual_connect',
+ value: 'manual_connect',
+ label: __(
'Manual Connect',
'woocommerce-paypal-payments'
- ) }
- description={ sprintf(
- // translators: %s: Link to creating PayPal REST application
+ ),
+ description: sprintf(
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, click here .',
'woocommerce-paypal-payments'
),
'#'
- ) }
- >
-
-
-
- { __(
- 'Connect Account',
- 'woocommerce-paypal-payments'
- ) }
-
-
-
-
+ ),
+ additionalContent: (
+ <>
+
+
+
+ updateFormValue(
+ 'sandboxManuallyConnected',
+ true
+ )
+ } // Add this handler if needed
+ >
+ { __(
+ 'Connect Account',
+ 'woocommerce-paypal-payments'
+ ) }
+
+ >
+ ),
+ },
+ ] }
+ actionProps={ {
+ name: 'paypal_connect_sandbox',
+ key: 'sandboxMode',
+ currentValue: settings.sandboxMode,
+ callback: updateFormValue,
+ } }
+ />
) }
-
+
);
};
+
export default Sandbox;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js
index 8fdefbb9c..f10907c4c 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js
@@ -1,75 +1,83 @@
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_TYPE_EMPTY,
- SETTINGS_BLOCK_TYPE_TOGGLE,
-} from '../../../../ReusableComponents/SettingsBlock';
import { __, sprintf } from '@wordpress/i18n';
+import {
+ SettingsBlock,
+ ToggleSettingsBlock,
+ Title,
+ Description,
+} from '../../../../ReusableComponents/SettingsBlocks';
+import { Header } from '../../../../ReusableComponents/SettingsBlocks/SettingsBlockElements';
const SavePaymentMethods = ( { updateFormValue, settings } ) => {
return (
future payments[MISSING_LINK] and subscriptions[MISSING_LINK] , simplifying checkout and enabling recurring transactions.',
- 'woocommerce-paypal-payments'
+ components={ [
+ () => (
+ <>
+
+
+ { __(
+ 'Save payment methods',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+ { __(
+ 'Securely store customers’ payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+ >
),
- '#',
- '#'
- ) }
- type={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY }
- style={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY }
- actionProps={ {
- type: SETTINGS_BLOCK_TYPE_EMPTY,
- } }
- >
- This will disable all Pay Later features and Alternative Payment Methods on your site.',
- 'woocommerce-paypal-payments'
- ),
- 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later',
- 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
- ) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
- value={ settings.savePaypalAndVenmo }
- actionProps={ {
- type: SETTINGS_BLOCK_TYPE_TOGGLE,
- value: settings.savePaypalAndVenmo,
- callback: updateFormValue,
- key: 'savePaypalAndVenmo',
- } }
- />
-
-
+ () => (
+ This will disable all Pay Later features and Alternative Payment Methods on your site.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later',
+ 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
+ ),
+ } }
+ />
+ }
+ actionProps={ {
+ value: settings.savePaypalAndVenmo,
+ callback: updateFormValue,
+ key: 'savePaypalAndVenmo',
+ } }
+ />
+ ),
+ () => (
+
+ ),
+ ] }
+ />
);
};
+
export default SavePaymentMethods;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js
index fdc4ad28f..f53a360c7 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js
@@ -1,65 +1,73 @@
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_TYPE_BUTTON,
- SETTINGS_BLOCK_TYPE_EMPTY,
- SETTINGS_BLOCK_TYPE_TOGGLE,
- SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT,
-} from '../../../../ReusableComponents/SettingsBlock';
import { __ } from '@wordpress/i18n';
+import {
+ Header,
+ Title,
+ Description,
+ AccordionSettingsBlock,
+ ToggleSettingsBlock,
+ ButtonSettingsBlock,
+} from '../../../../ReusableComponents/SettingsBlocks';
+import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
const Troubleshooting = ( { updateFormValue, settings } ) => {
return (
-
-
-
-
+ components={ [
+ () => (
+ <>
+
+
+ >
+ ),
+ ] }
+ />
- {
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
actionProps={ {
- type: SETTINGS_BLOCK_TYPE_BUTTON,
buttonType: 'secondary',
callback: () =>
console.log(
@@ -83,14 +89,13 @@ const Troubleshooting = ( { updateFormValue, settings } ) => {
),
} }
/>
-
console.log(
@@ -103,7 +108,7 @@ const Troubleshooting = ( { updateFormValue, settings } ) => {
),
} }
/>
-
+
);
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js
index 0066a5fcd..dad5da83e 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js
@@ -1,10 +1,8 @@
import { __ } from '@wordpress/i18n';
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_TYPE_INPUT,
- SETTINGS_BLOCK_TYPE_TOGGLE,
-} from '../../../ReusableComponents/SettingsBlock';
+import {
+ InputSettingsBlock,
+ ToggleSettingsBlock,
+} from '../../../ReusableComponents/SettingsBlocks';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import OrderIntent from './Blocks/OrderIntent';
import SavePaymentMethods from './Blocks/SavePaymentMethods';
@@ -13,19 +11,21 @@ const CommonSettings = ( { updateFormValue, settings } ) => {
return (
- {
),
} }
/>
+
+
- {
'Let PayPal customers skip the Order Review page by selecting shipping options directly within PayPal.',
'woocommerce-paypal-payments'
) }
- style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY }
actionProps={ {
- type: SETTINGS_BLOCK_TYPE_TOGGLE,
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js
new file mode 100644
index 000000000..b1018d44c
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js
@@ -0,0 +1,53 @@
+import { __ } from '@wordpress/i18n';
+import SettingsCard from '../../../ReusableComponents/SettingsCard';
+import ConnectionInfo, {
+ connectionStatusDataDefault,
+} from '../../../ReusableComponents/ConnectionInfo';
+import TitleBadge, {
+ TITLE_BADGE_NEGATIVE,
+ TITLE_BADGE_POSITIVE,
+} from '../../../ReusableComponents/TitleBadge';
+const ConnectionStatus = () => {
+ return (
+
+
+
+
+ { connectionStatusDataDefault.connectionStatus ? (
+
+ ) : (
+
+ ) }
+
+
+ { connectionStatusDataDefault.connectionStatus && (
+
+ ) }
+
+
+ );
+};
+export default ConnectionStatus;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js
index 78e8bcd20..56a8e63c6 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js
@@ -1,11 +1,9 @@
import { __ } from '@wordpress/i18n';
-import SettingsBlock, {
- SETTINGS_BLOCK_STYLING_TYPE_PRIMARY,
- SETTINGS_BLOCK_STYLING_TYPE_SECONDARY,
- SETTINGS_BLOCK_TYPE_SELECT,
- SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT,
-} from '../../../ReusableComponents/SettingsBlock';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
+import {
+ Content,
+ ContentWrapper,
+} from '../../../ReusableComponents/SettingsBlocks';
import Sandbox from './Blocks/Sandbox';
import Troubleshooting from './Blocks/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
@@ -25,25 +23,37 @@ const ExpertSettings = ( { updateFormValue, settings } ) => {
callback: updateFormValue,
key: 'payNowExperience',
} }
+ contentContainer={ false }
>
-
+
+
+
+
-
+
+
+
-
-
+
+
+
+
+
+
+
+
);
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js
index e0634343c..bc79b34b3 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js
@@ -1,20 +1,51 @@
+import { useEffect, 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...
;
- }
+ // Disable the "Changes you made might not be saved" browser warning.
+ useEffect( () => {
+ const suppressBeforeUnload = ( event ) => {
+ event.stopImmediatePropagation();
+ return undefined;
+ };
- if ( ! onboardingProgress.completed ) {
- return ;
- }
+ window.addEventListener( 'beforeunload', suppressBeforeUnload );
- return ;
+ return () => {
+ window.removeEventListener( 'beforeunload', suppressBeforeUnload );
+ };
+ }, [] );
+
+ const wrapperClass = classNames( 'ppcp-r-app', {
+ loading: ! onboardingProgress.isReady,
+ } );
+
+ const Content = useMemo( () => {
+ if ( ! onboardingProgress.isReady ) {
+ return (
+
+ );
+ }
+
+ if ( ! onboardingProgress.completed ) {
+ return ;
+ }
+
+ return ;
+ }, [ onboardingProgress ] );
+
+ return { Content }
;
};
export default Settings;
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..34e831508 100644
--- a/modules/ppcp-settings/resources/js/data/common/action-types.js
+++ b/modules/ppcp-settings/resources/js/data/common/action-types.js
@@ -10,10 +10,17 @@ export default {
// Persistent data.
SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
+ 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',
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 619aaca5f..7dd13206e 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.
*
@@ -52,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 },
} );
/**
@@ -118,17 +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 connectViaSandbox = function* () {
- yield setIsBusy( true );
+export const connectToSandbox = function* () {
+ return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
+};
- const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
- yield setIsBusy( false );
-
- return result;
+/**
+ * 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 = [] ) {
+ return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products };
};
/**
@@ -140,15 +173,19 @@ export const connectViaIdAndSecret = function* () {
const { clientId, clientSecret, useSandbox } =
yield select( STORE_NAME ).persistentData();
- yield setIsBusy( true );
-
- const result = yield {
+ return yield {
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
clientId,
clientSecret,
useSandbox,
};
- yield setIsBusy( false );
-
- return result;
+};
+
+/**
+ * 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 c7ea9b4c1..9499ef069 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.
*
@@ -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.
*
@@ -36,11 +45,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..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_SANDBOX_CONNECTION_PATH,
+ REST_CONNECTION_URL_PATH,
+ REST_HYDRATE_MERCHANT_PATH,
} from './constants';
import ACTION_TYPES from './action-types';
@@ -34,11 +37,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 ) {
@@ -77,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/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js
index 8be3857b0..8eaaa3924 100644
--- a/modules/ppcp-settings/resources/js/data/common/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/common/hooks.js
@@ -31,7 +31,8 @@ const useHooks = () => {
setManualConnectionMode,
setClientId,
setClientSecret,
- connectViaSandbox,
+ connectToSandbox,
+ connectToProduction,
connectViaIdAndSecret,
} = useDispatch( STORE_NAME );
@@ -44,6 +45,15 @@ 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(),
+ []
+ );
+
const savePersistent = async ( setter, value ) => {
setter( value );
await persist();
@@ -67,25 +77,24 @@ const useHooks = () => {
setClientSecret: ( value ) => {
return savePersistent( setClientSecret, value );
},
- connectViaSandbox,
+ connectToSandbox,
+ connectToProduction,
connectViaIdAndSecret,
- };
-};
-
-export const useBusyState = () => {
- const { setIsBusy } = useDispatch( STORE_NAME );
- const isBusy = useTransient( 'isBusy' );
-
- return {
- isBusy,
- setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ),
+ merchant,
+ wooSettings,
};
};
export const useSandbox = () => {
- const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks();
+ const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks();
- return { isSandboxMode, setSandboxMode, connectViaSandbox };
+ return { isSandboxMode, setSandboxMode, connectToSandbox };
+};
+
+export const useProduction = () => {
+ const { connectToProduction } = useHooks();
+
+ return { connectToProduction };
};
export const useManualConnection = () => {
@@ -109,3 +118,64 @@ export const useManualConnection = () => {
connectViaIdAndSecret,
};
};
+
+export const useWooSettings = () => {
+ const { wooSettings } = useHooks();
+
+ 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 = () => {
+ 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.
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js
index 3f822468b..7d3f5697f 100644
--- a/modules/ppcp-settings/resources/js/data/common/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/common/reducer.js
@@ -12,17 +12,30 @@ import ACTION_TYPES from './action-types';
// Store structure.
-const defaultTransient = {
+const defaultTransient = Object.freeze( {
isReady: false,
- isBusy: false,
-};
+ activities: new Map(),
-const defaultPersistent = {
+ // Read only values, provided by the server via hydrate.
+ merchant: Object.freeze( {
+ isConnected: false,
+ isSandbox: false,
+ id: '',
+ email: '',
+ } ),
+
+ wooSettings: Object.freeze( {
+ storeCountry: '',
+ storeCurrency: '',
+ } ),
+} );
+
+const defaultPersistent = Object.freeze( {
useSandbox: false,
useManualConnection: false,
clientId: '',
clientSecret: '',
-};
+} );
// Reducer logic.
@@ -38,8 +51,56 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) =>
setPersistent( state, action ),
- [ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
- setPersistent( state, payload.data ),
+ [ 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.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.DO_REFRESH_MERCHANT ]: ( state ) => ( {
+ ...state,
+ merchant: Object.freeze( { ...defaultTransient.merchant } ),
+ } ),
+
+ [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
+ const newState = setPersistent( state, payload.data );
+
+ // Populate read-only properties.
+ [ 'wooSettings', 'merchant' ].forEach( ( key ) => {
+ if ( ! payload[ key ] ) {
+ return;
+ }
+
+ newState[ key ] = Object.freeze( {
+ ...newState[ key ],
+ ...payload[ key ],
+ } );
+ } );
+
+ return newState;
+ },
} );
export default commonReducer;
diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js
index 14334fcf3..fde5d8c9e 100644
--- a/modules/ppcp-settings/resources/js/data/common/selectors.js
+++ b/modules/ppcp-settings/resources/js/data/common/selectors.js
@@ -16,6 +16,20 @@ export const persistentData = ( state ) => {
};
export const transientData = ( state ) => {
- const { data, ...transientState } = getState( state );
+ const { data, merchant, wooSettings, ...transientState } =
+ getState( state );
return transientState || EMPTY_OBJ;
};
+
+export const getActivityList = ( state ) => {
+ const { activities = new Map() } = 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;
+};
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 = () => {
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
index 4ae5bd947..e8582821e 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,
};
};
@@ -113,3 +118,24 @@ export const useSteps = () => {
return { flags, isReady, step, setStep, completed, setCompleted };
};
+
+export const useNavigationState = () => {
+ const products = useProducts();
+ const business = useBusiness();
+
+ return {
+ products,
+ business,
+ };
+};
+
+export const useDetermineProducts = () => {
+ const { determineProducts } = useHooks();
+
+ return determineProducts;
+};
+
+export const useFlags = () => {
+ const { flags } = useHooks();
+ return flags;
+};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
index 176d4875d..2b16e2416 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
@@ -12,24 +12,25 @@ 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,
- },
-};
+ canUseSubscriptions: false,
+ } ),
+} );
-const defaultPersistent = {
+const defaultPersistent = Object.freeze( {
completed: false,
step: 0,
isCasualSeller: null, // null value will uncheck both options in the UI.
- areOptionalPaymentMethodsEnabled: true,
+ areOptionalPaymentMethodsEnabled: null,
products: [],
-};
+} );
// Reducer logic.
@@ -45,15 +46,28 @@ 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 );
// 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;
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
index d4d57ef4d..2e0953437 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
@@ -23,3 +23,50 @@ 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 = [];
+
+ 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;
+};
diff --git a/modules/ppcp-settings/resources/js/hooks/useAccordionState.js b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js
new file mode 100644
index 000000000..f54018262
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js
@@ -0,0 +1,39 @@
+import { useEffect, useState } from '@wordpress/element';
+
+const checkIfCurrentTab = ( id ) => {
+ return id && window.location.hash === `#${ id }`;
+};
+
+const determineInitialState = ( id, initiallyOpen ) => {
+ if ( initiallyOpen !== null ) {
+ return initiallyOpen;
+ }
+ return checkIfCurrentTab( id );
+};
+
+export function useAccordionState( { id = '', initiallyOpen = null } ) {
+ const [ isOpen, setIsOpen ] = useState(
+ determineInitialState( id, initiallyOpen )
+ );
+
+ useEffect( () => {
+ const handleHashChange = () => {
+ if ( checkIfCurrentTab( id ) ) {
+ setIsOpen( true );
+ }
+ };
+
+ window.addEventListener( 'hashchange', handleHashChange );
+ return () => {
+ window.removeEventListener( 'hashchange', handleHashChange );
+ };
+ }, [ id ] );
+
+ const toggleOpen = ( ev ) => {
+ setIsOpen( ! isOpen );
+ ev?.preventDefault();
+ return false;
+ };
+
+ return { isOpen, toggleOpen };
+}
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..d34e74f42
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js
@@ -0,0 +1,214 @@
+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 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'
+ ),
+ LOGIN_FAILED: __(
+ 'Login was not successful. Please try again.',
+ 'woocommerce-paypal-payments'
+ ),
+};
+
+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 );
+
+ 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 = () => {
+ const { setCompleted } = OnboardingHooks.useSteps();
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+ const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
+
+ return {
+ handleFailed: ( res, genericMessage ) => {
+ console.error( 'Connection error', res );
+ createErrorNotice( res?.message ?? genericMessage );
+ },
+ handleCompleted: async () => {
+ try {
+ const loginSuccessful = await verifyLoginStatus();
+
+ if ( loginSuccessful ) {
+ createSuccessNotice( MESSAGES.CONNECTED );
+ await setCompleted( true );
+ } else {
+ createErrorNotice( MESSAGES.LOGIN_FAILED );
+ }
+ } catch ( error ) {
+ createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED );
+ }
+ },
+ createErrorNotice,
+ };
+};
+
+const useConnectionAttempt = ( connectFn, errorMessage ) => {
+ const { handleFailed, createErrorNotice, handleCompleted } =
+ useConnectionBase();
+
+ return async ( ...args ) => {
+ const res = await connectFn( ...args );
+
+ if ( ! res.success || ! res.data ) {
+ handleFailed( res, errorMessage );
+ return false;
+ }
+
+ const popupClosed = await handlePopupWithCompletion(
+ res.data,
+ createErrorNotice
+ );
+
+ if ( popupClosed ) {
+ await handleCompleted();
+ }
+
+ return popupClosed;
+ };
+};
+
+export const useSandboxConnection = () => {
+ const { connectToSandbox, isSandboxMode, setSandboxMode } =
+ CommonHooks.useSandbox();
+ 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,
+ setSandboxMode,
+ };
+};
+
+export const useProductionConnection = () => {
+ const { connectToProduction } = CommonHooks.useProduction();
+ const { withActivity } = CommonHooks.useBusyState();
+ const products = OnboardingHooks.useDetermineProducts();
+ 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 { handleFailed, handleCompleted, createErrorNotice } =
+ useConnectionBase();
+ const { withActivity } = CommonHooks.useBusyState();
+ const {
+ connectViaIdAndSecret,
+ isManualConnectionMode,
+ setManualConnectionMode,
+ clientId,
+ setClientId,
+ clientSecret,
+ setClientSecret,
+ } = CommonHooks.useManualConnection();
+
+ const handleConnectViaIdAndSecret = async ( { validation } = {} ) => {
+ 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 handleCompleted();
+ } else {
+ handleFailed( res, MESSAGES.MANUAL_ERROR );
+ }
+
+ return res.success;
+ }
+ );
+ };
+
+ return {
+ handleConnectViaIdAndSecret,
+ isManualConnectionMode,
+ setManualConnectionMode,
+ clientId,
+ setClientId,
+ clientSecret,
+ setClientSecret,
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js b/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js
new file mode 100644
index 000000000..2b40aa3e9
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js
@@ -0,0 +1,44 @@
+/**
+ * Taken from WooCommerce core:
+ * https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/client/hooks/useIsScrolled.js
+ */
+
+import { useEffect, useRef, useState } from '@wordpress/element';
+
+const isAtBottom = () =>
+ window.innerHeight + window.scrollY >= document.body.scrollHeight;
+
+const useIsScrolled = () => {
+ const [ isScrolled, setIsScrolled ] = useState( false );
+ const [ atBottom, setAtBottom ] = useState( isAtBottom() );
+ const rafHandle = useRef( null );
+ useEffect( () => {
+ const updateIsScrolled = () => {
+ setIsScrolled( window.pageYOffset > 20 );
+ setAtBottom( isAtBottom() );
+ };
+
+ const scrollListener = () => {
+ rafHandle.current =
+ window.requestAnimationFrame( updateIsScrolled );
+ };
+
+ window.addEventListener( 'scroll', scrollListener );
+
+ window.addEventListener( 'resize', scrollListener );
+
+ return () => {
+ window.removeEventListener( 'scroll', scrollListener );
+ window.removeEventListener( 'resize', scrollListener );
+ window.cancelAnimationFrame( rafHandle.current );
+ };
+ }, [] );
+
+ return {
+ isScrolled,
+ atBottom,
+ atTop: ! isScrolled,
+ };
+};
+
+export default useIsScrolled;
diff --git a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js
index 193efd584..34bfc8e7f 100644
--- a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js
+++ b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js
@@ -1,5 +1,5 @@
export const countryPriceInfo = {
- us: {
+ US: {
currencySymbol: '$',
fixedFee: 0.49,
checkout: 3.49,
@@ -9,7 +9,7 @@ export const countryPriceInfo = {
fastlane: 2.59,
standardCardFields: 2.99,
},
- uk: {
+ UK: {
currencySymbol: '£',
fixedFee: 0.3,
checkout: 2.9,
@@ -18,7 +18,7 @@ export const countryPriceInfo = {
apm: 1.2,
standardCardFields: 1.2,
},
- ca: {
+ CA: {
currencySymbol: '$',
fixedFee: 0.3,
checkout: 2.9,
@@ -27,7 +27,7 @@ export const countryPriceInfo = {
apm: 2.9,
standardCardFields: 2.9,
},
- au: {
+ AU: {
currencySymbol: '$',
fixedFee: 0.3,
checkout: 2.6,
@@ -36,7 +36,7 @@ export const countryPriceInfo = {
apm: 2.6,
standardCardFields: 2.6,
},
- fr: {
+ FR: {
currencySymbol: '€',
fixedFee: 0.35,
checkout: 2.9,
@@ -45,7 +45,7 @@ export const countryPriceInfo = {
apm: 1.2,
standardCardFields: 1.2,
},
- it: {
+ IT: {
currencySymbol: '€',
fixedFee: 0.35,
checkout: 3.4,
@@ -54,7 +54,7 @@ export const countryPriceInfo = {
apm: 1.2,
standardCardFields: 1.2,
},
- de: {
+ DE: {
currencySymbol: '€',
fixedFee: 0.39,
checkout: 2.99,
@@ -63,7 +63,7 @@ export const countryPriceInfo = {
apm: 2.99,
standardCardFields: 2.99,
},
- es: {
+ ES: {
currencySymbol: '€',
fixedFee: 0.35,
checkout: 2.9,
diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php
index d213aa4c0..c1eeca241 100644
--- a/modules/ppcp-settings/services.php
+++ b/modules/ppcp-settings/services.php
@@ -19,7 +19,9 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
+use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
+use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
@@ -37,6 +39,8 @@ return array(
$can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' );
$can_use_vaulting = $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' );
$can_use_card_payments = $container->has( 'card-fields.eligible' ) && $container->get( 'card-fields.eligible' );
+ $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' )
+ ->plugin_is_active();
// Card payments are disabled for this plugin when WooPayments is active.
// TODO: Move this condition to the card-fields.eligible service?
@@ -47,14 +51,18 @@ return array(
return new OnboardingProfile(
$can_use_casual_selling,
$can_use_vaulting,
- $can_use_card_payments
+ $can_use_card_payments,
+ $can_use_subscriptions
);
},
'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
return new GeneralSettings();
},
'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings {
- return new CommonSettings();
+ return new CommonSettings(
+ $container->get( 'api.shop.country' ),
+ $container->get( 'api.shop.currency.getter' )->get(),
+ );
},
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
@@ -131,9 +139,25 @@ return array(
return in_array( $country, $eligible_countries, true );
},
+ 'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
+ $page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : '';
+
+ return new ConnectionListener(
+ $page_id,
+ $container->get( 'settings.data.common' ),
+ $container->get( 'settings.service.onboarding-url-manager' ),
+ $container->get( 'woocommerce.logger.woocommerce' )
+ );
+ },
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
return new Cache( 'ppcp-paypal-signup-link' );
},
+ 'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
+ return new OnboardingUrlManager(
+ $container->get( 'settings.service.signup-link-cache' ),
+ $container->get( 'woocommerce.logger.woocommerce' )
+ );
+ },
'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array {
// Define available environments.
$environments = array(
@@ -152,8 +176,8 @@ return array(
$generators[ $environment ] = new ConnectionUrlGenerator(
$config['partner_referrals'],
$container->get( 'api.repository.partner-referrals-data' ),
- $container->get( 'settings.service.signup-link-cache' ),
$environment,
+ $container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
}
diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php
index 8f7dd1ddf..1894255ff 100644
--- a/modules/ppcp-settings/src/Data/CommonSettings.php
+++ b/modules/ppcp-settings/src/Data/CommonSettings.php
@@ -29,6 +29,25 @@ class CommonSettings extends AbstractDataModel {
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-common';
+ /**
+ * List of customization flags, provided by the server (read-only).
+ *
+ * @var array
+ */
+ protected array $woo_settings = array();
+
+ /**
+ * Constructor.
+ *
+ * @param string $country WooCommerce store country.
+ * @param string $currency WooCommerce store currency.
+ */
+ public function __construct( string $country, string $currency ) {
+ parent::__construct();
+ $this->woo_settings['country'] = $country;
+ $this->woo_settings['currency'] = $currency;
+ }
+
/**
* Get default values for the model.
*
@@ -40,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' => '',
);
}
@@ -116,4 +141,67 @@ class CommonSettings extends AbstractDataModel {
public function set_client_secret( string $client_secret ) : void {
$this->data['client_secret'] = sanitize_text_field( $client_secret );
}
+
+ /**
+ * Returns the list of read-only customization flags.
+ *
+ * @return array
+ */
+ 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'];
+ }
}
diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php
index 03a0a7d1c..a2d8e6c36 100644
--- a/modules/ppcp-settings/src/Data/OnboardingProfile.php
+++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php
@@ -42,19 +42,22 @@ class OnboardingProfile extends AbstractDataModel {
* @param bool $can_use_casual_selling Whether casual selling is enabled in the store's country.
* @param bool $can_use_vaulting Whether vaulting is enabled in the store's country.
* @param bool $can_use_card_payments Whether credit card payments are possible.
+ * @param bool $can_use_subscriptions Whether WC Subscriptions plugin is active.
*
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
*/
public function __construct(
bool $can_use_casual_selling = false,
bool $can_use_vaulting = false,
- bool $can_use_card_payments = false
+ bool $can_use_card_payments = false,
+ bool $can_use_subscriptions = false
) {
parent::__construct();
$this->flags['can_use_casual_selling'] = $can_use_casual_selling;
$this->flags['can_use_vaulting'] = $can_use_vaulting;
$this->flags['can_use_card_payments'] = $can_use_card_payments;
+ $this->flags['can_use_subscriptions'] = $can_use_subscriptions;
}
/**
@@ -67,7 +70,7 @@ class OnboardingProfile extends AbstractDataModel {
'completed' => false,
'step' => 0,
'is_casual_seller' => null,
- 'are_optional_payment_methods_enabled' => true,
+ 'are_optional_payment_methods_enabled' => null,
'products' => array(),
);
}
diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
index c7345148e..3c0131759 100644
--- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
@@ -60,6 +60,40 @@ class CommonRestEndpoint extends RestEndpoint {
),
);
+ /**
+ * 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
+ */
+ private array $woo_settings_map = array(
+ 'country' => array(
+ 'js_name' => 'storeCountry',
+ ),
+ 'currency' => array(
+ 'js_name' => 'storeCurrency',
+ ),
+ );
+
/**
* Constructor.
*
@@ -96,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' ),
+ ),
+ )
+ );
}
/**
@@ -109,7 +155,10 @@ class CommonRestEndpoint extends RestEndpoint {
$this->field_map
);
- return $this->return_success( $js_data );
+ $extra_data = $this->add_woo_settings( array() );
+ $extra_data = $this->add_merchant_info( $extra_data );
+
+ return $this->return_success( $js_data, $extra_data );
}
/**
@@ -130,4 +179,50 @@ 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.
+ *
+ * @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.
+ *
+ * @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;
+ }
}
diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
index 02e7c80cd..d4273228f 100644
--- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
@@ -77,6 +77,9 @@ class OnboardingRestEndpoint extends RestEndpoint {
'can_use_card_payments' => array(
'js_name' => 'canUseCardPayments',
),
+ 'can_use_subscriptions' => array(
+ 'js_name' => 'canUseSubscriptions',
+ ),
);
/**
diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php
new file mode 100644
index 000000000..a24a82231
--- /dev/null
+++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php
@@ -0,0 +1,241 @@
+settings_page_id = $settings_page_id;
+ $this->settings = $settings;
+ $this->url_manager = $url_manager;
+ $this->logger = $logger ?: new NullLogger();
+
+ // Initialize as "guest", the real ID is provided via process().
+ $this->user_id = 0;
+ }
+
+ /**
+ * Process the request data, and extract connection details, if present.
+ *
+ * @param int $user_id The current user ID.
+ * @param array $request Request details to process.
+ */
+ public function process( int $user_id, array $request ) : void {
+ $this->user_id = $user_id;
+
+ if ( ! $this->is_valid_request( $request ) ) {
+ return;
+ }
+
+ $token = $this->get_token_from_request( $request );
+ if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) {
+ return;
+ }
+
+ $data = $this->extract_data( $request );
+ if ( ! $data ) {
+ return;
+ }
+
+ $this->logger->info( 'Found merchant data in request', $data );
+
+ $this->store_data(
+ $data['is_sandbox'],
+ $data['merchant_id'],
+ $data['merchant_email']
+ );
+ }
+
+ /**
+ * Determine, if the request details contain connection data that should be
+ * extracted and stored.
+ *
+ * @param array $request Request details to verify.
+ *
+ * @return bool True, if the request contains valid connection details.
+ */
+ protected function is_valid_request( array $request ) : bool {
+ if ( $this->user_id < 1 || ! $this->settings_page_id ) {
+ return false;
+ }
+
+ if ( ! user_can( $this->user_id, 'manage_woocommerce' ) ) {
+ return false;
+ }
+
+ $required_params = array(
+ 'merchantIdInPayPal',
+ 'merchantId',
+ 'ppcpToken',
+ );
+
+ foreach ( $required_params as $param ) {
+ if ( empty( $request[ $param ] ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Extract the merchant details (ID & email) from the request details.
+ *
+ * @param array $request The full request details.
+ *
+ * @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
+ * or an empty array on failure.
+ */
+ protected function extract_data( array $request ) : array {
+ $this->logger->info( 'Extracting connection data from request...' );
+
+ $merchant_id = $this->get_merchant_id_from_request( $request );
+ $merchant_email = $this->get_merchant_email_from_request( $request );
+
+ if ( ! $merchant_id || ! $merchant_email ) {
+ return array();
+ }
+
+ return array(
+ 'is_sandbox' => $this->settings->get_sandbox(),
+ 'merchant_id' => $merchant_id,
+ 'merchant_email' => $merchant_email,
+ );
+ }
+
+ /**
+ * Persist the merchant details to the database.
+ *
+ * @param bool $is_sandbox Whether the details are for a sandbox account.
+ * @param string $merchant_id The anonymized merchant ID.
+ * @param string $merchant_email The merchant's email.
+ */
+ protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
+ $this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" );
+
+ $this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email );
+ $this->settings->save();
+ }
+
+ /**
+ * Returns the sanitized connection token from the incoming request.
+ *
+ * @param array $request Full request details.
+ *
+ * @return string The sanitized token, or an empty string.
+ */
+ protected function get_token_from_request( array $request ) : string {
+ return $this->sanitize_string( $request['ppcpToken'] ?? '' );
+ }
+
+ /**
+ * Returns the sanitized merchant ID from the incoming request.
+ *
+ * @param array $request Full request details.
+ *
+ * @return string The sanitized merchant ID, or an empty string.
+ */
+ protected function get_merchant_id_from_request( array $request ) : string {
+ return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' );
+ }
+
+ /**
+ * Returns the sanitized merchant email from the incoming request.
+ *
+ * Note that the email is provided via the argument "merchantId", which
+ * looks incorrect at first, but PayPal uses the email address as merchant
+ * IDm and offers a more anonymous ID via the "merchantIdInPayPal" argument.
+ *
+ * @param array $request Full request details.
+ *
+ * @return string The sanitized merchant email, or an empty string.
+ */
+ protected function get_merchant_email_from_request( array $request ) : string {
+ return $this->sanitize_merchant_email( $request['merchantId'] ?? '' );
+ }
+
+ /**
+ * Sanitizes a request-argument for processing.
+ *
+ * @param string $value Value from the request argument.
+ *
+ * @return string Sanitized value.
+ */
+ protected function sanitize_string( string $value ) : string {
+ return trim( sanitize_text_field( wp_unslash( $value ) ) );
+ }
+
+ /**
+ * Sanitizes the merchant's email address for processing.
+ *
+ * @param string $email The plain email.
+ *
+ * @return string Sanitized email address.
+ */
+ protected function sanitize_merchant_email( string $email ) : string {
+ return sanitize_text_field( str_replace( ' ', '+', $email ) );
+ }
+}
diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
index 6e91aba3a..028740cb9 100644
--- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
+++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
@@ -14,9 +14,11 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
-use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
+// TODO: Replace the OnboardingUrl with a new implementation for this module.
+use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
+
/**
* Generator that builds the ISU connection URL.
*/
@@ -36,11 +38,11 @@ class ConnectionUrlGenerator {
protected PartnerReferralsData $referrals_data;
/**
- * The cache
+ * Manages access to OnboardingUrl instances
*
- * @var Cache
+ * @var OnboardingUrlManager
*/
- protected Cache $cache;
+ protected OnboardingUrlManager $url_manager;
/**
* Which environment is used for the connection URL.
@@ -54,7 +56,7 @@ class ConnectionUrlGenerator {
*
* @var LoggerInterface
*/
- private $logger;
+ private LoggerInterface $logger;
/**
* Constructor for the ConnectionUrlGenerator class.
@@ -63,23 +65,22 @@ class ConnectionUrlGenerator {
*
* @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation.
* @param PartnerReferralsData $referrals_data Default partner referrals data.
- * @param Cache $cache The cache object used for storing and
- * retrieving data.
* @param string $environment Environment that is used to generate the URL.
* ['production'|'sandbox'].
+ * @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances.
* @param ?LoggerInterface $logger The logger object for logging messages.
*/
public function __construct(
PartnerReferrals $partner_referrals,
PartnerReferralsData $referrals_data,
- Cache $cache,
string $environment,
+ OnboardingUrlManager $url_manager,
?LoggerInterface $logger = null
) {
$this->partner_referrals = $partner_referrals;
$this->referrals_data = $referrals_data;
- $this->cache = $cache;
$this->environment = $environment;
+ $this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger();
}
@@ -107,7 +108,7 @@ class ConnectionUrlGenerator {
public function generate( array $products = array() ) : string {
$cache_key = $this->cache_key( $products );
$user_id = get_current_user_id();
- $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, $user_id );
+ $onboarding_url = $this->url_manager->get( $cache_key, $user_id );
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
if ( $cached_url ) {
diff --git a/modules/ppcp-settings/src/Service/OnboardingUrlManager.php b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php
new file mode 100644
index 000000000..f2463af46
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php
@@ -0,0 +1,101 @@
+cache = $cache;
+ $this->logger = $logger ?: new NullLogger();
+ }
+
+ /**
+ * Returns a new Onboarding Url instance.
+ *
+ * @param string $cache_key_prefix The prefix for the cache entry.
+ * @param int $user_id User ID to associate the link with.
+ *
+ * @return OnboardingUrl
+ */
+ public function get( string $cache_key_prefix, int $user_id ) : OnboardingUrl {
+ return new OnboardingUrl( $this->cache, $cache_key_prefix, $user_id );
+ }
+
+ /**
+ * Validates the authentication token; if it's valid, the token is instantly
+ * invalidated (deleted), so it cannot be validated again.
+ *
+ * @param string $token The token to validate.
+ * @param int $user_id User ID who generated the token.
+ *
+ * @return bool True, if the token is valid. False otherwise.
+ */
+ public function validate_token_and_delete( string $token, int $user_id ) : bool {
+ if ( $user_id < 1 || strlen( $token ) < 10 ) {
+ return false;
+ }
+
+ $log_token = ( (string) substr( $token, 0, 2 ) ) . '...' . ( (string) substr( $token, - 6 ) );
+ $this->logger->debug( 'Validating onboarding ppcpToken: ' . $log_token );
+
+ if ( OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id ) ) {
+ $this->logger->info( 'Validated onboarding ppcpToken: ' . $log_token );
+
+ return true;
+ }
+
+ if ( OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id ) ) {
+ // TODO: Do we need this here? Previous logic was to reload the page without doing anything in this case.
+ $this->logger->info( 'Validated previous token, silently redirecting: ' . $log_token );
+
+ return true;
+ }
+
+ $this->logger->error( 'Failed to validate onboarding ppcpToken: ' . $log_token );
+
+ return false;
+ }
+}
diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php
index 7c9dca2f8..7cb55bb02 100644
--- a/modules/ppcp-settings/src/SettingsModule.php
+++ b/modules/ppcp-settings/src/SettingsModule.php
@@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
@@ -85,7 +86,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
}
);
- $endpoint = $container->get( 'settings.switch-ui.endpoint' );
+ $endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null;
assert( $endpoint instanceof SwitchSettingsUiEndpoint );
add_action(
@@ -189,6 +190,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
}
);
+ add_action(
+ 'admin_init',
+ static function () use ( $container ) : void {
+ $connection_handler = $container->get( 'settings.handler.connection-listener' );
+ assert( $connection_handler instanceof ConnectionListener );
+
+ // @phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce; sanitation done by the handler
+ $connection_handler->process( get_current_user_id(), $_GET );
+ }
+ );
+
return true;
}
diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php
index fbd511e93..8c807474e 100644
--- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php
+++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php
@@ -653,7 +653,10 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$listener = $container->get( 'wcgateway.settings.listener' );
assert( $listener instanceof SettingsListener );
- $listener->listen_for_merchant_id();
+ $use_new_ui = $container->get( 'wcgateway.settings.admin-settings-enabled' );
+ if ( ! $use_new_ui ) {
+ $listener->listen_for_merchant_id();
+ }
try {
$listener->listen_for_vaulting_enabled();