WooCommerce Subscriptions enabled.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'https://woocommerce.com/products/woocommerce-subscriptions/'
+ ),
+ } }
+ />
+ ) }
+ { showNotice && (
+
+ { __(
+ '* Business account is required for subscriptions.',
+ 'woocommerce-paypal-payments'
+ ) }
+
+ ) }
+ >
);
-
-const productChoicesFull = [
- {
- value: PRODUCT_TYPES.VIRTUAL,
- title: __( 'Virtual', 'woocommerce-paypal-payments' ),
- description: __(
- 'Items do not require shipping.',
- 'woocommerce-paypal-payments'
- ),
- contents:
@@ -20,17 +38,16 @@ const StepWelcome = ( { setStep, currentStep } ) => {
'Welcome to PayPal Payments',
'woocommerce-paypal-payments'
) }
- description={ __(
- 'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
- 'woocommerce-paypal-payments'
- ) }
+ description={ onboardingHeaderDescription }
/>
-
+
{ __(
- `Click the button below to be guided through connecting your existing PayPal account or creating a new one.You will be able to choose the payment options that are right for your store.`,
+ 'Click the button below to be guided through connecting your existing PayPal account or creating a new one. You will be able to choose the payment options that are right for your store.',
'woocommerce-paypal-payments'
) }
@@ -49,9 +66,8 @@ const StepWelcome = ( { setStep, currentStep } ) => {
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js
index 2162add6c..cf680db70 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js
@@ -36,8 +36,7 @@ const ALL_STEPS = [
id: 'methods',
title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ),
StepComponent: StepPaymentMethods,
- canProceed: ( { methods } ) =>
- methods.areOptionalPaymentMethodsEnabled !== null,
+ canProceed: ( { methods } ) => methods.optionalMethods !== null,
},
{
id: 'complete',
@@ -60,8 +59,8 @@ export const getSteps = ( flags ) => {
const steps = filterSteps( ALL_STEPS, [
// Casual selling: Unlock the "Personal Account" choice.
( step ) => flags.canUseCasualSelling || step.id !== 'business',
- // Card payments: Unlocks the "Extended Checkout" choice.
- ( step ) => flags.canUseCardPayments || step.id !== 'methods',
+ // Skip payment methods screen.
+ ( step ) => ! flags.shouldSkipPaymentMethods || step.id !== 'methods',
] );
const totalStepsCount = steps.length;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js
index 4b16edef0..b4524fd0c 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js
@@ -28,6 +28,7 @@ const defaultConfig = {
// Extended: Items on right side for ACDC-flow.
extendedMethods: [
+ { name: 'CardFields', Component: CardFields },
{ name: 'DigitalWallets', Component: DigitalWallets },
{ name: 'APMs', Component: AlternativePaymentMethods },
],
@@ -41,6 +42,26 @@ const defaultConfig = {
'with additional application',
'woocommerce-paypal-payments'
),
+
+ // PayPal Checkout description.
+ paypalCheckoutDescription: __(
+ 'Our all-in-one checkout solution lets you offer PayPal, Pay Later options, and more to help maximise conversion',
+ 'woocommerce-paypal-payments'
+ ),
+
+ // Icon groups.
+ bcdcIcons: [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ],
+ acdcIcons: [
+ 'paypal',
+ 'visa',
+ 'mastercard',
+ 'amex',
+ 'discover',
+ 'apple-pay',
+ 'google-pay',
+ 'ideal',
+ 'bancontact',
+ ],
};
const countrySpecificConfigs = {
@@ -60,11 +81,35 @@ const countrySpecificConfigs = {
{ name: 'APMs', Component: AlternativePaymentMethods },
{ name: 'Fastlane', Component: Fastlane },
],
+ paypalCheckoutDescription: __(
+ 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
+ 'woocommerce-paypal-payments'
+ ),
optionalTitle: __( 'Expanded Checkout', 'woocommerce-paypal-payments' ),
optionalDescription: __(
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
'woocommerce-paypal-payments'
),
+ bcdcIcons: [
+ 'paypal',
+ 'venmo',
+ 'visa',
+ 'mastercard',
+ 'amex',
+ 'discover',
+ ],
+ acdcIcons: [
+ 'paypal',
+ 'venmo',
+ 'visa',
+ 'mastercard',
+ 'amex',
+ 'discover',
+ 'apple-pay',
+ 'google-pay',
+ 'ideal',
+ 'bancontact',
+ ],
},
GB: {
includedMethods: [
@@ -72,6 +117,12 @@ const countrySpecificConfigs = {
{ name: 'PayInThree', Component: PayInThree },
],
},
+ MX: {
+ extendedMethods: [
+ { name: 'CardFields', Component: CardFields },
+ { name: 'APMs', Component: AlternativePaymentMethods },
+ ],
+ },
};
const filterMethods = ( methods, conditions ) => {
@@ -80,22 +131,12 @@ const filterMethods = ( methods, conditions ) => {
);
};
-export const usePaymentConfig = (
- country,
- isPayLater,
- useAcdc,
- isFastlane
-) => {
+export const usePaymentConfig = ( country, useAcdc, isFastlane ) => {
return useMemo( () => {
const countryConfig = countrySpecificConfigs[ country ] || {};
const config = { ...defaultConfig, ...countryConfig };
const learnMoreConfig = learnMoreLinks[ country ] || {};
- // Filter the "left side" list. PayLater is conditional.
- const includedMethods = filterMethods( config.includedMethods, [
- ( method ) => isPayLater || method.name !== 'PayLater',
- ] );
-
// Determine the "right side" items: Either BCDC or ACDC items.
const optionalMethods = useAcdc
? config.extendedMethods
@@ -108,9 +149,10 @@ export const usePaymentConfig = (
return {
...config,
- includedMethods,
optionalMethods: availableOptionalMethods,
learnMoreConfig,
+ acdcIcons: config.acdcIcons,
+ bcdcIcons: config.bcdcIcons,
};
- }, [ country, isPayLater, useAcdc, isFastlane ] );
+ }, [ country, useAcdc, isFastlane ] );
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js
index 274ef91c2..37adfe648 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js
@@ -1,5 +1,5 @@
-import React, { useEffect } from 'react';
import { PayLaterMessagingHooks } from '../../../data';
+import { useEffect } from '@wordpress/element';
const TabPayLaterMessaging = () => {
const {
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
index 6ea843c67..1b06eb222 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
@@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
-import { useSaveSettings } from '../../../../hooks/useSaveSettings';
+import { useStoreManager } from '../../../../hooks/useStoreManager';
import { CommonHooks } from '../../../../data';
import TabBar from '../../../ReusableComponents/TabBar';
import classNames from 'classnames';
@@ -20,7 +20,7 @@ const SettingsNavigation = ( {
activePanel,
setActivePanel,
} ) => {
- const { persistAll } = useSaveSettings();
+ const { persistAll } = useStoreManager();
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/FeatureDescription.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/FeatureDescription.js
new file mode 100644
index 000000000..9bf754d13
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/FeatureDescription.js
@@ -0,0 +1,36 @@
+import { __ } from '@wordpress/i18n';
+import { Button, Icon } from '@wordpress/components';
+import { reusableBlock } from '@wordpress/icons';
+
+const FeatureDescription = ( { refreshHandler, isRefreshing } ) => {
+ const buttonLabel = isRefreshing
+ ? __( 'Refreshing…', 'woocommerce-paypal-payments' )
+ : __( 'Refresh', 'woocommerce-paypal-payments' );
+
+ return (
+ <>
+
+ { __(
+ 'Enable additional features and capabilities on your WooCommerce store.',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+ { __(
+ 'Click Refresh to update your current features after making changes.',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+
+ { buttonLabel }
+
+ >
+ );
+};
+
+export default FeatureDescription;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/FeatureItem.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/FeatureItem.js
new file mode 100644
index 000000000..4f3e375d3
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/FeatureItem.js
@@ -0,0 +1,74 @@
+import { __ } from '@wordpress/i18n';
+import { FeatureSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
+import { Content } from '../../../../../ReusableComponents/Elements';
+import { TITLE_BADGE_POSITIVE } from '../../../../../ReusableComponents/TitleBadge';
+import { selectTab, TAB_IDS } from '../../../../../../utils/tabSelector';
+import { useDispatch } from '@wordpress/data';
+import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
+
+const FeatureItem = ( {
+ isBusy,
+ isSandbox,
+ title,
+ description,
+ buttons,
+ enabled,
+ notes,
+} ) => {
+ const { setActiveModal } = useDispatch( COMMON_STORE_NAME );
+ const getButtonUrl = ( button ) => {
+ if ( button.urls ) {
+ return isSandbox ? button.urls.sandbox : button.urls.live;
+ }
+
+ return button.url;
+ };
+
+ const visibleButtons = buttons.filter(
+ ( button ) =>
+ ! button.showWhen || // Learn more buttons
+ ( enabled && button.showWhen === 'enabled' ) ||
+ ( ! enabled && button.showWhen === 'disabled' )
+ );
+ const handleClick = async ( feature ) => {
+ if ( feature.action?.type === 'tab' ) {
+ const highlight = Boolean( feature.action?.highlight );
+ const tabId = TAB_IDS[ feature.action.tab.toUpperCase() ];
+ await selectTab( tabId, feature.action.section, highlight );
+ }
+
+ if ( feature.action?.modal ) {
+ setActiveModal( feature.action.modal );
+ }
+ };
+
+ const actionProps = {
+ isBusy,
+ enabled,
+ notes,
+ buttons: visibleButtons.map( ( button ) => ( {
+ ...button,
+ url: getButtonUrl( button ),
+ onClick: () => handleClick( button ),
+ } ) ),
+ };
+
+ if ( enabled ) {
+ actionProps.badge = {
+ text: __( 'Active', 'woocommerce-paypal-payments' ),
+ type: TITLE_BADGE_POSITIVE,
+ };
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default FeatureItem;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js
new file mode 100644
index 000000000..c8a20186d
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js
@@ -0,0 +1,95 @@
+import { __, sprintf } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
+import { useDispatch } from '@wordpress/data';
+import { store as noticesStore } from '@wordpress/notices';
+import FeatureItem from './FeatureItem';
+import FeatureDescription from './FeatureDescription';
+import { ContentWrapper } from '../../../../../ReusableComponents/Elements';
+import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
+import { useMerchantInfo } from '../../../../../../data/common/hooks';
+import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
+import {
+ NOTIFICATION_ERROR,
+ NOTIFICATION_SUCCESS,
+} from '../../../../../ReusableComponents/Icons';
+import { useFeatures } from '../../../../../../data/features/hooks';
+
+const Features = () => {
+ const [ isRefreshing, setIsRefreshing ] = useState( false );
+ const { merchant } = useMerchantInfo();
+ const { features, fetchFeatures } = useFeatures();
+ const { refreshFeatureStatuses } = useDispatch( COMMON_STORE_NAME );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+
+ if ( ! features || features.length === 0 ) {
+ return null;
+ }
+
+ const refreshHandler = async () => {
+ setIsRefreshing( true );
+ try {
+ const statusResult = await refreshFeatureStatuses();
+ if ( ! statusResult?.success ) {
+ throw new Error(
+ statusResult?.message || 'Failed to refresh status'
+ );
+ }
+
+ const featuresResult = await fetchFeatures();
+ if ( featuresResult.success ) {
+ createSuccessNotice(
+ __(
+ 'Features refreshed successfully.',
+ 'woocommerce-paypal-payments'
+ ),
+ { icon: NOTIFICATION_SUCCESS }
+ );
+ } else {
+ throw new Error(
+ featuresResult?.message || 'Failed to fetch features'
+ );
+ }
+ } catch ( error ) {
+ createErrorNotice(
+ sprintf(
+ /* translators: %s: error message */
+ __( 'Operation failed: %s', 'woocommerce-paypal-payments' ),
+ error.message ||
+ __( 'Unknown error', 'woocommerce-paypal-payments' )
+ ),
+ { icon: NOTIFICATION_ERROR }
+ );
+ } finally {
+ setIsRefreshing( false );
+ }
+ };
+
+ return (
+
+ }
+ contentContainer={ false }
+ >
+
+ { features.map( ( { id, enabled, ...feature } ) => (
+
+ ) ) }
+
+
+ );
+};
+
+export default Features;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Help/Help.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Help/Help.js
new file mode 100644
index 000000000..afb1cc1f2
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Help/Help.js
@@ -0,0 +1,72 @@
+import { __ } from '@wordpress/i18n';
+import { FeatureSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
+import {
+ Content,
+ ContentWrapper,
+} from '../../../../../ReusableComponents/Elements';
+import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
+
+const Help = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Help;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js
new file mode 100644
index 000000000..6be06ab6b
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js
@@ -0,0 +1,84 @@
+import { __ } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
+import { Button, Icon } from '@wordpress/components';
+import { useDispatch } from '@wordpress/data';
+import { reusableBlock } from '@wordpress/icons';
+import { store as noticesStore } from '@wordpress/notices';
+import { TodoSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
+import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
+import { useTodos } from '../../../../../../data/todos/hooks';
+import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
+import { STORE_NAME as TODOS_STORE_NAME } from '../../../../../../data/todos';
+import { NOTIFICATION_SUCCESS } from '../../../../../ReusableComponents/Icons';
+
+const Todos = () => {
+ const [ isResetting, setIsResetting ] = useState( false );
+ const { todos, isReady: areTodosReady, dismissTodo } = useTodos();
+ // eslint-disable-next-line no-shadow
+ const { setActiveModal } = useDispatch( COMMON_STORE_NAME );
+ const { resetDismissedTodos, setDismissedTodos } =
+ useDispatch( TODOS_STORE_NAME );
+ const { createSuccessNotice } = useDispatch( noticesStore );
+
+ const showTodos = areTodosReady && todos.length > 0;
+
+ const resetHandler = async () => {
+ setIsResetting( true );
+ try {
+ await setDismissedTodos( [] );
+ await resetDismissedTodos();
+
+ createSuccessNotice(
+ __(
+ 'Dismissed items restored successfully.',
+ 'woocommerce-paypal-payments'
+ ),
+ { icon: NOTIFICATION_SUCCESS }
+ );
+ } finally {
+ setIsResetting( false );
+ }
+ };
+
+ if ( ! showTodos ) {
+ return null;
+ }
+
+ return (
+
+
+ { __(
+ 'Complete these tasks to keep your store updated with the latest products and services.',
+ 'woocommerce-paypal-payments'
+ ) }
+
+
+
+ { isResetting
+ ? __( 'Restoring…', 'woocommerce-paypal-payments' )
+ : __(
+ 'Restore dismissed Things To Do',
+ 'woocommerce-paypal-payments'
+ ) }
+
+ >
+ }
+ >
+
+
+ );
+};
+
+export default Todos;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/features-config.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/features-config.js
deleted file mode 100644
index 3a01b436f..000000000
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/features-config.js
+++ /dev/null
@@ -1,278 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { TAB_IDS, selectTab } from '../../../../../utils/tabSelector';
-import { payLaterMessaging } from './pay-later-messaging';
-
-export const getFeatures = ( setActiveModal ) => {
- const storeCountry = ppcpSettings?.storeCountry;
- const features = [
- {
- id: 'save_paypal_and_venmo',
- title: __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
- description: __(
- 'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.',
- 'woocommerce-paypal-payments'
- ),
- buttons: [
- {
- type: 'secondary',
- text: __( 'Configure', 'woocommerce-paypal-payments' ),
- onClick: () => {
- selectTab(
- TAB_IDS.SETTINGS,
- 'ppcp--save-payment-methods'
- );
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'secondary',
- text: __( 'Sign up', 'woocommerce-paypal-payments' ),
- urls: {
- sandbox:
- 'https://www.sandbox.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
- live: 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
- },
- showWhen: 'disabled',
- class: 'small-button',
- },
- {
- type: 'tertiary',
- text: __( 'Learn more', 'woocommerce-paypal-payments' ),
- url: 'https://www.paypal.com/us/enterprise/payment-processing/accept-venmo',
- class: 'small-button',
- },
- ],
- },
- {
- id: 'advanced_credit_and_debit_cards',
- title: __(
- 'Advanced Credit and Debit Cards',
- 'woocommerce-paypal-payments'
- ),
- description: __(
- 'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
- 'woocommerce-paypal-payments'
- ),
- buttons: [
- {
- type: 'secondary',
- text: __( 'Configure', 'woocommerce-paypal-payments' ),
- onClick: () => {
- selectTab(
- TAB_IDS.PAYMENT_METHODS,
- 'ppcp-card-payments-card'
- ).then( () => {
- setActiveModal( 'ppcp-credit-card-gateway' );
- } );
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'secondary',
- text: __( 'Sign up', 'woocommerce-paypal-payments' ),
- urls: {
- sandbox:
- 'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp',
- live: 'https://www.paypal.com/bizsignup/entry?product=ppcp',
- },
- showWhen: 'disabled',
- class: 'small-button',
- },
- {
- type: 'tertiary',
- text: __( 'Learn more', 'woocommerce-paypal-payments' ),
- url: 'https://developer.paypal.com/studio/checkout/advanced',
- class: 'small-button',
- },
- ],
- },
- {
- id: 'alternative_payment_methods',
- title: __(
- 'Alternative Payment Methods',
- 'woocommerce-paypal-payments'
- ),
- description: __(
- 'Offer global, country-specific payment options for your customers.',
- 'woocommerce-paypal-payments'
- ),
- buttons: [
- {
- type: 'secondary',
- text: __( 'Configure', 'woocommerce-paypal-payments' ),
- onClick: () => {
- selectTab(
- TAB_IDS.PAYMENT_METHODS,
- 'ppcp-alternative-payments-card'
- );
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'secondary',
- text: __( 'Sign up', 'woocommerce-paypal-payments' ),
- url: 'https://developer.paypal.com/docs/checkout/apm/',
- showWhen: 'disabled',
- class: 'small-button',
- },
- {
- type: 'tertiary',
- text: __( 'Learn more', 'woocommerce-paypal-payments' ),
- url: 'https://developer.paypal.com/docs/checkout/apm/',
- class: 'small-button',
- },
- ],
- },
- {
- id: 'google_pay',
- title: __( 'Google Pay', 'woocommerce-paypal-payments' ),
- description: __(
- 'Let customers pay using their Google Pay wallet.',
- 'woocommerce-paypal-payments'
- ),
- buttons: [
- {
- type: 'secondary',
- text: __( 'Configure', 'woocommerce-paypal-payments' ),
- onClick: () => {
- selectTab(
- TAB_IDS.PAYMENT_METHODS,
- 'ppcp-card-payments-card'
- ).then( () => {
- setActiveModal( 'ppcp-googlepay' );
- } );
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'secondary',
- text: __( 'Sign up', 'woocommerce-paypal-payments' ),
- urls: {
- sandbox:
- 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
- live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
- },
- showWhen: 'disabled',
- class: 'small-button',
- },
- {
- type: 'tertiary',
- text: __( 'Learn more', 'woocommerce-paypal-payments' ),
- url: 'https://developer.paypal.com/docs/checkout/apm/google-pay/',
- class: 'small-button',
- },
- ],
- notes: [
- __(
- '¹PayPal Q2 Earnings-2021.',
- 'woocommerce-paypal-payments'
- ),
- ],
- },
- {
- id: 'apple_pay',
- title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
- description: __(
- 'Let customers pay using their Apple Pay wallet.',
- 'woocommerce-paypal-payments'
- ),
- buttons: [
- {
- type: 'secondary',
- text: __( 'Configure', 'woocommerce-paypal-payments' ),
- onClick: () => {
- selectTab(
- TAB_IDS.PAYMENT_METHODS,
- 'ppcp-card-payments-card'
- ).then( () => {
- setActiveModal( 'ppcp-applepay' );
- } );
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'secondary',
- text: __(
- 'Domain registration',
- 'woocommerce-paypal-payments'
- ),
- urls: {
- sandbox:
- 'https://www.sandbox.paypal.com/uccservicing/apm/applepay',
- live: 'https://www.paypal.com/uccservicing/apm/applepay',
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'secondary',
- text: __( 'Sign up', 'woocommerce-paypal-payments' ),
- urls: {
- sandbox:
- 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
- live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
- },
- showWhen: 'disabled',
- class: 'small-button',
- },
- {
- type: 'tertiary',
- text: __( 'Learn more', 'woocommerce-paypal-payments' ),
- url: 'https://developer.paypal.com/docs/checkout/apm/apple-pay/',
- class: 'small-button',
- },
- ],
- },
- ];
-
- const countryData = payLaterMessaging[ storeCountry ] || {};
-
- if (
- !! window.ppcpSettings?.isPayLaterConfiguratorAvailable &&
- countryData
- ) {
- const countryLocation = [
- 'UK',
- 'ES',
- 'IT',
- 'FR',
- 'US',
- 'DE',
- 'AU',
- ].includes( storeCountry )
- ? storeCountry.toLowerCase()
- : 'us';
- features.push( {
- id: 'pay_later_messaging',
- title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
- description: __(
- 'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
- 'woocommerce-paypal-payments'
- ),
- buttons: [
- {
- type: 'secondary',
- text: __( 'Configure', 'woocommerce-paypal-payments' ),
- onClick: () => {
- selectTab( TAB_IDS.PAY_LATER_MESSAGING );
- },
- showWhen: 'enabled',
- class: 'small-button',
- },
- {
- type: 'tertiary',
- text: __( 'Learn more', 'woocommerce-paypal-payments' ),
- url: `https://www.paypal.com/${ countryLocation }/business/accept-payments/checkout/installments`,
- class: 'small-button',
- },
- ],
- } );
- }
-
- return features;
-};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/Modal.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/Modal.js
index a749c7420..cddf17bf5 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/Modal.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/Modal.js
@@ -100,14 +100,16 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
case 'radio':
return (
<>
-
- { field.label }
-
- { field.description && (
-
- { field.description }
-
- ) }
+
+
+ { field.label }
+
+ { field.description && (
+
+ { field.description }
+
+ ) }
+
{
+ const displayName = parentName || parentId;
+
+ return createInterpolateElement(
+ /* translators: %s: payment method name */
+ __(
+ 'This payment method requires to be enabled.',
+ 'woocommerce-paypal-payments'
+ ),
+ {
+ methodLink: (
+
+ {
+ e.preventDefault();
+ scrollAndHighlight( parentId );
+ } }
+ >
+ { displayName }
+
+
+ ),
+ }
+ );
+};
+
+export default PaymentDependencyMessage;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodCard.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodCard.js
new file mode 100644
index 000000000..191c75be2
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodCard.js
@@ -0,0 +1,108 @@
+import { useEffect } from '@wordpress/element';
+import SettingsCard from '../../../../ReusableComponents/SettingsCard';
+import { PaymentMethodsBlock } from '../../../../ReusableComponents/SettingsBlocks';
+import usePaymentDependencyState from '../../../../../hooks/usePaymentDependencyState';
+import useSettingDependencyState from '../../../../../hooks/useSettingDependencyState';
+import PaymentDependencyMessage from './PaymentDependencyMessage';
+import SettingDependencyMessage from './SettingDependencyMessage';
+import SpinnerOverlay from '../../../../ReusableComponents/SpinnerOverlay';
+import { PaymentHooks, SettingsHooks } from '../../../../../data';
+import { useNavigation } from '../../../../../hooks/useNavigation';
+
+/**
+ * Renders a payment method card with dependency handling
+ *
+ * @param {Object} props - Component props
+ * @param {string} props.id - Unique identifier for the card
+ * @param {string} props.title - Title of the payment method card
+ * @param {string} props.description - Description of the payment method
+ * @param {string} props.icon - Icon path for the payment method
+ * @param {Array} props.methods - List of payment methods to display
+ * @param {Object} props.methodsMap - Map of all payment methods by ID
+ * @param {Function} props.onTriggerModal - Callback when a method is clicked
+ * @param {boolean} props.isDisabled - Whether the entire card is disabled
+ * @return {JSX.Element} The rendered component
+ */
+const PaymentMethodCard = ( {
+ id,
+ title,
+ description,
+ icon,
+ methods,
+ methodsMap = {},
+ onTriggerModal,
+ isDisabled = false,
+} ) => {
+ const { isReady: isPaymentStoreReady } = PaymentHooks.useStore();
+ const { isReady: isSettingsStoreReady } = SettingsHooks.useStore();
+ const { handleHighlightFromUrl } = useNavigation();
+
+ const paymentDependencies = usePaymentDependencyState(
+ methods,
+ methodsMap
+ );
+
+ const settingDependencies = useSettingDependencyState( methods );
+
+ useEffect( () => {
+ if ( isPaymentStoreReady && isSettingsStoreReady ) {
+ handleHighlightFromUrl();
+ }
+ }, [ handleHighlightFromUrl, isPaymentStoreReady, isSettingsStoreReady ] );
+
+ if ( ! isPaymentStoreReady || ! isSettingsStoreReady ) {
+ return ;
+ }
+
+ return (
+
+ {
+ const paymentDependency =
+ paymentDependencies?.[ method.id ];
+ const settingDependency =
+ settingDependencies?.[ method.id ];
+
+ let dependencyMessage = null;
+ let isMethodDisabled = method.isDisabled || isDisabled;
+
+ if ( paymentDependency ) {
+ dependencyMessage = (
+
+ );
+ isMethodDisabled = true;
+ } else if ( settingDependency?.isDisabled ) {
+ dependencyMessage = (
+
+ );
+ isMethodDisabled = true;
+ }
+
+ return {
+ ...method,
+ isDisabled: isMethodDisabled,
+ disabledMessage: dependencyMessage,
+ };
+ } ) }
+ onTriggerModal={ onTriggerModal }
+ />
+
+ );
+};
+
+export default PaymentMethodCard;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/SettingDependencyMessage.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/SettingDependencyMessage.js
new file mode 100644
index 000000000..e824031e7
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/SettingDependencyMessage.js
@@ -0,0 +1,113 @@
+import { createInterpolateElement } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+import { selectTab, TAB_IDS } from '../../../../../utils/tabSelector';
+import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight';
+
+/**
+ * Transforms camelCase section IDs to kebab-case with ppcp prefix
+ *
+ * @param {string} sectionId - The original section ID in camelCase
+ * @return {string} The transformed section ID in kebab-case with ppcp prefix
+ */
+const transformSectionId = ( sectionId ) => {
+ if ( ! sectionId ) {
+ return sectionId;
+ }
+
+ // Convert camelCase to kebab-case.
+ // This regex finds capital letters and replaces them with "-lowercase".
+ const kebabCase = sectionId.replace( /([A-Z])/g, '-$1' ).toLowerCase();
+
+ // Add ppcp- prefix if it doesn't already have it.
+ const prefixed = kebabCase.startsWith( 'ppcp-' )
+ ? kebabCase
+ : `ppcp-${ kebabCase }`;
+
+ return prefixed;
+};
+
+/**
+ * Creates a setting link element
+ *
+ * @param {Object} props - Component props
+ * @param {string} props.settingName - Display name for the setting
+ * @param {string} props.sectionId - Section ID to scroll to
+ * @return {JSX.Element} The formatted link element
+ */
+const SettingLink = ( { settingName, sectionId } ) => (
+
+ {
+ e.preventDefault();
+
+ if ( sectionId ) {
+ const tabId = TAB_IDS.SETTINGS;
+
+ // Transform the section ID before passing to selectTab.
+ const transformedSectionId =
+ transformSectionId( sectionId );
+
+ selectTab( tabId );
+
+ setTimeout( () => {
+ scrollAndHighlight( transformedSectionId );
+ }, 100 );
+ }
+ } }
+ >
+ { settingName }
+
+
+);
+
+/**
+ * Component to display a setting dependency message
+ *
+ * @param {Object} props - Component props
+ * @param {string} props.settingId - ID of the required setting
+ * @param {*} props.requiredValue - Required value for the setting
+ * @return {JSX.Element} The formatted message
+ */
+const SettingDependencyMessage = ( { settingId, requiredValue } ) => {
+ // Setting names mapping.
+ const settingNames = {
+ savePaypalAndVenmo: 'Save PayPal and Venmo',
+ };
+
+ // Get a human-friendly setting name.
+ const settingName = settingNames[ settingId ] || settingId;
+
+ const settingLink = (
+
+ );
+
+ const templates = {
+ true: __(
+ 'This payment method requires to be enabled.',
+ 'woocommerce-paypal-payments'
+ ),
+ false: __(
+ 'This payment method requires to be disabled.',
+ 'woocommerce-paypal-payments'
+ ),
+ };
+
+ return typeof requiredValue === 'boolean'
+ ? createInterpolateElement( templates[ requiredValue ], {
+ settingLink,
+ } )
+ : createInterpolateElement(
+ sprintf(
+ /* translators: %s: required setting value */
+ __(
+ 'This payment method requires to be set to "%s".',
+ 'woocommerce-paypal-payments'
+ ),
+ requiredValue
+ ),
+ { settingLink }
+ );
+};
+
+export default SettingDependencyMessage;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/WarningMessages.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/WarningMessages.js
new file mode 100644
index 000000000..001e57e27
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/WarningMessages.js
@@ -0,0 +1,34 @@
+import { Icon } from '@wordpress/components';
+import { warning } from '@wordpress/icons';
+
+/**
+ * Component to display warning messages for payment methods
+ *
+ * @param {Object} props - Component props
+ * @param {Object} props.warningMessages - The warning messages to display
+ * @return {JSX.Element|null} The formatted warning messages or null
+ */
+const WarningMessages = ( { warningMessages } ) => {
+ const messages = Object.values( warningMessages || {} );
+
+ if ( messages.length === 0 ) {
+ return null;
+ }
+
+ return (
+
+
+
+ { messages.map( ( message, index ) => (
+
+ ) ) }
+
+
+ );
+};
+
+export default WarningMessages;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/OrderIntent.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/OrderIntent.js
index 77fab7362..c67c8a768 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/OrderIntent.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/OrderIntent.js
@@ -1,4 +1,5 @@
import { __ } from '@wordpress/i18n';
+import { useEffect } from 'react';
import { ControlToggleButton } from '../../../../../ReusableComponents/Controls';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
@@ -12,6 +13,12 @@ const OrderIntent = () => {
setCaptureVirtualOnlyOrders,
} = SettingsHooks.useSettings();
+ useEffect( () => {
+ if ( ! authorizeOnly && captureVirtualOnlyOrders ) {
+ setCaptureVirtualOnlyOrders( false );
+ }
+ }, [ authorizeOnly ] );
+
return (
{
) }
onChange={ setCaptureVirtualOnlyOrders }
value={ captureVirtualOnlyOrders }
+ disabled={ ! authorizeOnly }
/>
);
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/PaypalSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/PaypalSettings.js
index 62240b35b..86a0c81af 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/PaypalSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/PaypalSettings.js
@@ -142,10 +142,10 @@ const PaypalSettings = () => {
};
const languagesExample = [
- { value: 'en', label: 'English' },
- { value: 'de', label: 'German' },
- { value: 'es', label: 'Spanish' },
- { value: 'it', label: 'Italian' },
+ { value: 'en_US', label: 'English' },
+ { value: 'de_DE', label: 'German' },
+ { value: 'es_ES', label: 'Spanish' },
+ { value: 'it_IT', label: 'Italian' },
];
const subtotalAdjustmentChoices = [
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js
index 9b77076e2..0297c0802 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js
@@ -3,6 +3,7 @@ import { __, sprintf } from '@wordpress/i18n';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
import { ControlToggleButton } from '../../../../../ReusableComponents/Controls';
import { SettingsHooks } from '../../../../../../data';
+import { useMerchantInfo } from '../../../../../../data/common/hooks';
const SavePaymentMethods = () => {
const {
@@ -12,6 +13,8 @@ const SavePaymentMethods = () => {
setSaveCardDetails,
} = SettingsHooks.useSettings();
+ const { features } = useMerchantInfo();
+
return (
{
className="ppcp--save-payment-methods"
>
This will disable all Pay Later features and Alternative Payment Methods on your site.',
+ 'Securely store your customers\' PayPal accounts for a seamless checkout experience. This will disable the Pay Later payment method 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'
+ 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later'
) }
- value={ savePaypalAndVenmo }
+ value={
+ features.save_paypal_and_venmo.enabled
+ ? savePaypalAndVenmo
+ : false
+ }
onChange={ setSavePaypalAndVenmo }
+ disabled={ ! features.save_paypal_and_venmo.enabled }
/>
{
const merchant = CommonHooks.useMerchant();
@@ -21,18 +22,20 @@ const ConnectionStatus = () => {
title={ __( 'Connection status', 'woocommerce-paypal-payments' ) }
description={ }
>
-
+
}
/>
@@ -59,7 +62,9 @@ const ConnectionDescription = () => {
'Your PayPal account connection details.',
'woocommerce-paypal-payments'
) }
-
+
+
+
>
);
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/ExpertSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/ExpertSettings.js
index 13f8e0928..495bb81a4 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/ExpertSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/ExpertSettings.js
@@ -4,7 +4,6 @@ import {
Content,
ContentWrapper,
} from '../../../../ReusableComponents/Elements';
-import ConnectionDetails from './Blocks/ConnectionDetails';
import Troubleshooting from './Blocks/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';
@@ -29,12 +28,12 @@ const ExpertSettings = () => {
contentContainer={ false }
>
-
+ { /*
-
+ */ }
{
+const ConnectionStatusBadge = ( { isActive, isSandbox, isBusinessSeller } ) => {
if ( isActive ) {
- const label = isSandbox
- ? __( 'Sandbox Mode', 'woocommerce-paypal-payments' )
- : __( 'Active', 'woocommerce-paypal-payments' );
+ let label;
+
+ if ( isBusinessSeller ) {
+ label = isSandbox
+ ? __( 'Business | Sandbox', 'woocommerce-paypal-payments' )
+ : __( 'Business | Live', 'woocommerce-paypal-payments' );
+ } else {
+ label = isSandbox
+ ? __( 'Sandbox', 'woocommerce-paypal-payments' )
+ : __( 'Active', 'woocommerce-paypal-payments' );
+ }
return ;
}
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Parts/DisconnectButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Parts/DisconnectButton.js
index aabdd2192..83e014b84 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Parts/DisconnectButton.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Parts/DisconnectButton.js
@@ -1,26 +1,30 @@
import { __ } from '@wordpress/i18n';
-import { Button, Modal } from '@wordpress/components';
+import { Button, Modal, ToggleControl } from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';
import { CommonHooks } from '../../../../../../data';
+import { useToggleState } from '../../../../../../hooks/useToggleState';
import { HStack } from '../../../../../ReusableComponents/Stack';
+import { useNavigation } from '../../../../../../hooks/useNavigation';
const DisconnectButton = () => {
- const [ isOpen, setIsOpen ] = useState( false );
+ const { isOpen, setIsOpen } = useToggleState( 'disconnect-merchant' );
+ const [ resetFlag, setResetFlag ] = useState( false );
const { disconnectMerchant } = CommonHooks.useDisconnectMerchant();
+ const { goToPluginSettings } = useNavigation();
const handleOpen = useCallback( () => {
setIsOpen( true );
- }, [] );
+ }, [ setIsOpen ] );
const handleCancel = useCallback( () => {
setIsOpen( false );
- }, [] );
+ }, [ setIsOpen ] );
const handleConfirm = useCallback( async () => {
- await disconnectMerchant();
- window.location.reload();
- }, [ disconnectMerchant ] );
+ await disconnectMerchant( resetFlag );
+ goToPluginSettings();
+ }, [ disconnectMerchant, resetFlag ] );
const confirmationTitle = __(
'Disconnect from PayPal?',
@@ -39,6 +43,7 @@ const DisconnectButton = () => {
{ isOpen && (
{
'woocommerce-paypal-payments'
) }
-
+
+
{ __( 'Cancel', 'woocommerce-paypal-payments' ) }
{ __(
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js
index 785d6f08c..191898b45 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js
@@ -2,22 +2,21 @@ import { __ } from '@wordpress/i18n';
import { PaymentHooks, StylingHooks } from '../../../../../../data';
import { CheckboxStylingSection } from '../Layout';
+import { useMemo } from '@wordpress/element';
const SectionPaymentMethods = ( { location } ) => {
const { paymentMethods, setPaymentMethods, choices } =
StylingHooks.usePaymentMethodProps( location );
+ const { all: allMethods } = PaymentHooks.usePaymentMethods();
- const methods = PaymentHooks.usePaymentMethods();
- const methodIds = [];
- methods.all.forEach( ( method ) => {
- if ( method.enabled === true ) {
- methodIds.push( method.id );
- }
- } );
-
- const filteredChoices = choices.filter( ( choice ) => {
- return methodIds.includes( choice.paymentMethod );
- } );
+ const filteredChoices = useMemo( () => {
+ return choices.filter( ( choice ) => {
+ const methodConfig = allMethods.find(
+ ( i ) => i.id === choice.value
+ );
+ return methodConfig?.enabled;
+ } );
+ }, [ choices, allMethods ] );
return (
{
+ const { isReady: areTodosReady } = TodosHooks.useTodos();
+ const { isReady: merchantIsReady } = CommonHooks.useMerchantInfo();
+ const { isReady: featuresIsReady } = FeaturesHooks.useFeatures();
+
+ if ( ! areTodosReady || ! merchantIsReady || ! featuresIsReady ) {
+ return ;
+ }
+
return (
-
-
-
+
+
+
);
};
export default TabOverview;
-
-const OverviewTodos = () => {
- const [ isResetting, setIsResetting ] = useState( false );
- const { todos, isReady: areTodosReady, dismissTodo } = useTodos();
- const { setActiveModal, setActiveHighlight } =
- useDispatch( COMMON_STORE_NAME );
- const { resetDismissedTodos, setDismissedTodos } =
- useDispatch( TODOS_STORE_NAME );
- const { createSuccessNotice } = useDispatch( noticesStore );
-
- const showTodos = areTodosReady && todos.length > 0;
-
- const resetHandler = async () => {
- setIsResetting( true );
- try {
- await setDismissedTodos( [] );
- await resetDismissedTodos();
-
- createSuccessNotice(
- __(
- 'Dismissed items restored successfully.',
- 'woocommerce-paypal-payments'
- ),
- { icon: NOTIFICATION_SUCCESS }
- );
- } finally {
- setIsResetting( false );
- }
- };
-
- if ( ! showTodos ) {
- return null;
- }
-
- return (
-
-
- { __(
- 'Complete these tasks to keep your store updated with the latest products and services.',
- 'woocommerce-paypal-payments'
- ) }
-
-
-
- { isResetting
- ? __( 'Restoring…', 'woocommerce-paypal-payments' )
- : __(
- 'Restore dismissed Things To Do',
- 'woocommerce-paypal-payments'
- ) }
-
- >
- }
- >
-
-
- );
-};
-
-const OverviewFeatures = () => {
- const [ isRefreshing, setIsRefreshing ] = useState( false );
- const { merchant, features: merchantFeatures } = useMerchantInfo();
- const { refreshFeatureStatuses, setActiveModal } =
- useDispatch( COMMON_STORE_NAME );
- const { createSuccessNotice, createErrorNotice } =
- useDispatch( noticesStore );
-
- // Get the features data with access to setActiveModal
- const featuresData = useMemo(
- () => getFeatures( setActiveModal ),
- [ setActiveModal ]
- );
-
- // Map merchant features status to the config
- const features = useMemo( () => {
- return featuresData.map( ( feature ) => {
- const merchantFeature = merchantFeatures?.[ feature.id ];
- return {
- ...feature,
- enabled: merchantFeature?.enabled ?? false,
- };
- } );
- }, [ featuresData, merchantFeatures ] );
-
- const refreshHandler = async () => {
- setIsRefreshing( true );
-
- try {
- const result = await refreshFeatureStatuses();
- if ( result && ! result.success ) {
- const errorMessage = sprintf(
- /* translators: %s: error message */
- __(
- 'Operation failed: %s Check WooCommerce logs for more details.',
- 'woocommerce-paypal-payments'
- ),
- result.message ||
- __( 'Unknown error', 'woocommerce-paypal-payments' )
- );
-
- createErrorNotice( errorMessage, {
- icon: NOTIFICATION_ERROR,
- } );
- console.error(
- 'Failed to refresh features:',
- result.message || 'Unknown error'
- );
- } else {
- createSuccessNotice(
- __(
- 'Features refreshed successfully.',
- 'woocommerce-paypal-payments'
- ),
- {
- icon: NOTIFICATION_SUCCESS,
- }
- );
- }
- } finally {
- setIsRefreshing( false );
- }
- };
-
- return (
-
- }
- contentContainer={ false }
- >
-
- { features.map( ( { id, ...feature } ) => (
-
- ) ) }
-
-
- );
-};
-
-const OverviewFeatureItem = ( {
- isBusy,
- isSandbox,
- title,
- description,
- buttons,
- enabled,
- notes,
-} ) => {
- const getButtonUrl = ( button ) => {
- if ( button.urls ) {
- return isSandbox ? button.urls.sandbox : button.urls.live;
- }
-
- return button.url;
- };
-
- const visibleButtons = buttons.filter(
- ( button ) =>
- ! button.showWhen || // Learn more buttons
- ( enabled && button.showWhen === 'enabled' ) ||
- ( ! enabled && button.showWhen === 'disabled' )
- );
-
- const actionProps = {
- isBusy,
- enabled,
- notes,
- buttons: visibleButtons.map( ( button ) => ( {
- ...button,
- url: getButtonUrl( button ),
- } ) ),
- };
-
- if ( enabled ) {
- actionProps.badge = {
- text: __( 'Active', 'woocommerce-paypal-payments' ),
- type: TITLE_BADGE_POSITIVE,
- };
- }
-
- return (
-
-
-
- );
-};
-
-const OverviewFeatureDescription = ( { refreshHandler, isRefreshing } ) => {
- const buttonLabel = isRefreshing
- ? __( 'Refreshing…', 'woocommerce-paypal-payments' )
- : __( 'Refresh', 'woocommerce-paypal-payments' );
-
- return (
- <>
-
- { __(
- 'Enable additional features and capabilities on your WooCommerce store.',
- 'woocommerce-paypal-payments'
- ) }
-
-
- { __(
- 'Click Refresh to update your current features after making changes.',
- 'woocommerce-paypal-payments'
- ) }
-
-
-
- { buttonLabel }
-
- >
- );
-};
-
-const OverviewHelp = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js
index 48da574f0..a85fd7c73 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js
@@ -1,17 +1,23 @@
import { __ } from '@wordpress/i18n';
import { useCallback } from '@wordpress/element';
-import SettingsCard from '../../../ReusableComponents/SettingsCard';
-import { PaymentMethodsBlock } from '../../../ReusableComponents/SettingsBlocks';
-import { PaymentHooks } from '../../../../data';
+import { CommonHooks, OnboardingHooks, PaymentHooks } from '../../../../data';
import { useActiveModal } from '../../../../data/common/hooks';
import Modal from '../Components/Payment/Modal';
+import PaymentMethodCard from '../Components/Payment/PaymentMethodCard';
const TabPaymentMethods = () => {
const methods = PaymentHooks.usePaymentMethods();
- const { setPersistent, changePaymentSettings } = PaymentHooks.useStore();
+ const store = PaymentHooks.useStore();
+ const { setPersistent, changePaymentSettings } = store;
const { activeModal, setActiveModal } = useActiveModal();
+ // Get all methods as a map for dependency checking
+ const methodsMap = {};
+ methods.all.forEach( ( method ) => {
+ methodsMap[ method.id ] = method;
+ } );
+
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
@@ -45,6 +51,9 @@ const TabPaymentMethods = () => {
[ changePaymentSettings, setActiveModal, setPersistent ]
);
+ const merchant = CommonHooks.useMerchant();
+ const { canUseCardPayments } = OnboardingHooks.useFlags();
+
return (
{
icon="icon-checkout-standard.svg"
methods={ methods.paypal }
onTriggerModal={ setActiveModal }
+ methodsMap={ methodsMap }
/>
-
+ { merchant.isBusinessSeller && canUseCardPayments && (
+
+ ) }
{
icon="icon-checkout-alternative-methods.svg"
methods={ methods.apm }
onTriggerModal={ setActiveModal }
+ methodsMap={ methodsMap }
/>
{ activeModal && (
@@ -99,25 +113,3 @@ const TabPaymentMethods = () => {
};
export default TabPaymentMethods;
-
-const PaymentMethodCard = ( {
- id,
- title,
- description,
- icon,
- methods,
- onTriggerModal,
-} ) => (
-
-
-
-);
diff --git a/modules/ppcp-settings/resources/js/data/_example/actions.js b/modules/ppcp-settings/resources/js/data/_example/actions.js
index f9cd9a556..51b516bb1 100644
--- a/modules/ppcp-settings/resources/js/data/_example/actions.js
+++ b/modules/ppcp-settings/resources/js/data/_example/actions.js
@@ -82,3 +82,16 @@ export function persist() {
} );
};
}
+
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/_example/hooks.js b/modules/ppcp-settings/resources/js/data/_example/hooks.js
index 21253d137..4c88523b5 100644
--- a/modules/ppcp-settings/resources/js/data/_example/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/_example/hooks.js
@@ -38,10 +38,16 @@ const useStoreData = () => {
};
export const useStore = () => {
- const { dispatch, useTransient } = useStoreData();
+ const { select, dispatch, useTransient } = useStoreData();
+ const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
- return { persist: dispatch.persist, isReady };
+ // Load persistent data from REST if not done yet.
+ if ( ! isReady ) {
+ select.persistentData();
+ }
+
+ return { persist, refresh, isReady };
};
// TODO: Replace with real hook.
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 4a5569b11..ca44a470e 100644
--- a/modules/ppcp-settings/resources/js/data/common/action-types.js
+++ b/modules/ppcp-settings/resources/js/data/common/action-types.js
@@ -12,6 +12,7 @@ export default {
SET_PERSISTENT: 'ppcp/common/SET_PERSISTENT',
RESET: 'ppcp/common/RESET',
HYDRATE: 'ppcp/common/HYDRATE',
+ SET_MERCHANT: 'ppcp/common/SET_MERCHANT',
RESET_MERCHANT: 'ppcp/common/RESET_MERCHANT',
// Activity management (advanced solution that replaces the isBusy state).
diff --git a/modules/ppcp-settings/resources/js/data/common/actions-thunk.js b/modules/ppcp-settings/resources/js/data/common/actions-thunk.js
index 0de08f245..0143b523c 100644
--- a/modules/ppcp-settings/resources/js/data/common/actions-thunk.js
+++ b/modules/ppcp-settings/resources/js/data/common/actions-thunk.js
@@ -28,45 +28,40 @@ export function persist() {
}
/**
- * Side effect. Fetches the ISU-login URL for a sandbox account.
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
-export function sandboxOnboardingUrl() {
- return async () => {
- try {
- return apiFetch( {
- path: REST_CONNECTION_URL_PATH,
- method: 'POST',
- data: {
- useSandbox: true,
- products: [ 'EXPRESS_CHECKOUT' ],
- },
- } );
- } catch ( e ) {
- return {
- success: false,
- error: e,
- };
- }
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
};
}
/**
- * Side effect. Fetches the ISU-login URL for a production account.
+ * Side effect. Fetches the ISU-login URL for an account.
*
- * @param {string[]} products Which products/features to display in the ISU popup.
+ * @param {string[]} [products=[]] Which products/features to display in the ISU popup.
+ * @param {Object} [options={}] Options to customize the onboarding workflow.
+ * @param isSandbox True if is sandbox, otherwise false.
* @return {Function} The thunk function.
*/
-export function productionOnboardingUrl( products = [] ) {
+export function onboardingUrl(
+ products = [],
+ options = {},
+ isSandbox = false
+) {
return async () => {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
- useSandbox: false,
+ useSandbox: isSandbox,
products,
+ options,
},
} );
} catch ( e ) {
@@ -153,13 +148,17 @@ export function authenticateWithOAuth( sharedId, authCode, useSandbox ) {
/**
* Side effect. Checks webhook simulation.
*
+ * @param {boolean} fullReset When true, all plugin settings are reset to initial values.
* @return {Function} The thunk function.
*/
-export function disconnectMerchant() {
+export function disconnectMerchant( fullReset = false ) {
return async () => {
return await apiFetch( {
path: REST_DISCONNECT_MERCHANT_PATH,
method: 'POST',
+ data: {
+ reset: fullReset,
+ },
} );
};
}
diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js
index 4dded01ac..82e4af331 100644
--- a/modules/ppcp-settings/resources/js/data/common/actions.js
+++ b/modules/ppcp-settings/resources/js/data/common/actions.js
@@ -74,15 +74,6 @@ export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
export const setActiveModal = ( activeModal ) =>
setTransient( 'activeModal', activeModal );
-/**
- * Transient. Sets the active settings highlight.
- *
- * @param {string} activeHighlight
- * @return {Action} The action.
- */
-export const setActiveHighlight = ( activeHighlight ) =>
- setTransient( 'activeHighlight', activeHighlight );
-
/**
* Persistent. Sets the sandbox mode on or off.
*
@@ -110,6 +101,17 @@ export const setManualConnectionMode = ( useManualConnection ) =>
export const setWebhooks = ( webhooks ) =>
setPersistent( 'webhooks', webhooks );
+/**
+ * Replace merchant details in the store.
+ *
+ * @param {Object} merchant - The new merchant details.
+ * @return {Action} The action.
+ */
+export const setMerchant = ( merchant ) => ( {
+ type: ACTION_TYPES.SET_MERCHANT,
+ payload: { merchant },
+} );
+
/**
* Reset merchant details in the store.
*
diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js
index 3ba658200..3f5b06fba 100644
--- a/modules/ppcp-settings/resources/js/data/common/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/common/hooks.js
@@ -13,43 +13,52 @@ import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
-const useHooks = () => {
+/**
+ * Single source of truth for access Redux details.
+ *
+ * This hook returns a stable API to access actions, selectors and special hooks to generate
+ * getter- and setters for transient or persistent properties.
+ *
+ * @return {{select, dispatch, useTransient, usePersistent}} Store data API.
+ */
+const useStoreData = () => {
+ const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
+ const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
+
+ return useMemo(
+ () => ( {
+ select,
+ dispatch,
+ useTransient,
+ usePersistent,
+ } ),
+ [ select, dispatch, useTransient, usePersistent ]
+ );
+};
+
+const useHooks = () => {
+ const { useTransient, usePersistent, dispatch, select } = useStoreData();
const {
persist,
- sandboxOnboardingUrl,
- productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
startWebhookSimulation,
checkWebhookSimulationState,
- } = useDispatch( STORE_NAME );
+ } = dispatch;
// Transient accessors.
- const [ isReady ] = useTransient( 'isReady' );
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
- const [ activeHighlight, setActiveHighlight ] =
- useTransient( 'activeHighlight' );
// Persistent accessors.
- const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
'useManualConnection'
);
// Read-only properties.
- const wooSettings = useSelect(
- ( select ) => select( STORE_NAME ).wooSettings(),
- []
- );
- const features = useSelect(
- ( select ) => select( STORE_NAME ).features(),
- []
- );
- const webhooks = useSelect(
- ( select ) => select( STORE_NAME ).webhooks(),
- []
- );
+ const wooSettings = select.wooSettings();
+ const features = select.features();
+ const webhooks = select.webhooks();
const savePersistent = async ( setter, value ) => {
setter( value );
@@ -57,21 +66,12 @@ const useHooks = () => {
};
return {
- isReady,
activeModal,
setActiveModal,
- activeHighlight,
- setActiveHighlight,
- isSandboxMode,
- setSandboxMode: ( state ) => {
- return savePersistent( setSandboxMode, state );
- },
isManualConnectionMode,
setManualConnectionMode: ( state ) => {
return savePersistent( setManualConnectionMode, state );
},
- sandboxOnboardingUrl,
- productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
wooSettings,
@@ -82,16 +82,39 @@ const useHooks = () => {
};
};
-export const useSandbox = () => {
- const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
+export const useStore = () => {
+ const { select, dispatch, useTransient } = useStoreData();
+ const { persist, refresh } = dispatch;
+ const [ isReady ] = useTransient( 'isReady' );
- return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl };
+ // Load persistent data from REST if not done yet.
+ if ( ! isReady ) {
+ select.persistentData();
+ }
+
+ return { persist, refresh, isReady };
+};
+
+export const useSandbox = () => {
+ const { dispatch, usePersistent } = useStoreData();
+ const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
+ const { onboardingUrl } = dispatch;
+
+ return {
+ isSandboxMode,
+ setSandboxMode: ( state ) => {
+ setSandboxMode( state );
+ return dispatch.persist();
+ },
+ onboardingUrl,
+ };
};
export const useProduction = () => {
- const { productionOnboardingUrl } = useHooks();
+ const { dispatch } = useStoreData();
+ const { onboardingUrl } = dispatch;
- return { productionOnboardingUrl };
+ return { onboardingUrl };
};
export const useAuthentication = () => {
@@ -139,26 +162,36 @@ export const useWebhooks = () => {
};
export const useMerchantInfo = () => {
- const { isReady, features } = useHooks();
+ const { features } = useHooks();
const merchant = useMerchant();
- const { refreshMerchantData } = useDispatch( STORE_NAME );
+ const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME );
+ const { isReady } = useStore();
const verifyLoginStatus = useCallback( async () => {
const result = await refreshMerchantData();
- if ( ! result.success ) {
+ if ( ! result.success || ! result.merchant ) {
throw new Error( result?.message || result?.error?.message );
}
+ const newMerchant = result.merchant;
+
// Verify if the server state is "connected" and we have a merchant ID.
- return merchant?.isConnected && merchant?.id;
- }, [ refreshMerchantData, merchant ] );
+ if ( newMerchant?.isConnected && newMerchant?.id ) {
+ // Update the verified merchant details in Redux.
+ setMerchant( newMerchant );
+
+ return true;
+ }
+
+ return false;
+ }, [ refreshMerchantData, setMerchant ] );
return {
- isReady,
merchant, // Merchant details
features, // Eligible merchant features
verifyLoginStatus, // Callback
+ isReady,
};
};
@@ -190,12 +223,9 @@ export const useActiveModal = () => {
return { activeModal, setActiveModal };
};
-export const useActiveHighlight = () => {
- const { activeHighlight, setActiveHighlight } = useHooks();
- return { activeHighlight, setActiveHighlight };
-};
-
-// -- Not using the `useHooks()` data provider --
+/*
+ * Busy state management hooks
+ */
export const useBusyState = () => {
const { startActivity, stopActivity } = useDispatch( STORE_NAME );
@@ -225,6 +255,8 @@ export const useBusyState = () => {
);
return {
+ startActivity,
+ stopActivity,
withActivity, // HOC
isBusy, // Boolean.
};
diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js
index b4b85c23a..667beb53a 100644
--- a/modules/ppcp-settings/resources/js/data/common/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/common/reducer.js
@@ -114,6 +114,10 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
features: Object.freeze( { ...defaultTransient.features } ),
} ),
+ [ ACTION_TYPES.SET_MERCHANT ]: ( state, payload ) => {
+ return changePersistent( state, { merchant: payload.merchant } );
+ },
+
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
const newState = changePersistent( state, payload.data );
diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js
index 88bdf40ae..286285892 100644
--- a/modules/ppcp-settings/resources/js/data/debug.js
+++ b/modules/ppcp-settings/resources/js/data/debug.js
@@ -18,10 +18,15 @@ export const addDebugTools = ( context, modules ) => {
if ( ! context.debug ) { return }
*/
+ const describe = ( fnName, fnInfo ) => {
+ // eslint-disable-next-line no-console
+ console.log( `\n%c${ fnName }:`, 'font-weight:bold', fnInfo, '\n\n' );
+ };
+
const debugApi = ( window.ppcpDebugger = window.ppcpDebugger || {} );
// Dump the current state of all our Redux stores.
- debugApi.dumpStore = async () => {
+ debugApi.dumpStore = async ( cbFilter = null ) => {
/* eslint-disable no-console */
if ( ! console?.groupCollapsed ) {
console.error( 'console.groupCollapsed is not supported.' );
@@ -34,11 +39,19 @@ export const addDebugTools = ( context, modules ) => {
console.group( `[STORE] ${ storeSelector }` );
const dumpStore = ( selector ) => {
- const contents = wp.data.select( storeName )[ selector ]();
+ let contents = wp.data.select( storeName )[ selector ]();
- console.groupCollapsed( `.${ selector }()` );
- console.table( contents );
- console.groupEnd();
+ if ( cbFilter ) {
+ contents = cbFilter( contents, selector, storeName );
+
+ if ( undefined !== contents && null !== contents ) {
+ console.log( `.${ selector }() [filtered]`, contents );
+ }
+ } else {
+ console.groupCollapsed( `.${ selector }()` );
+ console.table( contents );
+ console.groupEnd();
+ }
};
Object.keys( module.selectors ).forEach( dumpStore );
@@ -51,45 +64,89 @@ export const addDebugTools = ( context, modules ) => {
// Reset all Redux stores to their initial state.
debugApi.resetStore = () => {
const stores = [];
- const { isConnected } = wp.data.select( CommonStoreName ).merchant();
- if ( isConnected ) {
- // Make sure the Onboarding wizard is "completed".
- const onboarding = wp.data.dispatch( OnboardingStoreName );
- onboarding.setPersistent( 'completed', true );
- onboarding.persist();
+ describe(
+ 'resetStore',
+ 'Reset all Redux stores to their DEFAULT state, without changing any server-side data. The default state is defined in the JS code.'
+ );
- // Reset all stores, except for the onboarding store.
- stores.push( CommonStoreName );
- stores.push( PaymentStoreName );
- stores.push( SettingsStoreName );
- stores.push( StylingStoreName );
- stores.push( TodosStoreName );
- } else {
- // Only reset the common & onboarding stores to restart the onboarding wizard.
- stores.push( CommonStoreName );
+ const { completed } = wp.data
+ .select( OnboardingStoreName )
+ .persistentData();
+
+ // Reset all stores, except for the onboarding store.
+ stores.push( CommonStoreName );
+ stores.push( PaymentStoreName );
+ stores.push( SettingsStoreName );
+ stores.push( StylingStoreName );
+ stores.push( TodosStoreName );
+
+ // Only reset the onboarding store when the wizard is not completed.
+ if ( ! completed ) {
stores.push( OnboardingStoreName );
}
stores.forEach( ( storeName ) => {
const store = wp.data.dispatch( storeName );
- // eslint-disable-next-line no-console
- console.log( `Reset store: ${ storeName }...` );
-
try {
store.reset();
- store.persist();
+
+ // eslint-disable-next-line no-console
+ console.log( `Done: Store '${ storeName }' reset` );
} catch ( error ) {
- console.error( ' ... Reset failed, skipping this store' );
+ console.error(
+ `Failed: Could not reset store '${ storeName }'`
+ );
}
} );
+
+ // eslint-disable-next-line no-console
+ console.log( '---- Complete ----\n\n' );
+ };
+
+ debugApi.refreshStore = () => {
+ const stores = [];
+
+ describe(
+ 'refreshStore',
+ 'Refreshes all Redux details with details provided by the server. This has a similar effect as reloading the page without saving'
+ );
+
+ stores.push( CommonStoreName );
+ stores.push( PaymentStoreName );
+ stores.push( SettingsStoreName );
+ stores.push( StylingStoreName );
+ stores.push( TodosStoreName );
+ stores.push( OnboardingStoreName );
+
+ stores.forEach( ( storeName ) => {
+ const store = wp.data.dispatch( storeName );
+
+ try {
+ store.refresh();
+
+ // eslint-disable-next-line no-console
+ console.log(
+ `Done: Store '${ storeName }' refreshed from REST`
+ );
+ } catch ( error ) {
+ console.error(
+ `Failed: Could not refresh store '${ storeName }' from REST`
+ );
+ }
+ } );
+
+ // eslint-disable-next-line no-console
+ console.log( '---- Complete ----\n\n' );
};
// Disconnect the merchant and display the onboarding wizard.
debugApi.disconnect = () => {
const common = wp.data.dispatch( CommonStoreName );
+ describe();
+
common.disconnectMerchant();
// eslint-disable-next-line no-console
@@ -102,6 +159,11 @@ export const addDebugTools = ( context, modules ) => {
debugApi.onboardingMode = ( state ) => {
const onboarding = wp.data.dispatch( OnboardingStoreName );
+ describe(
+ 'onboardingMode',
+ 'Toggle between onboarding wizard and the settings screen.'
+ );
+
onboarding.setPersistent( 'completed', ! state );
onboarding.persist();
};
diff --git a/modules/ppcp-settings/resources/js/data/features/action-types.js b/modules/ppcp-settings/resources/js/data/features/action-types.js
new file mode 100644
index 000000000..dc21c5110
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/action-types.js
@@ -0,0 +1,14 @@
+/**
+ * Action Types: Define unique identifiers for actions across all store modules.
+ *
+ * @file
+ */
+
+export default {
+ // Transient data
+ SET_TRANSIENT: 'ppcp/features/SET_TRANSIENT',
+
+ // Persistant data
+ SET_FEATURES: 'ppcp/features/SET_FEATURES',
+ HYDRATE: 'ppcp/features/HYDRATE',
+};
diff --git a/modules/ppcp-settings/resources/js/data/features/actions.js b/modules/ppcp-settings/resources/js/data/features/actions.js
new file mode 100644
index 000000000..6dc43653f
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/actions.js
@@ -0,0 +1,87 @@
+/**
+ * Action Creators: Define functions to create action objects.
+ *
+ * These functions update state or trigger side effects (e.g., async operations).
+ * Actions are categorized as Transient or Side effect.
+ *
+ * @file
+ */
+
+import apiFetch from '@wordpress/api-fetch';
+import ACTION_TYPES from './action-types';
+import { REST_PATH } from './constants';
+
+/**
+ * @typedef {Object} Action An action object that is handled by a reducer or control.
+ * @property {string} type - The action type.
+ * @property {Object?} payload - Optional payload for the action.
+ */
+
+/**
+ * Set the full store details during app initialization.
+ *
+ * @param {{data: {}, flags?: {}}} payload
+ * @return {Action} The action.
+ */
+export const hydrate = ( payload ) => ( {
+ type: ACTION_TYPES.HYDRATE,
+ payload,
+} );
+
+/**
+ * Generic transient-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setTransient = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { [ prop ]: value },
+} );
+
+/**
+ * Transient. Marks the store as "ready", i.e., fully initialized.
+ *
+ * @param {boolean} isReady
+ * @return {Action} The action.
+ */
+export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
+
+/**
+ * Sets the features in the store.
+ *
+ * @param {Array} features The features to set.
+ * @return {Action} The action.
+ */
+export const setFeatures = ( features ) => ( {
+ type: ACTION_TYPES.SET_FEATURES,
+ payload: features,
+} );
+
+/**
+ * Fetches features from the server.
+ *
+ * @return {Promise} The features data.
+ */
+export const fetchFeatures = async () => {
+ try {
+ const response = await apiFetch( { path: REST_PATH } );
+ if ( response?.data ) {
+ return {
+ success: true,
+ features: response.data.features,
+ };
+ }
+ return {
+ success: false,
+ features: [],
+ };
+ } catch ( e ) {
+ return {
+ success: false,
+ error: e,
+ message: e.message,
+ };
+ }
+};
diff --git a/modules/ppcp-settings/resources/js/data/features/constants.js b/modules/ppcp-settings/resources/js/data/features/constants.js
new file mode 100644
index 000000000..88961407a
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/constants.js
@@ -0,0 +1,8 @@
+/**
+ * Constants: Define store configuration values.
+ *
+ * @file
+ */
+
+export const STORE_NAME = 'wc/paypal/features';
+export const REST_PATH = '/wc/v3/wc_paypal/features';
diff --git a/modules/ppcp-settings/resources/js/data/features/hooks.js b/modules/ppcp-settings/resources/js/data/features/hooks.js
new file mode 100644
index 000000000..476aab163
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/hooks.js
@@ -0,0 +1,67 @@
+/**
+ * Hooks: Provide the main API for components to interact with the features store.
+ *
+ * These encapsulate store interactions, offering a consistent interface.
+ * Hooks simplify data access and manipulation for components.
+ *
+ * @file
+ */
+
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useEffect } from '@wordpress/element';
+import apiFetch from '@wordpress/api-fetch';
+import { STORE_NAME, REST_PATH } from './constants';
+
+export const useFeatures = () => {
+ const { features, isReady } = useSelect( ( select ) => {
+ const store = select( STORE_NAME );
+
+ return {
+ features: store.getFeatures() || [],
+ isReady: select( STORE_NAME ).transientData()?.isReady || false,
+ };
+ }, [] );
+
+ const { setFeatures, setIsReady } = useDispatch( STORE_NAME );
+
+ useEffect( () => {
+ const loadInitialFeatures = async () => {
+ try {
+ const response = await apiFetch( { path: REST_PATH } );
+
+ if ( response?.data?.features ) {
+ const featuresData = response.data.features;
+
+ if ( featuresData.length > 0 ) {
+ await setFeatures( featuresData );
+ await setIsReady( true );
+ }
+ }
+ } catch ( error ) {}
+ };
+
+ if ( ! isReady ) {
+ loadInitialFeatures();
+ }
+ }, [ isReady, setFeatures, setIsReady ] );
+
+ return {
+ features,
+ isReady,
+ fetchFeatures: async () => {
+ try {
+ const response = await apiFetch( { path: REST_PATH } );
+ const featuresData = response.data?.features || [];
+
+ if ( featuresData.length > 0 ) {
+ await setFeatures( featuresData );
+ await setIsReady( true );
+ return { success: true, features: featuresData };
+ }
+ return { success: false, features: [] };
+ } catch ( error ) {
+ return { success: false, error, message: error.message };
+ }
+ },
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/data/features/index.js b/modules/ppcp-settings/resources/js/data/features/index.js
new file mode 100644
index 000000000..ef8fc1b80
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/index.js
@@ -0,0 +1,29 @@
+import { createReduxStore, register } from '@wordpress/data';
+
+import { STORE_NAME } from './constants';
+import reducer from './reducer';
+import * as selectors from './selectors';
+import * as actions from './actions';
+import * as hooks from './hooks';
+import * as resolvers from './resolvers';
+
+/**
+ * Initializes and registers the settings store with WordPress data layer.
+ * Combines custom controls with WordPress data controls.
+ *
+ * @return {boolean} True if initialization succeeded, false otherwise.
+ */
+export const initStore = () => {
+ const store = createReduxStore( STORE_NAME, {
+ reducer,
+ actions,
+ selectors,
+ resolvers,
+ } );
+
+ register( store );
+
+ return Boolean( wp.data.select( STORE_NAME ) );
+};
+
+export { hooks, selectors, STORE_NAME };
diff --git a/modules/ppcp-settings/resources/js/data/features/reducer.js b/modules/ppcp-settings/resources/js/data/features/reducer.js
new file mode 100644
index 000000000..d3932fca4
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/reducer.js
@@ -0,0 +1,75 @@
+/**
+ * Reducer: Defines store structure and state updates for features module.
+ *
+ * Manages both transient (temporary) and persistent (saved) state.
+ * The initial state must define all properties, as dynamic additions are not supported.
+ *
+ * @file
+ */
+
+import { createReducer, createReducerSetters } from '../utils';
+import ACTION_TYPES from './action-types';
+
+// Store structure.
+
+/**
+ * Transient: Values that are _not_ saved to the DB (like app lifecycle-flags).
+ * These reset on page reload.
+ */
+const defaultTransient = Object.freeze( {
+ isReady: false,
+} );
+
+/**
+ * Persistent: Values that are loaded from and saved to the DB.
+ * These represent the core features configuration.
+ */
+const defaultPersistent = Object.freeze( {
+ features: [],
+} );
+
+// Reducer logic.
+
+const [ changeTransient, changePersistent ] = createReducerSetters(
+ defaultTransient,
+ defaultPersistent
+);
+
+/**
+ * Reducer implementation mapping actions to state updates.
+ */
+const reducer = createReducer( defaultTransient, defaultPersistent, {
+ /**
+ * Updates temporary state values
+ *
+ * @param {Object} state Current state
+ * @param {Object} payload Update payload
+ * @return {Object} Updated state
+ */
+ [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
+ changeTransient( state, payload ),
+
+ /**
+ * Updates features list
+ *
+ * @param {Object} state Current state
+ * @param {Object} payload Update payload containing features array
+ * @return {Object} Updated state
+ */
+ [ ACTION_TYPES.SET_FEATURES ]: ( state, payload ) => {
+ return changePersistent( state, { features: payload } );
+ },
+
+ /**
+ * Initializes persistent state with data from the server
+ *
+ * @param {Object} state Current state
+ * @param {Object} payload Hydration payload containing server data
+ * @param {Object} payload.data The features data to hydrate
+ * @return {Object} Hydrated state
+ */
+ [ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
+ changePersistent( state, payload.data ),
+} );
+
+export default reducer;
diff --git a/modules/ppcp-settings/resources/js/data/features/resolvers.js b/modules/ppcp-settings/resources/js/data/features/resolvers.js
new file mode 100644
index 000000000..4d9de5d2c
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/resolvers.js
@@ -0,0 +1,34 @@
+/**
+ * Resolvers: Handle asynchronous data fetching for the store.
+ *
+ * These functions update store state with data from external sources.
+ * Each resolver corresponds to a specific selector (selector with same name must exist).
+ * Resolvers are called automatically when selectors request unavailable data.
+ *
+ * @file
+ */
+
+import { __ } from '@wordpress/i18n';
+import apiFetch from '@wordpress/api-fetch';
+
+import { REST_PATH } from './constants';
+
+/**
+ * Hydrates the features data from the API.
+ *
+ * @return {Object} Action to dispatch.
+ */
+export function getFeatures() {
+ return async ( { dispatch } ) => {
+ try {
+ const response = await apiFetch( { path: REST_PATH } );
+
+ if ( response?.features ) {
+ dispatch.setFeatures( response.features );
+ dispatch.setIsReady( true );
+ }
+ } catch ( error ) {
+ console.error( 'Error fetching features:', error );
+ }
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/features/selectors.js b/modules/ppcp-settings/resources/js/data/features/selectors.js
new file mode 100644
index 000000000..98100dec9
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/features/selectors.js
@@ -0,0 +1,27 @@
+/**
+ * Selectors: Extract specific pieces of state from the store.
+ *
+ * These functions provide a consistent interface for accessing store data.
+ * They allow components to retrieve data without knowing the store structure.
+ *
+ * @file
+ */
+
+const EMPTY_OBJ = Object.freeze( {} );
+const EMPTY_ARR = Object.freeze( [] );
+
+const getState = ( state ) => state || EMPTY_OBJ;
+
+export const persistentData = ( state ) => {
+ return getState( state ).data || EMPTY_OBJ;
+};
+
+export const transientData = ( state ) => {
+ const { data, ...transientState } = getState( state );
+ return transientState || EMPTY_OBJ;
+};
+
+export const getFeatures = ( state ) => {
+ const features = state?.features || persistentData( state ).features;
+ return features || EMPTY_ARR;
+};
diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js
index 227a62226..37e1892e7 100644
--- a/modules/ppcp-settings/resources/js/data/index.js
+++ b/modules/ppcp-settings/resources/js/data/index.js
@@ -6,6 +6,7 @@ import * as Settings from './settings';
import * as Styling from './styling';
import * as Todos from './todos';
import * as PayLaterMessaging from './pay-later-messaging';
+import * as Features from './features';
const stores = [
Onboarding,
@@ -15,6 +16,7 @@ const stores = [
Styling,
Todos,
PayLaterMessaging,
+ Features,
];
stores.forEach( ( store ) => {
@@ -40,6 +42,7 @@ export const SettingsHooks = Settings.hooks;
export const StylingHooks = Styling.hooks;
export const TodosHooks = Todos.hooks;
export const PayLaterMessagingHooks = PayLaterMessaging.hooks;
+export const FeaturesHooks = Features.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME;
@@ -48,6 +51,7 @@ export const SettingsStoreName = Settings.STORE_NAME;
export const StylingStoreName = Styling.STORE_NAME;
export const TodosStoreName = Todos.STORE_NAME;
export const PayLaterMessagingStoreName = PayLaterMessaging.STORE_NAME;
+export const FeaturesStoreName = Features.STORE_NAME;
export * from './configuration';
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js
index 0d2617095..70967168d 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js
@@ -87,3 +87,16 @@ export function persist() {
}
};
}
+
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/configuration.js b/modules/ppcp-settings/resources/js/data/onboarding/configuration.js
index 4b31689b5..280fee3af 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/configuration.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/configuration.js
@@ -24,3 +24,9 @@ export const PRODUCT_TYPES = {
PHYSICAL: 'physical',
SUBSCRIPTIONS: 'subscriptions',
};
+
+export const PAYPAL_PRODUCTS = {
+ ACDC: 'PPCP',
+ BCDC: 'EXPRESS_CHECKOUT',
+ VAULTING: 'ADVANCED_VAULTING',
+};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
index 7c9ba1ee9..4d7549f5a 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
@@ -19,10 +19,6 @@ const useHooks = () => {
// 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,7 +76,6 @@ const useHooks = () => {
);
return savePersistent( setProducts, validProducts );
},
- determineProducts,
};
};
@@ -141,9 +136,9 @@ export const useNavigationState = () => {
};
export const useDetermineProducts = () => {
- const { determineProducts } = useHooks();
-
- return determineProducts;
+ return useSelect( ( select ) => {
+ return select( STORE_NAME ).determineProductsAndCaps();
+ }, [] );
};
export const useFlags = () => {
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
index cec0629ed..c6d729184 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
@@ -23,6 +23,9 @@ const defaultTransient = Object.freeze( {
canUseVaulting: false,
canUseCardPayments: false,
canUseSubscriptions: false,
+ shouldSkipPaymentMethods: false,
+ canUseFastlane: false,
+ canUsePayLater: false,
} ),
} );
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
index 9f3a7f35d..8a137dff2 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
@@ -7,6 +7,8 @@
* @file
*/
+import { PAYPAL_PRODUCTS, PRODUCT_TYPES } from './configuration';
+
const EMPTY_OBJ = Object.freeze( {} );
const getState = ( state ) => state || EMPTY_OBJ;
@@ -25,44 +27,79 @@ export const flags = ( state ) => {
};
/**
- * Returns the products that we use for the production login link in the last onboarding step.
+ * Returns details about products and capabilities to 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.
+ * @return {{products:string[], options:{}}} The ISU products, based on choices made in the onboarding wizard.
*/
-export const determineProducts = ( state ) => {
- const derivedProducts = [];
+export const determineProductsAndCaps = ( state ) => {
+ /**
+ * An array of product-names that are used to build an onboarding URL via the
+ * PartnerReferrals API. To avoid confusion with the "products" property from the
+ * Redux store, this collection has a distinct name.
+ *
+ * On server-side, this value is referred to as "products" again.
+ */
+ const apiModules = [];
- const { isCasualSeller, areOptionalPaymentMethodsEnabled } =
+ /**
+ * Internal options that are parsed by the PartnerReferrals class to customize
+ * the API payload.
+ */
+ const options = {
+ useSubscriptions: false,
+ useCardPayments: false,
+ };
+
+ const { isCasualSeller, areOptionalPaymentMethodsEnabled, products } =
persistentData( state );
const { canUseVaulting, canUseCardPayments } = flags( state );
+ const cardPaymentsEligibleAndSelected =
+ canUseCardPayments && areOptionalPaymentMethodsEnabled;
- if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) {
+ if ( ! cardPaymentsEligibleAndSelected ) {
/**
* Branch 1: Credit Card Payments not available.
* The store uses the Express-checkout product.
*/
- derivedProducts.push( 'EXPRESS_CHECKOUT' );
+ apiModules.push( PAYPAL_PRODUCTS.BCDC );
+
+ if ( products?.includes( PRODUCT_TYPES.SUBSCRIPTIONS ) ) {
+ options.useSubscriptions = true;
+ }
+
+ if ( canUseVaulting ) {
+ apiModules.push( PAYPAL_PRODUCTS.VAULTING );
+ }
} else if ( isCasualSeller ) {
/**
* Branch 2: Merchant has no business.
* The store uses the Express-checkout product.
*/
- derivedProducts.push( 'EXPRESS_CHECKOUT' );
+ apiModules.push( PAYPAL_PRODUCTS.BCDC );
} else {
/**
* Branch 3: Merchant is business, and can use CC payments.
* The store uses the advanced PPCP product.
+ *
+ * This is the only branch that can use subscriptions.
*/
- derivedProducts.push( 'PPCP' );
+ apiModules.push( PAYPAL_PRODUCTS.ACDC );
+
+ if ( products?.includes( PRODUCT_TYPES.SUBSCRIPTIONS ) ) {
+ options.useSubscriptions = true;
+ }
+
+ if ( canUseVaulting ) {
+ apiModules.push( PAYPAL_PRODUCTS.VAULTING );
+ }
}
- if ( canUseVaulting ) {
- derivedProducts.push( 'ADVANCED_VAULTING' );
- }
+ options.useCardPayments = cardPaymentsEligibleAndSelected;
- return derivedProducts;
+ return { products: apiModules, options };
};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.test.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.test.js
new file mode 100644
index 000000000..77febe32d
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.test.js
@@ -0,0 +1,181 @@
+import '@testing-library/jest-dom';
+
+import { PRODUCT_TYPES } from './configuration';
+import { determineProductsAndCaps } from './selectors';
+
+describe( 'determineProductsAndCaps selector [casual seller]', () => {
+ const testCases = [
+ {
+ name: 'should return EXPRESS_CHECKOUT when card payments are not available',
+ state: {
+ data: {
+ isCasualSeller: true,
+ areOptionalPaymentMethodsEnabled: true,
+ },
+ flags: { canUseCardPayments: false, canUseVaulting: false },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ {
+ name: 'should return EXPRESS_CHECKOUT when optional payment methods are disabled',
+ state: {
+ data: {
+ isCasualSeller: true,
+ areOptionalPaymentMethodsEnabled: false,
+ },
+ flags: { canUseCardPayments: true, canUseVaulting: false },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ {
+ name: 'should return EXPRESS_CHECKOUT for casual sellers with card payments',
+ state: {
+ data: {
+ isCasualSeller: true,
+ areOptionalPaymentMethodsEnabled: true,
+ },
+ flags: { canUseCardPayments: true, canUseVaulting: false },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: true },
+ },
+ },
+ {
+ name: 'should return EXPRESS_CHECKOUT and ADVANCED_VAULTING when card payments are not available but vaulting is',
+ state: {
+ data: {
+ isCasualSeller: true,
+ areOptionalPaymentMethodsEnabled: true,
+ },
+ flags: { canUseCardPayments: false, canUseVaulting: true },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ {
+ name: 'should ignore SUBSCRIPTION product for casual sellers',
+ state: {
+ data: {
+ isCasualSeller: true,
+ areOptionalPaymentMethodsEnabled: true,
+ products: [ PRODUCT_TYPES.SUBSCRIPTIONS ],
+ },
+ flags: { canUseCardPayments: false, canUseVaulting: true },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ ];
+
+ test.each( testCases )( '$name', ( { state, expected } ) => {
+ const result = determineProductsAndCaps( state );
+ expect( result ).toEqual( expected );
+ } );
+} );
+
+describe( 'determineProductsAndCaps selector [business seller]', () => {
+ const testCases = [
+ {
+ name: 'should return EXPRESS_CHECKOUT when card payments are not available',
+ state: {
+ data: {
+ isCasualSeller: false,
+ areOptionalPaymentMethodsEnabled: true,
+ },
+ flags: { canUseCardPayments: false, canUseVaulting: false },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ {
+ name: 'should return EXPRESS_CHECKOUT when optional payment methods are disabled',
+ state: {
+ data: {
+ isCasualSeller: false,
+ areOptionalPaymentMethodsEnabled: false,
+ },
+ flags: { canUseCardPayments: true, canUseVaulting: false },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ {
+ name: 'should return PPCP for business merchants with card payments',
+ state: {
+ data: {
+ isCasualSeller: false,
+ areOptionalPaymentMethodsEnabled: true,
+ },
+ flags: { canUseCardPayments: true, canUseVaulting: false },
+ },
+ expected: {
+ products: [ 'PPCP' ],
+ options: { useSubscriptions: false, useCardPayments: true },
+ },
+ },
+ {
+ name: 'should include ADVANCED_VAULTING when vaulting is available',
+ state: {
+ data: {
+ isCasualSeller: false,
+ areOptionalPaymentMethodsEnabled: true,
+ },
+ flags: { canUseCardPayments: true, canUseVaulting: true },
+ },
+ expected: {
+ products: [ 'PPCP', 'ADVANCED_VAULTING' ],
+ options: { useSubscriptions: false, useCardPayments: true },
+ },
+ },
+ {
+ name: 'should return EXPRESS_CHECKOUT and ADVANCED_VAULTING when card payments are not available but vaulting is',
+ state: {
+ data: {
+ isCasualSeller: false,
+ areOptionalPaymentMethodsEnabled: true,
+ products: [ PRODUCT_TYPES.VIRTUAL ],
+ },
+ flags: { canUseCardPayments: false, canUseVaulting: true },
+ },
+ expected: {
+ products: [ 'EXPRESS_CHECKOUT' ],
+ options: { useSubscriptions: false, useCardPayments: false },
+ },
+ },
+ {
+ name: 'should enable the SUBSCRIPTIONS option when a business seller selects the subscriptions-product',
+ state: {
+ data: {
+ isCasualSeller: false,
+ areOptionalPaymentMethodsEnabled: true,
+ products: [ PRODUCT_TYPES.SUBSCRIPTIONS ],
+ },
+ flags: { canUseCardPayments: true, canUseVaulting: true },
+ },
+ expected: {
+ products: [ 'PPCP', 'ADVANCED_VAULTING' ],
+ options: { useSubscriptions: true, useCardPayments: true },
+ },
+ },
+ ];
+
+ test.each( testCases )( '$name', ( { state, expected } ) => {
+ const result = determineProductsAndCaps( state );
+ expect( result ).toEqual( expected );
+ } );
+} );
diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js
index f9cd9a556..51b516bb1 100644
--- a/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js
+++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js
@@ -82,3 +82,16 @@ export function persist() {
} );
};
}
+
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/payment/action-types.js b/modules/ppcp-settings/resources/js/data/payment/action-types.js
index ac59d5161..2e3a1deeb 100644
--- a/modules/ppcp-settings/resources/js/data/payment/action-types.js
+++ b/modules/ppcp-settings/resources/js/data/payment/action-types.js
@@ -7,6 +7,8 @@
export default {
// Transient data.
SET_TRANSIENT: 'PAYMENT:SET_TRANSIENT',
+ SET_DISABLED_BY_DEPENDENCY: 'PAYMENT:SET_DISABLED_BY_DEPENDENCY',
+ RESTORE_DEPENDENCY_STATE: 'PAYMENT:RESTORE_DEPENDENCY_STATE',
// Persistent data.
SET_PERSISTENT: 'PAYMENT:SET_PERSISTENT',
diff --git a/modules/ppcp-settings/resources/js/data/payment/actions.js b/modules/ppcp-settings/resources/js/data/payment/actions.js
index 1107d5bfb..dfa100f76 100644
--- a/modules/ppcp-settings/resources/js/data/payment/actions.js
+++ b/modules/ppcp-settings/resources/js/data/payment/actions.js
@@ -94,3 +94,16 @@ export function persist() {
} );
};
}
+
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js
index 253710580..6060041f4 100644
--- a/modules/ppcp-settings/resources/js/data/payment/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js
@@ -7,22 +7,58 @@
* @file
*/
-import { useDispatch } from '@wordpress/data';
+import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils';
+import { useMemo } from '@wordpress/element';
-const useHooks = () => {
+/**
+ * Single source of truth for access Redux details.
+ *
+ * This hook returns a stable API to access actions, selectors and special hooks to generate
+ * getter- and setters for transient or persistent properties.
+ *
+ * @return {{select, dispatch, useTransient, usePersistent}} Store data API.
+ */
+const useStoreData = () => {
+ const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
+ const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
- const { persist, setPersistent, changePaymentSettings } =
- useDispatch( STORE_NAME );
- // Read-only flags and derived state.
- // Nothing here yet.
+ return useMemo(
+ () => ( {
+ select,
+ dispatch,
+ useTransient,
+ usePersistent,
+ } ),
+ [ select, dispatch, useTransient, usePersistent ]
+ );
+};
- // Transient accessors.
+export const useStore = () => {
+ const { select, useTransient, dispatch } = useStoreData();
+ const { persist, refresh, setPersistent, changePaymentSettings } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
+ // Load persistent data from REST if not done yet.
+ if ( ! isReady ) {
+ select.persistentData();
+ }
+
+ return {
+ persist,
+ refresh,
+ setPersistent,
+ changePaymentSettings,
+ isReady,
+ };
+};
+
+export const usePaymentMethods = () => {
+ const { usePersistent } = useStoreData();
+
// PayPal checkout.
const [ paypal ] = usePersistent( 'ppcp-gateway' );
const [ venmo ] = usePersistent( 'venmo' );
@@ -47,79 +83,6 @@ const useHooks = () => {
const [ pui ] = usePersistent( 'ppcp-pay-upon-invoice-gateway' );
const [ oxxo ] = usePersistent( 'ppcp-oxxo-gateway' );
- // Custom modal data.
- const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' );
- const [ threeDSecure ] = usePersistent( 'threeDSecure' );
- const [ fastlaneCardholderName ] = usePersistent(
- 'fastlaneCardholderName'
- );
- const [ fastlaneDisplayWatermark ] = usePersistent(
- 'fastlaneDisplayWatermark'
- );
-
- return {
- persist,
- isReady,
- setPersistent,
- changePaymentSettings,
- paypal,
- venmo,
- payLater,
- creditCard,
- advancedCreditCard,
- fastlane,
- applePay,
- googlePay,
- bancontact,
- blik,
- eps,
- ideal,
- mybank,
- p24,
- trustly,
- multibanco,
- pui,
- oxxo,
- paypalShowLogo,
- threeDSecure,
- fastlaneCardholderName,
- fastlaneDisplayWatermark,
- };
-};
-
-export const useStore = () => {
- const { persist, isReady, setPersistent, changePaymentSettings } =
- useHooks();
- return { persist, isReady, setPersistent, changePaymentSettings };
-};
-
-export const usePaymentMethods = () => {
- const {
- // PayPal Checkout.
- paypal,
- venmo,
- payLater,
- creditCard,
-
- // Online card payments.
- advancedCreditCard,
- fastlane,
- applePay,
- googlePay,
-
- // Local APMs.
- bancontact,
- blik,
- eps,
- ideal,
- mybank,
- p24,
- trustly,
- multibanco,
- pui,
- oxxo,
- } = useHooks();
-
const payPalCheckout = [ paypal, venmo, payLater, creditCard ];
const onlineCardPayments = [
advancedCreditCard,
@@ -169,12 +132,16 @@ export const usePaymentMethods = () => {
};
export const usePaymentMethodsModal = () => {
- const {
- paypalShowLogo,
- threeDSecure,
- fastlaneCardholderName,
- fastlaneDisplayWatermark,
- } = useHooks();
+ const { usePersistent } = useStoreData();
+
+ const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' );
+ const [ threeDSecure ] = usePersistent( 'threeDSecure' );
+ const [ fastlaneCardholderName ] = usePersistent(
+ 'fastlaneCardholderName'
+ );
+ const [ fastlaneDisplayWatermark ] = usePersistent(
+ 'fastlaneDisplayWatermark'
+ );
return {
paypalShowLogo,
diff --git a/modules/ppcp-settings/resources/js/data/payment/index.js b/modules/ppcp-settings/resources/js/data/payment/index.js
index 66d4deb79..92e977103 100644
--- a/modules/ppcp-settings/resources/js/data/payment/index.js
+++ b/modules/ppcp-settings/resources/js/data/payment/index.js
@@ -7,6 +7,8 @@ import * as actions from './actions';
import * as hooks from './hooks';
import * as resolvers from './resolvers';
import { initTodoSync } from '../sync/todo-state-sync';
+import { initPaymentDependencySync } from '../sync/payment-methods-sync';
+import { initSettingBasedPaymentMethodsSync } from '../sync/setting-based-payment-methods-sync';
/**
* Initializes and registers the settings store with WordPress data layer.
@@ -24,9 +26,13 @@ export const initStore = () => {
register( store );
- // Initialize todo sync after store registration. Potentially should be moved elsewhere.
+ // Initialize todo sync after store registration.
initTodoSync();
+ // Initialize payment method dependency sync.
+ initPaymentDependencySync();
+ initSettingBasedPaymentMethodsSync();
+
return Boolean( wp.data.select( STORE_NAME ) );
};
diff --git a/modules/ppcp-settings/resources/js/data/payment/reducer.js b/modules/ppcp-settings/resources/js/data/payment/reducer.js
index d46106f6b..4bed93c49 100644
--- a/modules/ppcp-settings/resources/js/data/payment/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/payment/reducer.js
@@ -19,6 +19,7 @@ const defaultTransient = Object.freeze( {
// Persistent: Values that are loaded from the DB.
const defaultPersistent = Object.freeze( {
+ // Payment methods.
'ppcp-gateway': {},
venmo: {},
'pay-later': {},
@@ -37,10 +38,13 @@ const defaultPersistent = Object.freeze( {
'ppcp-multibanco': {},
'ppcp-pay-upon-invoice-gateway': {},
'ppcp-oxxo-gateway': {},
+
+ // Custom payment method properties.
paypalShowLogo: false,
threeDSecure: 'no-3d-secure',
fastlaneCardholderName: false,
fastlaneDisplayWatermark: false,
+ __meta: false,
} );
// Reducer logic.
@@ -85,6 +89,56 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
+
+ [ ACTION_TYPES.SET_DISABLED_BY_DEPENDENCY ]: ( state, payload ) => {
+ const { methodId } = payload;
+ const method = state.data[ methodId ];
+
+ if ( ! method ) {
+ return state;
+ }
+
+ // Create a new state with the method disabled due to dependency
+ const updatedData = {
+ ...state.data,
+ [ methodId ]: {
+ ...method,
+ enabled: false,
+ _disabledByDependency: true,
+ _originalState: method.enabled,
+ },
+ };
+
+ return {
+ ...state,
+ data: updatedData,
+ };
+ },
+
+ [ ACTION_TYPES.RESTORE_DEPENDENCY_STATE ]: ( state, payload ) => {
+ const { methodId } = payload;
+ const method = state.data[ methodId ];
+
+ if ( ! method || ! method._disabledByDependency ) {
+ return state;
+ }
+
+ // Restore the method to its original state
+ const updatedData = {
+ ...state.data,
+ [ methodId ]: {
+ ...method,
+ enabled: method._originalState === true,
+ _disabledByDependency: false,
+ _originalState: undefined,
+ },
+ };
+
+ return {
+ ...state,
+ data: updatedData,
+ };
+ },
} );
export default reducer;
diff --git a/modules/ppcp-settings/resources/js/data/settings/actions.js b/modules/ppcp-settings/resources/js/data/settings/actions.js
index 6633516bb..f99adae5a 100644
--- a/modules/ppcp-settings/resources/js/data/settings/actions.js
+++ b/modules/ppcp-settings/resources/js/data/settings/actions.js
@@ -84,3 +84,16 @@ export function persist() {
} );
};
}
+
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/settings/hooks.js b/modules/ppcp-settings/resources/js/data/settings/hooks.js
index f161ee48e..4a97afa9e 100644
--- a/modules/ppcp-settings/resources/js/data/settings/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/settings/hooks.js
@@ -6,17 +6,38 @@
*
* @file
*/
-import { useDispatch } from '@wordpress/data';
+import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils';
+import { useMemo } from '@wordpress/element';
+
+/**
+ * Single source of truth for access Redux details.
+ *
+ * This hook returns a stable API to access actions, selectors and special hooks to generate
+ * getter- and setters for transient or persistent properties.
+ *
+ * @return {{select, dispatch, useTransient, usePersistent}} Store data API.
+ */
+const useStoreData = () => {
+ const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
+ const dispatch = useDispatch( STORE_NAME );
+ const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
+
+ return useMemo(
+ () => ( {
+ select,
+ dispatch,
+ useTransient,
+ usePersistent,
+ } ),
+ [ select, dispatch, useTransient, usePersistent ]
+ );
+};
const useHooks = () => {
- const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
- const { persist } = useDispatch( STORE_NAME );
-
- // Read-only flags and derived state.
- const [ isReady ] = useTransient( 'isReady' );
+ const { usePersistent } = useStoreData();
// Persistent accessors.
const [ invoicePrefix, setInvoicePrefix ] =
@@ -47,8 +68,6 @@ const useHooks = () => {
usePersistent( 'disabledCards' );
return {
- persist,
- isReady,
invoicePrefix,
setInvoicePrefix,
authorizeOnly,
@@ -79,8 +98,16 @@ const useHooks = () => {
};
export const useStore = () => {
- const { persist, isReady } = useHooks();
- return { persist, isReady };
+ const { select, dispatch, useTransient } = useStoreData();
+ const { persist, refresh } = dispatch;
+ const [ isReady ] = useTransient( 'isReady' );
+
+ // Load persistent data from REST if not done yet.
+ if ( ! isReady ) {
+ select.persistentData();
+ }
+
+ return { persist, refresh, isReady };
};
export const useSettings = () => {
diff --git a/modules/ppcp-settings/resources/js/data/styling/actions.js b/modules/ppcp-settings/resources/js/data/styling/actions.js
index 9e1639c7f..72d4d9ea7 100644
--- a/modules/ppcp-settings/resources/js/data/styling/actions.js
+++ b/modules/ppcp-settings/resources/js/data/styling/actions.js
@@ -82,3 +82,16 @@ export function persist() {
} );
};
}
+
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
diff --git a/modules/ppcp-settings/resources/js/data/styling/configuration.js b/modules/ppcp-settings/resources/js/data/styling/configuration.js
index f6e3feddb..366ecbd33 100644
--- a/modules/ppcp-settings/resources/js/data/styling/configuration.js
+++ b/modules/ppcp-settings/resources/js/data/styling/configuration.js
@@ -104,33 +104,28 @@ export const STYLING_SHAPES = {
};
export const STYLING_PAYMENT_METHODS = {
- paypal: {
- value: '',
+ 'ppcp-gateway': {
+ value: 'ppcp-gateway',
label: __( 'PayPal', 'woocommerce-paypal-payments' ),
checked: true,
disabled: true,
- paymentMethod: 'ppcp-gateway',
},
venmo: {
value: 'venmo',
label: __( 'Venmo', 'woocommerce-paypal-payments' ),
isFunding: true,
- paymentMethod: 'venmo',
},
- paylater: {
- value: 'paylater',
+ 'pay-later': {
+ value: 'pay-later',
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
isFunding: true,
- paymentMethod: 'pay-later',
},
- googlepay: {
- value: 'googlepay',
+ 'ppcp-googlepay': {
+ value: 'ppcp-googlepay',
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),
- paymentMethod: 'ppcp-googlepay',
},
- applepay: {
- value: 'applepay',
+ 'ppcp-applepay': {
+ value: 'ppcp-applepay',
label: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
- paymentMethod: 'ppcp-applepay',
},
};
diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js
index 8227d0124..ecf999eaf 100644
--- a/modules/ppcp-settings/resources/js/data/styling/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js
@@ -7,7 +7,7 @@
* @file
*/
-import { useCallback } from '@wordpress/element';
+import { useCallback, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { createHooksForStore } from '../utils';
@@ -20,13 +20,37 @@ import {
STYLING_PAYMENT_METHODS,
STYLING_SHAPES,
} from './configuration';
+import { persistentData } from './selectors';
+
+/**
+ * Single source of truth for access Redux details.
+ *
+ * This hook returns a stable API to access actions, selectors and special hooks to generate
+ * getter- and setters for transient or persistent properties.
+ *
+ * @return {{select, dispatch, useTransient, usePersistent}} Store data API.
+ */
+const useStoreData = () => {
+ const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
+ const dispatch = useDispatch( STORE_NAME );
+ const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
+
+ return useMemo(
+ () => ( {
+ select,
+ dispatch,
+ useTransient,
+ usePersistent,
+ } ),
+ [ select, dispatch, useTransient, usePersistent ]
+ );
+};
const useHooks = () => {
- const { useTransient } = createHooksForStore( STORE_NAME );
- const { persist, setPersistent } = useDispatch( STORE_NAME );
+ const { useTransient, dispatch } = useStoreData();
+ const { setPersistent } = dispatch;
// Transient accessors.
- const [ isReady ] = useTransient( 'isReady' );
const [ location, setLocation ] = useTransient( 'location' );
// Persistent accessors.
@@ -61,8 +85,6 @@ const useHooks = () => {
);
return {
- persist,
- isReady,
location,
setLocation,
getLocationProp,
@@ -71,8 +93,16 @@ const useHooks = () => {
};
export const useStore = () => {
- const { persist, isReady } = useHooks();
- return { persist, isReady };
+ const { select, dispatch, useTransient } = useStoreData();
+ const { persist, refresh } = dispatch;
+ const [ isReady ] = useTransient( 'isReady' );
+
+ // Load persistent data from REST if not done yet.
+ if ( ! isReady ) {
+ select.persistentData();
+ }
+
+ return { persist, refresh, isReady };
};
export const useStylingLocation = () => {
diff --git a/modules/ppcp-settings/resources/js/data/sync/payment-methods-sync.js b/modules/ppcp-settings/resources/js/data/sync/payment-methods-sync.js
new file mode 100644
index 000000000..420b9462e
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/sync/payment-methods-sync.js
@@ -0,0 +1,128 @@
+import { subscribe, select } from '@wordpress/data';
+
+// Store name
+const PAYMENT_STORE = 'wc/paypal/payment';
+
+// Track original states of dependent methods
+const originalStates = {};
+
+/**
+ * Initialize payment method dependency synchronization
+ */
+export const initPaymentDependencySync = () => {
+ let previousPaymentState = null;
+ let isProcessing = false;
+
+ const unsubscribe = subscribe( () => {
+ if ( isProcessing ) {
+ return;
+ }
+
+ isProcessing = true;
+
+ try {
+ const paymentHooks = select( PAYMENT_STORE );
+ if ( ! paymentHooks ) {
+ isProcessing = false;
+ return;
+ }
+
+ const methods = paymentHooks.persistentData();
+ if ( ! methods ) {
+ isProcessing = false;
+ return;
+ }
+
+ if ( ! previousPaymentState ) {
+ previousPaymentState = { ...methods };
+ isProcessing = false;
+ return;
+ }
+
+ const changedMethods = Object.keys( methods )
+ .filter(
+ ( key ) =>
+ key !== '__meta' &&
+ methods[ key ] &&
+ previousPaymentState[ key ]
+ )
+ .filter(
+ ( methodId ) =>
+ methods[ methodId ].enabled !==
+ previousPaymentState[ methodId ].enabled
+ );
+
+ if ( changedMethods.length > 0 ) {
+ changedMethods.forEach( ( changedId ) => {
+ const isNowEnabled = methods[ changedId ].enabled;
+
+ const dependents = Object.entries( methods )
+ .filter(
+ ( [ key, method ] ) =>
+ key !== '__meta' &&
+ method &&
+ method.depends_on_payment_methods &&
+ method.depends_on_payment_methods.includes(
+ changedId
+ )
+ )
+ .map( ( [ key ] ) => key );
+
+ if ( dependents.length > 0 ) {
+ if ( ! isNowEnabled ) {
+ handleDisableDependents( dependents, methods );
+ } else {
+ handleRestoreDependents( dependents, methods );
+ }
+ }
+ } );
+ }
+
+ previousPaymentState = { ...methods };
+ } catch ( error ) {
+ // Keep error handling without the console.error
+ } finally {
+ isProcessing = false;
+ }
+ } );
+
+ return unsubscribe;
+};
+
+const handleDisableDependents = ( dependentIds, methods ) => {
+ dependentIds.forEach( ( methodId ) => {
+ if ( methods[ methodId ] ) {
+ if ( ! ( methodId in originalStates ) ) {
+ originalStates[ methodId ] = methods[ methodId ].enabled;
+ }
+ methods[ methodId ].enabled = false;
+ methods[ methodId ].isDisabled = true;
+ }
+ } );
+};
+
+const handleRestoreDependents = ( dependentIds, methods ) => {
+ dependentIds.forEach( ( methodId ) => {
+ if (
+ methods[ methodId ] &&
+ methodId in originalStates &&
+ checkAllDependenciesSatisfied( methodId, methods )
+ ) {
+ methods[ methodId ].enabled = originalStates[ methodId ];
+ methods[ methodId ].isDisabled = false;
+ delete originalStates[ methodId ];
+ }
+ } );
+};
+
+const checkAllDependenciesSatisfied = ( methodId, methods ) => {
+ const method = methods[ methodId ];
+ if ( ! method || ! method.depends_on_payment_methods ) {
+ return true;
+ }
+
+ return ! method.depends_on_payment_methods.some( ( parentId ) => {
+ const parent = methods[ parentId ];
+ return ! parent || parent.enabled === false;
+ } );
+};
diff --git a/modules/ppcp-settings/resources/js/data/sync/setting-based-payment-methods-sync.js b/modules/ppcp-settings/resources/js/data/sync/setting-based-payment-methods-sync.js
new file mode 100644
index 000000000..628936bf2
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/sync/setting-based-payment-methods-sync.js
@@ -0,0 +1,158 @@
+import { subscribe, select } from '@wordpress/data';
+
+// Store names
+const PAYMENT_STORE = 'wc/paypal/payment';
+const SETTINGS_STORE = 'wc/paypal/settings';
+
+// Track original states of methods affected by settings
+const settingDependentStates = {};
+
+/**
+ * Initialize setting dependency synchronization
+ */
+export const initSettingBasedPaymentMethodsSync = () => {
+ let previousSettingsState = null;
+ let isProcessing = false;
+
+ const unsubscribe = subscribe( () => {
+ if ( isProcessing ) {
+ return;
+ }
+
+ isProcessing = true;
+
+ try {
+ // Get both settings and payment stores
+ const settingsHooks = select( SETTINGS_STORE );
+ const paymentHooks = select( PAYMENT_STORE );
+
+ if ( ! settingsHooks || ! paymentHooks ) {
+ isProcessing = false;
+ return;
+ }
+
+ const settings = settingsHooks.persistentData();
+ const methods = paymentHooks.persistentData();
+
+ if ( ! settings || ! methods ) {
+ isProcessing = false;
+ return;
+ }
+
+ if ( ! previousSettingsState ) {
+ previousSettingsState = { ...settings };
+ isProcessing = false;
+ return;
+ }
+
+ // Find which settings changed
+ const changedSettings = Object.keys( settings ).filter(
+ ( key ) =>
+ previousSettingsState[ key ] !== undefined &&
+ settings[ key ] !== previousSettingsState[ key ]
+ );
+
+ if ( changedSettings.length > 0 ) {
+ // Process affected payment methods for each changed setting
+ for ( const methodId in methods ) {
+ if ( methodId === '__meta' || ! methods[ methodId ] ) {
+ continue;
+ }
+
+ const method = methods[ methodId ];
+
+ // Skip methods without setting dependencies
+ if ( ! method.depends_on_settings?.settings ) {
+ continue;
+ }
+
+ const { settings: dependencySettings } =
+ method.depends_on_settings;
+
+ // Check if any of the changed settings affects this method
+ const relevantSettings = Object.values(
+ dependencySettings
+ ).filter( ( setting ) =>
+ changedSettings.includes( setting.id )
+ );
+
+ if ( relevantSettings.length > 0 ) {
+ // Determine if method should be disabled based on new setting values
+ const shouldBeDisabled = relevantSettings.some(
+ ( setting ) =>
+ settings[ setting.id ] !== setting.value
+ );
+
+ if ( shouldBeDisabled ) {
+ // Store original state before disabling
+ if ( ! ( methodId in settingDependentStates ) ) {
+ settingDependentStates[ methodId ] =
+ method.enabled;
+ }
+
+ // Disable the method
+ methods[ methodId ].enabled = false;
+ methods[ methodId ].isDisabled = true;
+ } else {
+ // Check if all setting dependencies are now satisfied
+ const allSettingsSatisfied = Object.values(
+ dependencySettings
+ ).every(
+ ( setting ) =>
+ settings[ setting.id ] === setting.value
+ );
+
+ // Also check payment method dependencies
+ const paymentDependenciesSatisfied =
+ checkPaymentDependenciesSatisfied(
+ methodId,
+ methods
+ );
+
+ // If all dependencies are satisfied, restore the original state
+ if (
+ allSettingsSatisfied &&
+ paymentDependenciesSatisfied &&
+ methodId in settingDependentStates
+ ) {
+ methods[ methodId ].enabled =
+ settingDependentStates[ methodId ];
+ methods[ methodId ].isDisabled = false;
+ delete settingDependentStates[ methodId ];
+ }
+ }
+ }
+ }
+ }
+
+ previousSettingsState = { ...settings };
+ } catch ( error ) {
+ // Silent error handling
+ } finally {
+ isProcessing = false;
+ }
+ } );
+
+ return unsubscribe;
+};
+
+/**
+ * Check if all payment method dependencies are satisfied for a method
+ *
+ * @param {string} methodId - ID of the method to check
+ * @param {Object} methods - All payment methods
+ * @return {boolean} True if all dependencies are satisfied
+ */
+const checkPaymentDependenciesSatisfied = ( methodId, methods ) => {
+ const method = methods[ methodId ];
+ if ( ! method || ! method.depends_on_payment_methods ) {
+ return true;
+ }
+
+ return ! method.depends_on_payment_methods.some( ( parentId ) => {
+ const parent = methods[ parentId ];
+ return ! parent || parent.enabled === false;
+ } );
+};
+
+export default initSettingBasedPaymentMethodsSync;
diff --git a/modules/ppcp-settings/resources/js/data/todos/action-types.js b/modules/ppcp-settings/resources/js/data/todos/action-types.js
index 66d92218a..17bbe8a0f 100644
--- a/modules/ppcp-settings/resources/js/data/todos/action-types.js
+++ b/modules/ppcp-settings/resources/js/data/todos/action-types.js
@@ -5,6 +5,12 @@
*/
export default {
+ /**
+ * Resets the store state to its initial values.
+ * Used when needing to clear all store data.
+ */
+ RESET: 'ppcp/todos/RESET',
+
// Transient data
SET_TRANSIENT: 'ppcp/todos/SET_TRANSIENT',
SET_COMPLETED_TODOS: 'ppcp/todos/SET_COMPLETED_TODOS',
diff --git a/modules/ppcp-settings/resources/js/data/todos/actions.js b/modules/ppcp-settings/resources/js/data/todos/actions.js
index 41ac372cd..25b6d7647 100644
--- a/modules/ppcp-settings/resources/js/data/todos/actions.js
+++ b/modules/ppcp-settings/resources/js/data/todos/actions.js
@@ -17,11 +17,47 @@ import {
REST_RESET_DISMISSED_TODOS_PATH,
} from './constants';
-export const setIsReady = ( isReady ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { isReady },
+/**
+ * Special. Resets all values in the store to initial defaults.
+ *
+ * @return {Object} The action.
+ */
+export const reset = () => ( {
+ type: ACTION_TYPES.RESET,
} );
+/**
+ * Generic transient-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Object} The action.
+ */
+export const setTransient = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { [ prop ]: value },
+} );
+
+/**
+ * Generic persistent-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Object} The action.
+ */
+export const setPersistent = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { [ prop ]: value },
+} );
+
+/**
+ * Transient. Marks the store as "ready", i.e., fully initialized.
+ *
+ * @param {boolean} isReady Whether the store is ready
+ * @return {Object} The action.
+ */
+export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
+
export const setTodos = ( todos ) => ( {
type: ACTION_TYPES.SET_TODOS,
payload: todos,
@@ -39,6 +75,7 @@ export const setCompletedTodos = ( completedTodos ) => ( {
// Thunks
+// TODO: Possibly, this should be a resolver?
export function fetchTodos() {
return async () => {
const response = await apiFetch( { path: REST_PATH } );
@@ -46,9 +83,14 @@ export function fetchTodos() {
};
}
+/**
+ * Thunk action creator. Triggers the persistence of store data to the server.
+ *
+ * @return {Function} The thunk function.
+ */
export function persist() {
return async ( { select } ) => {
- return await apiFetch( {
+ await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data: select.persistentData(),
@@ -56,6 +98,19 @@ export function persist() {
};
}
+/**
+ * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
+ *
+ * @return {Function} The thunk function.
+ */
+export function refresh() {
+ return ( { dispatch, select } ) => {
+ dispatch.invalidateResolutionForStore();
+
+ select.persistentData();
+ };
+}
+
export function resetDismissedTodos() {
return async ( { dispatch } ) => {
try {
diff --git a/modules/ppcp-settings/resources/js/data/todos/hooks.js b/modules/ppcp-settings/resources/js/data/todos/hooks.js
index 7fc5da5c1..42f02989a 100644
--- a/modules/ppcp-settings/resources/js/data/todos/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/todos/hooks.js
@@ -10,31 +10,40 @@
import { useSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils';
+import { useMemo } from '@wordpress/element';
-const ensureArray = ( value ) => {
- if ( ! value ) {
- return [];
- }
- return Array.isArray( value ) ? value : Object.values( value );
+/**
+ * Single source of truth for access Redux details.
+ *
+ * This hook returns a stable API to access actions, selectors and special hooks to generate
+ * getter- and setters for transient or persistent properties.
+ *
+ * @return {{select, dispatch, useTransient, usePersistent}} Store data API.
+ */
+const useStoreData = () => {
+ const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
+ const dispatch = useDispatch( STORE_NAME );
+ const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
+
+ return useMemo(
+ () => ( {
+ select,
+ dispatch,
+ useTransient,
+ usePersistent,
+ } ),
+ [ select, dispatch, useTransient, usePersistent ]
+ );
};
const useHooks = () => {
- const { useTransient } = createHooksForStore( STORE_NAME );
- const { fetchTodos, setDismissedTodos, setCompletedTodos, persist } =
- useDispatch( STORE_NAME );
-
- // Read-only flags and derived state.
- const [ isReady ] = useTransient( 'isReady' );
+ const { dispatch, select } = useStoreData();
+ const { fetchTodos, setDismissedTodos, setCompletedTodos } = dispatch;
// Get todos data from store
- const { todos, dismissedTodos, completedTodos } = useSelect( ( select ) => {
- const store = select( STORE_NAME );
- return {
- todos: ensureArray( store.getTodos() ),
- dismissedTodos: ensureArray( store.getDismissedTodos() ),
- completedTodos: ensureArray( store.getCompletedTodos() ),
- };
- }, [] );
+ const todos = select.getTodos();
+ const dismissedTodos = select.getDismissedTodos();
+ const completedTodos = select.getCompletedTodos();
const dismissedSet = new Set( dismissedTodos );
@@ -62,8 +71,6 @@ const useHooks = () => {
);
return {
- persist,
- isReady,
todos: filteredTodos,
dismissedTodos,
completedTodos,
@@ -74,13 +81,21 @@ const useHooks = () => {
};
export const useStore = () => {
- const { persist, isReady } = useHooks();
- return { persist, isReady };
+ const { select, dispatch, useTransient } = useStoreData();
+ const { persist, refresh } = dispatch;
+ const [ isReady ] = useTransient( 'isReady' );
+
+ // Load persistent data from REST if not done yet.
+ if ( ! isReady ) {
+ select.getTodos();
+ }
+
+ return { persist, refresh, isReady };
};
export const useTodos = () => {
- const { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady } =
- useHooks();
+ const { todos, fetchTodos, dismissTodo, setTodoCompleted } = useHooks();
+ const { isReady } = useStore();
return { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady };
};
diff --git a/modules/ppcp-settings/resources/js/data/todos/reducer.js b/modules/ppcp-settings/resources/js/data/todos/reducer.js
index 6f4c74d46..f3cc412ac 100644
--- a/modules/ppcp-settings/resources/js/data/todos/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/todos/reducer.js
@@ -52,6 +52,21 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
+ /**
+ * Resets state to defaults while maintaining initialization status
+ *
+ * @param {Object} state Current state
+ * @return {Object} Reset state
+ */
+ [ ACTION_TYPES.RESET ]: ( state ) => {
+ const cleanState = changeTransient(
+ changePersistent( state, defaultPersistent ),
+ defaultTransient
+ );
+ cleanState.isReady = true; // Keep initialization flag
+ return cleanState;
+ },
+
/**
* Updates todos list
*
@@ -99,6 +114,7 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
},
/**
+ * TODO: This is not used anywhere. Remove "SET_TODOS" and use this resolver instead.
* Initializes persistent state with data from the server
*
* @param {Object} state Current state
diff --git a/modules/ppcp-settings/resources/js/data/todos/selectors.js b/modules/ppcp-settings/resources/js/data/todos/selectors.js
index 2f42c6ffc..1066ba3a8 100644
--- a/modules/ppcp-settings/resources/js/data/todos/selectors.js
+++ b/modules/ppcp-settings/resources/js/data/todos/selectors.js
@@ -11,7 +11,17 @@ const EMPTY_OBJ = Object.freeze( {} );
const EMPTY_ARR = Object.freeze( [] );
const getState = ( state ) => state || EMPTY_OBJ;
+const getArray = ( value ) => {
+ if ( Array.isArray( value ) ) {
+ return value;
+ }
+ if ( value ) {
+ return Object.values( value );
+ }
+ return EMPTY_ARR;
+};
+// TODO: Implement a persistentData resolver!
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
@@ -23,15 +33,15 @@ export const transientData = ( state ) => {
export const getTodos = ( state ) => {
const todos = state?.todos || persistentData( state ).todos;
- return todos || EMPTY_ARR;
+ return getArray( todos );
};
export const getDismissedTodos = ( state ) => {
const dismissed =
state?.dismissedTodos || persistentData( state ).dismissedTodos;
- return dismissed || EMPTY_ARR;
+ return getArray( dismissed );
};
export const getCompletedTodos = ( state ) => {
- return state?.completedTodos || EMPTY_ARR; // Only look at root state, not persistent data
+ return getArray( state?.completedTodos ); // Only look at root state, not persistent data
};
diff --git a/modules/ppcp-settings/resources/js/hooks/useAccordionState.js b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js
deleted file mode 100644
index f54018262..000000000
--- a/modules/ppcp-settings/resources/js/hooks/useAccordionState.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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
index f6837c488..aa00c3416 100644
--- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js
+++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js
@@ -4,25 +4,14 @@ import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks, OnboardingHooks } from '../data';
+import { useStoreManager } from './useStoreManager';
const PAYPAL_PARTNER_SDK_URL =
'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js';
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: __(
+ API_ERROR: __(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments'
),
@@ -33,30 +22,25 @@ const MESSAGES = {
};
const ACTIVITIES = {
- CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX',
- CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION',
- CONNECT_ISU: 'ISU_LOGIN',
- CONNECT_MANUAL: 'MANUAL_LOGIN',
+ OAUTH_VERIFY: 'oauth/login',
+ API_LOGIN: 'auth/api-login',
+ API_VERIFY: 'auth/verify-login',
};
export const useHandleOnboardingButton = ( isSandbox ) => {
- const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
- const { productionOnboardingUrl } = CommonHooks.useProduction();
- const products = OnboardingHooks.useDetermineProducts();
- const { withActivity } = CommonHooks.useBusyState();
+ const { onboardingUrl } = isSandbox
+ ? CommonHooks.useSandbox()
+ : CommonHooks.useProduction();
+ const { products, options } = OnboardingHooks.useDetermineProducts();
+ const { startActivity } = CommonHooks.useBusyState();
const { authenticateWithOAuth } = CommonHooks.useAuthentication();
- const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
+ const [ onboardingUrlState, setOnboardingUrl ] = useState( '' );
const [ scriptLoaded, setScriptLoaded ] = useState( false );
const timerRef = useRef( null );
useEffect( () => {
const fetchOnboardingUrl = async () => {
- let res;
- if ( isSandbox ) {
- res = await sandboxOnboardingUrl();
- } else {
- res = await productionOnboardingUrl( products );
- }
+ const res = await onboardingUrl( products, options, isSandbox );
if ( res.success && res.data ) {
setOnboardingUrl( res.data );
@@ -66,7 +50,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
};
fetchOnboardingUrl();
- }, [ isSandbox, productionOnboardingUrl, products, sandboxOnboardingUrl ] );
+ }, [ isSandbox, products, options, onboardingUrl ] );
useEffect( () => {
/**
@@ -74,7 +58,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
* When no buttons are present, a JS error is displayed; i.e. we should load this script
* only when the button is ready (with a valid href and data-attributes).
*/
- if ( ! onboardingUrl ) {
+ if ( ! onboardingUrlState ) {
return;
}
@@ -109,7 +93,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
}
} );
};
- }, [ onboardingUrl ] );
+ }, [ onboardingUrlState ] );
const setCompleteHandler = useCallback(
( environment ) => {
@@ -123,16 +107,15 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
* frame before the REST endpoint returns a value. Using "withActivity" is more of a
* visual cue to the user that something is still processing in the background.
*/
- await withActivity(
- ACTIVITIES.CONNECT_ISU,
- 'Validating the connection details',
- async () => {
- await authenticateWithOAuth(
- sharedId,
- authCode,
- 'sandbox' === environment
- );
- }
+ startActivity(
+ ACTIVITIES.OAUTH_VERIFY,
+ 'Validating the connection details'
+ );
+
+ await authenticateWithOAuth(
+ sharedId,
+ authCode,
+ environment === 'sandbox'
);
};
@@ -148,7 +131,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
// Ensure the onComplete handler is not removed by a PayPal init script.
timerRef.current = setInterval( addHandler, 250 );
},
- [ authenticateWithOAuth, withActivity ]
+ [ authenticateWithOAuth, startActivity ]
);
const removeCompleteHandler = useCallback( () => {
@@ -161,18 +144,21 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
}, [] );
return {
- onboardingUrl,
+ onboardingUrl: onboardingUrlState,
scriptLoaded,
setCompleteHandler,
removeCompleteHandler,
};
};
+// Base connection is only used for API login (manual connection).
const useConnectionBase = () => {
const { setCompleted } = OnboardingHooks.useSteps();
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
+ const { withActivity } = CommonHooks.useBusyState();
+ const { refreshAll } = useStoreManager();
return {
handleFailed: ( res, genericMessage ) => {
@@ -180,18 +166,27 @@ const useConnectionBase = () => {
createErrorNotice( res?.message ?? genericMessage );
},
handleCompleted: async () => {
- try {
- const loginSuccessful = await verifyLoginStatus();
+ await withActivity(
+ ACTIVITIES.API_VERIFY,
+ 'Verifying Authentication',
+ async () => {
+ try {
+ const loginSuccessful = await verifyLoginStatus();
- if ( loginSuccessful ) {
- createSuccessNotice( MESSAGES.CONNECTED );
- await setCompleted( true );
- } else {
- createErrorNotice( MESSAGES.LOGIN_FAILED );
+ if ( loginSuccessful ) {
+ createSuccessNotice( MESSAGES.CONNECTED );
+ await setCompleted( true );
+ refreshAll();
+ } else {
+ createErrorNotice( MESSAGES.LOGIN_FAILED );
+ }
+ } catch ( error ) {
+ createErrorNotice(
+ error.message ?? MESSAGES.LOGIN_FAILED
+ );
+ }
}
- } catch ( error ) {
- createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED );
- }
+ );
},
createErrorNotice,
};
@@ -218,7 +213,7 @@ export const useDirectAuthentication = () => {
const handleDirectAuthentication = async ( connectionDetails ) => {
return withActivity(
- ACTIVITIES.CONNECT_MANUAL,
+ ACTIVITIES.API_LOGIN,
'Connecting manually via Client ID and Secret',
async () => {
let data;
@@ -250,7 +245,7 @@ export const useDirectAuthentication = () => {
if ( res.success ) {
await handleCompleted();
} else {
- handleFailed( res, MESSAGES.MANUAL_ERROR );
+ handleFailed( res, MESSAGES.API_ERROR );
}
return res.success;
diff --git a/modules/ppcp-settings/resources/js/hooks/useNavigation.js b/modules/ppcp-settings/resources/js/hooks/useNavigation.js
index f477a0b91..af180fea3 100644
--- a/modules/ppcp-settings/resources/js/hooks/useNavigation.js
+++ b/modules/ppcp-settings/resources/js/hooks/useNavigation.js
@@ -1,3 +1,5 @@
+import { scrollAndHighlight } from '../utils/scrollAndHighlight';
+
/**
* Navigate to the WooCommerce "Payments" settings tab, i.e. exit the settings app.
*/
@@ -5,6 +7,55 @@ const goToWooCommercePaymentsTab = () => {
window.location.href = window.ppcpSettings.wcPaymentsTabUrl;
};
-export const useNavigation = () => {
- return { goToWooCommercePaymentsTab };
+/**
+ * Navigate to the main settings page, or to a defined tab (panel).
+ * Always initiates a browser navigation - if the user already is on the defined settings page,
+ * this function acts as a page-reload.
+ *
+ * @param {?string} [panel=null] Which settings tab to display.
+ */
+const goToPluginSettings = ( panel = null ) => {
+ let url = window.ppcpSettings.pluginSettingsUrl;
+
+ if ( panel ) {
+ url += '&panel=' + panel;
+ }
+
+ window.location.href = url;
+};
+
+/**
+ * Check URL for highlight parameter and scroll to the element if present.
+ *
+ * @return {boolean} Whether a highlight parameter was found and processed
+ */
+const handleHighlightFromUrl = () => {
+ const urlParams = new URLSearchParams( window.location.search );
+ const elementId = urlParams.get( 'highlight' );
+
+ if ( elementId ) {
+ setTimeout( () => {
+ scrollAndHighlight( elementId );
+
+ // Clean up the URL by removing the highlight parameter.
+ urlParams.delete( 'highlight' );
+ const newUrl =
+ window.location.pathname +
+ ( urlParams.toString() ? '?' + urlParams.toString() : '' ) +
+ window.location.hash;
+
+ window.history.replaceState( {}, document.title, newUrl );
+ }, 100 );
+ return true;
+ }
+
+ return false;
+};
+
+export const useNavigation = () => {
+ return {
+ goToWooCommercePaymentsTab,
+ goToPluginSettings,
+ handleHighlightFromUrl,
+ };
};
diff --git a/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js b/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js
new file mode 100644
index 000000000..3fb087c1f
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js
@@ -0,0 +1,78 @@
+/**
+ * Custom hook to handle payment-method-based dependencies
+ */
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Gets the display name for a parent payment method
+ *
+ * @param {string} parentId - ID of the parent payment method
+ * @param {Object} methodsMap - Map of all payment methods by ID
+ * @return {string} The display name to use for the parent method
+ */
+const getParentMethodName = ( parentId, methodsMap ) => {
+ const parentMethod = methodsMap[ parentId ];
+ return parentMethod
+ ? parentMethod.itemTitle || parentMethod.title || ''
+ : '';
+};
+
+/**
+ * Finds disabled parent dependencies for a method
+ *
+ * @param {Object} method - The payment method to check
+ * @param {Object} methodsMap - Map of all payment methods by ID
+ * @return {Array} List of disabled parent IDs, empty if none
+ */
+const findDisabledParents = ( method, methodsMap ) => {
+ const dependencies = method.depends_on_payment_methods;
+
+ if ( ! dependencies || ! Array.isArray( dependencies ) ) {
+ return [];
+ }
+
+ return dependencies.filter( ( parentId ) => {
+ const parent = methodsMap[ parentId ];
+ return parent && ! parent.enabled;
+ } );
+};
+
+/**
+ * Hook to evaluate payment method dependencies
+ *
+ * @param {Array} methods - List of payment methods
+ * @param {Object} methodsMap - Map of payment methods by ID
+ * @return {Object} Dependency state object keyed by method ID
+ */
+const usePaymentDependencyState = ( methods, methodsMap ) => {
+ return useSelect( () => {
+ const result = {};
+
+ if ( methods && methodsMap && Object.keys( methodsMap ).length > 0 ) {
+ methods.forEach( ( method ) => {
+ if ( method && method.id ) {
+ const disabledParents = findDisabledParents(
+ method,
+ methodsMap
+ );
+
+ if ( disabledParents.length > 0 ) {
+ const parentId = disabledParents[ 0 ];
+ result[ method.id ] = {
+ isDisabled: true,
+ parentId,
+ parentName: getParentMethodName(
+ parentId,
+ methodsMap
+ ),
+ };
+ }
+ }
+ } );
+ }
+
+ return result;
+ }, [ methods, methodsMap ] );
+};
+
+export default usePaymentDependencyState;
diff --git a/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js b/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js
deleted file mode 100644
index 7237aa4df..000000000
--- a/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { useCallback, useMemo } from '@wordpress/element';
-
-import {
- CommonHooks,
- PayLaterMessagingHooks,
- PaymentHooks,
- SettingsHooks,
- StylingHooks,
- TodosHooks,
-} from '../data';
-
-export const useSaveSettings = () => {
- const { withActivity } = CommonHooks.useBusyState();
-
- const { persist: persistPayment } = PaymentHooks.useStore();
- const { persist: persistSettings } = SettingsHooks.useStore();
- const { persist: persistStyling } = StylingHooks.useStore();
- const { persist: persistTodos } = TodosHooks.useStore();
- const { persist: persistPayLaterMessaging } =
- PayLaterMessagingHooks.useStore();
-
- const persistActions = useMemo(
- () => [
- {
- key: 'persist-methods',
- message: 'Save payment methods',
- action: persistPayment,
- },
- {
- key: 'persist-settings',
- message: 'Save the settings',
- action: persistSettings,
- },
- {
- key: 'persist-styling',
- message: 'Save styling details',
- action: persistStyling,
- },
- {
- key: 'persist-todos',
- message: 'Save todos state',
- action: persistTodos,
- },
- {
- key: 'persist-pay-later-messaging',
- message: 'Save pay later messaging details',
- action: persistPayLaterMessaging,
- },
- ],
- [
- persistPayLaterMessaging,
- persistPayment,
- persistSettings,
- persistStyling,
- persistTodos,
- ]
- );
-
- const persistAll = useCallback( () => {
- /**
- * Executes onSave on TabPayLaterMessaging component.
- *
- * Todo: find a better way for this, because it's highly unreliable
- * (it only works when the user is still on the "Pay Later Messaging" tab)
- */
- document.getElementById( 'configurator-publishButton' )?.click();
-
- persistActions.forEach( ( { key, message, action } ) => {
- withActivity( key, message, action );
- } );
- }, [ persistActions, withActivity ] );
-
- return { persistAll };
-};
diff --git a/modules/ppcp-settings/resources/js/hooks/useSettingDependencyState.js b/modules/ppcp-settings/resources/js/hooks/useSettingDependencyState.js
new file mode 100644
index 000000000..f7e57bb6c
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/useSettingDependencyState.js
@@ -0,0 +1,64 @@
+/**
+ * Custom hook to handle setting-based payment method dependencies
+ */
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Check setting dependencies for methods
+ *
+ * @param {Array} methods - Array of methods to check
+ * @return {Object} Setting dependency states mapped by method ID
+ */
+const useSettingDependencyState = ( methods ) => {
+ const dependencyState = useSelect(
+ ( select ) => {
+ const settingsStore = select( 'wc/paypal/settings' );
+
+ if ( ! settingsStore || ! methods?.length ) {
+ return null;
+ }
+
+ // Get settings data
+ const persistentData = settingsStore.persistentData();
+ const result = {};
+
+ // Process each method
+ methods.forEach( ( method ) => {
+ if ( ! method?.id || ! method.depends_on_settings ) {
+ return;
+ }
+
+ // Handle the settings object structure
+ if ( method.depends_on_settings.settings ) {
+ const settingsObj = method.depends_on_settings.settings;
+
+ for ( const [ settingId, settingData ] of Object.entries(
+ settingsObj
+ ) ) {
+ const requiredId = settingData.id;
+ const requiredValue = settingData.value;
+
+ const actualValue = persistentData[ requiredId ];
+
+ // Check if dependency is satisfied
+ if ( actualValue !== requiredValue ) {
+ result[ method.id ] = {
+ isDisabled: true,
+ settingId: requiredId,
+ requiredValue,
+ };
+ break; // Stop checking once we find a failed dependency
+ }
+ }
+ }
+ } );
+
+ return result;
+ },
+ [ methods ]
+ );
+
+ return dependencyState;
+};
+
+export default useSettingDependencyState;
diff --git a/modules/ppcp-settings/resources/js/hooks/useStoreManager.js b/modules/ppcp-settings/resources/js/hooks/useStoreManager.js
new file mode 100644
index 000000000..190b2ac88
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/useStoreManager.js
@@ -0,0 +1,76 @@
+import { useCallback, useMemo } from '@wordpress/element';
+
+import {
+ CommonHooks,
+ PayLaterMessagingHooks,
+ PaymentHooks,
+ SettingsHooks,
+ StylingHooks,
+ TodosHooks,
+} from '../data';
+
+export const useStoreManager = () => {
+ const { withActivity } = CommonHooks.useBusyState();
+
+ const paymentStore = PaymentHooks.useStore();
+ const settingsStore = SettingsHooks.useStore();
+ const stylingStore = StylingHooks.useStore();
+ const todosStore = TodosHooks.useStore();
+ const payLaterStore = PayLaterMessagingHooks.useStore();
+
+ const storeActions = useMemo(
+ () => [
+ {
+ key: 'methods',
+ message: 'Process payment methods',
+ store: paymentStore,
+ },
+ {
+ key: 'settings',
+ message: 'Process the settings',
+ store: settingsStore,
+ },
+ {
+ key: 'styling',
+ message: 'Process styling details',
+ store: stylingStore,
+ },
+ {
+ key: 'todos',
+ message: 'Process todos state',
+ store: todosStore,
+ },
+ {
+ key: 'pay-later-messaging',
+ message: 'Process pay later messaging details',
+ store: payLaterStore,
+ },
+ ],
+ [ payLaterStore, paymentStore, settingsStore, stylingStore, todosStore ]
+ );
+
+ const persistAll = useCallback( () => {
+ /**
+ * Executes onSave on TabPayLaterMessaging component.
+ *
+ * Todo: find a better way for this, because it's highly unreliable
+ * (it only works when the user is still on the "Pay Later Messaging" tab)
+ */
+ document.getElementById( 'configurator-publishButton' )?.click();
+
+ storeActions.forEach( ( { key, message, store } ) => {
+ withActivity( `persist-${ key }`, message, store.persist );
+ } );
+ }, [ storeActions, withActivity ] );
+
+ const refreshAll = useCallback( () => {
+ storeActions.forEach( ( { key, message, store } ) => {
+ withActivity( `refresh-${ key }`, message, store.refresh );
+ } );
+ }, [ storeActions, withActivity ] );
+
+ return {
+ persistAll,
+ refreshAll,
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/hooks/useToggleState.js b/modules/ppcp-settings/resources/js/hooks/useToggleState.js
new file mode 100644
index 000000000..ebad28575
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/hooks/useToggleState.js
@@ -0,0 +1,50 @@
+import { useCallback, 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 );
+};
+
+/**
+ * Allows managing a toggle-able component, such as an accordion or a modal dialog.
+ *
+ * @param {string} [id=''] - If provided, the toggle can be opened via the URL.
+ * @param {null|boolean} [initiallyOpen=null] - If provided, it defines the initial open state.
+ * If omitted, the initial open state is determined by using the "id" logic (inspecting the URL).
+ * @return {{isOpen: unknown, toggleOpen: (function(*): boolean)}} Hook object.
+ */
+export function useToggleState( 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 = useCallback(
+ ( ev ) => {
+ setIsOpen( ! isOpen );
+ ev?.preventDefault();
+ return false;
+ },
+ [ isOpen ]
+ );
+
+ return { isOpen, setIsOpen, toggleOpen };
+}
diff --git a/modules/ppcp-settings/resources/js/utils/countryInfoLinks.js b/modules/ppcp-settings/resources/js/utils/countryInfoLinks.js
index 9562a3963..7d03cb147 100644
--- a/modules/ppcp-settings/resources/js/utils/countryInfoLinks.js
+++ b/modules/ppcp-settings/resources/js/utils/countryInfoLinks.js
@@ -24,7 +24,7 @@ export const learnMoreLinks = {
'https://www.paypal.com/uk/business/paypal-business-fees',
PayPalCheckout:
'https://www.paypal.com/uk/business/accept-payments/checkout',
- PayLater:
+ PayInThree:
'https://www.paypal.com/uk/business/accept-payments/checkout/installments',
},
FR: {
diff --git a/modules/ppcp-settings/resources/js/utils/navigation.js b/modules/ppcp-settings/resources/js/utils/navigation.js
index f2106bc1d..c9c1943b5 100644
--- a/modules/ppcp-settings/resources/js/utils/navigation.js
+++ b/modules/ppcp-settings/resources/js/utils/navigation.js
@@ -22,11 +22,14 @@ export const getQuery = () =>
/**
* Updates the query parameters of the current page.
*
- * @param {Object} query Object of params to be updated.
+ * @param {Object} query Object of params to be updated.
+ * @param {boolean} [replace=false] Whether to add the query vars (false) or replace previous query vars with the new details (true).
* @throws {TypeError} If the query is not an object.
*/
-export const updateQueryString = ( query ) =>
- pushHistory( getNewPath( query ) );
+export const updateQueryString = ( query, replace = false ) => {
+ const newQuery = replace ? query : { ...getQuery(), ...query };
+ return pushHistory( getNewPath( newQuery ) );
+};
/**
* Return a URL with set query parameters.
@@ -36,4 +39,42 @@ export const updateQueryString = ( query ) =>
* @return {string} Updated URL merging query params into existing params.
*/
export const getNewPath = ( query, basePath = getPath() ) =>
- addQueryArgs( basePath, { ...getQuery(), ...query } );
+ addQueryArgs( basePath, query );
+
+/**
+ * Filter an object to only include specified keys.
+ *
+ * @param {Object} obj The object to filter.
+ * @param {string[]} allowedKeys An array of allowed key names.
+ * @return {Object} A new object with only the allowed keys.
+ */
+export const filterObjectKeys = ( obj, allowedKeys ) => {
+ return Object.keys( obj ).reduce( ( acc, key ) => {
+ if ( allowedKeys.includes( key ) ) {
+ acc[ key ] = obj[ key ];
+ }
+ return acc;
+ }, {} );
+};
+
+/**
+ * Clean the browser URL by removing unsupported query parameters.
+ *
+ * @param {string[]} supportedArgs An array of supported query parameter names.
+ * @return {boolean} Returns true if the URL was modified (cleaned), false if nothing changed.
+ */
+export const cleanUrlQueryParams = ( supportedArgs ) => {
+ const currentQuery = getQuery();
+ const cleanedQuery = filterObjectKeys( currentQuery, supportedArgs );
+
+ const isUrlClean =
+ Object.keys( cleanedQuery ).length ===
+ Object.keys( currentQuery ).length;
+
+ if ( isUrlClean ) {
+ return false;
+ }
+
+ updateQueryString( cleanedQuery, true );
+ return true;
+};
diff --git a/modules/ppcp-settings/resources/js/utils/scrollAndHighlight.js b/modules/ppcp-settings/resources/js/utils/scrollAndHighlight.js
new file mode 100644
index 000000000..4b96ed994
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/utils/scrollAndHighlight.js
@@ -0,0 +1,49 @@
+/**
+ * Scroll to a specific element and highlight it
+ *
+ * @param {string} elementId - ID of the element to scroll to
+ * @param {boolean} [highlight=true] - Whether to highlight the element
+ * @return {Promise} - Resolves when scroll and highlight are complete
+ */
+export const scrollAndHighlight = ( elementId, highlight = true ) => {
+ return new Promise( ( resolve ) => {
+ const scrollTarget = document.getElementById( elementId );
+
+ if ( scrollTarget ) {
+ const navContainer = document.querySelector(
+ '.ppcp-r-navigation-container'
+ );
+ const navHeight = navContainer ? navContainer.offsetHeight : 0;
+
+ // Get the current scroll position and element's position relative to viewport
+ const rect = scrollTarget.getBoundingClientRect();
+
+ // Calculate the final position with offset
+ const scrollPosition =
+ rect.top + window.scrollY - ( navHeight + 55 );
+
+ window.scrollTo( {
+ top: scrollPosition,
+ behavior: 'smooth',
+ } );
+
+ // Add highlight if requested
+ if ( highlight ) {
+ scrollTarget.classList.add( 'ppcp-highlight' );
+
+ // Remove highlight after animation
+ setTimeout( () => {
+ scrollTarget.classList.remove( 'ppcp-highlight' );
+ }, 2000 );
+ }
+
+ // Resolve after scroll animation
+ setTimeout( resolve, 300 );
+ } else {
+ console.error(
+ `Failed to scroll: Element with ID "${ elementId }" not found`
+ );
+ resolve();
+ }
+ } );
+};
diff --git a/modules/ppcp-settings/resources/js/utils/tabSelector.js b/modules/ppcp-settings/resources/js/utils/tabSelector.js
index bde022fbe..1fe67acf7 100644
--- a/modules/ppcp-settings/resources/js/utils/tabSelector.js
+++ b/modules/ppcp-settings/resources/js/utils/tabSelector.js
@@ -7,56 +7,27 @@ export const TAB_IDS = {
PAY_LATER_MESSAGING: 'tab-panel-0-pay-later-messaging',
};
+import { scrollAndHighlight } from './scrollAndHighlight';
+
/**
* Select a tab by simulating a click event and scroll to specified element,
* accounting for navigation container height
*
* TODO: Once the TabPanel gets migrated to Tabs (TabPanel v2) we need to remove this in favor of programmatic tab switching: https://github.com/WordPress/gutenberg/issues/52997
*
- * @param {string} tabId - The ID of the tab to select
- * @param {string} [scrollToId] - Optional ID of the element to scroll to
+ * @param {string} tabId - The ID of the tab to select
+ * @param {string} [scrollToId] - Optional ID of the element to scroll to
+ * @param {boolean} highlight - Whether to highlight the element after scrolling to it
* @return {Promise} - Resolves when tab switch and scroll are complete
*/
-export const selectTab = ( tabId, scrollToId ) => {
+export const selectTab = ( tabId, scrollToId, highlight = false ) => {
return new Promise( ( resolve ) => {
const tab = document.getElementById( tabId );
if ( tab ) {
tab.click();
setTimeout( () => {
- const scrollTarget = scrollToId
- ? document.getElementById( scrollToId )
- : document.getElementById( 'ppcp-settings-container' );
-
- if ( scrollTarget ) {
- const navContainer = document.querySelector(
- '.ppcp-r-navigation-container'
- );
- const navHeight = navContainer
- ? navContainer.offsetHeight
- : 0;
-
- // Get the current scroll position and element's position relative to viewport
- const rect = scrollTarget.getBoundingClientRect();
-
- // Calculate the final position with offset
- const scrollPosition =
- rect.top + window.scrollY - ( navHeight + 55 );
-
- window.scrollTo( {
- top: scrollPosition,
- behavior: 'smooth',
- } );
-
- // Resolve after scroll animation
- setTimeout( resolve, 300 );
- } else {
- console.error(
- `Failed to scroll: Element with ID "${
- scrollToId || 'ppcp-settings-container'
- }" not found`
- );
- resolve();
- }
+ const targetId = scrollToId || 'ppcp-settings-container';
+ scrollAndHighlight( targetId, highlight ).then( resolve );
}, 100 );
} else {
console.error(
diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php
index b1643c13b..98d51ce1d 100644
--- a/modules/ppcp-settings/services.php
+++ b/modules/ppcp-settings/services.php
@@ -10,7 +10,21 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
+use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
+use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
+use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
+use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\IDealGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MultibancoGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway;
+use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway;
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Data\Definition\FeaturesDefinition;
+use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
@@ -20,13 +34,12 @@ use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\CompleteOnClickEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\FeaturesRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PaymentRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\ResetDismissedTodosEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SettingsRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint;
@@ -34,13 +47,29 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\TodosRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
+use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
+use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
+use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
+use WooCommerce\PayPalCommerce\Settings\Service\SettingsDataManager;
+use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
+use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
+use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
+use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
+use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
+use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
+use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
+use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
+use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
+use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
+use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
+use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService;
return array(
- 'settings.url' => static function ( ContainerInterface $container ) : string {
+ 'settings.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
@@ -51,106 +80,171 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
- 'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
+ 'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
$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?
- if ( class_exists( '\WC_Payments' ) ) {
- $can_use_card_payments = false;
- }
+ $should_skip_payment_methods = class_exists( '\WC_Payments' );
+ $can_use_fastlane = $container->get( 'axo.eligible' );
+ $can_use_pay_later = $container->get( 'button.helper.messages-apply' );
return new OnboardingProfile(
$can_use_casual_selling,
$can_use_vaulting,
$can_use_card_payments,
- $can_use_subscriptions
+ $can_use_subscriptions,
+ $should_skip_payment_methods,
+ $can_use_fastlane,
+ $can_use_pay_later->for_country()
);
},
- 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
+ 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
return new GeneralSettings(
$container->get( 'api.shop.country' ),
$container->get( 'api.shop.currency.getter' )->get(),
$container->get( 'wcgateway.is-send-only-country' )
);
},
- 'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
+ 'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
return new StylingSettings(
$container->get( 'settings.service.sanitizer' )
);
},
- 'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
+ 'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
return new PaymentSettings();
},
- 'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
+ 'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
return new SettingsModel(
$container->get( 'settings.service.sanitizer' )
);
},
+ 'settings.data.paylater-messaging' => static function ( ContainerInterface $container ) : array {
+ // TODO: Create an AbstractDataModel wrapper for this configuration!
+
+ $config_factors = $container->get( 'paylater-configurator.factory.config' );
+ assert( $config_factors instanceof ConfigFactory );
+
+ $save_config = $container->get( 'paylater-configurator.endpoint.save-config' );
+ assert( $save_config instanceof SaveConfig );
+
+ $settings = $container->get( 'wcgateway.settings' );
+ assert( $settings instanceof Settings );
+
+ $pay_later_config = $config_factors->from_settings( $settings );
+
+ return array(
+ 'read' => $pay_later_config,
+ 'save' => $save_config,
+ );
+ },
/**
- * Checks if valid merchant connection details are stored in the DB.
+ * Merchant connection details, which includes the connection status
+ * (onboarding/connected) and connection-aware environment checks.
+ * This is the preferred solution to check environment and connection state.
*/
- 'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
+ 'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState {
$data = $container->get( 'settings.data.general' );
assert( $data instanceof GeneralSettings );
- return $data->is_merchant_connected();
+ $is_connected = $data->is_merchant_connected();
+ $environment = new Environment( $data->is_sandbox_merchant() );
+
+ return new ConnectionState( $is_connected, $environment );
},
- 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
+ 'settings.environment' => static function ( ContainerInterface $container ) : Environment {
+ // We should remove this service in favor of directly using `settings.connection-state`.
+ $state = $container->get( 'settings.connection-state' );
+ assert( $state instanceof ConnectionState );
+
+ return $state->get_environment();
+ },
+ /**
+ * Checks if valid merchant connection details are stored in the DB.
+ */
+ 'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
+ /*
+ * This service only resolves the connection status once per request.
+ * We should remove this service in favor of directly using `settings.connection-state`.
+ */
+ $state = $container->get( 'settings.connection-state' );
+ assert( $state instanceof ConnectionState );
+
+ return $state->is_connected();
+ },
+ /**
+ * Checks if the merchant is connected to a sandbox environment.
+ */
+ 'settings.flag.is-sandbox' => static function ( ContainerInterface $container ) : bool {
+ /*
+ * This service only resolves the sandbox flag once per request.
+ * We should remove this service in favor of directly using `settings.connection-state`.
+ */
+ $state = $container->get( 'settings.connection-state' );
+ assert( $state instanceof ConnectionState );
+
+ return $state->is_sandbox();
+ },
+ 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
},
- 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
- return new CommonRestEndpoint( $container->get( 'settings.data.general' ) );
+ 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
+ return new CommonRestEndpoint(
+ $container->get( 'settings.data.general' ),
+ $container->get( 'api.endpoint.partners' )
+ );
},
- 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
- return new PaymentRestEndpoint( $container->get( 'settings.data.payment' ) );
+ 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
+ return new PaymentRestEndpoint(
+ $container->get( 'settings.data.payment' ),
+ $container->get( 'settings.data.definition.methods' ),
+ $container->get( 'settings.data.definition.method_dependencies' )
+ );
},
- 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
+ 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
return new StylingRestEndpoint(
$container->get( 'settings.data.styling' ),
$container->get( 'settings.service.sanitizer' )
);
},
- 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
+ 'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
return new RefreshFeatureStatusEndpoint(
$container->get( 'wcgateway.settings' ),
new Cache( 'ppcp-timeout' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
- 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
+ 'settings.rest.authentication' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
return new AuthenticationRestEndpoint(
$container->get( 'settings.service.authentication_manager' ),
+ $container->get( 'settings.service.data-manager' )
);
},
- 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
+ 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
return new LoginLinkRestEndpoint(
$container->get( 'settings.service.connection-url-generator' ),
);
},
- 'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
+ 'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
return new WebhookSettingsEndpoint(
$container->get( 'api.endpoint.webhook' ),
$container->get( 'webhook.registrar' ),
$container->get( 'webhook.status.simulation' )
);
},
- 'settings.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint {
+ 'settings.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint {
return new PayLaterMessagingEndpoint(
$container->get( 'wcgateway.settings' ),
$container->get( 'paylater-configurator.endpoint.save-config' )
);
},
- 'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint {
+ 'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint {
return new SettingsRestEndpoint(
$container->get( 'settings.data.settings' )
);
},
- 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
+ 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AR',
'AU',
@@ -200,13 +294,13 @@ return array(
'VN',
);
},
- 'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool {
+ 'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool {
$country = $container->get( 'api.shop.country' );
$eligible_countries = $container->get( 'settings.casual-selling.supported-countries' );
return in_array( $country, $eligible_countries, true );
},
- 'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
+ '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(
@@ -217,16 +311,16 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
- 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
+ '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 {
+ '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-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
+ 'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
return new ConnectionUrlGenerator(
$container->get( 'api.env.endpoint.partner-referrals' ),
$container->get( 'api.repository.partner-referrals-data' ),
@@ -234,19 +328,38 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
- 'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
+ 'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
return new AuthenticationManager(
$container->get( 'settings.data.general' ),
$container->get( 'api.env.paypal-host' ),
$container->get( 'api.env.endpoint.login-seller' ),
$container->get( 'api.repository.partner-referrals-data' ),
- $container->get( 'woocommerce.logger.woocommerce' ),
+ $container->get( 'settings.connection-state' ),
+ $container->get( 'settings.service.rest-service' ),
+ $container->get( 'woocommerce.logger.woocommerce' )
);
},
- 'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
+ 'settings.service.rest-service' => static function ( ContainerInterface $container ) : InternalRestService {
+ return new InternalRestService(
+ $container->get( 'woocommerce.logger.woocommerce' )
+ );
+ },
+ 'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
return new DataSanitizer();
},
- 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
+ 'settings.service.data-manager' => static function ( ContainerInterface $container ) : SettingsDataManager {
+ return new SettingsDataManager(
+ $container->get( 'settings.data.definition.methods' ),
+ $container->get( 'settings.data.onboarding' ),
+ $container->get( 'settings.data.general' ),
+ $container->get( 'settings.data.settings' ),
+ $container->get( 'settings.data.styling' ),
+ $container->get( 'settings.data.payment' ),
+ $container->get( 'settings.data.paylater-messaging' ),
+ $container->get( 'settings.data.todos' ),
+ );
+ },
+ 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
return new SwitchSettingsUiEndpoint(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.request-data' ),
@@ -254,23 +367,163 @@ return array(
$container->get( 'api.merchant_id' ) !== ''
);
},
- 'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
+ 'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
return new TodosRestEndpoint(
$container->get( 'settings.data.todos' ),
$container->get( 'settings.data.definition.todos' ),
- $container->get( 'settings.rest.settings' )
+ $container->get( 'settings.rest.settings' ),
+ $container->get( 'settings.service.todos_sorting' )
);
},
- 'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
+ 'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
return new TodosModel();
},
- 'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
+ 'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
return new TodosDefinition(
$container->get( 'settings.service.todos_eligibilities' ),
$container->get( 'settings.data.general' )
);
},
- 'settings.service.todos_eligibilities' => static function( ContainerInterface $container ): TodosEligibilityService {
+ 'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
+ $axo_checkout_config_notice = $container->get( 'axo.checkout-config-notice.raw' );
+ $axo_incompatible_plugins_notice = $container->get( 'axo.incompatible-plugins-notice.raw' );
+
+ // Combine the notices - only include non-empty ones.
+ $axo_notices = array_filter(
+ array(
+ $axo_checkout_config_notice,
+ $axo_incompatible_plugins_notice,
+ )
+ );
+
+ return new PaymentMethodsDefinition(
+ $container->get( 'settings.data.payment' ),
+ $axo_notices
+ );
+ },
+ 'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition {
+ return new PaymentMethodsDependenciesDefinition( $container->get( 'wcgateway.settings' ) );
+ },
+ 'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
+ $pay_later_endpoint = $container->get( 'settings.rest.pay_later_messaging' );
+ $pay_later_settings = $pay_later_endpoint->get_details()->get_data();
+
+ $pay_later_statuses = array(
+ 'cart' => $pay_later_settings['data']['cart']['status'] === 'enabled',
+ 'checkout' => $pay_later_settings['data']['checkout']['status'] === 'enabled',
+ 'product' => $pay_later_settings['data']['product']['status'] === 'enabled',
+ 'shop' => $pay_later_settings['data']['shop']['status'] === 'enabled',
+ 'home' => $pay_later_settings['data']['home']['status'] === 'enabled',
+ 'custom_placement' => ! empty( $pay_later_settings['data']['custom_placement'] ) &&
+ $pay_later_settings['data']['custom_placement'][0]['status'] === 'enabled',
+ );
+
+ $is_pay_later_messaging_enabled_for_any_location = ! array_filter( $pay_later_statuses );
+
+ return array(
+ 'statuses' => $pay_later_statuses,
+ 'is_enabled_for_any_location' => $is_pay_later_messaging_enabled_for_any_location,
+ );
+ },
+ 'settings.service.button_locations' => static function ( ContainerInterface $container ) : array {
+ $styling_endpoint = $container->get( 'settings.rest.styling' );
+ $styling_data = $styling_endpoint->get_details()->get_data()['data'];
+
+ return array(
+ 'cart_enabled' => $styling_data['cart']->enabled ?? false,
+ 'block_checkout_enabled' => $styling_data['expressCheckout']->enabled ?? false,
+ 'product_enabled' => $styling_data['product']->enabled ?? false,
+ );
+ },
+ 'settings.service.gateways_status' => static function ( ContainerInterface $container ) : array {
+ $payment_endpoint = $container->get( 'settings.rest.payment' );
+ $settings = $payment_endpoint->get_details()->get_data();
+
+ return array(
+ 'apple_pay' => $settings['data']['ppcp-applepay']['enabled'] ?? false,
+ 'google_pay' => $settings['data']['ppcp-googlepay']['enabled'] ?? false,
+ 'axo' => $settings['data']['ppcp-axo-gateway']['enabled'] ?? false,
+ 'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
+ );
+ },
+ 'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array {
+ $features = apply_filters(
+ 'woocommerce_paypal_payments_rest_common_merchant_features',
+ array()
+ );
+
+ return array(
+ 'apple_pay' => $features['apple_pay']['enabled'] ?? false,
+ 'google_pay' => $features['google_pay']['enabled'] ?? false,
+ 'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false,
+ 'save_paypal' => $features['save_paypal_and_venmo']['enabled'] ?? false,
+ 'apm' => $features['alternative_payment_methods']['enabled'] ?? false,
+ 'paylater' => $features['pay_later_messaging']['enabled'] ?? false,
+ );
+ },
+
+ 'settings.service.todos_eligibilities' => static function ( ContainerInterface $container ) : TodosEligibilityService {
+ $pay_later_service = $container->get( 'settings.service.pay_later_status' );
+ $pay_later_statuses = $pay_later_service['statuses'];
+ $is_pay_later_messaging_enabled_for_any_location = $pay_later_service['is_enabled_for_any_location'];
+
+ $button_locations = $container->get( 'settings.service.button_locations' );
+ $gateways = $container->get( 'settings.service.gateways_status' );
+ $capabilities = $container->get( 'settings.service.merchant_capabilities' );
+
+ /**
+ * Initializes TodosEligibilityService with eligibility conditions for various PayPal features.
+ * Each parameter determines whether a specific feature should be shown in the Things To Do list.
+ *
+ * Logic relies on three main factors:
+ * 1. $container->get( 'x.eligible' ) - Module based eligibility check, usually whether the WooCommerce store is using a supported country/currency matrix.
+ * 2. $capabilities - Whether the merchant is eligible for specific features on their PayPal account.
+ * 3. $gateways, $pay_later_statuses, $button_locations - Plugin settings (enabled/disabled status).
+ *
+ * @param bool $is_fastlane_eligible - Show if merchant is eligible (ACDC) but hasn't enabled Fastlane gateway.
+ * @param bool $is_pay_later_messaging_eligible - Show if Pay Later messaging is enabled for at least one location.
+ * @param bool $is_pay_later_messaging_product_eligible - Show if Pay Later is not enabled anywhere and specifically not on product page.
+ * @param bool $is_pay_later_messaging_cart_eligible - Show if Pay Later is not enabled anywhere and specifically not on cart.
+ * @param bool $is_pay_later_messaging_checkout_eligible - Show if Pay Later is not enabled anywhere and specifically not on checkout.
+ * @param bool $is_subscription_eligible - Show if WooCommerce Subscriptions plugin is active but merchant is not eligible for PayPal Vaulting.
+ * @param bool $is_paypal_buttons_cart_eligible - Show if PayPal buttons are not enabled on cart page.
+ * @param bool $is_paypal_buttons_block_checkout_eligible - Show if PayPal buttons are not enabled on blocks checkout.
+ * @param bool $is_paypal_buttons_product_eligible - Show if PayPal buttons are not enabled on product page.
+ * @param bool $is_apple_pay_domain_eligible - Show if merchant has Apple Pay capability on PayPal account.
+ * @param bool $is_digital_wallet_eligible - Show if merchant is eligible (ACDC) but doesn't have both wallet types on PayPal.
+ * @param bool $is_apple_pay_eligible - Show if merchant is eligible (ACDC) but doesn't have Apple Pay on PayPal.
+ * @param bool $is_google_pay_eligible - Show if merchant is eligible (ACDC) but doesn't have Google Pay on PayPal.
+ * @param bool $is_enable_apple_pay_eligible - Show if merchant has Apple Pay capability but hasn't enabled the gateway.
+ * @param bool $is_enable_google_pay_eligible - Show if merchant has Google Pay capability but hasn't enabled the gateway.
+ */
+ return new TodosEligibilityService(
+ $container->get( 'axo.eligible' ) && $capabilities['acdc'] && ! $gateways['axo'], // Enable Fastlane.
+ $is_pay_later_messaging_enabled_for_any_location, // Enable Pay Later messaging.
+ ! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['product'], // Add Pay Later messaging (Product page).
+ ! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['cart'], // Add Pay Later messaging (Cart).
+ ! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['checkout'], // Add Pay Later messaging (Checkout).
+ $container->has( 'save-payment-methods.eligible' ) &&
+ ! $container->get( 'save-payment-methods.eligible' ) &&
+ $container->has( 'wc-subscriptions.helper' ) &&
+ $container->get( 'wc-subscriptions.helper' )->plugin_is_active(), // Configure a PayPal Subscription.
+ ! $button_locations['cart_enabled'], // Add PayPal buttons to cart.
+ ! $button_locations['block_checkout_enabled'], // Add PayPal buttons to block checkout.
+ ! $button_locations['product_enabled'], // Add PayPal buttons to product.
+ $capabilities['apple_pay'], // Register Domain for Apple Pay.
+ $capabilities['acdc'] && ! ( $capabilities['apple_pay'] && $capabilities['google_pay'] ), // Add digital wallets to your account.
+ $container->get( 'applepay.eligible' ) && $capabilities['acdc'] && ! $capabilities['apple_pay'], // Add Apple Pay to your account.
+ $container->get( 'googlepay.eligible' ) && $capabilities['acdc'] && ! $capabilities['google_pay'], // Add Google Pay to your account.
+ $container->get( 'applepay.eligible' ) && $capabilities['apple_pay'] && ! $gateways['apple_pay'], // Enable Apple Pay.
+ $container->get( 'googlepay.eligible' ) && $capabilities['google_pay'] && ! $gateways['google_pay'],
+ );
+ },
+ 'settings.rest.features' => static function ( ContainerInterface $container ) : FeaturesRestEndpoint {
+ return new FeaturesRestEndpoint(
+ $container->get( 'settings.data.definition.features' ),
+ $container->get( 'settings.rest.settings' )
+ );
+ },
+ 'settings.data.definition.features' => static function ( ContainerInterface $container ) : FeaturesDefinition {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
@@ -281,13 +534,9 @@ return array(
// Settings status.
$gateways = array(
- 'apple_pay' => $settings['data']['ppcp-applepay']['enabled'] ?? false,
- 'google_pay' => $settings['data']['ppcp-googlepay']['enabled'] ?? false,
- 'axo' => $settings['data']['ppcp-axo-gateway']['enabled'] ?? false,
'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
);
-
- // Merchant eligibility.
+ // Merchant capabilities, serve to show active or inactive badge and buttons.
$capabilities = array(
'apple_pay' => $features['apple_pay']['enabled'] ?? false,
'google_pay' => $features['google_pay']['enabled'] ?? false,
@@ -296,21 +545,71 @@ return array(
'apm' => $features['alternative_payment_methods']['enabled'] ?? false,
'paylater' => $features['pay_later_messaging']['enabled'] ?? false,
);
+ $merchant_capabilities = array(
+ 'save_paypal' => $capabilities['save_paypal'], // Save PayPal and Venmo eligibility.
+ 'acdc' => $capabilities['acdc'] && ! $gateways['card-button'], // Advanced credit and debit cards eligibility.
+ 'apm' => $capabilities['apm'], // Alternative payment methods eligibility.
+ 'google_pay' => $capabilities['acdc'] && $capabilities['google_pay'], // Google Pay eligibility.
+ 'apple_pay' => $capabilities['acdc'] && $capabilities['apple_pay'], // Apple Pay eligibility.
+ 'pay_later' => $capabilities['paylater'],
+ );
+ return new FeaturesDefinition(
+ $container->get( 'settings.service.features_eligibilities' ),
+ $container->get( 'settings.data.general' ),
+ $merchant_capabilities,
+ $container->get( 'settings.data.settings' )
+ );
+ },
+ 'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
- return new TodosEligibilityService(
- $capabilities['acdc'] && ! $gateways['axo'], // Enable Fastlane.
- $capabilities['acdc'] && ! $gateways['card-button'], // Enable Credit and Debit Cards on your checkout.
- true, // Enable Pay Later messaging.
- true, // Add Pay Later messaging.
- true, // Configure a PayPal Subscription.
- true, // Add PayPal buttons.
- true, // Register Domain for Apple Pay.
- $capabilities['acdc'] && ! ( $capabilities['apple_pay'] && $capabilities['google_pay'] ), // Add digital wallets to your account.
- $capabilities['acdc'] && ! $capabilities['apple_pay'], // Add Apple Pay to your account.
- $capabilities['acdc'] && ! $capabilities['google_pay'], // Add Google Pay to your account.
- true, // Configure a PayPal Subscription.
- $capabilities['apple_pay'] && ! $gateways['apple_pay'], // Enable Apple Pay.
- $capabilities['google_pay'] && ! $gateways['google_pay'] // Enable Google Pay.
+ $messages_apply = $container->get( 'button.helper.messages-apply' );
+ assert( $messages_apply instanceof MessagesApply );
+ $pay_later_eligible = $messages_apply->for_country();
+
+ $merchant_country = $container->get( 'api.shop.country' );
+ $ineligible_countries = array( 'RU', 'BR', 'JP' );
+ $apm_eligible = ! in_array( $merchant_country, $ineligible_countries, true );
+
+ return new FeaturesEligibilityService(
+ $container->get( 'save-payment-methods.eligible' ), // Save PayPal and Venmo eligibility.
+ $container->get( 'card-fields.eligible' ), // Advanced credit and debit cards eligibility.
+ $apm_eligible, // Alternative payment methods eligibility.
+ $container->get( 'googlepay.eligible' ), // Google Pay eligibility.
+ $container->get( 'applepay.eligible' ), // Apple Pay eligibility.
+ $pay_later_eligible, // Pay Later eligibility.
+ );
+ },
+ 'settings.service.todos_sorting' => static function ( ContainerInterface $container ) : TodosSortingAndFilteringService {
+ return new TodosSortingAndFilteringService(
+ $container->get( 'settings.data.todos' )
+ );
+ },
+ 'settings.service.gateway-redirect' => static function (): GatewayRedirectService {
+ return new GatewayRedirectService();
+ },
+ /**
+ * Returns a list of all payment gateway IDs created by this plugin.
+ *
+ * @returns string[] The list of all gateway IDs.
+ */
+ 'settings.config.all-gateway-ids' => static function (): array {
+ return array(
+ PayPalGateway::ID,
+ CardButtonGateway::ID,
+ CreditCardGateway::ID,
+ AxoGateway::ID,
+ ApplePayGateway::ID,
+ GooglePayGateway::ID,
+ BancontactGateway::ID,
+ BlikGateway::ID,
+ EPSGateway::ID,
+ IDealGateway::ID,
+ MyBankGateway::ID,
+ P24Gateway::ID,
+ TrustlyGateway::ID,
+ MultibancoGateway::ID,
+ PayUponInvoiceGateway::ID,
+ OXXO::ID,
);
},
);
diff --git a/modules/ppcp-settings/src/DTO/ConfigurationFlagsDTO.php b/modules/ppcp-settings/src/DTO/ConfigurationFlagsDTO.php
new file mode 100644
index 000000000..4f00ff405
--- /dev/null
+++ b/modules/ppcp-settings/src/DTO/ConfigurationFlagsDTO.php
@@ -0,0 +1,47 @@
+is_sandbox = $is_sandbox;
- $this->client_id = $client_id;
- $this->client_secret = $client_secret;
- $this->merchant_id = $merchant_id;
- $this->merchant_email = $merchant_email;
- $this->seller_type = $seller_type;
+ $this->is_sandbox = $is_sandbox;
+ $this->client_id = $client_id;
+ $this->client_secret = $client_secret;
+ $this->merchant_id = $merchant_id;
+ $this->merchant_email = $merchant_email;
+ $this->merchant_country = $merchant_country;
+ $this->seller_type = $seller_type;
}
}
diff --git a/modules/ppcp-settings/src/Data/AbstractDataModel.php b/modules/ppcp-settings/src/Data/AbstractDataModel.php
index 070015af2..362c2d2d5 100644
--- a/modules/ppcp-settings/src/Data/AbstractDataModel.php
+++ b/modules/ppcp-settings/src/Data/AbstractDataModel.php
@@ -58,7 +58,7 @@ abstract class AbstractDataModel {
*/
public function load() : void {
$saved_data = get_option( static::OPTION_KEY, array() );
- $filtered_data = array_intersect_key( $saved_data, $this->data );
+ $filtered_data = array_intersect_key( (array) $saved_data, $this->data );
$this->data = array_merge( $this->data, $filtered_data );
}
@@ -69,6 +69,13 @@ abstract class AbstractDataModel {
update_option( static::OPTION_KEY, $this->data );
}
+ /**
+ * Deletes the settings entry from the WordPress database.
+ */
+ public function purge() : void {
+ delete_option( static::OPTION_KEY );
+ }
+
/**
* Gets all model data as an array.
*
diff --git a/modules/ppcp-settings/src/Data/Definition/FeaturesDefinition.php b/modules/ppcp-settings/src/Data/Definition/FeaturesDefinition.php
new file mode 100644
index 000000000..1cc635422
--- /dev/null
+++ b/modules/ppcp-settings/src/Data/Definition/FeaturesDefinition.php
@@ -0,0 +1,325 @@
+eligibilities = $eligibilities;
+ $this->settings = $settings;
+ $this->merchant_capabilities = $merchant_capabilities;
+ $this->plugin_settings = $plugin_settings;
+ }
+
+ /**
+ * Returns the full list of feature definitions with their eligibility conditions.
+ *
+ * @return array The array of feature definitions.
+ */
+ public function get(): array {
+ $all_features = $this->all_available_features();
+ $eligible_features = array();
+ $eligibility_checks = $this->eligibilities->get_eligibility_checks();
+ foreach ( $all_features as $feature_key => $feature ) {
+ if ( $eligibility_checks[ $feature_key ]() ) {
+ $eligible_features[ $feature_key ] = $feature;
+ }
+ }
+ return $eligible_features;
+ }
+
+ /**
+ * Returns all available features.
+ *
+ * @return array[] The array of all available features.
+ */
+ public function all_available_features(): array {
+ $paylater_countries = array(
+ 'UK',
+ 'ES',
+ 'IT',
+ 'FR',
+ 'US',
+ 'DE',
+ 'AU',
+ );
+ $store_country = $this->settings->get_woo_settings()['country'];
+ $country_location = in_array( $store_country, $paylater_countries, true ) ? strtolower( $store_country ) : 'us';
+ $save_paypal_and_venmo = $this->plugin_settings->get_save_paypal_and_venmo();
+
+ return array(
+ 'save_paypal_and_venmo' => array(
+ 'title' => __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.', 'woocommerce-paypal-payments' ),
+ 'enabled' => $this->merchant_capabilities['save_paypal'],
+ 'buttons' => array(
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'settings',
+ 'section' => 'ppcp-save-paypal-and-venmo',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
+ 'urls' => array(
+ 'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
+ 'live' => 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
+ ),
+ 'showWhen' => 'disabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'tertiary',
+ 'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
+ 'url' => 'https://www.paypal.com/us/enterprise/payment-processing/accept-venmo',
+ 'class' => 'small-button',
+ ),
+ ),
+ ),
+ 'advanced_credit_and_debit_cards' => array(
+ 'title' => __( 'Advanced Credit and Debit Cards', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.', 'woocommerce-paypal-payments' ),
+ 'enabled' => $this->merchant_capabilities['acdc'],
+ 'buttons' => array(
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'payment_methods',
+ 'section' => 'ppcp-credit-card-gateway',
+ 'highlight' => 'ppcp-credit-card-gateway',
+ 'modal' => 'ppcp-credit-card-gateway',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
+ 'urls' => array(
+ 'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp',
+ 'live' => 'https://www.paypal.com/bizsignup/entry?product=ppcp',
+ ),
+ 'showWhen' => 'disabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'tertiary',
+ 'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
+ 'url' => 'https://developer.paypal.com/studio/checkout/advanced',
+ 'class' => 'small-button',
+ ),
+ ),
+ ),
+ 'alternative_payment_methods' => array(
+ 'title' => __( 'Alternative Payment Methods', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Offer global, country-specific payment options for your customers.', 'woocommerce-paypal-payments' ),
+ 'enabled' => $this->merchant_capabilities['apm'],
+ 'buttons' => array(
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'payment_methods',
+ 'section' => 'ppcp-alternative-payments-card',
+ 'highlight' => 'ppcp-alternative-payments-card',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
+ 'url' => 'https://developer.paypal.com/docs/checkout/apm/',
+ 'showWhen' => 'disabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'tertiary',
+ 'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
+ 'url' => 'https://developer.paypal.com/docs/checkout/apm/',
+ 'class' => 'small-button',
+ ),
+ ),
+ ),
+ 'google_pay' => array(
+ 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Let customers pay using their Google Pay wallet.', 'woocommerce-paypal-payments' ),
+ 'enabled' => $this->merchant_capabilities['google_pay'],
+ 'buttons' => array(
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'payment_methods',
+ 'section' => 'ppcp-googlepay',
+ 'highlight' => 'ppcp-googlepay',
+ 'modal' => 'ppcp-googlepay',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
+ 'urls' => array(
+ 'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
+ 'live' => 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
+ ),
+ 'showWhen' => 'disabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'tertiary',
+ 'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
+ 'url' => 'https://developer.paypal.com/docs/checkout/apm/google-pay/',
+ 'class' => 'small-button',
+ ),
+ ),
+ 'notes' => array(
+ __( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
+ ),
+ ),
+ 'apple_pay' => array(
+ 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Let customers pay using their Apple Pay wallet.', 'woocommerce-paypal-payments' ),
+ 'enabled' => $this->merchant_capabilities['apple_pay'],
+ 'buttons' => array(
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'payment_methods',
+ 'section' => 'ppcp-card-payments-card',
+ 'highlight' => 'ppcp-applepay',
+ 'modal' => 'ppcp-applepay',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Domain registration', 'woocommerce-paypal-payments' ),
+ 'urls' => array(
+ 'sandbox' => 'https://www.sandbox.paypal.com/uccservicing/apm/applepay',
+ 'live' => 'https://www.paypal.com/uccservicing/apm/applepay',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
+ 'urls' => array(
+ 'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
+ 'live' => 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
+ ),
+ 'showWhen' => 'disabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'tertiary',
+ 'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
+ 'url' => 'https://developer.paypal.com/docs/checkout/apm/apple-pay/',
+ 'class' => 'small-button',
+ ),
+ ),
+ ),
+ 'pay_later' => array(
+ 'title' => __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ 'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'enabled' => $this->merchant_capabilities['pay_later'] && ! $save_paypal_and_venmo,
+ 'buttons' => array(
+ array(
+ 'type' => 'secondary',
+ 'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'pay_later_messaging',
+ ),
+ 'showWhen' => 'enabled',
+ 'class' => 'small-button',
+ ),
+ array(
+ 'type' => 'tertiary',
+ 'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
+ 'url' => "https://www.paypal.com/$country_location/business/accept-payments/checkout/installments",
+ 'class' => 'small-button',
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php
new file mode 100644
index 000000000..02d552a54
--- /dev/null
+++ b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php
@@ -0,0 +1,402 @@
+settings = $settings;
+ $this->axo_conflicts_notices = $axo_conflicts_notices;
+ }
+
+ /**
+ * Returns the payment method definitions.
+ *
+ * @return array
+ */
+ public function get_definitions() : array {
+ // Refresh the WooCommerce gateway details before we build the definitions.
+ $this->wc_gateways = WC()->payment_gateways()->payment_gateways();
+ $all_methods = array_merge(
+ $this->group_paypal_methods(),
+ $this->group_card_methods(),
+ $this->group_apms(),
+ );
+ $result = array();
+ foreach ( $all_methods as $method ) {
+ $method_id = $method['id'];
+
+ $result[ $method_id ] = $this->build_method_definition(
+ $method_id,
+ $method['title'],
+ $method['description'],
+ $method['icon'],
+ $method['fields'] ?? array(),
+ $method['warningMessages'] ?? array(),
+ );
+ }
+ return $result;
+ }
+
+ /**
+ * Returns a new payment method configuration array that contains all
+ * common attributes which must be present in every method definition.
+ *
+ * @param string $gateway_id The payment method ID.
+ * @param string $title Admin-side payment method title.
+ * @param string $description Admin-side info about the payment method.
+ * @param string $icon Admin-side icon of the payment method.
+ * @param array|false $fields Optional. Additional fields to display in the edit modal.
+ * Setting this to false omits all fields.
+ * @param array $warning_messages Optional. Warning messages to display in the UI.
+ * @return array Payment method definition.
+ */
+ private function build_method_definition(
+ string $gateway_id,
+ string $title,
+ string $description,
+ string $icon,
+ $fields = array(),
+ array $warning_messages = array()
+ ) : array {
+ $gateway = $this->wc_gateways[ $gateway_id ] ?? null;
+
+ $gateway_title = $gateway ? $gateway->get_title() : $title;
+ $gateway_description = $gateway ? $gateway->get_description() : $description;
+
+ $config = array(
+ 'id' => $gateway_id,
+ 'enabled' => $this->settings->is_method_enabled( $gateway_id ),
+ 'title' => str_replace( '&', '&', $gateway_title ),
+ 'description' => $gateway_description,
+ 'icon' => $icon,
+ 'itemTitle' => $title,
+ 'itemDescription' => $description,
+ 'warningMessages' => $warning_messages,
+ );
+
+ if ( is_array( $fields ) ) {
+ $config['fields'] = array_merge(
+ array(
+ 'checkoutPageTitle' => array(
+ 'type' => 'text',
+ 'default' => $gateway_title,
+ 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
+ ),
+ 'checkoutPageDescription' => array(
+ 'type' => 'text',
+ 'default' => $gateway ? $gateway->get_description() : '',
+ 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
+ ),
+ ),
+ $fields
+ );
+ }
+
+ return $config;
+ }
+
+ // Payment method groups.
+
+ /**
+ * Define PayPal related payment methods.
+ *
+ * @return array
+ */
+ public function group_paypal_methods() : array {
+ $group = array(
+ array(
+ 'id' => PayPalGateway::ID,
+ 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', 'woocommerce-paypal-payments' ),
+ 'icon' => 'payment-method-paypal',
+ 'fields' => array(
+ 'paypalShowLogo' => array(
+ 'type' => 'toggle',
+ 'default' => $this->settings->get_paypal_show_logo(),
+ 'label' => __( 'Show logo', 'woocommerce-paypal-payments' ),
+ ),
+ ),
+ ),
+ array(
+ 'id' => 'venmo',
+ 'title' => __( 'Venmo', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ 'Offer Venmo at checkout to millions of active users.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'icon' => 'payment-method-venmo',
+ 'fields' => false,
+ ),
+ array(
+ 'id' => 'pay-later',
+ 'title' => __( 'Pay Later', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'icon' => 'payment-method-paypal',
+ 'fields' => false,
+ ),
+ array(
+ 'id' => CardButtonGateway::ID,
+ 'title' => __( 'Credit and debit card payments', 'woocommerce-paypal-payments' ),
+ 'description' => __( "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account . ", 'woocommerce-paypal-payments' ),
+ 'icon' => 'payment-method-cards',
+ ),
+ );
+
+ return apply_filters( 'woocommerce_paypal_payments_gateway_group_paypal', $group );
+ }
+
+ /**
+ * Define card related payment methods.
+ *
+ * @return array
+ */
+ public function group_card_methods() : array {
+ $group = array(
+ array(
+ 'id' => CreditCardGateway::ID,
+ 'title' => __( 'Advanced Credit and Debit Card Payments', 'woocommerce-paypal-payments' ),
+ 'description' => __( "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", 'woocommerce-paypal-payments' ),
+ 'icon' => 'payment-method-advanced-cards',
+ 'fields' => array(
+ 'threeDSecure' => array(
+ 'type' => 'radio',
+ 'default' => $this->settings->get_three_d_secure(),
+ 'label' => __( '3D Secure', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ 'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'options' => array(
+ array(
+ 'label' => __(
+ 'No 3D Secure',
+ 'woocommerce-paypal-payments'
+ ),
+ 'value' => 'no-3d-secure',
+ ),
+ array(
+ 'label' => __(
+ 'Only when required',
+ 'woocommerce-paypal-payments'
+ ),
+ 'value' => 'only-required-3d-secure',
+ ),
+ array(
+ 'label' => __(
+ 'Always require 3D Secure',
+ 'woocommerce-paypal-payments'
+ ),
+ 'value' => 'always-3d-secure',
+ ),
+ ),
+ ),
+ ),
+ ),
+ array(
+ 'id' => AxoGateway::ID,
+ '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.", 'woocommerce-paypal-payments' ),
+ 'icon' => 'payment-method-fastlane',
+ 'fields' => array(
+ 'fastlaneCardholderName' => array(
+ 'type' => 'toggle',
+ 'default' => $this->settings->get_fastlane_cardholder_name(),
+ 'label' => __(
+ 'Display cardholder name',
+ 'woocommerce-paypal-payments'
+ ),
+ ),
+ 'fastlaneDisplayWatermark' => array(
+ 'type' => 'toggle',
+ 'default' => $this->settings->get_fastlane_display_watermark(),
+ 'label' => __(
+ 'Display Fastlane Watermark',
+ 'woocommerce-paypal-payments'
+ ),
+ ),
+ ),
+ 'warningMessages' => $this->axo_conflicts_notices,
+ ),
+ array(
+ 'id' => ApplePayGateway::ID,
+ 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Allow customers to pay via their Apple Pay digital wallet.', 'woocommerce-paypal-payments' ),
+ 'icon' => 'payment-method-apple-pay',
+ ),
+ array(
+ 'id' => GooglePayGateway::ID,
+ 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Allow customers to pay via their Google Pay digital wallet.', 'woocommerce-paypal-payments' ),
+ 'icon' => 'payment-method-google-pay',
+ ),
+ );
+
+ return apply_filters( 'woocommerce_paypal_payments_gateway_group_cards', $group );
+ }
+
+ /**
+ * Builds an array of payment method definitions, which includes details
+ * of all APM gateways.
+ *
+ * @return array List of payment method definitions.
+ */
+ public function group_apms() : array {
+ $group = array(
+ array(
+ 'id' => BancontactGateway::ID,
+ 'title' => __( 'Bancontact', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'icon' => 'payment-method-bancontact',
+ ),
+ array(
+ 'id' => BlikGateway::ID,
+ 'title' => __( 'BLIK', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ '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',
+ ),
+ array(
+ 'id' => EPSGateway::ID,
+ 'title' => __( 'eps', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ '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',
+ ),
+ array(
+ 'id' => IDealGateway::ID,
+ 'title' => __( 'iDEAL', 'woocommerce-paypal-payments' ),
+ 'description' => __(
+ 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.',
+ 'woocommerce-paypal-payments'
+ ),
+ 'icon' => 'payment-method-ideal',
+ ),
+ array(
+ 'id' => MyBankGateway::ID,
+ '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',
+ ),
+ array(
+ 'id' => P24Gateway::ID,
+ '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',
+ ),
+ array(
+ 'id' => TrustlyGateway::ID,
+ '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',
+ ),
+ array(
+ 'id' => MultibancoGateway::ID,
+ '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',
+ ),
+ array(
+ 'id' => PayUponInvoiceGateway::ID,
+ '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' => '',
+ ),
+ array(
+ 'id' => OXXO::ID,
+ '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',
+ ),
+ );
+
+ return apply_filters( 'woocommerce_paypal_payments_gateway_group_apm', $group );
+ }
+}
diff --git a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php
new file mode 100644
index 000000000..9c7771be9
--- /dev/null
+++ b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php
@@ -0,0 +1,200 @@
+settings = $settings;
+ }
+
+ /**
+ * Get payment method to payment method dependencies
+ *
+ * Maps dependent method ID => array of parent method IDs.
+ * A dependent method is disabled if ANY of its required parents is disabled.
+ *
+ * @return array The dependency relationships between payment methods
+ */
+ public function get_payment_method_dependencies(): array {
+ $dependencies = array(
+ CardButtonGateway::ID => array( PayPalGateway::ID ),
+ CreditCardGateway::ID => array( PayPalGateway::ID ),
+ AxoGateway::ID => array( PayPalGateway::ID, CreditCardGateway::ID ),
+ ApplePayGateway::ID => array( PayPalGateway::ID, CreditCardGateway::ID ),
+ GooglePayGateway::ID => array( PayPalGateway::ID, CreditCardGateway::ID ),
+ BancontactGateway::ID => array( PayPalGateway::ID ),
+ BlikGateway::ID => array( PayPalGateway::ID ),
+ EPSGateway::ID => array( PayPalGateway::ID ),
+ IDealGateway::ID => array( PayPalGateway::ID ),
+ MultibancoGateway::ID => array( PayPalGateway::ID ),
+ MyBankGateway::ID => array( PayPalGateway::ID ),
+ P24Gateway::ID => array( PayPalGateway::ID ),
+ TrustlyGateway::ID => array( PayPalGateway::ID ),
+ PayUponInvoiceGateway::ID => array( PayPalGateway::ID ),
+ OXXO::ID => array( PayPalGateway::ID ),
+ 'venmo' => array( PayPalGateway::ID ),
+ 'pay-later' => array( PayPalGateway::ID ),
+ );
+
+ return apply_filters(
+ 'woocommerce_paypal_payments_payment_method_dependencies',
+ $dependencies
+ );
+ }
+
+ /**
+ * Get setting to payment method dependencies.
+ *
+ * Maps method ID => array of required settings with their values.
+ * A method is disabled if ANY of its required settings doesn't match the required value.
+ *
+ * @return array The dependency relationships between settings and payment methods
+ */
+ public function get_setting_dependencies(): array {
+ $dependencies = array(
+ 'pay-later' => array(
+ 'savePaypalAndVenmo' => false,
+ ),
+ );
+
+ return apply_filters(
+ 'woocommerce_paypal_payments_setting_dependencies',
+ $dependencies
+ );
+ }
+
+ /**
+ * Get method setting dependencies
+ *
+ * Returns the setting dependencies for a specific method ID.
+ *
+ * @param string $method_id Method ID to check.
+ * @return array Setting dependencies for the method or empty array if none exist
+ */
+ public function get_method_setting_dependencies( string $method_id ): array {
+ $setting_dependencies = $this->get_setting_dependencies();
+ return $setting_dependencies[ $method_id ] ?? array();
+ }
+
+ /**
+ * Add dependency information to the payment method definitions
+ *
+ * @param array $methods Payment method definitions.
+ * @return array Payment method definitions with dependency information
+ */
+ public function add_dependency_info_to_methods( array $methods ): array {
+ foreach ( $methods as $method_id => &$method ) {
+ // Skip the __meta key.
+ if ( $method_id === '__meta' ) {
+ continue;
+ }
+
+ // Add payment method dependency info if applicable.
+ $payment_method_dependencies = $this->get_method_payment_method_dependencies( $method_id );
+ if ( ! empty( $payment_method_dependencies ) ) {
+ $method['depends_on_payment_methods'] = $payment_method_dependencies;
+ }
+
+ // Check if this method has setting dependencies.
+ $method_setting_dependencies = $this->get_method_setting_dependencies( $method_id );
+ if ( ! empty( $method_setting_dependencies ) ) {
+ $settings = array();
+ foreach ( $method_setting_dependencies as $setting_id => $required_value ) {
+ $settings[ $setting_id ] = array(
+ 'id' => $setting_id,
+ 'value' => $required_value,
+ );
+ }
+
+ $method['depends_on_settings'] = array(
+ 'settings' => $settings,
+ );
+ }
+ }
+
+ // Add global metadata about settings that affect dependencies.
+ if ( ! isset( $methods['__meta'] ) ) {
+ $methods['__meta'] = array();
+ }
+
+ $methods['__meta']['settings_affecting_methods'] = $this->get_all_dependent_settings();
+
+ return $methods;
+ }
+
+ /**
+ * Get payment method dependencies for a specific method
+ *
+ * @param string $method_id Method ID to check.
+ * @return array Array of parent method IDs
+ */
+ public function get_method_payment_method_dependencies( string $method_id ): array {
+ return $this->get_payment_method_dependencies()[ $method_id ] ?? array();
+ }
+
+ /**
+ * Get all settings that affect payment methods
+ *
+ * @return array Array of unique setting keys that affect payment methods
+ */
+ public function get_all_dependent_settings(): array {
+ $settings = array();
+ $dependencies = $this->get_setting_dependencies();
+
+ foreach ( $dependencies as $method_settings ) {
+ if ( isset( $method_settings['settings'] ) ) {
+ foreach ( $method_settings['settings'] as $setting_data ) {
+ if ( ! in_array( $setting_data['id'], $settings, true ) ) {
+ $settings[] = $setting_data['id'];
+ }
+ }
+ }
+ }
+
+ return $settings;
+ }
+}
diff --git a/modules/ppcp-settings/src/Data/Definition/TodosDefinition.php b/modules/ppcp-settings/src/Data/Definition/TodosDefinition.php
index 7c509adf6..7af943b7b 100644
--- a/modules/ppcp-settings/src/Data/Definition/TodosDefinition.php
+++ b/modules/ppcp-settings/src/Data/Definition/TodosDefinition.php
@@ -1,6 +1,6 @@
eligibilities->get_eligibility_checks();
return array(
- 'enable_fastlane' => array(
+ 'enable_fastlane' => array(
'title' => __( 'Enable Fastlane', 'woocommerce-paypal-payments' ),
'description' => __( 'Accelerate your guest checkout with Fastlane by PayPal', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['enable_fastlane'],
@@ -67,57 +67,90 @@ class TodosDefinition {
'section' => 'ppcp-axo-gateway',
'highlight' => 'ppcp-axo-gateway',
),
+ 'priority' => 1,
),
- 'enable_credit_debit_cards' => array(
- 'title' => __( 'Enable Credit and Debit Cards on your checkout', 'woocommerce-paypal-payments' ),
- 'description' => __( 'Credit and Debit Cards is now available for Blocks checkout pages', 'woocommerce-paypal-payments' ),
- 'isEligible' => $eligibility_checks['enable_credit_debit_cards'],
- 'action' => array(
- 'type' => 'tab',
- 'tab' => 'payment_methods',
- 'section' => 'ppcp-card-button-gateway',
- 'highlight' => 'ppcp-card-button-gateway',
- ),
- ),
- 'enable_pay_later_messaging' => array(
+ 'enable_pay_later_messaging' => array(
'title' => __( 'Enable Pay Later messaging', 'woocommerce-paypal-payments' ),
- 'description' => __( 'Show Pay Later messaging to boost conversion rate and increase cart size.', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Show Pay Later messaging to boost conversion rate and increase cart size', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['enable_pay_later_messaging'],
'action' => array(
- 'type' => 'tab',
- 'tab' => 'overview',
- 'section' => 'pay_later_messaging',
+ 'type' => 'tab',
+ 'tab' => 'pay_later_messaging',
),
+ 'priority' => 3,
),
- 'add_pay_later_messaging' => array(
- 'title' => __( 'Add Pay Later messaging', 'woocommerce-paypal-payments' ),
- 'description' => __( 'Present Pay Later messaging on your page to boost conversion rate and increase cart size.', 'woocommerce-paypal-payments' ),
- 'isEligible' => $eligibility_checks['add_pay_later_messaging'],
+ 'add_pay_later_messaging_product_page' => array(
+ 'title' => __( 'Add Pay Later messaging to the Product page', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Present Pay Later messaging on your Product page to boost conversion rate and increase cart size', 'woocommerce-paypal-payments' ),
+ 'isEligible' => $eligibility_checks['add_pay_later_messaging_product_page'],
'action' => array(
- 'type' => 'tab',
- 'tab' => 'overview',
- 'section' => 'pay_later_messaging',
+ 'type' => 'tab',
+ 'tab' => 'pay_later_messaging',
),
+ 'priority' => 4,
),
- 'configure_paypal_subscription' => array(
+ 'add_pay_later_messaging_cart' => array(
+ 'title' => __( 'Add Pay Later messaging to the Cart page', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Present Pay Later messaging on your Cart page to boost conversion rate and increase cart size', 'woocommerce-paypal-payments' ),
+ 'isEligible' => $eligibility_checks['add_pay_later_messaging_cart'],
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'pay_later_messaging',
+ ),
+ 'priority' => 4,
+ ),
+ 'add_pay_later_messaging_checkout' => array(
+ 'title' => __( 'Add Pay Later messaging to the Checkout page', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Present Pay Later messaging on your Checkout page to boost conversion rate and increase cart size', 'woocommerce-paypal-payments' ),
+ 'isEligible' => $eligibility_checks['add_pay_later_messaging_checkout'],
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'pay_later_messaging',
+ ),
+ 'priority' => 4,
+ ),
+ 'configure_paypal_subscription' => array(
'title' => __( 'Configure a PayPal Subscription', 'woocommerce-paypal-payments' ),
'description' => __( 'Connect a subscriptions-type product from WooCommerce with PayPal', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['configure_paypal_subscription'],
'action' => array(
- 'type' => 'external',
- 'url' => admin_url( 'edit.php?post_type=product&product_type=subscription' ),
+ 'type' => 'external',
+ 'url' => 'https://woocommerce.com/document/woocommerce-paypal-payments/#paypal-subscriptions',
+ 'completeOnClick' => true,
),
+ 'priority' => 5,
),
- 'add_paypal_buttons' => array(
- 'title' => __( 'Add PayPal buttons', 'woocommerce-paypal-payments' ),
- 'description' => __( 'Allow customers to check out quickly and securely from the page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
- 'isEligible' => $eligibility_checks['add_paypal_buttons'],
+ 'add_paypal_buttons_cart' => array(
+ 'title' => __( 'Add PayPal buttons to the Cart page', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Allow customers to check out quickly and securely from the Cart page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
+ 'isEligible' => $eligibility_checks['add_paypal_buttons_cart'],
'action' => array(
'type' => 'tab',
'tab' => 'styling',
),
+ 'priority' => 6,
),
- 'register_domain_apple_pay' => array(
+ 'add_paypal_buttons_block_checkout' => array(
+ 'title' => __( 'Add PayPal buttons to the Express Checkout page', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Allow customers to check out quickly and securely from the Express Checkout page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
+ 'isEligible' => $eligibility_checks['add_paypal_buttons_block_checkout'],
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'styling',
+ ),
+ 'priority' => 6,
+ ),
+ 'add_paypal_buttons_product' => array(
+ 'title' => __( 'Add PayPal buttons to the Product page', 'woocommerce-paypal-payments' ),
+ 'description' => __( 'Allow customers to check out quickly and securely from the Product page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
+ 'isEligible' => $eligibility_checks['add_paypal_buttons_product'],
+ 'action' => array(
+ 'type' => 'tab',
+ 'tab' => 'styling',
+ ),
+ 'priority' => 6,
+ ),
+ 'register_domain_apple_pay' => array(
'title' => __( 'Register Domain for Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'To enable Apple Pay, you must register your domain with PayPal', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['register_domain_apple_pay'],
@@ -128,8 +161,9 @@ class TodosDefinition {
: 'https://www.paypal.com/uccservicing/apm/applepay',
'completeOnClick' => true,
),
+ 'priority' => 7,
),
- 'add_digital_wallets' => array(
+ 'add_digital_wallets' => array(
'title' => __( 'Add digital wallets to your account', 'woocommerce-paypal-payments' ),
'description' => __( 'Add the ability to accept Apple Pay & Google Pay to your PayPal account', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_digital_wallets'],
@@ -137,8 +171,9 @@ class TodosDefinition {
'type' => 'external',
'url' => 'https://www.paypal.com/businessmanage/account/settings',
),
+ 'priority' => 8,
),
- 'add_apple_pay' => array(
+ 'add_apple_pay' => array(
'title' => __( 'Add Apple Pay to your account', 'woocommerce-paypal-payments' ),
'description' => __( 'Add the ability to accept Apple Pay to your PayPal account', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_apple_pay'],
@@ -146,8 +181,9 @@ class TodosDefinition {
'type' => 'external',
'url' => 'https://www.paypal.com/businessmanage/account/settings',
),
+ 'priority' => 9,
),
- 'add_google_pay' => array(
+ 'add_google_pay' => array(
'title' => __( 'Add Google Pay to your account', 'woocommerce-paypal-payments' ),
'description' => __( 'Add the ability to accept Google Pay to your PayPal account', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_google_pay'],
@@ -155,8 +191,9 @@ class TodosDefinition {
'type' => 'external',
'url' => 'https://www.paypal.com/businessmanage/account/settings',
),
+ 'priority' => 10,
),
- 'enable_apple_pay' => array(
+ 'enable_apple_pay' => array(
'title' => __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow your buyers to check out via Apple Pay', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['enable_apple_pay'],
@@ -166,8 +203,9 @@ class TodosDefinition {
'section' => 'ppcp-applepay',
'highlight' => 'ppcp-applepay',
),
+ 'priority' => 11,
),
- 'enable_google_pay' => array(
+ 'enable_google_pay' => array(
'title' => __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow your buyers to check out via Google Pay', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['enable_google_pay'],
@@ -177,6 +215,7 @@ class TodosDefinition {
'section' => 'ppcp-googlepay',
'highlight' => 'ppcp-googlepay',
),
+ 'priority' => 12,
),
);
}
diff --git a/modules/ppcp-settings/src/Data/GeneralSettings.php b/modules/ppcp-settings/src/Data/GeneralSettings.php
index b06c518d2..5c816c4cb 100644
--- a/modules/ppcp-settings/src/Data/GeneralSettings.php
+++ b/modules/ppcp-settings/src/Data/GeneralSettings.php
@@ -74,6 +74,7 @@ class GeneralSettings extends AbstractDataModel {
'sandbox_merchant' => false,
'merchant_id' => '',
'merchant_email' => '',
+ 'merchant_country' => '',
'client_id' => '',
'client_secret' => '',
'seller_type' => 'unknown',
@@ -138,6 +139,7 @@ class GeneralSettings extends AbstractDataModel {
$this->data['sandbox_merchant'] = $connection->is_sandbox;
$this->data['merchant_id'] = sanitize_text_field( $connection->merchant_id );
$this->data['merchant_email'] = sanitize_email( $connection->merchant_email );
+ $this->data['merchant_country'] = sanitize_text_field( $connection->merchant_country );
$this->data['client_id'] = sanitize_text_field( $connection->client_id );
$this->data['client_secret'] = sanitize_text_field( $connection->client_secret );
$this->data['seller_type'] = sanitize_text_field( $connection->seller_type );
@@ -156,6 +158,7 @@ class GeneralSettings extends AbstractDataModel {
$this->data['client_secret'],
$this->data['merchant_id'],
$this->data['merchant_email'],
+ $this->data['merchant_country'],
$this->data['seller_type']
);
}
@@ -171,6 +174,7 @@ class GeneralSettings extends AbstractDataModel {
$this->data['sandbox_merchant'] = $defaults['sandbox_merchant'];
$this->data['merchant_id'] = $defaults['merchant_id'];
$this->data['merchant_email'] = $defaults['merchant_email'];
+ $this->data['merchant_country'] = $defaults['merchant_country'];
$this->data['client_id'] = $defaults['client_id'];
$this->data['client_secret'] = $defaults['client_secret'];
$this->data['seller_type'] = $defaults['seller_type'];
@@ -239,4 +243,18 @@ class GeneralSettings extends AbstractDataModel {
public function get_merchant_email() : string {
return $this->data['merchant_email'];
}
+
+ /**
+ * Gets the currently connected merchant's country.
+ *
+ * @return string
+ */
+ public function get_merchant_country() : string {
+ // When we don't know the merchant's real country, we assume it's the Woo store-country.
+ if ( empty( $this->data['merchant_country'] ) ) {
+ return $this->woo_settings['country'];
+ }
+
+ return $this->data['merchant_country'];
+ }
}
diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php
index a2d8e6c36..6c41a1718 100644
--- a/modules/ppcp-settings/src/Data/OnboardingProfile.php
+++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php
@@ -43,6 +43,9 @@ class OnboardingProfile extends AbstractDataModel {
* @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.
+ * @param bool $should_skip_payment_methods Whether it should skip payment methods screen.
+ * @param bool $can_use_fastlane Whether it can use Fastlane or not.
+ * @param bool $can_use_pay_later Whether it can use Pay Later or not.
*
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
*/
@@ -50,14 +53,20 @@ class OnboardingProfile extends AbstractDataModel {
bool $can_use_casual_selling = false,
bool $can_use_vaulting = false,
bool $can_use_card_payments = false,
- bool $can_use_subscriptions = false
+ bool $can_use_subscriptions = false,
+ bool $should_skip_payment_methods = false,
+ bool $can_use_fastlane = false,
+ bool $can_use_pay_later = 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;
+ $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;
+ $this->flags['should_skip_payment_methods'] = $should_skip_payment_methods;
+ $this->flags['can_use_fastlane'] = $can_use_fastlane;
+ $this->flags['can_use_pay_later'] = $can_use_pay_later;
}
/**
@@ -67,11 +76,12 @@ class OnboardingProfile extends AbstractDataModel {
*/
protected function get_defaults() : array {
return array(
- 'completed' => false,
- 'step' => 0,
- 'is_casual_seller' => null,
- 'are_optional_payment_methods_enabled' => null,
- 'products' => array(),
+ 'completed' => false,
+ 'step' => 0,
+ 'is_casual_seller' => null,
+ 'accept_card_payments' => null,
+ 'products' => array(),
+ 'setup_done' => false,
);
}
@@ -89,10 +99,10 @@ class OnboardingProfile extends AbstractDataModel {
/**
* Sets the 'completed' flag.
*
- * @param bool $step Whether the onboarding process has been completed.
+ * @param bool $state Whether the onboarding process has been completed.
*/
- public function set_completed( bool $step ) : void {
- $this->data['completed'] = $step;
+ public function set_completed( bool $state ) : void {
+ $this->data['completed'] = $state;
}
/**
@@ -132,12 +142,21 @@ class OnboardingProfile extends AbstractDataModel {
}
/**
- * Sets the optional payment methods flag.
+ * Whether the merchant wants to accept card payments via the PayPal plugin.
*
- * @param bool|null $are_optional_payment_methods_enabled Whether the PayPal optional payment methods are enabled.
+ * @return bool
*/
- public function set_are_optional_payment_methods_enabled( ?bool $are_optional_payment_methods_enabled ) : void {
- $this->data['are_optional_payment_methods_enabled'] = $are_optional_payment_methods_enabled;
+ public function get_accept_card_payments() : bool {
+ return (bool) $this->data['accept_card_payments'];
+ }
+
+ /**
+ * Sets the "accept card payments" flag.
+ *
+ * @param bool|null $accept_cards Whether to accept card payments via the PayPal plugin.
+ */
+ public function set_accept_card_payments( ?bool $accept_cards ) : void {
+ $this->data['accept_card_payments'] = $accept_cards;
}
/**
@@ -166,4 +185,22 @@ class OnboardingProfile extends AbstractDataModel {
public function get_flags() : array {
return $this->flags;
}
+
+ /**
+ * Gets the 'setup_done' flag.
+ *
+ * @return bool
+ */
+ public function is_setup_done() : bool {
+ return (bool) $this->data['setup_done'];
+ }
+
+ /**
+ * Sets the 'setup_done' flag.
+ *
+ * @param bool $done Whether the onboarding process has been setup_done.
+ */
+ public function set_setup_done( bool $done ) : void {
+ $this->data['setup_done'] = $done;
+ }
}
diff --git a/modules/ppcp-settings/src/Data/PaymentSettings.php b/modules/ppcp-settings/src/Data/PaymentSettings.php
index ffea3153d..52ac3e419 100644
--- a/modules/ppcp-settings/src/Data/PaymentSettings.php
+++ b/modules/ppcp-settings/src/Data/PaymentSettings.php
@@ -9,6 +9,8 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
+use WC_Payment_Gateway;
+
/**
* Class PaymentSettings
*/
@@ -21,12 +23,19 @@ class PaymentSettings extends AbstractDataModel {
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-payment';
+ /**
+ * List of WC_Payment_Gateway instances that need to be saved.
+ *
+ * @var WC_Payment_Gateway[]
+ */
+ private array $unsaved_gateways = array();
+
/**
* Get default values for the model.
*
* @return array
*/
- protected function get_defaults(): array {
+ protected function get_defaults() : array {
return array(
'paypal_show_logo' => false,
'three_d_secure' => 'no-3d-secure',
@@ -37,12 +46,115 @@ class PaymentSettings extends AbstractDataModel {
);
}
+ /**
+ * Saves the model data to WordPress options.
+ */
+ public function save() : void {
+ parent::save();
+
+ foreach ( $this->unsaved_gateways as $gateway ) {
+ $gateway->settings['enabled'] = $gateway->enabled;
+ $gateway->settings['title'] = $gateway->title;
+ $gateway->settings['description'] = $gateway->description;
+
+ update_option( $gateway->get_option_key(), $gateway->settings );
+ }
+
+ $this->unsaved_gateways = array();
+ }
+
+ /**
+ * Enables or disables the defined payment method, if it exists.
+ *
+ * @param string $method_id ID of the payment method.
+ * @param bool $is_enabled Whether to enable the method.
+ */
+ public function toggle_method_state( string $method_id, bool $is_enabled ) : void {
+ switch ( $method_id ) {
+ case 'venmo':
+ $this->set_venmo_enabled( $is_enabled );
+ break;
+
+ case 'pay-later':
+ $this->set_paylater_enabled( $is_enabled );
+ break;
+
+ default:
+ $gateway = $this->get_gateway( $method_id );
+
+ if ( $gateway ) {
+ $gateway->enabled = wc_bool_to_string( $is_enabled );
+
+ $this->modified_gateway( $gateway );
+ }
+ }
+ }
+
+ /**
+ * Checks, if the provided payment method is enabled.
+ *
+ * @param string $method_id ID of the payment method.
+ * @return bool True, if the method is enabled. False if it's disabled or not existing.
+ */
+ public function is_method_enabled( string $method_id ) : bool {
+ switch ( $method_id ) {
+ case 'venmo':
+ return $this->get_venmo_enabled();
+
+ case 'pay-later':
+ return $this->get_paylater_enabled();
+
+ default:
+ $gateway = $this->get_gateway( $method_id );
+
+ if ( $gateway ) {
+ return wc_string_to_bool( $gateway->enabled );
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Updates the payment method title.
+ *
+ * @param string $method_id ID of the payment method.
+ * @param string $title The new title.
+ * @return void
+ */
+ public function set_method_title( string $method_id, string $title ) : void {
+ $gateway = $this->get_gateway( $method_id );
+
+ if ( $gateway ) {
+ $gateway->title = $title;
+
+ $this->modified_gateway( $gateway );
+ }
+ }
+
+ /**
+ * Updates the payment method description.
+ *
+ * @param string $method_id ID of the payment method.
+ * @param string $description The new description.
+ * @return void
+ */
+ public function set_method_description( string $method_id, string $description ) : void {
+ $gateway = $this->get_gateway( $method_id );
+
+ if ( $gateway ) {
+ $gateway->description = $description;
+
+ $this->modified_gateway( $gateway );
+ }
+ }
+
/**
* Get PayPal show logo.
*
* @return bool
*/
- public function get_paypal_show_logo(): bool {
+ public function get_paypal_show_logo() : bool {
return (bool) $this->data['paypal_show_logo'];
}
@@ -51,7 +163,7 @@ class PaymentSettings extends AbstractDataModel {
*
* @return string
*/
- public function get_three_d_secure(): string {
+ public function get_three_d_secure() : string {
return $this->data['three_d_secure'];
}
@@ -60,7 +172,7 @@ class PaymentSettings extends AbstractDataModel {
*
* @return bool
*/
- public function get_fastlane_cardholder_name(): bool {
+ public function get_fastlane_cardholder_name() : bool {
return (bool) $this->data['fastlane_cardholder_name'];
}
@@ -69,7 +181,7 @@ class PaymentSettings extends AbstractDataModel {
*
* @return bool
*/
- public function get_fastlane_display_watermark(): bool {
+ public function get_fastlane_display_watermark() : bool {
return (bool) $this->data['fastlane_display_watermark'];
}
@@ -78,7 +190,7 @@ class PaymentSettings extends AbstractDataModel {
*
* @return bool
*/
- public function get_venmo_enabled(): bool {
+ public function get_venmo_enabled() : bool {
return (bool) $this->data['venmo_enabled'];
}
@@ -87,7 +199,7 @@ class PaymentSettings extends AbstractDataModel {
*
* @return bool
*/
- public function get_paylater_enabled(): bool {
+ public function get_paylater_enabled() : bool {
return (bool) $this->data['paylater_enabled'];
}
@@ -97,7 +209,7 @@ class PaymentSettings extends AbstractDataModel {
* @param bool $value The value.
* @return void
*/
- public function set_paypal_show_logo( bool $value ): void {
+ public function set_paypal_show_logo( bool $value ) : void {
$this->data['paypal_show_logo'] = $value;
}
@@ -107,7 +219,7 @@ class PaymentSettings extends AbstractDataModel {
* @param string $value The value.
* @return void
*/
- public function set_three_d_secure( string $value ): void {
+ public function set_three_d_secure( string $value ) : void {
$this->data['three_d_secure'] = $value;
}
@@ -117,7 +229,7 @@ class PaymentSettings extends AbstractDataModel {
* @param bool $value The value.
* @return void
*/
- public function set_fastlane_cardholder_name( bool $value ): void {
+ public function set_fastlane_cardholder_name( bool $value ) : void {
$this->data['fastlane_cardholder_name'] = $value;
}
@@ -127,7 +239,7 @@ class PaymentSettings extends AbstractDataModel {
* @param bool $value The value.
* @return void
*/
- public function set_fastlane_display_watermark( bool $value ): void {
+ public function set_fastlane_display_watermark( bool $value ) : void {
$this->data['fastlane_display_watermark'] = $value;
}
@@ -137,7 +249,7 @@ class PaymentSettings extends AbstractDataModel {
* @param bool $value The value.
* @return void
*/
- public function set_venmo_enabled( bool $value ): void {
+ public function set_venmo_enabled( bool $value ) : void {
$this->data['venmo_enabled'] = $value;
}
@@ -147,7 +259,40 @@ class PaymentSettings extends AbstractDataModel {
* @param bool $value The value.
* @return void
*/
- public function set_paylater_enabled( bool $value ): void {
+ public function set_paylater_enabled( bool $value ) : void {
$this->data['paylater_enabled'] = $value;
}
+
+ /**
+ * Get the gateway object for the given method ID.
+ *
+ * @param string $method_id ID of the payment method.
+ * @return WC_Payment_Gateway|null
+ */
+ private function get_gateway( string $method_id ) : ?WC_Payment_Gateway {
+ if ( isset( $this->unsaved_gateways[ $method_id ] ) ) {
+ return $this->unsaved_gateways[ $method_id ];
+ }
+
+ $gateways = WC()->payment_gateways()->payment_gateways();
+
+ if ( ! isset( $gateways[ $method_id ] ) ) {
+ return null;
+ }
+
+ $gateway = $gateways[ $method_id ];
+ $gateway->init_form_fields();
+
+ return $gateway;
+ }
+
+ /**
+ * Store the gateway object for later saving.
+ *
+ * @param WC_Payment_Gateway $gateway The gateway object.
+ * @return void
+ */
+ private function modified_gateway( WC_Payment_Gateway $gateway ) : void {
+ $this->unsaved_gateways[ $gateway->id ] = $gateway;
+ }
}
diff --git a/modules/ppcp-settings/src/Data/SettingsModel.php b/modules/ppcp-settings/src/Data/SettingsModel.php
index b88a899d3..713786651 100644
--- a/modules/ppcp-settings/src/Data/SettingsModel.php
+++ b/modules/ppcp-settings/src/Data/SettingsModel.php
@@ -71,9 +71,9 @@ class SettingsModel extends AbstractDataModel {
'soft_descriptor' => '',
// Enum-type string values.
- 'subtotal_adjustment' => 'skip_details', // Options: [correction|no_details].
+ 'subtotal_adjustment' => 'correction', // Options: [correction|no_details].
'landing_page' => 'any', // Options: [any|login|guest_checkout].
- 'button_language' => '', // empty or a 2-letter language code.
+ 'button_language' => '', // empty or a language locale code.
// Boolean flags.
'authorize_only' => false,
diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php
index 7ac964b2d..1915d998f 100644
--- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php
@@ -14,6 +14,7 @@ use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
+use WooCommerce\PayPalCommerce\Settings\Service\SettingsDataManager;
/**
* REST controller for authenticating and connecting to a PayPal merchant account.
@@ -40,6 +41,13 @@ class AuthenticationRestEndpoint extends RestEndpoint {
*/
private AuthenticationManager $authentication_manager;
+ /**
+ * Settings data manager service.
+ *
+ * @var SettingsDataManager
+ */
+ private SettingsDataManager $data_manager;
+
/**
* Defines the JSON response format (when connection was successful).
*
@@ -58,9 +66,12 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* Constructor.
*
* @param AuthenticationManager $authentication_manager The authentication manager.
+ * @param SettingsDataManager $data_manager Settings data manager, to reset
+ * settings.
*/
- public function __construct( AuthenticationManager $authentication_manager ) {
+ public function __construct( AuthenticationManager $authentication_manager, SettingsDataManager $data_manager ) {
$this->authentication_manager = $authentication_manager;
+ $this->data_manager = $data_manager;
}
/**
@@ -76,7 +87,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base . '/direct',
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -114,7 +125,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base . '/oauth',
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -144,7 +155,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/authenticate/disconnect
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base . '/disconnect',
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -175,7 +186,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
}
$account = $this->authentication_manager->get_account_details();
- $response = $this->sanitize_for_javascript( $this->response_map, $account );
+ $response = $this->sanitize_for_javascript( $account, $this->response_map );
return $this->return_success( $response );
}
@@ -209,11 +220,19 @@ class AuthenticationRestEndpoint extends RestEndpoint {
/**
* Disconnect the merchant and clear the authentication details.
*
+ * @param WP_REST_Request $request Full data about the request.
+ *
* @return WP_REST_Response
*/
- public function disconnect() : WP_REST_Response {
+ public function disconnect( WP_REST_Request $request ) : WP_REST_Response {
+ $reset_settings = $request->get_param( 'reset' );
+
$this->authentication_manager->disconnect();
+ if ( $reset_settings ) {
+ $this->data_manager->reset_all_settings();
+ }
+
return $this->return_success( 'OK' );
}
}
diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
index 8014e3817..57303c205 100644
--- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
@@ -9,10 +9,12 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
+use Exception;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
+use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
/**
* REST controller for "common" settings, which are used and modified by
@@ -22,6 +24,11 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
* internal data model.
*/
class CommonRestEndpoint extends RestEndpoint {
+ /**
+ * Full REST path to the merchant-details endpoint, relative to the namespace.
+ */
+ protected const SELLER_ACCOUNT_PATH = 'common/seller-account';
+
/**
* The base path for this REST controller.
*
@@ -36,6 +43,13 @@ class CommonRestEndpoint extends RestEndpoint {
*/
protected GeneralSettings $settings;
+ /**
+ * The Partners-Endpoint instance to request seller details from PayPal's API.
+ *
+ * @var PartnersEndpoint
+ */
+ protected PartnersEndpoint $partners_endpoint;
+
/**
* Field mapping for request to profile transformation.
*
@@ -104,10 +118,27 @@ class CommonRestEndpoint extends RestEndpoint {
/**
* Constructor.
*
- * @param GeneralSettings $settings The settings instance.
+ * @param GeneralSettings $settings The settings instance.
+ * @param PartnersEndpoint $partners_endpoint Partners-API to get merchant details from PayPal.
*/
- public function __construct( GeneralSettings $settings ) {
- $this->settings = $settings;
+ public function __construct( GeneralSettings $settings, PartnersEndpoint $partners_endpoint ) {
+ $this->settings = $settings;
+ $this->partners_endpoint = $partners_endpoint;
+ }
+
+ /**
+ * Returns the path to the "Get Seller Account Details" REST route.
+ * This is an internal route which is consumed by the plugin itself during onboarding.
+ *
+ * @param bool $full_route Whether to return the full endpoint path or just the route name.
+ * @return string The full path to the REST endpoint.
+ */
+ public static function seller_account_route( bool $full_route = false ) : string {
+ if ( $full_route ) {
+ return '/' . static::NAMESPACE . '/' . self::SELLER_ACCOUNT_PATH;
+ }
+
+ return self::SELLER_ACCOUNT_PATH;
}
/**
@@ -118,7 +149,7 @@ class CommonRestEndpoint extends RestEndpoint {
* GET /wp-json/wc/v3/wc_paypal/common
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
@@ -134,7 +165,7 @@ class CommonRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -147,7 +178,7 @@ class CommonRestEndpoint extends RestEndpoint {
* GET /wp-json/wc/v3/wc_paypal/common/merchant
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
"/$this->rest_base/merchant",
array(
'methods' => WP_REST_Server::READABLE,
@@ -155,6 +186,19 @@ class CommonRestEndpoint extends RestEndpoint {
'permission_callback' => array( $this, 'check_permission' ),
)
);
+
+ /**
+ * GET /wp-json/wc/v3/wc_paypal/common/seller-account
+ */
+ register_rest_route(
+ static::NAMESPACE,
+ self::seller_account_route(),
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_seller_account_info' ),
+ 'permission_callback' => array( $this, 'check_permission' ),
+ )
+ );
}
/**
@@ -205,6 +249,27 @@ class CommonRestEndpoint extends RestEndpoint {
return $this->return_success( $js_data, $extra_data );
}
+ /**
+ * Requests details from the PayPal API.
+ *
+ * Used during onboarding to enrich the merchant details in the DB.
+ *
+ * @return WP_REST_Response Seller details, provided by PayPal's API.
+ */
+ public function get_seller_account_info() : WP_REST_Response {
+ try {
+ $seller_status = $this->partners_endpoint->seller_status();
+
+ $seller_data = array(
+ 'country' => $seller_status->country(),
+ );
+
+ return $this->return_success( $seller_data );
+ } catch ( Exception $ex ) {
+ return $this->return_error( $ex->getMessage() );
+ }
+ }
+
/**
* Appends the "merchant" attribute to the extra_data collection, which
* contains details about the merchant's PayPal account, like the merchant ID.
diff --git a/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php b/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php
index 25d734b0a..a7290f136 100644
--- a/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php
@@ -62,7 +62,7 @@ class CompleteOnClickEndpoint extends RestEndpoint {
*/
public function register_routes(): void {
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
diff --git a/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php
new file mode 100644
index 000000000..f5c733e40
--- /dev/null
+++ b/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php
@@ -0,0 +1,99 @@
+features_definition = $features_definition;
+ $this->settings = $settings;
+ }
+
+ /**
+ * Registers the REST API routes for features management.
+ */
+ public function register_routes(): void {
+ // GET /features - Get features list.
+ register_rest_route(
+ static::NAMESPACE,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_features' ),
+ 'permission_callback' => array( $this, 'check_permission' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Retrieves the current features.
+ *
+ * @return WP_REST_Response The response containing features data.
+ */
+ public function get_features(): WP_REST_Response {
+ $features = array();
+ foreach ( $this->features_definition->get() as $id => $feature ) {
+ $features[] = array_merge(
+ array( 'id' => $id ),
+ $feature
+ );
+ }
+
+ return $this->return_success(
+ array(
+ 'features' => $features,
+ )
+ );
+ }
+}
diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php
index c2b0e9ff3..3d6c45843 100644
--- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php
@@ -60,7 +60,7 @@ class LoginLinkRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -82,6 +82,16 @@ class LoginLinkRestEndpoint extends RestEndpoint {
return array_map( 'sanitize_text_field', $products );
},
),
+ 'options' => array(
+ 'requires' => false,
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'bool',
+ ),
+ 'sanitize_callback' => function ( $flags ) {
+ return array_map( array( $this, 'to_boolean' ), $flags );
+ },
+ ),
),
)
);
@@ -97,9 +107,10 @@ class LoginLinkRestEndpoint extends RestEndpoint {
public function get_login_url( WP_REST_Request $request ) : WP_REST_Response {
$use_sandbox = $request->get_param( 'useSandbox' );
$products = $request->get_param( 'products' );
+ $flags = (array) $request->get_param( 'options' );
try {
- $url = $this->url_generator->generate( $products, $use_sandbox );
+ $url = $this->url_generator->generate( $products, $flags, $use_sandbox );
return $this->return_success( $url );
} catch ( \Exception $e ) {
diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
index b055ab3e2..e9459be9d 100644
--- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
@@ -41,23 +41,23 @@ class OnboardingRestEndpoint extends RestEndpoint {
* @var array
*/
private array $field_map = array(
- 'completed' => array(
+ 'completed' => array(
'js_name' => 'completed',
'sanitize' => 'to_boolean',
),
- 'step' => array(
+ 'step' => array(
'js_name' => 'step',
'sanitize' => 'to_number',
),
- 'is_casual_seller' => array(
+ 'is_casual_seller' => array(
'js_name' => 'isCasualSeller',
'sanitize' => 'to_boolean',
),
- 'are_optional_payment_methods_enabled' => array(
+ 'accept_card_payments' => array(
'js_name' => 'areOptionalPaymentMethodsEnabled',
'sanitize' => 'to_boolean',
),
- 'products' => array(
+ 'products' => array(
'js_name' => 'products',
),
);
@@ -68,18 +68,27 @@ class OnboardingRestEndpoint extends RestEndpoint {
* @var array
*/
private array $flag_map = array(
- 'can_use_casual_selling' => array(
+ 'can_use_casual_selling' => array(
'js_name' => 'canUseCasualSelling',
),
- 'can_use_vaulting' => array(
+ 'can_use_vaulting' => array(
'js_name' => 'canUseVaulting',
),
- 'can_use_card_payments' => array(
+ 'can_use_card_payments' => array(
'js_name' => 'canUseCardPayments',
),
- 'can_use_subscriptions' => array(
+ 'can_use_subscriptions' => array(
'js_name' => 'canUseSubscriptions',
),
+ 'should_skip_payment_methods' => array(
+ 'js_name' => 'shouldSkipPaymentMethods',
+ ),
+ 'can_use_fastlane' => array(
+ 'js_name' => 'canUseFastlane',
+ ),
+ 'can_use_pay_later' => array(
+ 'js_name' => 'canUsePayLater',
+ ),
);
/**
@@ -101,7 +110,7 @@ class OnboardingRestEndpoint extends RestEndpoint {
* GET /wp-json/wc/v3/wc_paypal/onboarding
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
@@ -117,7 +126,7 @@ class OnboardingRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
diff --git a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php
index 5713ce570..d8a322215 100644
--- a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php
@@ -63,7 +63,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint {
* GET wc/v3/wc_paypal/pay_later_messaging
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
@@ -76,7 +76,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint {
* POST wc/v3/wc_paypal/pay_later_messaging
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php
index 5047b484d..428e05c9a 100644
--- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php
@@ -29,6 +29,8 @@ use WP_REST_Response;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway;
+use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
+use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
/**
* REST controller for the "Payment Methods" settings tab.
@@ -49,7 +51,21 @@ class PaymentRestEndpoint extends RestEndpoint {
*
* @var PaymentSettings
*/
- protected PaymentSettings $settings;
+ protected PaymentSettings $payment_settings;
+
+ /**
+ * The payment method details.
+ *
+ * @var PaymentMethodsDefinition
+ */
+ protected PaymentMethodsDefinition $payment_methods_definition;
+
+ /**
+ * The payment method dependencies.
+ *
+ * @var PaymentMethodsDependenciesDefinition
+ */
+ protected PaymentMethodsDependenciesDefinition $payment_methods_dependencies;
/**
* Field mapping for request to profile transformation.
@@ -78,10 +94,18 @@ class PaymentRestEndpoint extends RestEndpoint {
/**
* Constructor.
*
- * @param PaymentSettings $settings The settings instance.
+ * @param PaymentSettings $payment_settings The settings instance.
+ * @param PaymentMethodsDefinition $payment_methods_definition Payment Method details.
+ * @param PaymentMethodsDependenciesDefinition $payment_methods_dependencies The payment method dependencies.
*/
- public function __construct( PaymentSettings $settings ) {
- $this->settings = $settings;
+ public function __construct(
+ PaymentSettings $payment_settings,
+ PaymentMethodsDefinition $payment_methods_definition,
+ PaymentMethodsDependenciesDefinition $payment_methods_dependencies
+ ) {
+ $this->payment_settings = $payment_settings;
+ $this->payment_methods_definition = $payment_methods_definition;
+ $this->payment_methods_dependencies = $payment_methods_dependencies;
}
/**
@@ -89,515 +113,11 @@ class PaymentRestEndpoint extends RestEndpoint {
*
* @return array[]
*/
- protected function gateways():array {
- return array(
- // PayPal checkout.
- PayPalGateway::ID => array(
- 'id' => PayPalGateway::ID,
- 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ),
- 'description' => __(
- 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-paypal',
- 'itemTitle' => __( 'PayPal', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.',
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( PayPalGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( PayPalGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- 'paypalShowLogo' => array(
- 'type' => 'toggle',
- 'default' => $this->settings->get_paypal_show_logo(),
- 'label' => __( 'Show logo', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- 'venmo' => array(
- 'id' => 'venmo',
- 'itemTitle' => __( 'Venmo', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'Offer Venmo at checkout to millions of active users.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-venmo',
- 'enabled' => $this->settings->get_venmo_enabled(),
- ),
- 'pay-later' => array(
- 'id' => 'pay-later',
- 'itemTitle' => __( 'Pay Later', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-paypal',
- 'enabled' => $this->settings->get_paylater_enabled(),
- ),
- CardButtonGateway::ID => array(
- 'id' => CardButtonGateway::ID,
- 'title' => __(
- 'Credit and debit card payments',
- 'woocommerce-paypal-payments'
- ),
- 'description' => __(
- "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.",
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-cards',
- 'itemTitle' => __(
- 'Credit and debit card payments',
- 'woocommerce-paypal-payments'
- ),
- 'itemDescription' => __(
- "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.",
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( CardButtonGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( CardButtonGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
+ protected function gateways() : array {
+ $methods = $this->payment_methods_definition->get_definitions();
- // Online card Payments.
- CreditCardGateway::ID => array(
- 'id' => CreditCardGateway::ID,
- 'title' => __(
- 'Advanced Credit and Debit Card Payments',
- 'woocommerce-paypal-payments'
- ),
- 'description' => __(
- "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.",
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-advanced-cards',
- 'itemTitle' => __(
- 'Advanced Credit and Debit Card Payments',
- 'woocommerce-paypal-payments'
- ),
- 'itemDescription' => __(
- "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.",
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( CreditCardGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( CreditCardGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- 'threeDSecure' => array(
- 'type' => 'radio',
- 'default' => $this->settings->get_three_d_secure(),
- 'label' => __( '3D Secure', 'woocommerce-paypal-payments' ),
- 'description' => __(
- 'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
- 'woocommerce-paypal-payments'
- ),
- 'options' => array(
- array(
- 'label' => __(
- 'No 3D Secure',
- 'woocommerce-paypal-payments'
- ),
- 'value' => 'no-3d-secure',
- ),
- array(
- 'label' => __(
- 'Only when required',
- 'woocommerce-paypal-payments'
- ),
- 'value' => 'only-required-3d-secure',
- ),
- array(
- 'label' => __(
- 'Always require 3D Secure',
- 'woocommerce-paypal-payments'
- ),
- 'value' => 'always-3d-secure',
- ),
- ),
- ),
- ),
- ),
- AxoGateway::ID => array(
- 'id' => AxoGateway::ID,
- '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.",
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-fastlane',
- 'itemTitle' => __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- "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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( AxoGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( AxoGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- 'fastlaneCardholderName' => array(
- 'type' => 'toggle',
- 'default' => $this->settings->get_fastlane_cardholder_name(),
- 'label' => __(
- 'Display cardholder name',
- 'woocommerce-paypal-payments'
- ),
- ),
- 'fastlaneDisplayWatermark' => array(
- 'type' => 'toggle',
- 'default' => $this->settings->get_fastlane_display_watermark(),
- 'label' => __(
- 'Display Fastlane Watermark',
- 'woocommerce-paypal-payments'
- ),
- ),
- ),
- ),
- ApplePayGateway::ID => array(
- 'id' => ApplePayGateway::ID,
- 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
- 'description' => __(
- 'Allow customers to pay via their Apple Pay digital wallet.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-apple-pay',
- 'itemTitle' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'Allow customers to pay via their Apple Pay digital wallet.',
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( ApplePayGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( ApplePayGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- GooglePayGateway::ID => array(
- 'id' => GooglePayGateway::ID,
- 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
- 'description' => __(
- 'Allow customers to pay via their Google Pay digital wallet.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-google-pay',
- 'itemTitle' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'Allow customers to pay via their Google Pay digital wallet.',
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( GooglePayGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( GooglePayGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
-
- // Alternative payment methods.
- BancontactGateway::ID => array(
- 'id' => BancontactGateway::ID,
- 'title' => __( 'Bancontact', 'woocommerce-paypal-payments' ),
- 'description' => __(
- 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-bancontact',
- 'itemTitle' => __( 'Bancontact', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.',
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( BancontactGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( BancontactGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- BlikGateway::ID => array(
- 'id' => BlikGateway::ID,
- 'title' => __( 'BLIK', 'woocommerce-paypal-payments' ),
- 'description' => __(
- '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',
- 'itemTitle' => __( 'BLIK', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( BlikGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( BlikGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- EPSGateway::ID => array(
- 'id' => EPSGateway::ID,
- 'title' => __( 'eps', 'woocommerce-paypal-payments' ),
- 'description' => __(
- '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',
- 'itemTitle' => __( 'eps', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( EPSGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( EPSGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- IDealGateway::ID => array(
- 'id' => IDealGateway::ID,
- 'title' => __( 'iDEAL', 'woocommerce-paypal-payments' ),
- 'description' => __(
- 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.',
- 'woocommerce-paypal-payments'
- ),
- 'icon' => 'payment-method-ideal',
- 'itemTitle' => __( 'iDEAL', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.',
- 'woocommerce-paypal-payments'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( IDealGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( IDealGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- MyBankGateway::ID => array(
- 'id' => MyBankGateway::ID,
- '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',
- 'itemTitle' => __( 'MyBank', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( MyBankGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( MyBankGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- P24Gateway::ID => array(
- 'id' => P24Gateway::ID,
- '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',
- 'itemTitle' => __( 'Przelewy24', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( P24Gateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( P24Gateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- TrustlyGateway::ID => array(
- 'id' => TrustlyGateway::ID,
- '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',
- 'itemTitle' => __( 'Trustly', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( TrustlyGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( TrustlyGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- MultibancoGateway::ID => array(
- 'id' => MultibancoGateway::ID,
- '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',
- 'itemTitle' => __( 'Multibanco', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( MultibancoGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( MultibancoGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- PayUponInvoiceGateway::ID => array(
- 'id' => PayUponInvoiceGateway::ID,
- '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' => '',
- 'itemTitle' => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( PayUponInvoiceGateway::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( PayUponInvoiceGateway::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- OXXO::ID => array(
- 'id' => OXXO::ID,
- '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',
- 'itemTitle' => __( 'OXXO', 'woocommerce-paypal-payments' ),
- 'itemDescription' => __(
- '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'
- ),
- 'fields' => array(
- 'checkoutPageTitle' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentTitle( OXXO::ID ) ?? '',
- 'label' => __( 'Checkout page title', 'woocommerce-paypal-payments' ),
- ),
- 'checkoutPageDescription' => array(
- 'type' => 'text',
- 'default' => $this->getPaymentDescription( OXXO::ID ) ?? '',
- 'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
- ),
- ),
- ),
- );
+ // Add dependency information to the methods.
+ return $this->payment_methods_dependencies->add_dependency_info_to_methods( $methods );
}
/**
@@ -608,7 +128,7 @@ class PaymentRestEndpoint extends RestEndpoint {
* GET wc/v3/wc_paypal/payment
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
@@ -628,7 +148,7 @@ class PaymentRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -644,51 +164,48 @@ class PaymentRestEndpoint extends RestEndpoint {
* @return WP_REST_Response The current payment methods details.
*/
public function get_details() : WP_REST_Response {
- $all_gateways = WC()->payment_gateways->payment_gateways();
+ $gateway_settings = array();
+ $all_payment_methods = $this->gateways();
- $gateway_settings = array();
-
- foreach ( $this->gateways() as $key => $value ) {
- // Here we handle the payment methods that are listed on the page but not registered as WooCommerce Payment gateways.
- if ( ! isset( $all_gateways[ $key ] ) ) {
- $gateway_settings[ $key ] = array(
- 'id' => $this->gateways()[ $key ]['id'] ?? '',
- 'title' => $this->gateways()[ $key ]['title'] ?? '',
- 'description' => $this->gateways()[ $key ]['description'] ?? '',
- 'enabled' => $this->gateways()[ $key ]['enabled'] ?? false,
- 'icon' => $this->gateways()[ $key ]['icon'] ?? '',
- 'itemTitle' => $this->gateways()[ $key ]['itemTitle'] ?? '',
- 'itemDescription' => $this->gateways()[ $key ]['itemDescription'] ?? '',
- );
-
- if ( isset( $this->gateways()[ $key ]['fields'] ) ) {
- $gateway_settings[ $key ]['fields'] = $this->gateways()[ $key ]['fields'];
- }
+ // First extract __meta if present.
+ if ( isset( $all_payment_methods['__meta'] ) ) {
+ $gateway_settings['__meta'] = $all_payment_methods['__meta'];
+ }
+ foreach ( $all_payment_methods as $key => $payment_method ) {
+ // Skip the __meta key as we've already handled it.
+ if ( $key === '__meta' ) {
continue;
}
- $gateway = $all_gateways[ $key ];
-
$gateway_settings[ $key ] = array(
- 'enabled' => 'yes' === $gateway->enabled,
- 'title' => str_replace( '&', '&', $gateway->get_title() ),
- 'description' => $gateway->get_description(),
- 'id' => $this->gateways()[ $key ]['id'] ?? $key,
- 'icon' => $this->gateways()[ $key ]['icon'] ?? '',
- 'itemTitle' => $this->gateways()[ $key ]['itemTitle'] ?? '',
- 'itemDescription' => $this->gateways()[ $key ]['itemDescription'] ?? '',
+ 'id' => $payment_method['id'],
+ 'title' => $payment_method['title'],
+ 'description' => $payment_method['description'],
+ 'enabled' => $payment_method['enabled'],
+ 'icon' => $payment_method['icon'],
+ 'itemTitle' => $payment_method['itemTitle'],
+ 'itemDescription' => $payment_method['itemDescription'],
+ 'warningMessages' => $payment_method['warningMessages'],
);
- if ( isset( $this->gateways()[ $key ]['fields'] ) ) {
- $gateway_settings[ $key ]['fields'] = $this->gateways()[ $key ]['fields'];
+ if ( isset( $payment_method['fields'] ) ) {
+ $gateway_settings[ $key ]['fields'] = $payment_method['fields'];
+ }
+
+ if ( isset( $payment_method['depends_on_payment_methods'] ) ) {
+ $gateway_settings[ $key ]['depends_on_payment_methods'] = $payment_method['depends_on_payment_methods'];
+ }
+
+ if ( isset( $payment_method['depends_on_settings'] ) ) {
+ $gateway_settings[ $key ]['depends_on_settings'] = $payment_method['depends_on_settings'];
}
}
- $gateway_settings['paypalShowLogo'] = $this->settings->get_paypal_show_logo();
- $gateway_settings['threeDSecure'] = $this->settings->get_three_d_secure();
- $gateway_settings['fastlaneCardholderName'] = $this->settings->get_fastlane_cardholder_name();
- $gateway_settings['fastlaneDisplayWatermark'] = $this->settings->get_fastlane_display_watermark();
+ $gateway_settings['paypalShowLogo'] = $this->payment_settings->get_paypal_show_logo();
+ $gateway_settings['threeDSecure'] = $this->payment_settings->get_three_d_secure();
+ $gateway_settings['fastlaneCardholderName'] = $this->payment_settings->get_fastlane_cardholder_name();
+ $gateway_settings['fastlaneDisplayWatermark'] = $this->payment_settings->get_fastlane_display_watermark();
return $this->return_success( apply_filters( 'woocommerce_paypal_payments_payment_methods', $gateway_settings ) );
}
@@ -701,52 +218,26 @@ class PaymentRestEndpoint extends RestEndpoint {
* @return WP_REST_Response The updated payment methods details.
*/
public function update_details( WP_REST_Request $request ) : WP_REST_Response {
- $all_gateways = WC()->payment_gateways->payment_gateways();
-
$request_data = $request->get_params();
+ $all_methods = $this->gateways();
- foreach ( $this->gateways() as $key => $value ) {
- // Here we handle the payment methods that are listed on the page but not registered as WooCommerce Payment gateways.
- if ( ! isset( $all_gateways[ $key ] ) && isset( $request_data[ $key ] ) ) {
- switch ( $key ) {
- case 'venmo':
- $this->settings->set_venmo_enabled( $request_data[ $key ]['enabled'] );
- break;
- case 'pay-later':
- $this->settings->set_paylater_enabled( $request_data[ $key ]['enabled'] );
- break;
- }
-
+ foreach ( $all_methods as $key => $value ) {
+ $new_data = $request_data[ $key ] ?? null;
+ if ( ! $new_data ) {
continue;
}
- // Check if the REST body contains details for this gateway.
- if ( ! isset( $request_data[ $key ] ) || ! isset( $all_gateways[ $key ] ) ) {
- continue;
- }
-
- $gateway = $all_gateways[ $key ];
- $new_data = $request_data[ $key ];
- $gateway->init_form_fields();
- $settings = $gateway->settings;
-
if ( isset( $new_data['enabled'] ) ) {
- $settings['enabled'] = wc_bool_to_string( $new_data['enabled'] );
- $gateway->enabled = $settings['enabled'];
+ $this->payment_settings->toggle_method_state( $key, $new_data['enabled'] );
}
if ( isset( $new_data['title'] ) ) {
- $settings['title'] = sanitize_text_field( $new_data['title'] );
- $gateway->title = $settings['title'];
+ $this->payment_settings->set_method_title( $key, sanitize_text_field( $new_data['title'] ) );
}
if ( isset( $new_data['description'] ) ) {
- $settings['description'] = wp_kses_post( $new_data['description'] );
- $gateway->description = $settings['description'];
+ $this->payment_settings->set_method_description( $key, wp_kses_post( $new_data['description'] ) );
}
-
- $gateway->settings = $settings;
- update_option( $gateway->get_option_key(), $settings );
}
$wp_data = $this->sanitize_for_wordpress(
@@ -754,37 +245,9 @@ class PaymentRestEndpoint extends RestEndpoint {
$this->field_map
);
- $this->settings->from_array( $wp_data );
- $this->settings->save();
+ $this->payment_settings->from_array( $wp_data );
+ $this->payment_settings->save();
return $this->get_details();
}
-
- /**
- * Returns title for the given gateway.
- *
- * @param string $id Gateway ID.
- * @return string
- */
- private function getPaymentTitle( string $id ): string {
- if ( ! isset( WC()->payment_gateways()->payment_gateways()[ $id ] ) ) {
- return '';
- }
-
- return WC()->payment_gateways()->payment_gateways()[ $id ]->get_title() ?? '';
- }
-
- /**
- * Returns title for the given gateway.
- *
- * @param string $id Gateway ID.
- * @return string
- */
- private function getPaymentDescription( string $id ): string {
- if ( ! isset( WC()->payment_gateways()->payment_gateways()[ $id ] ) ) {
- return '';
- }
-
- return WC()->payment_gateways()->payment_gateways()[ $id ]->get_description() ?? '';
- }
}
diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php
index 3b17b84ed..a5be3f207 100644
--- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php
@@ -87,7 +87,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/refresh-features
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
diff --git a/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php b/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php
index b922127d2..5906836d0 100644
--- a/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php
@@ -54,7 +54,7 @@ class ResetDismissedTodosEndpoint extends RestEndpoint {
* POST wc/v3/wc_paypal/reset-dismissed-todos
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php
index e9e9948ab..f0cf1306a 100644
--- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php
@@ -18,10 +18,8 @@ use WP_REST_Response;
abstract class RestEndpoint extends WC_REST_Controller {
/**
* Endpoint namespace.
- *
- * @var string
*/
- protected $namespace = 'wc/v3/wc_paypal';
+ protected const NAMESPACE = 'wc/v3/wc_paypal';
/**
* Verify access.
diff --git a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php
index 9f200ed99..06f639bba 100644
--- a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php
@@ -109,7 +109,7 @@ class SettingsRestEndpoint extends RestEndpoint {
* POST wc/v3/wc_paypal/settings
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
array(
diff --git a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php
index 450549e43..7e4057704 100644
--- a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php
@@ -107,7 +107,7 @@ class StylingRestEndpoint extends RestEndpoint {
* GET wc/v3/wc_paypal/styling
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
@@ -123,7 +123,7 @@ class StylingRestEndpoint extends RestEndpoint {
* }
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
diff --git a/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php
index 42d3c69da..fa922f2f7 100644
--- a/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php
@@ -17,6 +17,7 @@ use WP_REST_Response;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
+use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService;
/**
* REST controller for the "Things To Do" items in the Overview tab.
@@ -54,21 +55,31 @@ class TodosRestEndpoint extends RestEndpoint {
*/
protected SettingsRestEndpoint $settings;
+ /**
+ * The todos sorting service.
+ *
+ * @var TodosSortingAndFilteringService
+ */
+ protected TodosSortingAndFilteringService $sorting_service;
+
/**
* TodosRestEndpoint constructor.
*
- * @param TodosModel $todos The todos model instance.
- * @param TodosDefinition $todos_definition The todos definition instance.
- * @param SettingsRestEndpoint $settings The settings endpoint instance.
+ * @param TodosModel $todos The todos model instance.
+ * @param TodosDefinition $todos_definition The todos definition instance.
+ * @param SettingsRestEndpoint $settings The settings endpoint instance.
+ * @param TodosSortingAndFilteringService $sorting_service The todos sorting service.
*/
public function __construct(
TodosModel $todos,
TodosDefinition $todos_definition,
- SettingsRestEndpoint $settings
+ SettingsRestEndpoint $settings,
+ TodosSortingAndFilteringService $sorting_service
) {
$this->todos = $todos;
$this->todos_definition = $todos_definition;
$this->settings = $settings;
+ $this->sorting_service = $sorting_service;
}
/**
@@ -77,7 +88,7 @@ class TodosRestEndpoint extends RestEndpoint {
public function register_routes(): void {
// GET/POST /todos - Get todos list and update dismissed todos.
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
array(
@@ -95,7 +106,7 @@ class TodosRestEndpoint extends RestEndpoint {
// POST /todos/reset - Reset dismissed todos.
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base . '/reset',
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -106,7 +117,7 @@ class TodosRestEndpoint extends RestEndpoint {
// POST /todos/complete - Mark todo as completed on click.
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base . '/complete',
array(
'methods' => WP_REST_Server::EDITABLE,
@@ -146,9 +157,11 @@ class TodosRestEndpoint extends RestEndpoint {
}
}
+ $filtered_todos = $this->sorting_service->apply_all_priority_filters( $todos );
+
return $this->return_success(
array(
- 'todos' => $todos,
+ 'todos' => $filtered_todos,
'dismissedTodos' => $dismissed_ids,
'completedOnClickTodos' => $completed_onclick_ids,
)
diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
index 81e8f4335..6bf3c6352 100644
--- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php
@@ -79,7 +79,7 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/webhooks
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base,
array(
array(
@@ -100,7 +100,7 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/webhooks/simulate
*/
register_rest_route(
- $this->namespace,
+ static::NAMESPACE,
'/' . $this->rest_base . '/simulate',
array(
array(
diff --git a/modules/ppcp-settings/src/Enum/ProductChoicesEnum.php b/modules/ppcp-settings/src/Enum/ProductChoicesEnum.php
new file mode 100644
index 000000000..57a4a8ee6
--- /dev/null
+++ b/modules/ppcp-settings/src/Enum/ProductChoicesEnum.php
@@ -0,0 +1,56 @@
+common_settings = $common_settings;
- $this->connection_host = $connection_host;
- $this->login_endpoint = $login_endpoint;
- $this->referrals_data = $referrals_data;
- $this->logger = $logger ?: new NullLogger();
+ $this->common_settings = $common_settings;
+ $this->connection_host = $connection_host;
+ $this->login_endpoint = $login_endpoint;
+ $this->referrals_data = $referrals_data;
+ $this->connection_state = $connection_state;
+ $this->rest_service = $rest_service;
+ $this->logger = $logger ?: new NullLogger();
}
/**
@@ -112,6 +136,9 @@ class AuthenticationManager {
$this->common_settings->reset_merchant_data();
$this->common_settings->save();
+ // Update the connection status and clear the environment flags.
+ $this->connection_state->disconnect();
+
/**
* Broadcast, that the plugin disconnected from PayPal. This allows other
* modules to clean up merchant-related details, such as eligibility flags.
@@ -162,6 +189,7 @@ class AuthenticationManager {
* PayPal account using a client ID and secret.
*
* Part of the "Direct Connection" (Manual Connection) flow.
+ * This connection type is only available to business merchants.
*
* @param bool $use_sandbox Whether to use the sandbox mode.
* @param string $client_id The client ID.
@@ -188,6 +216,7 @@ class AuthenticationManager {
$client_secret,
$payee['merchant_id'],
$payee['email_address'],
+ '',
SellerTypeEnum::BUSINESS
);
@@ -402,6 +431,73 @@ class AuthenticationManager {
);
}
+ /**
+ * Fetches additional details about the connected merchant from PayPal
+ * and stores them in the DB.
+ *
+ * This process only works after persisting basic connection details.
+ *
+ * @return void
+ */
+ private function enrich_merchant_details() : void {
+ if ( ! $this->common_settings->is_merchant_connected() ) {
+ return;
+ }
+
+ try {
+ $endpoint = CommonRestEndpoint::seller_account_route( true );
+ $response = $this->rest_service->get_response( $endpoint );
+
+ if ( ! $response['success'] ) {
+ $this->enrichment_failed( 'Server failed to provide data', $response );
+
+ return;
+ }
+
+ $details = $response['data'];
+ } catch ( Throwable $exception ) {
+ $this->enrichment_failed( $exception->getMessage() );
+
+ return;
+ }
+
+ if ( ! isset( $details['country'] ) ) {
+ $this->enrichment_failed( 'Missing country in merchant details' );
+
+ return;
+ }
+
+ // Request the merchant details via a PayPal API request.
+ $connection = $this->common_settings->get_merchant_data();
+
+ // Enrich the connection details with additional details.
+ $connection->merchant_country = $details['country'];
+
+ // Persist the changes.
+ $this->common_settings->set_merchant_data( $connection );
+ $this->common_settings->save();
+ }
+
+ /**
+ * When the `enrich_merchant_details()` call fails, this method might
+ * set up a cron task to retry the attempt after some time.
+ *
+ * @param string $reason Reason for the failure, will be logged.
+ * @param mixed $details Optional. Additional details to log.
+ * @return void
+ */
+ private function enrichment_failed( string $reason, $details = null ) : void {
+ $this->logger->warning(
+ 'Failed to enrich merchant details: ' . $reason,
+ array(
+ 'reason' => $reason,
+ 'details' => $details,
+ )
+ );
+
+ // TODO: Schedule a cron task to retry the enrichment, e.g. with wp_schedule_single_event().
+ }
+
/**
* Stores the provided details in the data model.
*
@@ -420,6 +516,12 @@ class AuthenticationManager {
if ( $this->common_settings->is_merchant_connected() ) {
$this->logger->info( 'Merchant successfully connected to PayPal' );
+ // Update the connection status and set the environment flags.
+ $this->connection_state->connect( $connection->is_sandbox );
+
+ // At this point, we can use the PayPal API to get more details about the seller.
+ $this->enrich_merchant_details();
+
/**
* Request to flush caches before authenticating the merchant, to
* ensure the new merchant does not use stale data from previous
diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
index 279821bee..c3204ccf1 100644
--- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
+++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
@@ -82,12 +82,13 @@ class ConnectionUrlGenerator {
*
* @param array $products An array of product identifiers to include in the sign-up process.
* These determine the PayPal onboarding experience.
+ * @param array $flags Onboarding choices that will customize the ISU payload.
* @param bool $use_sandbox Whether to generate a sandbox URL.
*
* @return string The generated PayPal onboarding URL.
*/
- public function generate( array $products = array(), bool $use_sandbox = false ) : string {
- $cache_key = $this->cache_key( $products, $use_sandbox );
+ public function generate( array $products = array(), array $flags = array(), bool $use_sandbox = false ) : string {
+ $cache_key = $this->cache_key( $products, $flags, $use_sandbox );
$user_id = get_current_user_id();
$onboarding_url = $this->url_manager->get( $cache_key, $user_id );
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
@@ -100,7 +101,7 @@ class ConnectionUrlGenerator {
$this->logger->info( 'Generating onboarding URL for: ' . $cache_key );
- $url = $this->generate_new_url( $use_sandbox, $products, $onboarding_url, $cache_key );
+ $url = $this->generate_new_url( $use_sandbox, $products, $flags, $onboarding_url, $cache_key );
if ( $url ) {
$this->persist_url( $onboarding_url, $url );
@@ -112,18 +113,28 @@ class ConnectionUrlGenerator {
/**
* Generates a cache key from the environment and sorted product array.
*
+ * Q: Why do we cache the connection URL?
+ * A: The URL is generated by a partner-referrals API, i.e. it requires a
+ * remote request; caching the response avoids unnecessary API calls.
+ *
* @param array $products Product identifiers that are part of the cache key.
+ * @param array $flags Onboarding flags.
* @param bool $for_sandbox Whether the cache contains a sandbox URL.
*
* @return string The cache key, defining the product list and environment.
*/
- protected function cache_key( array $products, bool $for_sandbox ) : string {
+ protected function cache_key( array $products, array $flags, bool $for_sandbox ) : string {
$environment = $for_sandbox ? 'sandbox' : 'production';
// Sort products alphabetically, to improve cache implementation.
sort( $products );
- return $environment . '-' . implode( '-', $products );
+ // Extract the names of active flags.
+ $active_flags = array_keys( array_filter( $flags ) );
+
+ return strtolower(
+ $environment . '-' . implode( '-', $products ) . '-' . implode( '-', $active_flags )
+ );
}
/**
@@ -162,12 +173,13 @@ class ConnectionUrlGenerator {
*
* @param bool $for_sandbox Whether to generate a sandbox URL.
* @param array $products The products array.
+ * @param array $flags Onboarding flags.
* @param OnboardingUrl $onboarding_url The OnboardingUrl object.
* @param string $cache_key The cache key.
*
* @return string The generated URL or an empty string on failure.
*/
- protected function generate_new_url( bool $for_sandbox, array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string {
+ protected function generate_new_url( bool $for_sandbox, array $products, array $flags, OnboardingUrl $onboarding_url, string $cache_key ) : string {
$query_args = array( 'displayMode' => 'minibrowser' );
$onboarding_url->init();
@@ -179,7 +191,7 @@ class ConnectionUrlGenerator {
return '';
}
- $data = $this->prepare_referral_data( $products, $onboarding_token );
+ $data = $this->prepare_referral_data( $products, $flags, $onboarding_token );
try {
$referral = $this->partner_referrals->get_value( $for_sandbox );
@@ -197,16 +209,18 @@ class ConnectionUrlGenerator {
* Prepares the referral data.
*
* @param array $products The products array.
+ * @param array $flags Onboarding flags.
* @param string $onboarding_token The onboarding token.
*
* @return array The prepared referral data.
*/
- protected function prepare_referral_data( array $products, string $onboarding_token ) : array {
- $data = $this->referrals_data
- ->with_products( $products )
- ->data();
-
- return $this->referrals_data->append_onboarding_token( $data, $onboarding_token );
+ protected function prepare_referral_data( array $products, array $flags, string $onboarding_token ) : array {
+ return $this->referrals_data->data(
+ $products,
+ $onboarding_token,
+ (bool) ( $flags['useSubscriptions'] ?? false ),
+ (bool) ( $flags['useCardPayments'] ?? false )
+ );
}
/**
diff --git a/modules/ppcp-settings/src/Service/FeaturesEligibilityService.php b/modules/ppcp-settings/src/Service/FeaturesEligibilityService.php
new file mode 100644
index 000000000..9f3fe4db7
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/FeaturesEligibilityService.php
@@ -0,0 +1,103 @@
+is_save_paypal_and_venmo_eligible = $is_save_paypal_and_venmo_eligible;
+ $this->is_advanced_credit_and_debit_cards_eligible = $is_advanced_credit_and_debit_cards_eligible;
+ $this->is_alternative_payment_methods_eligible = $is_alternative_payment_methods_eligible;
+ $this->is_google_pay_eligible = $is_google_pay_eligible;
+ $this->is_apple_pay_eligible = $is_apple_pay_eligible;
+ $this->is_pay_later_eligible = $is_pay_later_eligible;
+ }
+
+ /**
+ * Returns all eligibility checks as callables.
+ *
+ * @return array
+ */
+ public function get_eligibility_checks(): array {
+ return array(
+ 'save_paypal_and_venmo' => fn() => $this->is_save_paypal_and_venmo_eligible,
+ 'advanced_credit_and_debit_cards' => fn() => $this->is_advanced_credit_and_debit_cards_eligible,
+ 'alternative_payment_methods' => fn() => $this->is_alternative_payment_methods_eligible,
+ 'google_pay' => fn() => $this->is_google_pay_eligible,
+ 'apple_pay' => fn() => $this->is_apple_pay_eligible,
+ 'pay_later' => fn() => $this->is_pay_later_eligible,
+ );
+ }
+}
diff --git a/modules/ppcp-settings/src/Service/GatewayRedirectService.php b/modules/ppcp-settings/src/Service/GatewayRedirectService.php
new file mode 100644
index 000000000..7b043d04f
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/GatewayRedirectService.php
@@ -0,0 +1,129 @@
+gateways = array(
+ AxoGateway::ID,
+ GooglePayGateway::ID,
+ ApplePayGateway::ID,
+ CreditCardGateway::ID,
+ CardButtonGateway::ID,
+ BancontactGateway::ID,
+ BlikGateway::ID,
+ EPSGateway::ID,
+ IDealGateway::ID,
+ MyBankGateway::ID,
+ P24Gateway::ID,
+ TrustlyGateway::ID,
+ MultibancoGateway::ID,
+ OXXO::ID,
+ PayUponInvoiceGateway::ID,
+ );
+ }
+
+ /**
+ * Register hooks.
+ *
+ * @return void
+ */
+ public function register(): void {
+ add_action(
+ 'admin_init',
+ array( $this, 'handle_redirects' )
+ );
+ }
+
+ /**
+ * Handle redirects for gateway settings pages.
+ *
+ * @return void
+ */
+ public function handle_redirects(): void {
+ if ( ! is_admin() ) {
+ return;
+ }
+
+ // Get current URL parameters.
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ // The sanitize_get_param method handles unslashing and sanitization internally.
+ $page = isset( $_GET['page'] ) ? $this->sanitize_get_param( $_GET['page'] ) : '';
+ $tab = isset( $_GET['tab'] ) ? $this->sanitize_get_param( $_GET['tab'] ) : '';
+ $section = isset( $_GET['section'] ) ? $this->sanitize_get_param( $_GET['section'] ) : '';
+ // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
+
+ // Check if we're on a WooCommerce settings page and checkout tab.
+ if ( $page !== 'wc-settings' || $tab !== 'checkout' ) {
+ return;
+ }
+
+ // Check if we're on one of the gateway settings pages we want to redirect.
+ if ( in_array( $section, $this->gateways, true ) ) {
+ $redirect_url = admin_url(
+ sprintf(
+ 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=payment-methods&highlight=%s',
+ $section
+ )
+ );
+
+ wp_safe_redirect( $redirect_url );
+ exit;
+ }
+ }
+
+ /**
+ * Sanitizes a GET parameter that could be string or array.
+ *
+ * @param mixed $param The parameter to sanitize.
+ * @return string The sanitized parameter.
+ */
+ private function sanitize_get_param( $param ): string {
+ if ( is_array( $param ) ) {
+ return '';
+ }
+ return sanitize_text_field( wp_unslash( $param ) );
+ }
+}
diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php
new file mode 100644
index 000000000..baa57a80b
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/InternalRestService.php
@@ -0,0 +1,135 @@
+logger = $logger;
+ }
+
+ /**
+ * Performs a REST call to the defined local REST endpoint.
+ *
+ * @param string $endpoint The endpoint for which the token is generated.
+ * @return mixed The REST response.
+ * @throws RuntimeException In case the remote request fails, an exception is thrown.
+ */
+ public function get_response( string $endpoint ) {
+ $rest_url = rest_url( $endpoint );
+ $rest_nonce = wp_create_nonce( 'wp_rest' );
+ $auth_cookies = $this->build_authentication_cookie();
+
+ $this->logger->info( "Calling internal REST endpoint: $rest_url" );
+
+ $response = wp_remote_request(
+ $rest_url,
+ array(
+ 'method' => 'GET',
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ 'X-WP-Nonce' => $rest_nonce,
+ ),
+ 'cookies' => $auth_cookies,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ // Error: The wp_remote_request() call failed (timeout or similar).
+ $error = new RuntimeException( 'Internal REST error' );
+ $this->logger->error( $error->getMessage(), array( 'response' => $response ) );
+
+ throw $error;
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+
+ try {
+ $json = json_decode( $body, true, 512, JSON_THROW_ON_ERROR );
+ } catch ( Throwable $exception ) {
+ // Error: The returned body-string is not valid JSON.
+ $error = new RuntimeException( 'Internal REST error: Invalid JSON response' );
+ $this->logger->error(
+ $error->getMessage(),
+ array(
+ 'error' => $exception->getMessage(),
+ 'response_body' => $body,
+ )
+ );
+
+ throw $error;
+ }
+
+ $this->logger->info( 'Internal REST success!', array( 'json' => $json ) );
+
+ return $json;
+ }
+
+ /**
+ * Generate the cookie collection with relevant WordPress authentication
+ * cookies, which allows us to extend the current user's session to the
+ * called REST endpoint.
+ *
+ * @return array A list of cookies that are required to authenticate the user.
+ */
+ private function build_authentication_cookie() : array {
+ $cookies = array();
+
+ // Cookie names are defined in constants and can be changed by site owners.
+ $wp_cookie_constants = array( 'AUTH_COOKIE', 'SECURE_AUTH_COOKIE', 'LOGGED_IN_COOKIE' );
+
+ foreach ( $wp_cookie_constants as $cookie_const ) {
+ $cookie_name = (string) constant( $cookie_const );
+
+ if ( ! isset( $_COOKIE[ $cookie_name ] ) ) {
+ continue;
+ }
+
+ $cookies[] = new WP_Http_Cookie(
+ array(
+ 'name' => $cookie_name,
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ 'value' => wp_unslash( $_COOKIE[ $cookie_name ] ),
+ )
+ );
+ }
+
+ return $cookies;
+ }
+}
diff --git a/modules/ppcp-settings/src/Service/SettingsDataManager.php b/modules/ppcp-settings/src/Service/SettingsDataManager.php
new file mode 100644
index 000000000..a061e03ec
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/SettingsDataManager.php
@@ -0,0 +1,339 @@
+models_to_reset[] = $data_model;
+ }
+ }
+
+ $this->models_to_reset[] = $onboarding_profile;
+ $this->models_to_reset[] = $general_settings;
+ $this->models_to_reset[] = $payment_settings;
+ $this->models_to_reset[] = $styling_settings;
+ $this->models_to_reset[] = $payment_methods;
+
+ $this->methods_definition = $methods_definition;
+ $this->onboarding_profile = $onboarding_profile;
+ $this->payment_settings = $payment_settings;
+ $this->styling_settings = $styling_settings;
+ $this->payment_methods = $payment_methods;
+ $this->paylater_messaging = $paylater_messaging;
+ }
+
+ /**
+ * Completely purges all settings from the DB.
+ *
+ * @return void
+ */
+ public function reset_all_settings() : void {
+ /**
+ * Broadcast the settings-reset event to allow other modules to perform
+ * cleanup tasks, if needed.
+ */
+ do_action( 'woocommerce_paypal_payments_reset_settings' );
+
+ foreach ( $this->models_to_reset as $model ) {
+ $model->purge();
+ }
+
+ // Clear any caches.
+ wp_cache_flush();
+ }
+
+ /**
+ * Applies a default configuration to the plugin for a new merchant.
+ *
+ * This method checks the onboarding "setup_done" flag to determine if
+ * the defaults should be applied. At the end of this method, the
+ * "setup_done" flag is set, so future calls to the method have no effect.
+ *
+ * @param ConfigurationFlagsDTO $flags The configuration flags.
+ * @return void
+ */
+ public function set_defaults_for_new_merchant( ConfigurationFlagsDTO $flags ) : void {
+ if ( $this->onboarding_profile->is_setup_done() ) {
+ return;
+ }
+
+ $this->apply_configuration( $flags );
+
+ $this->onboarding_profile->set_setup_done( true );
+ $this->onboarding_profile->save();
+
+ /**
+ * Fires after the core merchant configuration was applied.
+ *
+ * This action indicates that a merchant completed the onboarding wizard.
+ * The flags contain several choices which the merchant took during the
+ * onboarding wizard, and provide additional context on which defaults
+ * should be applied for the new merchant.
+ *
+ * Other modules or integrations can use this hook to initialize
+ * additional plugin settings on first merchant login.
+ */
+ do_action( 'woocommerce_paypal_payments_apply_default_configuration', $flags );
+ }
+
+ /**
+ * Applies a default configuration to the plugin, without any condition.
+ *
+ * @param ConfigurationFlagsDTO $flags The configuration flags.
+ * @return void
+ */
+ protected function apply_configuration( ConfigurationFlagsDTO $flags ) : void {
+ // Apply defaults for the "Payment Methods" tab.
+ $this->toggle_payment_gateways( $flags );
+
+ // Apply defaults for the "Settings" tab.
+ $this->apply_payment_settings( $flags );
+
+ // Assign defaults for the "Styling" tab.
+ $this->apply_location_styles( $flags );
+
+ // Assign defaults for the "Pay Later Messaging" tab.
+ $this->apply_pay_later_messaging( $flags );
+ }
+
+ /**
+ * Enables or disables payment gateways depending on the provided
+ * configuration flags.
+ *
+ * @param ConfigurationFlagsDTO $flags Shop configuration flags.
+ * @return void
+ */
+ protected function toggle_payment_gateways( ConfigurationFlagsDTO $flags ) : void {
+ // First, disable all payment methods.
+ $methods_paypal = $this->methods_definition->group_paypal_methods();
+ $methods_cards = $this->methods_definition->group_card_methods();
+ $methods_apm = $this->methods_definition->group_apms();
+ $all_methods = array_merge( $methods_paypal, $methods_cards, $methods_apm );
+
+ foreach ( $all_methods as $method ) {
+ $this->payment_methods->toggle_method_state( $method['id'], false );
+ }
+
+ // Always enable PayPal, Venmo and Pay Later.
+ $this->payment_methods->toggle_method_state( PayPalGateway::ID, true );
+ $this->payment_methods->toggle_method_state( 'venmo', true );
+ $this->payment_methods->toggle_method_state( 'pay-later', true );
+
+ if ( $flags->is_business_seller && $flags->use_card_payments ) {
+ // Use BCDC for casual sellers.
+ $this->payment_methods->toggle_method_state( CardButtonGateway::ID, true );
+ }
+
+ if ( $flags->is_business_seller ) {
+ if ( $flags->use_card_payments ) {
+ // Enable ACDC for business sellers.
+ $this->payment_methods->toggle_method_state( CreditCardGateway::ID, true );
+
+ // Apple Pay and Google Pay depend on the ACDC gateway.
+ $this->payment_methods->toggle_method_state( ApplePayGateway::ID, true );
+ $this->payment_methods->toggle_method_state( GooglePayGateway::ID, true );
+ }
+
+ // Enable all APM methods.
+ foreach ( $methods_apm as $method ) {
+ $this->payment_methods->toggle_method_state( $method['id'], true );
+ }
+ }
+
+ $this->payment_methods->save();
+ }
+
+ /**
+ * Applies the default payment settings that are relevant for the provided
+ * configuration flags.
+ *
+ * @param ConfigurationFlagsDTO $flags Shop configuration flags.
+ * @return void
+ */
+ protected function apply_payment_settings( ConfigurationFlagsDTO $flags ) : void {
+ // Enable Pay-Now experience for all merchants.
+ $this->payment_settings->set_enable_pay_now( true );
+
+ if ( $flags->is_business_seller && $flags->use_subscriptions ) {
+ $this->payment_settings->set_save_paypal_and_venmo( true );
+ $this->payment_settings->set_save_card_details( true );
+ }
+
+ $this->payment_settings->save();
+ }
+
+ /**
+ * Applies the default styling details for the shop.
+ *
+ * @param ConfigurationFlagsDTO $flags Shop configuration flags.
+ * @return void
+ */
+ protected function apply_location_styles( ConfigurationFlagsDTO $flags ) : void {
+ $methods_full = array(
+ PayPalGateway::ID,
+ 'venmo',
+ 'pay-later',
+ ApplePayGateway::ID,
+ GooglePayGateway::ID,
+ );
+
+ $methods_own = array(
+ PayPalGateway::ID,
+ 'venmo',
+ 'pay-later',
+ );
+
+ /**
+ * Initialize the styling options using the defaults.
+ *
+ * - Cart: Enabled, display PayPal, Venmo, Pay Later, Google Pay, Apple Pay.
+ * - Classic Checkout: Display PayPal, Venmo, Pay Later, Google Pay, Apple Pay.
+ * - Express Checkout: Display PayPal, Venmo, Pay Later, Google Pay, Apple Pay.
+ * - Mini Cart: Display PayPal, Venmo, Pay Later, Google Pay, Apple Pay.
+ * - Product Page: Display PayPal, Venmo, Pay Later.
+ */
+ $location_styles = array(
+ 'cart' => new LocationStylingDTO( 'cart', true, $methods_full ),
+ 'classic_checkout' => new LocationStylingDTO( 'classic_checkout', true, $methods_full ),
+ 'express_checkout' => new LocationStylingDTO( 'express_checkout', true, $methods_full ),
+ 'mini_cart' => new LocationStylingDTO( 'mini_cart', true, $methods_full ),
+ 'product' => new LocationStylingDTO( 'product', true, $methods_own ),
+ );
+
+ // Apply the settings and persist them to the DB. All merchants use the same options.
+ $this->styling_settings->from_array( $location_styles );
+ $this->styling_settings->save();
+ }
+
+ /**
+ * Applies the default pay later messaging details for the shop.
+ *
+ * @param ConfigurationFlagsDTO $flags Shop configuration flags.
+ * @return void
+ */
+ protected function apply_pay_later_messaging( ConfigurationFlagsDTO $flags ) : void {
+ $config = $this->paylater_messaging['read'];
+
+ $config['cart']['status'] = 'enabled';
+ $config['checkout']['status'] = 'enabled';
+ $config['product']['status'] = 'enabled';
+ $config['shop']['status'] = 'disabled';
+ $config['home']['status'] = 'disabled';
+
+ foreach ( $config['custom_placement'] as $key => $placement ) {
+ $config['custom_placement'][ $key ]['status'] = 'disabled';
+ }
+
+ $this->paylater_messaging['save']->save_config( $config );
+ }
+}
diff --git a/modules/ppcp-settings/src/Service/TodosEligibilityService.php b/modules/ppcp-settings/src/Service/TodosEligibilityService.php
index ce64f7768..18bdb6671 100644
--- a/modules/ppcp-settings/src/Service/TodosEligibilityService.php
+++ b/modules/ppcp-settings/src/Service/TodosEligibilityService.php
@@ -1,9 +1,9 @@
is_fastlane_eligible = $is_fastlane_eligible;
- $this->is_card_payment_eligible = $is_card_payment_eligible;
- $this->is_pay_later_messaging_eligible = $is_pay_later_messaging_eligible;
- $this->is_pay_later_messaging_ui_eligible = $is_pay_later_messaging_ui_eligible;
- $this->is_subscription_eligible = $is_subscription_eligible;
- $this->is_paypal_buttons_eligible = $is_paypal_buttons_eligible;
- $this->is_apple_pay_domain_eligible = $is_apple_pay_domain_eligible;
- $this->is_digital_wallet_eligible = $is_digital_wallet_eligible;
- $this->is_apple_pay_eligible = $is_apple_pay_eligible;
- $this->is_google_pay_eligible = $is_google_pay_eligible;
- $this->is_add_subscription_eligible = $is_add_subscription_eligible;
- $this->is_enable_apple_pay_eligible = $is_enable_apple_pay_eligible;
- $this->is_enable_google_pay_eligible = $is_enable_google_pay_eligible;
+ $this->is_fastlane_eligible = $is_fastlane_eligible;
+ $this->is_pay_later_messaging_eligible = $is_pay_later_messaging_eligible;
+ $this->is_pay_later_messaging_product_eligible = $is_pay_later_messaging_product_eligible;
+ $this->is_pay_later_messaging_cart_eligible = $is_pay_later_messaging_cart_eligible;
+ $this->is_pay_later_messaging_checkout_eligible = $is_pay_later_messaging_checkout_eligible;
+ $this->is_subscription_eligible = $is_subscription_eligible;
+ $this->is_paypal_buttons_cart_eligible = $is_paypal_buttons_cart_eligible;
+ $this->is_paypal_buttons_block_checkout_eligible = $is_paypal_buttons_block_checkout_eligible;
+ $this->is_paypal_buttons_product_eligible = $is_paypal_buttons_product_eligible;
+ $this->is_apple_pay_domain_eligible = $is_apple_pay_domain_eligible;
+ $this->is_digital_wallet_eligible = $is_digital_wallet_eligible;
+ $this->is_apple_pay_eligible = $is_apple_pay_eligible;
+ $this->is_google_pay_eligible = $is_google_pay_eligible;
+ $this->is_enable_apple_pay_eligible = $is_enable_apple_pay_eligible;
+ $this->is_enable_google_pay_eligible = $is_enable_google_pay_eligible;
}
/**
@@ -162,18 +182,21 @@ class TodosEligibilityService {
*/
public function get_eligibility_checks(): array {
return array(
- 'enable_fastlane' => fn() => $this->is_fastlane_eligible,
- 'enable_credit_debit_cards' => fn() => $this->is_card_payment_eligible,
- 'enable_pay_later_messaging' => fn() => $this->is_pay_later_messaging_eligible,
- 'add_pay_later_messaging' => fn() => $this->is_pay_later_messaging_eligible,
- 'configure_paypal_subscription' => fn() => $this->is_subscription_eligible,
- 'add_paypal_buttons' => fn() => $this->is_paypal_buttons_eligible,
- 'register_domain_apple_pay' => fn() => $this->is_apple_pay_domain_eligible,
- 'add_digital_wallets' => fn() => $this->is_digital_wallet_eligible,
- 'add_apple_pay' => fn() => $this->is_apple_pay_eligible,
- 'add_google_pay' => fn() => $this->is_google_pay_eligible,
- 'enable_apple_pay' => fn() => $this->is_enable_apple_pay_eligible,
- 'enable_google_pay' => fn() => $this->is_enable_google_pay_eligible,
+ 'enable_fastlane' => fn() => $this->is_fastlane_eligible,
+ 'enable_pay_later_messaging' => fn() => $this->is_pay_later_messaging_eligible,
+ 'add_pay_later_messaging_product_page' => fn() => $this->is_pay_later_messaging_product_eligible,
+ 'add_pay_later_messaging_cart' => fn() => $this->is_pay_later_messaging_cart_eligible,
+ 'add_pay_later_messaging_checkout' => fn() => $this->is_pay_later_messaging_checkout_eligible,
+ 'configure_paypal_subscription' => fn() => $this->is_subscription_eligible,
+ 'add_paypal_buttons_cart' => fn() => $this->is_paypal_buttons_cart_eligible,
+ 'add_paypal_buttons_block_checkout' => fn() => $this->is_paypal_buttons_block_checkout_eligible,
+ 'add_paypal_buttons_product' => fn() => $this->is_paypal_buttons_product_eligible,
+ 'register_domain_apple_pay' => fn() => $this->is_apple_pay_domain_eligible,
+ 'add_digital_wallets' => fn() => $this->is_digital_wallet_eligible,
+ 'add_apple_pay' => fn() => $this->is_apple_pay_eligible,
+ 'add_google_pay' => fn() => $this->is_google_pay_eligible,
+ 'enable_apple_pay' => fn() => $this->is_enable_apple_pay_eligible,
+ 'enable_google_pay' => fn() => $this->is_enable_google_pay_eligible,
);
}
}
diff --git a/modules/ppcp-settings/src/Service/TodosSortingAndFilteringService.php b/modules/ppcp-settings/src/Service/TodosSortingAndFilteringService.php
new file mode 100644
index 000000000..6a00bacf6
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/TodosSortingAndFilteringService.php
@@ -0,0 +1,182 @@
+todos_model = $todos_model;
+ }
+
+ /**
+ * Returns Pay Later messaging todo IDs in priority order.
+ *
+ * @return array Pay Later messaging todo IDs.
+ */
+ public function get_pay_later_ids(): array {
+ return self::PAY_LATER_IDS;
+ }
+
+ /**
+ * Returns Button Placement todo IDs in priority order.
+ *
+ * @return array Button Placement todo IDs.
+ */
+ public function get_button_placement_ids(): array {
+ return self::BUTTON_PLACEMENT_IDS;
+ }
+
+ /**
+ * Sorts todos by their priority value.
+ *
+ * @param array $todos Array of todos to sort.
+ * @return array Sorted array of todos.
+ */
+ public function sort_todos_by_priority( array $todos ): array {
+ usort(
+ $todos,
+ function( $a, $b ) {
+ $priority_a = $a['priority'] ?? 999;
+ $priority_b = $b['priority'] ?? 999;
+ return $priority_a <=> $priority_b;
+ }
+ );
+
+ return $todos;
+ }
+
+ /**
+ * Filters a group of todos to show only the highest priority one.
+ * Takes into account dismissed todos.
+ *
+ * @param array $todos The array of todos to filter.
+ * @param array $group_ids Array of todo IDs in priority order.
+ * @return array Filtered todos with only one todo from the specified group.
+ */
+ public function filter_highest_priority_todo( array $todos, array $group_ids ): array {
+ $dismissed_todos = $this->todos_model->get_dismissed_todos();
+
+ $group_todos = array_filter(
+ $todos,
+ function( $todo ) use ( $group_ids ) {
+ return in_array( $todo['id'], $group_ids, true );
+ }
+ );
+
+ $other_todos = array_filter(
+ $todos,
+ function( $todo ) use ( $group_ids ) {
+ return ! in_array( $todo['id'], $group_ids, true );
+ }
+ );
+
+ // Find the highest priority todo from the group that's eligible AND not dismissed.
+ $priority_todo = null;
+ foreach ( $group_ids as $todo_id ) {
+ // Skip if this todo ID is dismissed.
+ if ( in_array( $todo_id, $dismissed_todos, true ) ) {
+ continue;
+ }
+
+ $matching_todo = current(
+ array_filter(
+ $group_todos,
+ function( $todo ) use ( $todo_id ) {
+ return $todo['id'] === $todo_id;
+ }
+ )
+ );
+
+ if ( $matching_todo ) {
+ $priority_todo = $matching_todo;
+ break;
+ }
+ }
+
+ return $priority_todo
+ ? array_merge( $other_todos, array( $priority_todo ) )
+ : $other_todos;
+ }
+
+ /**
+ * Filter pay later todos to show only the highest priority eligible one.
+ *
+ * @param array $todos The array of todos to filter.
+ * @return array Filtered todos.
+ */
+ public function filter_pay_later_todos( array $todos ): array {
+ return $this->filter_highest_priority_todo( $todos, self::PAY_LATER_IDS );
+ }
+
+ /**
+ * Filter button placement todos to show only the highest priority eligible one.
+ *
+ * @param array $todos The array of todos to filter.
+ * @return array Filtered todos.
+ */
+ public function filter_button_placement_todos( array $todos ): array {
+ return $this->filter_highest_priority_todo( $todos, self::BUTTON_PLACEMENT_IDS );
+ }
+
+ /**
+ * Apply all priority filters to the todos list.
+ *
+ * This method applies sorting and all priority filtering in the correct order.
+ *
+ * @param array $todos The original todos array.
+ * @return array Fully filtered and sorted todos.
+ */
+ public function apply_all_priority_filters( array $todos ): array {
+ $sorted_todos = $this->sort_todos_by_priority( $todos );
+ $filtered_todos = $this->filter_pay_later_todos( $sorted_todos );
+ $filtered_todos = $this->filter_button_placement_todos( $filtered_todos );
+
+ return $filtered_todos;
+ }
+}
diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php
index 1ff11a72d..46eca79dd 100644
--- a/modules/ppcp-settings/src/SettingsModule.php
+++ b/modules/ppcp-settings/src/SettingsModule.php
@@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Settings;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
+use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway;
@@ -22,10 +23,12 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway;
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
+use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
@@ -33,9 +36,16 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
+use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
+use WooCommerce\PayPalCommerce\Settings\Service\SettingsDataManager;
+use WooCommerce\PayPalCommerce\Settings\DTO\ConfigurationFlagsDTO;
+use WooCommerce\PayPalCommerce\Settings\Enum\ProductChoicesEnum;
+use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
+use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
+use WooCommerce\PayPalCommerce\Axo\Helper\CompatibilityChecker;
/**
* Class SettingsModule
@@ -47,9 +57,19 @@ class SettingsModule implements ServiceModule, ExecutableModule {
* Returns whether the old settings UI should be loaded.
*/
public static function should_use_the_old_ui() : bool {
+ // New merchants should never see the legacy UI.
+ $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' );
+
+ if ( $show_new_ux ) {
+ return false;
+ }
+
+ // Existing merchants can opt-in to see the new UI.
+ $opt_out_choice = 'yes' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI );
+
return apply_filters(
'woocommerce_paypal_payments_should_use_the_old_ui',
- get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === 'yes'
+ $opt_out_choice
);
}
@@ -102,7 +122,11 @@ class SettingsModule implements ServiceModule, ExecutableModule {
)
);
- wp_enqueue_script( 'ppcp-switch-settings-ui' );
+ wp_enqueue_script( 'ppcp-switch-settings-ui', '', array( 'wp-i18n' ), $script_asset_file['version'] );
+ wp_set_script_translations(
+ 'ppcp-switch-settings-ui',
+ 'woocommerce-paypal-payments',
+ );
}
);
@@ -122,7 +146,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
add_action(
'woocommerce_paypal_payments_gateway_migrate_on_update',
- static fn () => ! get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI )
+ static fn() => ! get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI )
&& update_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI, 'yes' )
);
@@ -155,7 +179,11 @@ class SettingsModule implements ServiceModule, ExecutableModule {
true
);
- wp_enqueue_script( 'ppcp-admin-settings' );
+ wp_enqueue_script( 'ppcp-admin-settings', '', array( 'wp-i18n' ), $script_asset_file['version'] );
+ wp_set_script_translations(
+ 'ppcp-admin-settings',
+ 'woocommerce-paypal-payments',
+ );
/**
* Require resolves.
@@ -185,6 +213,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'imagesUrl' => $module_url . '/images/',
),
'wcPaymentsTabUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout' ),
+ 'pluginSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ),
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'isPayLaterConfiguratorAvailable' => $is_pay_later_configurator_available,
'storeCountry' => $container->get( 'wcgateway.store-country' ),
@@ -194,11 +223,14 @@ class SettingsModule implements ServiceModule, ExecutableModule {
wp_enqueue_script(
'ppcp-paylater-configurator-lib',
'https://www.paypalobjects.com/merchant-library/merchant-configurator.js',
- array(),
+ array( 'wp-i18n' ),
$script_asset_file['version'],
true
);
-
+ wp_set_script_translations(
+ 'ppcp-paylater-configurator-lib',
+ 'woocommerce-paypal-payments',
+ );
$script_data['PcpPayLaterConfigurator'] = array(
'config' => array(),
'merchantClientId' => $settings->get( 'client_id' ),
@@ -232,7 +264,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$endpoints = array(
'onboarding' => $container->get( 'settings.rest.onboarding' ),
'common' => $container->get( 'settings.rest.common' ),
- 'connect_manual' => $container->get( 'settings.rest.connect_manual' ),
+ 'connect_manual' => $container->get( 'settings.rest.authentication' ),
'login_link' => $container->get( 'settings.rest.login_link' ),
'webhooks' => $container->get( 'settings.rest.webhooks' ),
'refresh_feature_status' => $container->get( 'settings.rest.refresh_feature_status' ),
@@ -241,6 +273,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'styling' => $container->get( 'settings.rest.styling' ),
'todos' => $container->get( 'settings.rest.todos' ),
'pay_later_messaging' => $container->get( 'settings.rest.pay_later_messaging' ),
+ 'features' => $container->get( 'settings.rest.features' ),
);
foreach ( $endpoints as $endpoint ) {
@@ -288,12 +321,28 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$onboarding_profile->set_completed( true );
$onboarding_profile->save();
+
+ // Try to apply a default configuration for the current store.
+ $data_manager = $container->get( 'settings.service.data-manager' );
+ assert( $data_manager instanceof SettingsDataManager );
+
+ $general_settings = $container->get( 'settings.data.general' );
+ assert( $general_settings instanceof GeneralSettings );
+
+ $flags = new ConfigurationFlagsDTO();
+
+ $flags->country_code = $general_settings->get_merchant_country();
+ $flags->is_business_seller = $general_settings->is_business_seller();
+ $flags->use_card_payments = $onboarding_profile->get_accept_card_payments();
+ $flags->use_subscriptions = in_array( ProductChoicesEnum::SUBSCRIPTIONS, $onboarding_profile->get_products(), true );
+
+ $data_manager->set_defaults_for_new_merchant( $flags );
}
);
add_filter(
'woocommerce_paypal_payments_payment_methods',
- function( array $payment_methods ) use ( $container ) : array {
+ function ( array $payment_methods ) use ( $container ) : array {
$all_payment_methods = $payment_methods;
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
@@ -308,8 +357,15 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
assert( $dcc_applies instanceof DCCApplies );
- // Unset BCDC if merchant is eligible for ACDC.
- if ( $dcc_product_status->is_active() && ! $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) {
+ $general_settings = $container->get( 'settings.data.general' );
+ assert( $general_settings instanceof GeneralSettings );
+
+ $merchant_data = $general_settings->get_merchant_data();
+ $merchant_country = $merchant_data->merchant_country;
+
+ // Unset BCDC if merchant is eligible for ACDC and country is eligible for card fields.
+ $card_fields_eligible = $container->get( 'card-fields.eligible' );
+ if ( $dcc_product_status->is_active() && $card_fields_eligible ) {
unset( $payment_methods[ CardButtonGateway::ID ] );
}
@@ -318,21 +374,31 @@ class SettingsModule implements ServiceModule, ExecutableModule {
unset( $payment_methods['venmo'] );
}
- // Unset if not eligible for Google Pay.
- if ( ! $googlepay_product_status->is_active() ) {
+ // Unset if country/currency is not supported or merchant not eligible for Google Pay.
+ if ( ! $container->get( 'googlepay.eligible' ) || ! $googlepay_product_status->is_active() ) {
unset( $payment_methods['ppcp-googlepay'] );
}
- // Unset if not eligible for Apple Pay.
- if ( ! $applepay_product_status->is_active() ) {
+ // Unset if country/currency is not supported or merchant not eligible for Apple Pay.
+ if ( ! $container->get( 'applepay.eligible' ) || ! $applepay_product_status->is_active() ) {
unset( $payment_methods['ppcp-applepay'] );
}
- // Unset Fastlane if store location is not United States or merchant is not eligible for ACDC.
- if ( $container->get( 'api.shop.country' ) !== 'US' || ! $dcc_product_status->is_active() ) {
+ // Unset Fastlane if country/currency is not supported or merchant is not eligible for BCDC.
+ if ( ! $container->get( 'axo.eligible' ) || ! $dcc_product_status->is_active() ) {
unset( $payment_methods['ppcp-axo-gateway'] );
}
+ // Unset OXXO if merchant country is not Mexico.
+ if ( 'MX' !== $merchant_country ) {
+ unset( $payment_methods[ OXXO::ID ] );
+ }
+
+ // Unset Pay Unon Invoice if merchant country is not Germany.
+ if ( 'DE' !== $merchant_country ) {
+ unset( $payment_methods[ PayUponInvoiceGateway::ID ] );
+ }
+
// For non-ACDC regions unset ACDC, local APMs and set BCDC.
if ( ! $dcc_applies ) {
unset( $payment_methods[ CreditCardGateway::ID ] );
@@ -361,11 +427,15 @@ class SettingsModule implements ServiceModule, ExecutableModule {
*
* @psalm-suppress MissingClosureParamType
*/
- static function ( $methods ) use ( $container ): array {
- if ( ! is_array( $methods ) ) {
+ function ( $methods ) use ( $container ) : array {
+ $is_onboarded = $container->get( 'api.merchant_id' ) !== '';
+ if ( ! is_array( $methods ) || ! $is_onboarded ) {
return $methods;
}
+ $card_button_gateway = $container->get( 'wcgateway.card-button-gateway' );
+ assert( $card_button_gateway instanceof CardButtonGateway );
+
$googlepay_gateway = $container->get( 'googlepay.wc-gateway' );
assert( $googlepay_gateway instanceof WC_Payment_Gateway );
@@ -375,17 +445,68 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$axo_gateway = $container->get( 'axo.gateway' );
assert( $axo_gateway instanceof WC_Payment_Gateway );
+ $methods[] = $card_button_gateway;
$methods[] = $googlepay_gateway;
$methods[] = $applepay_gateway;
$methods[] = $axo_gateway;
+ $is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' );
+ $all_gateway_ids = $container->get( 'settings.config.all-gateway-ids' );
+
+ if ( $is_payments_page ) {
+ $methods = array_filter(
+ $methods,
+ function ( $method ) use ( $all_gateway_ids ): bool {
+ if ( ! is_object( $method )
+ || $method->id === PayPalGateway::ID
+ || ! in_array( $method->id, $all_gateway_ids, true )
+ ) {
+ return true;
+ }
+
+ if ( ! $this->is_gateway_enabled( $method->id ) ) {
+ return false;
+ }
+
+ return true;
+ }
+ );
+ }
+
+ return $methods;
+ },
+ 99
+ );
+
+ // Remove the Fastlane gateway if the customer is logged in, ensuring that we don't interfere with the Fastlane gateway status in the settings UI.
+ add_filter(
+ 'woocommerce_available_payment_gateways',
+ /**
+ * Param types removed to avoid third-party issues.
+ *
+ * @psalm-suppress MissingClosureParamType
+ */
+ static function ( $methods ) use ( $container ) : array {
+ if ( ! is_array( $methods ) ) {
+ return $methods;
+ }
+
+ if ( is_user_logged_in() && ! is_admin() ) {
+ foreach ( $methods as $key => $method ) {
+ if ( $method instanceof WC_Payment_Gateway && $method->id === 'ppcp-axo-gateway' ) {
+ unset( $methods[ $key ] );
+ break;
+ }
+ }
+ }
+
return $methods;
}
);
add_filter(
'woocommerce_paypal_payments_gateway_title',
- function( string $title, WC_Payment_Gateway $gateway ) {
+ function ( string $title, WC_Payment_Gateway $gateway ) {
return $gateway->get_option( 'title', $title );
},
10,
@@ -393,16 +514,18 @@ class SettingsModule implements ServiceModule, ExecutableModule {
);
add_filter(
'woocommerce_paypal_payments_gateway_description',
- function( string $description, WC_Payment_Gateway $gateway ) {
+ function ( string $description, WC_Payment_Gateway $gateway ) {
return $gateway->get_option( 'description', $description );
},
10,
2
);
+ add_filter( 'woocommerce_paypal_payments_card_button_gateway_should_register_gateway', '__return_true' );
+
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_form_fields',
- function( array $form_fields ) {
+ function ( array $form_fields ) {
$form_fields['enabled'] = array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
@@ -419,7 +542,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_title',
- function( string $title, WC_Payment_Gateway $gateway ) {
+ function ( string $title, WC_Payment_Gateway $gateway ) {
return $gateway->get_option( 'title', $title );
},
10,
@@ -427,31 +550,85 @@ class SettingsModule implements ServiceModule, ExecutableModule {
);
add_filter(
'woocommerce_paypal_payments_credit_card_gateway_description',
- function( string $description, WC_Payment_Gateway $gateway ) {
+ function ( string $description, WC_Payment_Gateway $gateway ) {
return $gateway->get_option( 'description', $description );
},
10,
2
);
- add_filter( 'woocommerce_paypal_payments_axo_gateway_should_update_enabled', '__return_false' );
+ if ( is_admin() ) {
+ add_filter( 'woocommerce_paypal_payments_axo_gateway_should_update_enabled', '__return_false' );
+ add_filter(
+ 'woocommerce_paypal_payments_axo_gateway_title',
+ function ( string $title, WC_Payment_Gateway $gateway ) {
+ return $gateway->get_option( 'title', $title );
+ },
+ 10,
+ 2
+ );
+ add_filter(
+ 'woocommerce_paypal_payments_axo_gateway_description',
+ function ( string $description, WC_Payment_Gateway $gateway ) {
+ return $gateway->get_option( 'description', $description );
+ },
+ 10,
+ 2
+ );
+ }
+
+ /**
+ * Unsets the BCDC black button if merchant is eligible for ACDC.
+ */
add_filter(
- 'woocommerce_paypal_payments_axo_gateway_title',
- function( string $title, WC_Payment_Gateway $gateway ) {
- return $gateway->get_option( 'title', $title );
- },
- 10,
- 2
+ 'woocommerce_paypal_payments_disabled_funding_sources',
+ /**
+ * Unsets the BCDC black button if merchant is eligible for ACDC.
+ *
+ * @param int[]|string[]|mixed $disable_funding The disabled funding sources.
+ * @return int[]|string[]|mixed The disabled funding sources.
+ *
+ * @psalm-suppress MissingClosureParamType
+ */
+ static function ( $disable_funding ) use ( $container ) {
+ if ( ! is_array( $disable_funding ) || in_array( 'card', $disable_funding, true ) ) {
+ return $disable_funding;
+ }
+
+ $dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
+ assert( $dcc_product_status instanceof DCCProductStatus );
+
+ if ( $dcc_product_status->is_active() ) {
+ $disable_funding[] = 'card';
+ }
+
+ return $disable_funding;
+ }
);
- add_filter(
- 'woocommerce_paypal_payments_axo_gateway_description',
- function( string $description, WC_Payment_Gateway $gateway ) {
- return $gateway->get_option( 'description', $description );
- },
- 10,
- 2
+
+ // Enable Fastlane after onboarding if the store is compatible.
+ add_action(
+ 'woocommerce_paypal_payments_apply_default_configuration',
+ static function () use ( $container ) {
+ $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
+ assert( $compatibility_checker instanceof CompatibilityChecker );
+
+ $payment_settings = $container->get( 'settings.data.payment' );
+ assert( $payment_settings instanceof PaymentSettings );
+
+ if ( $compatibility_checker->is_fastlane_compatible() ) {
+ $payment_settings->toggle_method_state( AxoGateway::ID, true );
+ }
+
+ $payment_settings->save();
+ }
);
+ // Redirect payment method links in the WC Payment Gateway to the new UI Payment Methods tab.
+ $gateway_redirect_service = $container->get( 'settings.service.gateway-redirect' );
+ assert( $gateway_redirect_service instanceof GatewayRedirectService );
+ $gateway_redirect_service->register();
+
return true;
}
@@ -474,4 +651,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
protected function render_content() : void {
echo '
';
}
+
+ /**
+ * Checks if the payment gateway with the given name is enabled.
+ *
+ * @param string $gateway_name The gateway name.
+ * @return bool True if the payment gateway with the given name is enabled, otherwise false.
+ */
+ protected function is_gateway_enabled( string $gateway_name ): bool {
+ $gateway_settings = get_option( "woocommerce_{$gateway_name}_settings", array() );
+ $gateway_enabled = $gateway_settings['enabled'] ?? false;
+
+ return $gateway_enabled === 'yes';
+ }
}
diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php
index da015d586..2baa411eb 100644
--- a/modules/ppcp-status-report/src/StatusReportModule.php
+++ b/modules/ppcp-status-report/src/StatusReportModule.php
@@ -21,7 +21,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\WebhookEventStorage;
/**
@@ -58,8 +57,8 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
$subscriptions_mode_settings = $c->get( 'wcgateway.settings.fields.subscriptions_mode' ) ?: array();
- /* @var State $state The state. */
- $state = $c->get( 'onboarding.state' );
+ /* @var bool $is_connected Whether onboarding is complete. */
+ $is_connected = $c->get( 'settings.flag.is-connected' );
/* @var Bearer $bearer The bearer. */
$bearer = $c->get( 'api.bearer' );
@@ -92,7 +91,7 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
'exported_label' => 'Onboarded',
'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
- $this->onboarded( $bearer, $state )
+ $this->onboarded( $bearer, $is_connected )
),
),
array(
@@ -230,19 +229,18 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
/**
* It returns the current onboarding status.
*
- * @param Bearer $bearer The bearer.
- * @param State $state The state.
+ * @param Bearer $bearer The bearer.
+ * @param bool $is_connected Whether onboarding is complete.
* @return bool
*/
- private function onboarded( Bearer $bearer, State $state ): bool {
+ private function onboarded( Bearer $bearer, bool $is_connected ): bool {
try {
$token = $bearer->bearer();
} catch ( RuntimeException $exception ) {
return false;
}
- $current_state = $state->current_state();
- return $token->is_valid() && $current_state === $state::STATE_ONBOARDED;
+ return $is_connected && $token->is_valid();
}
/**
diff --git a/modules/ppcp-uninstall/src/UninstallModule.php b/modules/ppcp-uninstall/src/UninstallModule.php
index 524460327..5f333b902 100644
--- a/modules/ppcp-uninstall/src/UninstallModule.php
+++ b/modules/ppcp-uninstall/src/UninstallModule.php
@@ -100,6 +100,8 @@ class UninstallModule implements ServiceModule, ExtendingModule, ExecutableModul
$clear_db->clear_scheduled_actions( $scheduled_action_names );
$clear_db->clear_actions( $action_names );
+ update_option( 'woocommerce-ppcp-is-new-merchant', '1' );
+
wp_send_json_success();
return true;
} catch ( Exception $error ) {
diff --git a/modules/ppcp-vaulting/services.php b/modules/ppcp-vaulting/services.php
index 45355225d..da0d1d209 100644
--- a/modules/ppcp-vaulting/services.php
+++ b/modules/ppcp-vaulting/services.php
@@ -37,7 +37,7 @@ return array(
$container->get( 'api.factory.payer' ),
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'api.endpoint.order' ),
- $container->get( 'onboarding.environment' ),
+ $container->get( 'settings.environment' ),
$container->get( 'wcgateway.processor.authorized-payments' ),
$container->get( 'wcgateway.settings' )
);
diff --git a/modules/ppcp-wc-gateway/extensions.php b/modules/ppcp-wc-gateway/extensions.php
index 77fea0abd..ef6c85c77 100644
--- a/modules/ppcp-wc-gateway/extensions.php
+++ b/modules/ppcp-wc-gateway/extensions.php
@@ -26,7 +26,7 @@ return array(
return $settings->has( 'merchant_id' ) ? (string) $settings->get( 'merchant_id' ) : '';
},
'api.partner_merchant_id' => static function ( string $previous, ContainerInterface $container ): string {
- $environment = $container->get( 'onboarding.environment' );
+ $environment = $container->get( 'settings.environment' );
/**
* The environment.
diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php
index 77c6438e3..9bb5ae0c9 100644
--- a/modules/ppcp-wc-gateway/services.php
+++ b/modules/ppcp-wc-gateway/services.php
@@ -87,35 +87,21 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
return array(
'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway {
- $order_processor = $container->get( 'wcgateway.order-processor' );
- $settings_renderer = $container->get( 'wcgateway.settings.render' );
- $funding_source_renderer = $container->get( 'wcgateway.funding-source.renderer' );
- $settings = $container->get( 'wcgateway.settings' );
- $session_handler = $container->get( 'session.handler' );
- $refund_processor = $container->get( 'wcgateway.processor.refunds' );
- $state = $container->get( 'onboarding.state' );
- $transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
- $subscription_helper = $container->get( 'wc-subscriptions.helper' );
- $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
- $payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
- $environment = $container->get( 'onboarding.environment' );
- $logger = $container->get( 'woocommerce.logger.woocommerce' );
- $api_shop_country = $container->get( 'api.shop.country' );
return new PayPalGateway(
- $settings_renderer,
- $funding_source_renderer,
- $order_processor,
- $settings,
- $session_handler,
- $refund_processor,
- $state,
- $transaction_url_provider,
- $subscription_helper,
- $page_id,
- $environment,
- $payment_token_repository,
- $logger,
- $api_shop_country,
+ $container->get( 'wcgateway.settings.render' ),
+ $container->get( 'wcgateway.funding-source.renderer' ),
+ $container->get( 'wcgateway.order-processor' ),
+ $container->get( 'wcgateway.settings' ),
+ $container->get( 'session.handler' ),
+ $container->get( 'wcgateway.processor.refunds' ),
+ $container->get( 'settings.flag.is-connected' ),
+ $container->get( 'wcgateway.transaction-url-provider' ),
+ $container->get( 'wc-subscriptions.helper' ),
+ $container->get( 'wcgateway.current-ppcp-settings-page-id' ),
+ $container->get( 'settings.environment' ),
+ $container->get( 'vaulting.repository.payment-token' ),
+ $container->get( 'woocommerce.logger.woocommerce' ),
+ $container->get( 'api.shop.country' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.place-order-button-text' ),
@@ -127,42 +113,26 @@ return array(
);
},
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
- $order_processor = $container->get( 'wcgateway.order-processor' );
- $settings_renderer = $container->get( 'wcgateway.settings.render' );
- $settings = $container->get( 'wcgateway.settings' );
- $dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
- $module_url = $container->get( 'wcgateway.url' );
- $session_handler = $container->get( 'session.handler' );
- $refund_processor = $container->get( 'wcgateway.processor.refunds' );
- $state = $container->get( 'onboarding.state' );
- $transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
- $subscription_helper = $container->get( 'wc-subscriptions.helper' );
- $payments_endpoint = $container->get( 'api.endpoint.payments' );
- $logger = $container->get( 'woocommerce.logger.woocommerce' );
- $vaulted_credit_card_handler = $container->get( 'vaulting.credit-card-handler' );
- $icons = $container->get( 'wcgateway.credit-card-icons' );
-
return new CreditCardGateway(
- $settings_renderer,
- $order_processor,
- $settings,
- $dcc_configuration,
- $icons,
- $module_url,
- $session_handler,
- $refund_processor,
- $state,
- $transaction_url_provider,
- $subscription_helper,
- $payments_endpoint,
- $vaulted_credit_card_handler,
- $container->get( 'onboarding.environment' ),
+ $container->get( 'wcgateway.settings.render' ),
+ $container->get( 'wcgateway.order-processor' ),
+ $container->get( 'wcgateway.settings' ),
+ $container->get( 'wcgateway.configuration.dcc' ),
+ $container->get( 'wcgateway.credit-card-icons' ),
+ $container->get( 'wcgateway.url' ),
+ $container->get( 'session.handler' ),
+ $container->get( 'wcgateway.processor.refunds' ),
+ $container->get( 'wcgateway.transaction-url-provider' ),
+ $container->get( 'wc-subscriptions.helper' ),
+ $container->get( 'api.endpoint.payments' ),
+ $container->get( 'vaulting.credit-card-handler' ),
+ $container->get( 'settings.environment' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'wcgateway.endpoint.capture-card-payment' ),
$container->get( 'api.prefix' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.wc-payment-tokens' ),
- $logger
+ $container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.credit-card-labels' => static function ( ContainerInterface $container ) : array {
@@ -234,11 +204,11 @@ return array(
$container->get( 'wcgateway.settings' ),
$container->get( 'session.handler' ),
$container->get( 'wcgateway.processor.refunds' ),
- $container->get( 'onboarding.state' ),
+ $container->get( 'settings.flag.is-connected' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'wcgateway.settings.allow_card_button_gateway.default' ),
- $container->get( 'onboarding.environment' ),
+ $container->get( 'settings.environment' ),
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.factory.paypal-checkout-url' ),
@@ -321,10 +291,9 @@ return array(
$section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '';
$ppcp_tab = isset( $_GET[ SectionsRenderer::KEY ] ) ? sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) : '';
- $state = $container->get( 'onboarding.state' );
- assert( $state instanceof State );
+ $is_connected = $container->get( 'settings.flag.is-connected' );
- if ( ! $ppcp_tab && PayPalGateway::ID === $section && $state->current_state() !== State::STATE_ONBOARDED ) {
+ if ( ! $ppcp_tab && PayPalGateway::ID === $section && ! $is_connected ) {
return Settings::CONNECTION_TAB_ID;
}
@@ -343,29 +312,25 @@ return array(
}
),
'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice {
- $state = $container->get( 'onboarding.state' );
- $settings = $container->get( 'wcgateway.settings' );
- $is_current_country_send_only = $container->get( 'wcgateway.is-send-only-country' );
- return new ConnectAdminNotice( $state, $settings, $is_current_country_send_only );
+ return new ConnectAdminNotice(
+ $container->get( 'settings.flag.is-connected' ),
+ $container->get( 'wcgateway.settings' ),
+ $container->get( 'wcgateway.is-send-only-country' )
+ );
},
'wcgateway.notice.currency-unsupported' => static function ( ContainerInterface $container ): UnsupportedCurrencyAdminNotice {
- $state = $container->get( 'onboarding.state' );
- $shop_currency = $container->get( 'api.shop.currency.getter' );
- $supported_currencies = $container->get( 'api.supported-currencies' );
- $is_wc_gateways_list_page = $container->get( 'wcgateway.is-wc-gateways-list-page' );
- $is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' );
return new UnsupportedCurrencyAdminNotice(
- $state,
- $shop_currency,
- $supported_currencies,
- $is_wc_gateways_list_page,
- $is_ppcp_settings_page
+ $container->get( 'settings.flag.is-connected' ),
+ $container->get( 'api.shop.currency.getter' ),
+ $container->get( 'api.supported-currencies' ),
+ $container->get( 'wcgateway.is-wc-gateways-list-page' ),
+ $container->get( 'wcgateway.is-ppcp-settings-page' )
);
},
'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice {
return new GatewayWithoutPayPalAdminNotice(
CreditCardGateway::ID,
- $container->get( 'onboarding.state' ),
+ $container->get( 'settings.flag.is-connected' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.is-wc-payments-page' ),
$container->get( 'wcgateway.is-ppcp-settings-page' )
@@ -374,7 +339,7 @@ return array(
'wcgateway.notice.card-button-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice {
return new GatewayWithoutPayPalAdminNotice(
CardButtonGateway::ID,
- $container->get( 'onboarding.state' ),
+ $container->get( 'settings.flag.is-connected' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.is-wc-payments-page' ),
$container->get( 'wcgateway.is-ppcp-settings-page' ),
@@ -477,14 +442,12 @@ return array(
return in_array( $store_country, $send_only_countries, true );
},
'wcgateway.notice.send-only-country' => static function ( ContainerInterface $container ) {
- $onboarding_state = $container->get( 'onboarding.state' );
- assert( $onboarding_state instanceof State );
return new SendOnlyCountryNotice(
$container->get( 'wcgateway.send-only-message' ),
$container->get( 'wcgateway.is-send-only-country' ),
$container->get( 'wcgateway.is-ppcp-settings-page' ),
$container->get( 'wcgateway.is-wc-gateways-list-page' ),
- $onboarding_state->current_state()
+ $container->get( 'settings.flag.is-connected' )
);
},
@@ -527,7 +490,7 @@ return array(
'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer {
return new SectionsRenderer(
$container->get( 'wcgateway.current-ppcp-settings-page-id' ),
- $container->get( 'onboarding.state' ),
+ $container->get( 'settings.flag.is-connected' ),
$container->get( 'wcgateway.helper.dcc-product-status' ),
$container->get( 'api.helpers.dccapplies' ),
$container->get( 'button.helper.messages-apply' ),
@@ -545,57 +508,36 @@ return array(
return new SettingsStatus( $settings );
},
'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer {
- $settings = $container->get( 'wcgateway.settings' );
- $state = $container->get( 'onboarding.state' );
- $fields = $container->get( 'wcgateway.settings.fields' );
- $dcc_applies = $container->get( 'api.helpers.dccapplies' );
- $messages_apply = $container->get( 'button.helper.messages-apply' );
- $dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
- $settings_status = $container->get( 'wcgateway.settings.status' );
- $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
- $api_shop_country = $container->get( 'api.shop.country' );
return new SettingsRenderer(
- $settings,
- $state,
- $fields,
- $dcc_applies,
- $messages_apply,
- $dcc_product_status,
- $settings_status,
- $page_id,
- $api_shop_country
+ $container->get( 'wcgateway.settings' ),
+ $container->get( 'onboarding.state' ), // Correct.
+ $container->get( 'wcgateway.settings.fields' ),
+ $container->get( 'api.helpers.dccapplies' ),
+ $container->get( 'button.helper.messages-apply' ),
+ $container->get( 'wcgateway.helper.dcc-product-status' ),
+ $container->get( 'wcgateway.settings.status' ),
+ $container->get( 'wcgateway.current-ppcp-settings-page-id' ),
+ $container->get( 'api.shop.country' )
);
},
'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener {
- $settings = $container->get( 'wcgateway.settings' );
- $fields = $container->get( 'wcgateway.settings.fields' );
- $webhook_registrar = $container->get( 'webhook.registrar' );
- $state = $container->get( 'onboarding.state' );
- $cache = $container->get( 'api.paypal-bearer-cache' );
- $bearer = $container->get( 'api.bearer' );
- $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
- $signup_link_cache = $container->get( 'onboarding.signup-link-cache' );
- $signup_link_ids = $container->get( 'onboarding.signup-link-ids' );
- $pui_status_cache = $container->get( 'pui.status-cache' );
- $dcc_status_cache = $container->get( 'dcc.status-cache' );
- $logger = $container->get( 'woocommerce.logger.woocommerce' );
return new SettingsListener(
- $settings,
- $fields,
- $webhook_registrar,
- $cache,
- $state,
- $bearer,
- $page_id,
- $signup_link_cache,
- $signup_link_ids,
- $pui_status_cache,
- $dcc_status_cache,
+ $container->get( 'wcgateway.settings' ),
+ $container->get( 'wcgateway.settings.fields' ),
+ $container->get( 'webhook.registrar' ),
+ $container->get( 'api.paypal-bearer-cache' ),
+ $container->get( 'onboarding.state' ), // Correct.
+ $container->get( 'api.bearer' ),
+ $container->get( 'wcgateway.current-ppcp-settings-page-id' ),
+ $container->get( 'onboarding.signup-link-cache' ),
+ $container->get( 'onboarding.signup-link-ids' ),
+ $container->get( 'pui.status-cache' ),
+ $container->get( 'dcc.status-cache' ),
$container->get( 'http.redirector' ),
$container->get( 'api.partner_merchant_id-production' ),
$container->get( 'api.partner_merchant_id-sandbox' ),
$container->get( 'api.endpoint.billing-agreements' ),
- $logger,
+ $container->get( 'woocommerce.logger.woocommerce' ),
new Cache( 'ppcp-client-credentials-cache' )
);
},
@@ -607,7 +549,7 @@ return array(
$threed_secure = $container->get( 'button.helper.three-d-secure' );
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
$settings = $container->get( 'wcgateway.settings' );
- $environment = $container->get( 'onboarding.environment' );
+ $environment = $container->get( 'settings.environment' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
$order_helper = $container->get( 'api.order-helper' );
@@ -728,6 +670,8 @@ return array(
return array();
}
+ // Legacy settings service, correct use of `State` class.
+
$state = $container->get( 'onboarding.state' );
assert( $state instanceof State );
@@ -1485,12 +1429,12 @@ return array(
$container->get( 'wcgateway.pay-upon-invoice-order-endpoint' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'wcgateway.pay-upon-invoice-payment-source-factory' ),
- $container->get( 'onboarding.environment' ),
+ $container->get( 'settings.environment' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.pay-upon-invoice-helper' ),
$container->get( 'wcgateway.checkout-helper' ),
- $container->get( 'onboarding.state' ),
+ $container->get( 'settings.flag.is-connected' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.url' )
);
@@ -1524,7 +1468,7 @@ return array(
$container->get( 'wcgateway.pay-upon-invoice-order-endpoint' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.settings' ),
- $container->get( 'onboarding.state' ),
+ $container->get( 'settings.flag.is-connected' ),
$container->get( 'wcgateway.current-ppcp-settings-page-id' ),
$container->get( 'wcgateway.pay-upon-invoice-product-status' ),
$container->get( 'wcgateway.pay-upon-invoice-helper' ),
@@ -1549,7 +1493,7 @@ return array(
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'wcgateway.url' ),
$container->get( 'wcgateway.transaction-url-provider' ),
- $container->get( 'onboarding.environment' ),
+ $container->get( 'settings.environment' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@@ -1717,15 +1661,15 @@ return array(
return 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING';
},
'wcgateway.settings.connection.dcc-status-text' => static function ( ContainerInterface $container ): string {
- $state = $container->get( 'onboarding.state' );
- if ( $state->current_state() < State::STATE_ONBOARDED ) {
+ $is_connected = $container->get( 'settings.flag.is-connected' );
+ if ( ! $is_connected ) {
return '';
}
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
assert( $dcc_product_status instanceof DCCProductStatus );
- $environment = $container->get( 'onboarding.environment' );
+ $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment );
$dcc_enabled = $dcc_product_status->is_active();
@@ -1755,7 +1699,7 @@ return array(
);
},
'wcgateway.settings.connection.reference-transactions-status-text' => static function ( ContainerInterface $container ): string {
- $environment = $container->get( 'onboarding.environment' );
+ $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment );
$billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' );
@@ -1788,15 +1732,15 @@ return array(
);
},
'wcgateway.settings.connection.pui-status-text' => static function ( ContainerInterface $container ): string {
- $state = $container->get( 'onboarding.state' );
- if ( $state->current_state() < State::STATE_ONBOARDED ) {
+ $is_connected = $container->get( 'settings.flag.is-connected' );
+ if ( ! $is_connected ) {
return '';
}
$pui_product_status = $container->get( 'wcgateway.pay-upon-invoice-product-status' );
assert( $pui_product_status instanceof PayUponInvoiceProductStatus );
- $environment = $container->get( 'onboarding.environment' );
+ $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment );
$pui_enabled = $pui_product_status->is_active();
@@ -1910,6 +1854,13 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
+ if ( apply_filters(
+ 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
+ getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
+ ) ) {
+ return true;
+ }
+
return $settings->has( 'fraudnet_enabled' ) && $settings->get( 'fraudnet_enabled' );
},
'wcgateway.fraudnet-assets' => function( ContainerInterface $container ) : FraudNetAssets {
@@ -1917,7 +1868,7 @@ return array(
$container->get( 'wcgateway.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'wcgateway.fraudnet' ),
- $container->get( 'onboarding.environment' ),
+ $container->get( 'settings.environment' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.gateway-repository' ),
$container->get( 'session.handler' ),
diff --git a/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php b/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php
index f67c7c6f1..1bb186a25 100644
--- a/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php
+++ b/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php
@@ -62,6 +62,7 @@ class SettingsCommand {
$value = false;
}
+ // TODO new-ux: The setting must also be updated in the new settings.
$this->settings->set( $key, $value );
$this->settings->persist();
diff --git a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php
index 2a8f5e6b6..00054c3da 100644
--- a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php
+++ b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php
@@ -14,7 +14,6 @@ use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
@@ -70,13 +69,6 @@ class CardButtonGateway extends \WC_Payment_Gateway {
*/
private $refund_processor;
- /**
- * The state.
- *
- * @var State
- */
- protected $state;
-
/**
* Service able to provide transaction url for an order.
*
@@ -141,7 +133,7 @@ class CardButtonGateway extends \WC_Payment_Gateway {
* @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor.
- * @param State $state The state.
+ * @param bool $is_connected Whether onboarding was completed.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param bool $default_enabled Whether the gateway should be enabled by default.
@@ -157,7 +149,7 @@ class CardButtonGateway extends \WC_Payment_Gateway {
ContainerInterface $config,
SessionHandler $session_handler,
RefundProcessor $refund_processor,
- State $state,
+ bool $is_connected,
TransactionUrlProvider $transaction_url_provider,
SubscriptionHelper $subscription_helper,
bool $default_enabled,
@@ -173,12 +165,11 @@ class CardButtonGateway extends \WC_Payment_Gateway {
$this->config = $config;
$this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
- $this->state = $state;
$this->transaction_url_provider = $transaction_url_provider;
$this->subscription_helper = $subscription_helper;
$this->default_enabled = $default_enabled;
$this->environment = $environment;
- $this->onboarded = $state->current_state() === State::STATE_ONBOARDED;
+ $this->onboarded = $is_connected;
$this->payment_token_repository = $payment_token_repository;
$this->logger = $logger;
$this->paypal_checkout_url_factory = $paypal_checkout_url_factory;
diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php
index 8d0a2d786..8ba3d4e54 100644
--- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php
+++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php
@@ -19,7 +19,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
@@ -109,13 +108,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
private $refund_processor;
- /**
- * The state.
- *
- * @var State
- */
- protected $state;
-
/**
* Service to get transaction url for an order.
*
@@ -204,7 +196,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param string $module_url The URL to the module.
* @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The refund processor.
- * @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service able to provide view transaction url base.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
@@ -226,7 +217,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
string $module_url,
SessionHandler $session_handler,
RefundProcessor $refund_processor,
- State $state,
TransactionUrlProvider $transaction_url_provider,
SubscriptionHelper $subscription_helper,
PaymentsEndpoint $payments_endpoint,
@@ -247,7 +237,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->module_url = $module_url;
$this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
- $this->state = $state;
$this->transaction_url_provider = $transaction_url_provider;
$this->subscription_helper = $subscription_helper;
$this->payments_endpoint = $payments_endpoint;
diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php
index 82e4a5397..44e1e5471 100644
--- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php
+++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php
@@ -19,7 +19,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
@@ -104,13 +103,6 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/
private $refund_processor;
- /**
- * The state.
- *
- * @var State
- */
- protected $state;
-
/**
* Service able to provide transaction url for an order.
*
@@ -137,7 +129,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
*
* @var bool
*/
- private $onboarded;
+ private bool $onboarded;
/**
* ID of the current PPCP gateway settings page, or empty if it is not such page.
@@ -225,7 +217,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor.
- * @param State $state The state.
+ * @param bool $is_connected Whether onboarding was completed.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
@@ -249,7 +241,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
ContainerInterface $config,
SessionHandler $session_handler,
RefundProcessor $refund_processor,
- State $state,
+ bool $is_connected,
TransactionUrlProvider $transaction_url_provider,
SubscriptionHelper $subscription_helper,
string $page_id,
@@ -273,12 +265,11 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->config = $config;
$this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
- $this->state = $state;
$this->transaction_url_provider = $transaction_url_provider;
$this->subscription_helper = $subscription_helper;
$this->page_id = $page_id;
$this->environment = $environment;
- $this->onboarded = $state->current_state() === State::STATE_ONBOARDED;
+ $this->onboarded = $is_connected;
$this->payment_token_repository = $payment_token_repository;
$this->logger = $logger;
$this->api_shop_country = $api_shop_country;
diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php
index 5b1fc0d36..10ec5ab2c 100644
--- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php
+++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php
@@ -19,7 +19,6 @@ use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@@ -62,11 +61,11 @@ class PayUponInvoice {
protected $pui_helper;
/**
- * The onboarding state.
+ * Whether onboarding was completed and the merchant is connected to PayPal.
*
- * @var State
+ * @var bool
*/
- protected $state;
+ protected bool $is_connected;
/**
* Current PayPal settings page id.
@@ -102,7 +101,7 @@ class PayUponInvoice {
* @param PayUponInvoiceOrderEndpoint $pui_order_endpoint The PUI order endpoint.
* @param LoggerInterface $logger The logger.
* @param Settings $settings The settings.
- * @param State $state The onboarding state.
+ * @param bool $is_connected Whether onboarding was completed.
* @param string $current_ppcp_settings_page_id Current PayPal settings page id.
* @param PayUponInvoiceProductStatus $pui_product_status The PUI product status.
* @param PayUponInvoiceHelper $pui_helper The PUI helper.
@@ -113,7 +112,7 @@ class PayUponInvoice {
PayUponInvoiceOrderEndpoint $pui_order_endpoint,
LoggerInterface $logger,
Settings $settings,
- State $state,
+ bool $is_connected,
string $current_ppcp_settings_page_id,
PayUponInvoiceProductStatus $pui_product_status,
PayUponInvoiceHelper $pui_helper,
@@ -123,7 +122,7 @@ class PayUponInvoice {
$this->pui_order_endpoint = $pui_order_endpoint;
$this->logger = $logger;
$this->settings = $settings;
- $this->state = $state;
+ $this->is_connected = $is_connected;
$this->current_ppcp_settings_page_id = $current_ppcp_settings_page_id;
$this->pui_product_status = $pui_product_status;
$this->pui_helper = $pui_helper;
@@ -138,6 +137,10 @@ class PayUponInvoice {
*/
public function init(): void {
if ( $this->pui_helper->is_pui_gateway_enabled() ) {
+ /*
+ * TODO new-ux: Check if we still support this setting, or if it's always enabled.
+ * If fraudnet is not configurable in new UI, we can ignore this.
+ */
$this->settings->set( 'fraudnet_enabled', true );
$this->settings->persist();
}
@@ -437,7 +440,7 @@ class PayUponInvoice {
function ( $methods ) {
if (
! is_array( $methods )
- || State::STATE_ONBOARDED !== $this->state->current_state()
+ || ! $this->is_connected
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|| ! ( is_checkout() || isset( $_GET['pay_for_order'] ) && $_GET['pay_for_order'] === 'true' )
) {
diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php
index ecd6c1df1..1622123bc 100644
--- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php
+++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php
@@ -17,7 +17,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
@@ -89,13 +88,6 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
*/
protected $checkout_helper;
- /**
- * The onboarding state.
- *
- * @var State
- */
- protected $state;
-
/**
* The refund processor.
*
@@ -121,7 +113,7 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
* @param LoggerInterface $logger The logger.
* @param PayUponInvoiceHelper $pui_helper The PUI helper.
* @param CheckoutHelper $checkout_helper The checkout helper.
- * @param State $state The onboarding state.
+ * @param bool $is_connected Whether the onboarding was completed.
* @param RefundProcessor $refund_processor The refund processor.
* @param string $module_url The module URL.
*/
@@ -134,7 +126,7 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
LoggerInterface $logger,
PayUponInvoiceHelper $pui_helper,
CheckoutHelper $checkout_helper,
- State $state,
+ bool $is_connected,
RefundProcessor $refund_processor,
string $module_url
) {
@@ -169,8 +161,7 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
$this->module_url = $module_url;
$this->icon = apply_filters( 'woocommerce_paypal_payments_pay_upon_invoice_gateway_icon', esc_url( $this->module_url ) . 'assets/images/ratepay.svg' );
- $this->state = $state;
- if ( $state->current_state() === State::STATE_ONBOARDED ) {
+ if ( $is_connected ) {
$this->supports = array( 'refunds' );
}
$this->refund_processor = $refund_processor;
diff --git a/modules/ppcp-wc-gateway/src/Helper/ConnectionState.php b/modules/ppcp-wc-gateway/src/Helper/ConnectionState.php
new file mode 100644
index 000000000..3ee969a84
--- /dev/null
+++ b/modules/ppcp-wc-gateway/src/Helper/ConnectionState.php
@@ -0,0 +1,135 @@
+is_connected = $is_connected;
+ $this->environment = $environment;
+ }
+
+ /**
+ * Set connection status to "connected to PayPal" (end onboarding).
+ *
+ * @param bool $is_sandbox Whether to connect to a sandbox environment.
+ */
+ public function connect( bool $is_sandbox = false ) : void {
+ if ( ! $this->is_connected ) {
+ /**
+ * Action that fires before the connection status changes from
+ * disconnected to connected.
+ */
+ do_action( 'woocommerce_paypal_payments_merchant_connection_change', true );
+ }
+
+ $this->is_connected = true;
+ $this->environment->set_environment( $is_sandbox );
+ }
+
+ /**
+ * Set connection status to "not connected to PayPal" (start onboarding).
+ */
+ public function disconnect() : void {
+ if ( $this->is_connected ) {
+ /**
+ * Action that fires before the connection status changes from
+ * connected to disconnected.
+ */
+ do_action( 'woocommerce_paypal_payments_merchant_connection_change', false );
+ }
+
+ $this->is_connected = false;
+ }
+
+ /**
+ * Returns the managed environment instance.
+ *
+ * @return Environment The environment instance.
+ */
+ public function get_environment() : Environment {
+ return $this->environment;
+ }
+
+ /**
+ * Is the merchant connected to a PayPal account?
+ *
+ * @return bool True, if onboarding was completed and connection details are present.
+ */
+ public function is_connected() : bool {
+ return $this->is_connected;
+ }
+
+ /**
+ * Is the merchant currently in the "onboarding phase"?
+ *
+ * @return bool True, if we don't know merchant connection details.
+ */
+ public function is_onboarding() : bool {
+ return ! $this->is_connected;
+ }
+
+ /**
+ * Is the merchant connected to a sandbox environment?
+ *
+ * @return bool True, if connected to a sandbox environment.
+ */
+ public function is_sandbox() : bool {
+ return $this->is_connected && $this->environment->is_sandbox();
+ }
+
+ /**
+ * Is the merchant connected to a production environment and can receive payments?
+ *
+ * @return bool True, if connected to a production environment.
+ */
+ public function is_production() : bool {
+ return $this->is_connected && $this->environment->is_production();
+ }
+
+ /**
+ * Returns the current environment's name.
+ *
+ * @return string Name of the currently connected environment; empty string if not connected.
+ */
+ public function current_environment() : string {
+ return $this->is_connected ? $this->environment->current_environment() : '';
+ }
+}
diff --git a/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php b/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php
index d82271b60..980b4d2d0 100644
--- a/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php
+++ b/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php
@@ -103,6 +103,7 @@ class DCCProductStatus extends ProductStatus {
continue;
}
+ // Settings used as a cache; `settings->set` is compatible with new UI.
if ( in_array( 'CUSTOM_CARD_PROCESSING', $product->capabilities(), true ) ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
diff --git a/modules/ppcp-wc-gateway/src/Helper/Environment.php b/modules/ppcp-wc-gateway/src/Helper/Environment.php
index 143ebd843..e855b54b4 100644
--- a/modules/ppcp-wc-gateway/src/Helper/Environment.php
+++ b/modules/ppcp-wc-gateway/src/Helper/Environment.php
@@ -9,8 +9,6 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
-use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
-
/**
* Class Environment
*/
@@ -27,36 +25,73 @@ class Environment {
public const SANDBOX = 'sandbox';
/**
- * The Settings.
+ * Name of the current environment.
*
- * @var ContainerInterface
+ * @var string
*/
- private ContainerInterface $settings;
+ private string $environment_name;
/**
* Environment constructor.
*
- * @param ContainerInterface $settings The settings.
+ * @param bool $is_sandbox Whether this instance represents a sandbox environment.
*/
- public function __construct( ContainerInterface $settings ) {
- $this->settings = $settings;
+ public function __construct( bool $is_sandbox = false ) {
+ $this->environment_name = $this->prepare_environment_name( $is_sandbox );
}
/**
- * Returns the current environment.
+ * Returns a valid environment name based on the provided argument.
+ *
+ * @param bool $is_sandbox Whether this instance represents a sandbox environment.
+ * @return string The environment name.
+ */
+ private function prepare_environment_name( bool $is_sandbox ) : string {
+ if ( $is_sandbox ) {
+ return self::SANDBOX;
+ }
+
+ return self::PRODUCTION;
+ }
+
+ /**
+ * Updates the current environment.
+ *
+ * @param bool $is_sandbox Whether this instance represents a sandbox environment.
+ */
+ public function set_environment( bool $is_sandbox ) : void {
+ $new_environment = $this->prepare_environment_name( $is_sandbox );
+
+ if ( $new_environment !== $this->environment_name ) {
+ /**
+ * Action that fires before the environment status changes.
+ *
+ * @param string $new_environment The new environment name.
+ * @param string $old_environment The previous environment name.
+ */
+ do_action(
+ 'woocommerce_paypal_payments_merchant_environment_change',
+ $new_environment,
+ $this->environment_name
+ );
+ }
+
+ $this->environment_name = $new_environment;
+ }
+
+ /**
+ * Returns the current environment's name.
*
* @return string
*/
public function current_environment() : string {
- return (
- $this->settings->has( 'sandbox_on' ) && $this->settings->get( 'sandbox_on' )
- ) ? self::SANDBOX : self::PRODUCTION;
+ return $this->environment_name;
}
/**
* Detect whether the current environment equals $environment
*
- * @deprecated Use the is_sandbox() and is_production() methods instead.
+ * @deprecated 3.0.0 - Use the is_sandbox() and is_production() methods instead.
* These methods provide better encapsulation, are less error-prone,
* and improve code readability by removing the need to pass environment constants.
* @param string $environment The value to check against.
diff --git a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php
index 250138089..64192bd87 100644
--- a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php
+++ b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php
@@ -96,6 +96,7 @@ class PayUponInvoiceProductStatus extends ProductStatus {
continue;
}
+ // Settings used as a cache; `settings->set` is compatible with new UI.
if ( in_array( 'PAY_UPON_INVOICE', $product->capabilities(), true ) ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
$this->settings->persist();
diff --git a/modules/ppcp-wc-gateway/src/Notice/ConnectAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/ConnectAdminNotice.php
index 60085a992..c667277c6 100644
--- a/modules/ppcp-wc-gateway/src/Notice/ConnectAdminNotice.php
+++ b/modules/ppcp-wc-gateway/src/Notice/ConnectAdminNotice.php
@@ -10,7 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Notice;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@@ -20,11 +19,11 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class ConnectAdminNotice {
/**
- * The state.
+ * Whether the merchant completed the onboarding and is connected to PayPal.
*
- * @var State
+ * @var bool
*/
- private $state;
+ private bool $is_connected;
/**
* The settings.
@@ -43,12 +42,16 @@ class ConnectAdminNotice {
/**
* ConnectAdminNotice constructor.
*
- * @param State $state The state.
+ * @param bool $is_connected Whether onboarding was completed.
* @param ContainerInterface $settings The settings.
* @param bool $is_current_country_send_only Whether the current store's country is classified as a send-only country.
*/
- public function __construct( State $state, ContainerInterface $settings, bool $is_current_country_send_only ) {
- $this->state = $state;
+ public function __construct(
+ bool $is_connected,
+ ContainerInterface $settings,
+ bool $is_current_country_send_only
+ ) {
+ $this->is_connected = $is_connected;
$this->settings = $settings;
$this->is_current_country_send_only = $is_current_country_send_only;
}
@@ -77,9 +80,13 @@ class ConnectAdminNotice {
/**
* Whether the message should display.
*
+ * Only display the "almost ready" message for merchants that did not complete
+ * the onboarding wizard. Also, ensure their store country is eligible for
+ * collecting PayPal payments.
+ *
* @return bool
*/
protected function should_display(): bool {
- return $this->state->current_state() !== State::STATE_ONBOARDED && $this->is_current_country_send_only === false;
+ return ! $this->is_connected && ! $this->is_current_country_send_only;
}
}
diff --git a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php
index 2dfc88904..627d23e97 100644
--- a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php
+++ b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php
@@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Notice;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
@@ -32,11 +31,11 @@ class GatewayWithoutPayPalAdminNotice {
private $id;
/**
- * The state.
+ * Whether the merchant completed onboarding.
*
- * @var State
+ * @var bool
*/
- private $state;
+ private bool $is_connected;
/**
* The settings.
@@ -70,7 +69,7 @@ class GatewayWithoutPayPalAdminNotice {
* ConnectAdminNotice constructor.
*
* @param string $id The gateway ID.
- * @param State $state The state.
+ * @param bool $is_connected Whether onboading was completed.
* @param ContainerInterface $settings The settings.
* @param bool $is_payments_page Whether the current page is the WC payment page.
* @param bool $is_ppcp_settings_page Whether the current page is the PPCP settings page.
@@ -78,14 +77,14 @@ class GatewayWithoutPayPalAdminNotice {
*/
public function __construct(
string $id,
- State $state,
+ bool $is_connected,
ContainerInterface $settings,
bool $is_payments_page,
bool $is_ppcp_settings_page,
?SettingsStatus $settings_status = null
) {
$this->id = $id;
- $this->state = $state;
+ $this->is_connected = $is_connected;
$this->settings = $settings;
$this->is_payments_page = $is_payments_page;
$this->is_ppcp_settings_page = $is_ppcp_settings_page;
@@ -161,7 +160,7 @@ class GatewayWithoutPayPalAdminNotice {
* @return string One of the NOTICE_* constants.
*/
protected function check(): string {
- if ( State::STATE_ONBOARDED !== $this->state->current_state() ||
+ if ( ! $this->is_connected ||
( ! $this->is_payments_page && ! $this->is_ppcp_settings_page ) ) {
return self::NOTICE_OK;
}
diff --git a/modules/ppcp-wc-gateway/src/Notice/SendOnlyCountryNotice.php b/modules/ppcp-wc-gateway/src/Notice/SendOnlyCountryNotice.php
index f855e923c..0746c2ac2 100644
--- a/modules/ppcp-wc-gateway/src/Notice/SendOnlyCountryNotice.php
+++ b/modules/ppcp-wc-gateway/src/Notice/SendOnlyCountryNotice.php
@@ -10,7 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Notice;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
-use WooCommerce\PayPalCommerce\Onboarding\State;
/**
* Creates an admin message that notifies user about send only country.
@@ -47,9 +46,9 @@ class SendOnlyCountryNotice {
/**
* Onboarding state
*
- * @var int
+ * @var bool
*/
- private int $onboarding_state;
+ private bool $is_connected;
/**
* AdminNotice constructor.
@@ -58,20 +57,20 @@ class SendOnlyCountryNotice {
* @param bool $is_send_only_country Determines if current WC country is a send only country.
* @param bool $is_ppcp_settings_page Determines if current page is ppcp settings page.
* @param bool $is_wc_gateways_list_page Determines if current page is ppcp gateway list page.
- * @param int $onboarding_state Determines current onboarding state.
+ * @param bool $is_connected Whether onboarding was completed.
*/
public function __construct(
string $message_text,
bool $is_send_only_country,
bool $is_ppcp_settings_page,
bool $is_wc_gateways_list_page,
- int $onboarding_state
+ bool $is_connected
) {
$this->message_text = $message_text;
$this->is_send_only_country = $is_send_only_country;
$this->is_ppcp_settings_page = $is_ppcp_settings_page;
$this->is_wc_gateways_list_page = $is_wc_gateways_list_page;
- $this->onboarding_state = $onboarding_state;
+ $this->is_connected = $is_connected;
}
/**
@@ -80,10 +79,9 @@ class SendOnlyCountryNotice {
* @return Message|null
*/
public function message(): ?Message {
-
if ( ! $this->is_send_only_country ||
- ! $this->is_ppcp_page() ||
- $this->onboarding_state === State::STATE_START
+ ! $this->is_connected ||
+ ! $this->is_ppcp_page()
) {
return null;
}
diff --git a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php
index dfc9db81c..5b9dae7c4 100644
--- a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php
+++ b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php
@@ -11,9 +11,6 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Notice;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
-use WooCommerce\PayPalCommerce\Onboarding\State;
-use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
-use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class UnsupportedCurrencyAdminNotice
@@ -21,11 +18,11 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class UnsupportedCurrencyAdminNotice {
/**
- * The state.
+ * Whether the merchant completed onboarding.
*
- * @var State
+ * @var bool
*/
- private $state;
+ private bool $is_connected;
/**
* The supported currencies.
@@ -58,20 +55,20 @@ class UnsupportedCurrencyAdminNotice {
/**
* UnsupportedCurrencyAdminNotice constructor.
*
- * @param State $state The state.
+ * @param bool $is_connected Whether the merchant completed onboarding.
* @param CurrencyGetter $shop_currency The shop currency.
* @param array $supported_currencies The supported currencies.
* @param bool $is_wc_gateways_list_page Indicates if we're on the WooCommerce gateways list page.
* @param bool $is_ppcp_settings_page Indicates if we're on a PPCP Settings page.
*/
public function __construct(
- State $state,
+ bool $is_connected,
CurrencyGetter $shop_currency,
array $supported_currencies,
bool $is_wc_gateways_list_page,
bool $is_ppcp_settings_page
) {
- $this->state = $state;
+ $this->is_connected = $is_connected;
$this->shop_currency = $shop_currency;
$this->supported_currencies = $supported_currencies;
$this->is_wc_gateways_list_page = $is_wc_gateways_list_page;
@@ -110,7 +107,7 @@ class UnsupportedCurrencyAdminNotice {
* @return bool
*/
protected function should_display(): bool {
- return $this->state->current_state() === State::STATE_ONBOARDED
+ return $this->is_connected
&& ! $this->currency_supported()
&& ( $this->is_wc_gateways_list_page || $this->is_ppcp_settings_page );
}
diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php
index 409fde543..4a3265b23 100644
--- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php
+++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php
@@ -28,6 +28,8 @@ return function ( ContainerInterface $container, array $fields ): array {
return $fields;
}
+ // Legacy settings module, use of `State` class is correct.
+
$state = $container->get( 'onboarding.state' );
assert( $state instanceof State );
diff --git a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php
index f8f05f4ff..5fcf0aeb2 100644
--- a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php
+++ b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php
@@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway;
@@ -35,11 +34,11 @@ class SectionsRenderer {
protected $page_id;
/**
- * The onboarding state.
+ * Whether onboarding was completed and the merchant is connected to PayPal.
*
- * @var State
+ * @var bool
*/
- private $state;
+ private bool $is_connected;
/**
* The DCC product status
@@ -69,18 +68,11 @@ class SectionsRenderer {
*/
private $pui_product_status;
- /**
- * SectionsRenderer constructor.
- *
- * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
- * @param State $state The onboarding state.
- */
-
/**
* SectionsRenderer constructor.
*
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
- * @param State $state The onboarding state.
+ * @param bool $is_connected Whether the merchant completed onboarding.
* @param DCCProductStatus $dcc_product_status The DCC product status.
* @param DccApplies $dcc_applies The DCC applies.
* @param MessagesApply $messages_apply The Messages apply.
@@ -88,14 +80,14 @@ class SectionsRenderer {
*/
public function __construct(
string $page_id,
- State $state,
+ bool $is_connected,
DCCProductStatus $dcc_product_status,
DccApplies $dcc_applies,
MessagesApply $messages_apply,
PayUponInvoiceProductStatus $pui_product_status
) {
$this->page_id = $page_id;
- $this->state = $state;
+ $this->is_connected = $is_connected;
$this->dcc_product_status = $dcc_product_status;
$this->dcc_applies = $dcc_applies;
$this->messages_apply = $messages_apply;
@@ -108,9 +100,7 @@ class SectionsRenderer {
* @return bool
*/
public function should_render() : bool {
- return ! empty( $this->page_id ) &&
- ( $this->state->production_state() === State::STATE_ONBOARDED ||
- $this->state->sandbox_state() === State::STATE_ONBOARDED );
+ return $this->page_id && $this->is_connected;
}
/**
diff --git a/modules/ppcp-wc-gateway/src/Settings/Settings.php b/modules/ppcp-wc-gateway/src/Settings/Settings.php
index 9340357a6..920d411e8 100644
--- a/modules/ppcp-wc-gateway/src/Settings/Settings.php
+++ b/modules/ppcp-wc-gateway/src/Settings/Settings.php
@@ -120,7 +120,10 @@ class Settings implements ContainerInterface {
* @return bool
*/
public function has( string $id ) {
- if ( $this->settings_map_helper->has_mapped_key( $id ) ) {
+ if (
+ $this->settings_map_helper->has_mapped_key( $id )
+ && ! is_null( $this->settings_map_helper->mapped_value( $id ) )
+ ) {
return true;
}
@@ -156,7 +159,7 @@ class Settings implements ContainerInterface {
if ( $this->settings ) {
return false;
}
- $this->settings = get_option( self::KEY, array() );
+ $this->settings = (array) get_option( self::KEY, array() );
$defaults = array(
'title' => __( 'PayPal', 'woocommerce-paypal-payments' ),
diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
index 6c01830d6..b612de369 100644
--- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
+++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
@@ -214,6 +214,8 @@ class SettingsListener {
Cache $client_credentials_cache
) {
+ // This is a legacy settings class, it's correctly relying on the `Status` class.
+
$this->settings = $settings;
$this->setting_fields = $setting_fields;
$this->webhook_registrar = $webhook_registrar;
@@ -261,6 +263,8 @@ class SettingsListener {
// phpcs:enable WordPress.Security.NonceVerification.Missing
// phpcs:enable WordPress.Security.NonceVerification.Recommended
+ // This method is only used for legacy UI, `settings->set` is valid here.
+
$this->settings->set( 'merchant_id', $merchant_id );
$this->settings->set( 'merchant_email', $merchant_email );
@@ -363,6 +367,8 @@ class SettingsListener {
return;
}
+ // This method is only used for legacy UI, `settings->set` is valid here.
+
try {
$token = $this->bearer->bearer();
if ( ! $token->vaulting_available() ) {
@@ -472,6 +478,10 @@ class SettingsListener {
&& 1 === absint( $_POST['woocommerce_ppcp-gateway_enabled'] );
}
+ // This method initializes a feature cache. This initialization is not
+ // required by the new UI; we can ignore the `settings->set` usage.
+ // TODO new-ux: Test, if this method is called or some non-settings parts must be converted.
+
// phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing
if ( $credentials_change_status ) {
@@ -720,6 +730,8 @@ class SettingsListener {
/**
* Prevent enabling tracking if it is not enabled for merchant account.
*
+ * This method is not used anywhere. Not relevant for new-ux.
+ *
* @throws RuntimeException When API request fails.
*/
public function listen_for_tracking_enabled(): void {
@@ -765,6 +777,8 @@ class SettingsListener {
return;
}
+ // This method is only used for legacy UI, `settings->set` is valid here.
+
$existing_setting_value = $this->settings->has( $setting_slug ) ? $this->settings->get( $setting_slug ) : null;
if ( $condition ) {
diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php
index e32cbf0c4..f9a00170f 100644
--- a/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php
+++ b/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php
@@ -114,6 +114,8 @@ class SettingsRenderer {
string $api_shop_country
) {
+ // This is a legacy settings class, it's correctly relying on the `Status` class.
+
$this->settings = $settings;
$this->state = $state;
$this->fields = $fields;
diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php
index f8b54f534..2f82c5636 100644
--- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php
+++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php
@@ -14,8 +14,6 @@ use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
-use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
-use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
@@ -33,7 +31,6 @@ use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
@@ -59,7 +56,6 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
-use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrarInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
@@ -209,7 +205,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$c->get( 'button.client_id_for_admin' ),
$c->get( 'api.shop.currency.getter' ),
$c->get( 'api.shop.country' ),
- $c->get( 'onboarding.environment' ),
+ $c->get( 'settings.environment' ),
$settings_status->is_pay_later_button_enabled(),
$settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(),
$c->get( 'wcgateway.settings.funding-sources' ),
@@ -607,16 +603,14 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$methods[] = $paypal_gateway;
- $onboarding_state = $container->get( 'onboarding.state' );
- assert( $onboarding_state instanceof State );
-
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
$is_our_page = $container->get( 'wcgateway.is-ppcp-settings-page' );
$is_gateways_list_page = $container->get( 'wcgateway.is-wc-gateways-list-page' );
+ $is_connected = $container->get( 'settings.flag.is-connected' );
- if ( $onboarding_state->current_state() !== State::STATE_ONBOARDED ) {
+ if ( ! $is_connected ) {
return $methods;
}
@@ -648,7 +642,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$methods[] = $container->get( 'wcgateway.credit-card-gateway' );
}
- if ( $paypal_gateway_enabled && $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) {
+ if ( $paypal_gateway_enabled && apply_filters( 'woocommerce_paypal_payments_card_button_gateway_should_register_gateway', $container->get( 'wcgateway.settings.allow_card_button_gateway' ) ) ) {
$methods[] = $container->get( 'wcgateway.card-button-gateway' );
}
diff --git a/modules/ppcp-wc-subscriptions/services.php b/modules/ppcp-wc-subscriptions/services.php
index dc23fca9f..165f673b9 100644
--- a/modules/ppcp-wc-subscriptions/services.php
+++ b/modules/ppcp-wc-subscriptions/services.php
@@ -28,7 +28,7 @@ return array(
$endpoint = $container->get( 'api.endpoint.order' );
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' );
- $environment = $container->get( 'onboarding.environment' );
+ $environment = $container->get( 'settings.environment' );
$settings = $container->get( 'wcgateway.settings' );
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
$funding_source_renderer = $container->get( 'wcgateway.funding-source.renderer' );
diff --git a/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php b/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php
index 45e481728..8bc1e40f5 100644
--- a/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php
+++ b/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php
@@ -21,6 +21,7 @@ use WCS_Manual_Renewal_Manager;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
+use WP_Query;
/**
* Class SubscriptionHelper
@@ -342,4 +343,30 @@ class SubscriptionHelper {
return '';
}
+
+ /**
+ * Checks if any subscription products exist.
+ *
+ * @return bool
+ */
+ public function has_subscription_products(): bool {
+ // Query for subscription products.
+ $args = array(
+ 'post_type' => 'product',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 1,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'product_type',
+ 'field' => 'slug',
+ 'terms' => 'subscription',
+ ),
+ ),
+ );
+
+ $subscription_products = new WP_Query( $args );
+
+ return $subscription_products->have_posts();
+ }
}
diff --git a/modules/ppcp-webhooks/factories.php b/modules/ppcp-webhooks/factories.php
index 6d36ae4fa..e568b896a 100644
--- a/modules/ppcp-webhooks/factories.php
+++ b/modules/ppcp-webhooks/factories.php
@@ -10,7 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
@@ -18,8 +17,9 @@ return array(
$endpoint = $container->get( 'api.endpoint.webhook' );
assert( $endpoint instanceof WebhookEndpoint );
- $state = $container->get( 'onboarding.state' );
- if ( $state->current_state() >= State::STATE_ONBOARDED ) {
+ $is_connected = $container->get( 'settings.flag.is-connected' );
+
+ if ( $is_connected ) {
return $endpoint->list();
}
diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php
index e179fd24a..e000a1690 100644
--- a/modules/ppcp-webhooks/services.php
+++ b/modules/ppcp-webhooks/services.php
@@ -11,10 +11,8 @@ namespace WooCommerce\PayPalCommerce\Webhooks;
use Exception;
use Psr\Log\LoggerInterface;
-use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint;
@@ -183,7 +181,7 @@ return array(
return new WebhooksStatusPageAssets(
$container->get( 'webhook.module-url' ),
$container->get( 'ppcp.asset-version' ),
- $container->get( 'onboarding.environment' )
+ $container->get( 'settings.environment' )
);
},
diff --git a/modules/ppcp-webhooks/src/WebhookModule.php b/modules/ppcp-webhooks/src/WebhookModule.php
index f1f7a03b4..1886635f2 100644
--- a/modules/ppcp-webhooks/src/WebhookModule.php
+++ b/modules/ppcp-webhooks/src/WebhookModule.php
@@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
-use WC_Order;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use Exception;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
@@ -18,7 +16,6 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\FactoryModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
-use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
@@ -142,9 +139,10 @@ class WebhookModule implements ServiceModule, FactoryModule, ExtendingModule, Ex
);
try {
- $webhooks = $container->get( 'webhook.status.registered-webhooks' );
- $state = $container->get( 'onboarding.state' );
- if ( empty( $webhooks ) && $state->current_state() >= State::STATE_ONBOARDED ) {
+ $webhooks = $container->get( 'webhook.status.registered-webhooks' );
+ $is_connected = $container->get( 'settings.flag.is-connected' );
+
+ if ( empty( $webhooks ) && $is_connected ) {
$registrar = $container->get( 'webhook.registrar' );
assert( $registrar instanceof WebhookRegistrar );
$registrar->register();
diff --git a/package.json b/package.json
index cf4a58b7b..e7d0a3b76 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
- "version": "2.9.6",
+ "version": "3.0.0",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",
diff --git a/readme.txt b/readme.txt
index 290c2fad4..5fe0d876f 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, credit card
Requires at least: 6.5
Tested up to: 6.7
Requires PHP: 7.4
-Stable tag: 2.9.6
+Stable tag: 3.0.0
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -113,7 +113,7 @@ If you like this extension, please [leave a review on WordPress.org](https://wor
To install and configure WooCommerce PayPal Payments, you will need:
* WordPress Version 6.3 or newer (installed)
-* WooCommerce Version 6.9 or newer (installed and activated)
+* WooCommerce Version 9.6 or newer (installed and activated)
* PHP Version 7.4 or newer
* PayPal business **or** personal account
@@ -156,6 +156,22 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
+= 3.0.0 - 2025-03-17 =
+* Enhancement - Redesigned settings UI for new users #2908
+* Enhancement - Enable Fastlane by default on new store setups when eligible #3199
+* Enhancement - Enable support for advanced card payments and features for Hong Kong & Singapore #3089
+* Fix - Dependency conflict with more recent psr/log versions on PHP8+ #2993
+* Fix - PayPal Checkout Gateway subscription migration layer not renewing subscriptions #2699
+* Fix - Fatal error when gateway settings initialized too early by third-party plugin #2766
+* Fix - Next Payment date for Subscriptions not updating when processing a PayPal Subscriptions renewal order #2959
+* Fix - Changing the subscription payment method to ACDC triggers error #2891
+* Fix - Standard Card button not appearing in standalone gateway for free trial subscription products #2935
+* Fix - Validation error when using Trustly payment method #3031
+* Fix - Error in continuation mode due to wrong gateway selection on Checkout block #2996
+* Fix - Error in error in PayLaterConfigurator #2989
+* Tweak - Removed currency requirement for Vault v3 #2919
+* Tweak - Update plugin author from WooCommerce to PayPal
+
= 2.9.6 - 2025-01-06 =
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872
diff --git a/tests/PHPUnit/ApiClient/Repository/PartnerReferralsDataTest.php b/tests/PHPUnit/ApiClient/Repository/PartnerReferralsDataTest.php
new file mode 100644
index 000000000..17c06c504
--- /dev/null
+++ b/tests/PHPUnit/ApiClient/Repository/PartnerReferralsDataTest.php
@@ -0,0 +1,221 @@
+data()` method to ensure it's appended at the end of the
+ * return URL as-is.
+ */
+ private const TOKEN = 'SECURE_TOKEN';
+
+ /**
+ * Expected return URL to see at in the payload, including the ppcpToken.
+ */
+ private const RETURN_URL = 'https://example.com/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcpToken=SECURE_TOKEN';
+
+ private $testee;
+ private $dccApplies;
+
+ public function setUp() : void {
+ parent::setUp();
+
+ $this->dccApplies = Mockery::mock( DccApplies::class );
+ $this->testee = new PartnerReferralsData( $this->dccApplies );
+
+ when( 'admin_url' )->alias( static fn( string $path ) => self::ADMIN_URL . $path );
+ when( 'add_query_arg' )->justReturn( self::RETURN_URL );
+ }
+
+ /**
+ * Base structure of the API payload. Each test should modify the returned
+ * value of the method to meet its expectations.
+ *
+ * This avoids repeating the full structure, while also highlighting the
+ * specific changes that different params will generate.
+ *
+ * @return array
+ */
+ private function getBaseExpectedArray() : array {
+ return [
+ 'partner_config_override' => [
+ 'return_url' => self::RETURN_URL,
+ 'return_url_description' => 'Return to your shop.',
+ 'show_add_credit_card' => true,
+ ],
+ 'legal_consents' => [
+ [
+ 'type' => 'SHARE_DATA_CONSENT',
+ 'granted' => true,
+ ],
+ ],
+ 'operations' => [
+ [
+ 'operation' => 'API_INTEGRATION',
+ 'api_integration_preference' => [
+ 'rest_api_integration' => [
+ 'integration_method' => 'PAYPAL',
+ 'integration_type' => 'FIRST_PARTY',
+ 'first_party_details' => [
+ 'features' => [
+ 'PAYMENT',
+ 'REFUND',
+ 'ADVANCED_TRANSACTIONS_SEARCH',
+ 'TRACKING_SHIPMENT_READWRITE',
+ ],
+ 'seller_nonce' => self::DEFAULT_NONCE,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Data provider for testing flag combinations.
+ *
+ * @return array[] Test cases with [has_subscriptions, has_cards, expected_changes]
+ */
+ public function flagCombinationsProvider() : array {
+ return [
+ 'with subscriptions and cards' => [
+ true, // With subscription?
+ true, // With cards?
+ [
+ 'capabilities' => [ 'PAYPAL_WALLET_VAULTING_ADVANCED' ],
+ 'show_add_credit_card' => true,
+ 'has_vault_features' => true,
+ ],
+ ],
+ 'with subscriptions, no cards' => [
+ true, // With subscription?
+ false, // With cards?
+ [
+ 'capabilities' => [ 'PAYPAL_WALLET_VAULTING_ADVANCED' ],
+ 'show_add_credit_card' => false,
+ 'has_vault_features' => true,
+ ],
+ ],
+ 'no subscriptions, with cards' => [
+ false, // With subscription?
+ true, // With cards?
+ [
+ 'show_add_credit_card' => true,
+ 'has_vault_features' => false,
+ ],
+ ],
+ 'no subscriptions, no cards' => [
+ false, // With subscription?
+ false, // With cards?
+ [
+ 'show_add_credit_card' => false,
+ 'has_vault_features' => false,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Ensure the default "products" are derived from the DccApplies response.
+ */
+ public function testDefaultValues() : void {
+ /**
+ * Case 1: The data() method gets no parameters, and the DccApplies check
+ * returns TRUE. Onboarding payload should indicate "PPCP".
+ */
+ $this->dccApplies->expects( 'for_country_currency' )->andReturn( true );
+ $result = $this->testee->data();
+ $this->assertEquals( [ 'PPCP' ], $result['products'] );
+
+ /**
+ * Case 2: The data() method gets no parameters, and the DccApplies check
+ * returns FALSE. Onboarding payload should indicate "EXPRESS_CHECKOUT".
+ */
+ $this->dccApplies->expects( 'for_country_currency' )->andReturn( false );
+ $result = $this->testee->data();
+ $this->assertEquals( [ 'EXPRESS_CHECKOUT' ], $result['products'] );
+ }
+
+ /**
+ * Ensure the generated API payload is stable and contains the expected values.
+ *
+ * The test only verifies the "products" and "token" arguments, as those are the
+ * core params present in the legacy and new UI.
+ */
+ public function testDataStructure() : void {
+ /**
+ * Undefined subscription: Keep vaulting in first-party, but don't add the capability.
+ */
+ $result = $this->testee->data( [ 'PPCP' ], self::TOKEN );
+ $this->dccApplies->shouldNotHaveReceived( 'for_country_currency' );
+
+ $expected = $this->getBaseExpectedArray();
+
+ $expected['products'] = [ 'PPCP' ];
+
+ $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'FUTURE_PAYMENT';
+ $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'VAULT';
+
+ $this->assertArrayNotHasKey( 'capabilities', $expected );
+ $this->assertEquals( $expected, $result );
+ }
+
+ /**
+ * Test how different flag combinations affect the data structure.
+ * Those flags are present in the new UI.
+ *
+ * @dataProvider flagCombinationsProvider
+ */
+ public function testDataStructureWithFlags( bool $has_subscriptions, bool $has_cards, array $expected_changes ) : void {
+ $result = $this->testee->data( [ 'PPCP' ], self::TOKEN, $has_subscriptions, $has_cards );
+ $expected = $this->getBaseExpectedArray();
+
+ $expected['products'] = [ 'PPCP' ];
+
+ if ( isset( $expected_changes['capabilities'] ) ) {
+ $expected['capabilities'] = $expected_changes['capabilities'];
+ } else {
+ $this->assertArrayNotHasKey( 'capabilities', $expected );
+ }
+
+ $expected['partner_config_override']['show_add_credit_card'] = $expected_changes['show_add_credit_card'];
+
+ if ( $has_subscriptions ) {
+ $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'BILLING_AGREEMENT';
+ }
+
+ if ( $expected_changes['has_vault_features'] ) {
+ $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'FUTURE_PAYMENT';
+ $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'VAULT';
+ } else {
+ // Double-check that the features are not present in our expected array
+ $this->assertNotContains( 'FUTURE_PAYMENT', $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'] );
+ $this->assertNotContains( 'VAULT', $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'] );
+ }
+
+ $this->assertEquals( $expected, $result );
+ }
+}
diff --git a/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php
index 72b90e030..7fd20cc2f 100644
--- a/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php
+++ b/tests/PHPUnit/WcGateway/Gateway/CreditCardGatewayTest.php
@@ -10,7 +10,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\Vaulting\VaultedCreditCardHandler;
@@ -34,7 +33,6 @@ class CreditCardGatewayTest extends TestCase
private $moduleUrl;
private $sessionHandler;
private $refundProcessor;
- private $state;
private $transactionUrlProvider;
private $subscriptionHelper;
private $captureCardPayment;
@@ -60,7 +58,6 @@ class CreditCardGatewayTest extends TestCase
$this->moduleUrl = '';
$this->sessionHandler = Mockery::mock(SessionHandler::class);
$this->refundProcessor = Mockery::mock(RefundProcessor::class);
- $this->state = Mockery::mock(State::class);
$this->transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class);
$this->subscriptionHelper = Mockery::mock(SubscriptionHelper::class);
$this->captureCardPayment = Mockery::mock(CaptureCardPayment::class);
@@ -73,7 +70,6 @@ class CreditCardGatewayTest extends TestCase
$this->environment = Mockery::mock(Environment::class);
$this->orderEndpoint = Mockery::mock(OrderEndpoint::class);
- $this->state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$this->config->shouldReceive('has')->andReturn(true);
$this->config->shouldReceive('get')->andReturn('');
@@ -92,7 +88,6 @@ class CreditCardGatewayTest extends TestCase
$this->moduleUrl,
$this->sessionHandler,
$this->refundProcessor,
- $this->state,
$this->transactionUrlProvider,
$this->subscriptionHelper,
$this->paymentsEndpoint,
diff --git a/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php
index d806037ee..ff4f0eaa6 100644
--- a/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php
+++ b/tests/PHPUnit/WcGateway/Gateway/PayUponInvoice/PayUponInvoiceGatewayTest.php
@@ -11,7 +11,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
@@ -30,7 +29,7 @@ class PayUponInvoiceGatewayTest extends TestCase
private $testee;
private $pui_helper;
private $checkout_helper;
- private $state;
+ private $is_connected;
private $refund_processor;
public function setUp(): void
@@ -45,9 +44,7 @@ class PayUponInvoiceGatewayTest extends TestCase
$this->transaction_url_provider = Mockery::mock(TransactionUrlProvider::class);
$this->pui_helper = Mockery::mock(PayUponInvoiceHelper::class);
$this->checkout_helper = Mockery::mock(CheckoutHelper::class);
-
- $this->state = Mockery::mock(State::class);
- $this->state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
+ $this->is_connected = true;
$this->refund_processor = Mockery::mock(RefundProcessor::class);
@@ -63,7 +60,7 @@ class PayUponInvoiceGatewayTest extends TestCase
$this->logger,
$this->pui_helper,
$this->checkout_helper,
- $this->state,
+ $this->is_connected,
$this->refund_processor,
''
);
diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php
index a9e64ebb0..5f5cf722a 100644
--- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php
+++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php
@@ -10,7 +10,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
-use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
@@ -38,7 +37,7 @@ class WcGatewayTest extends TestCase
private $orderProcessor;
private $settings;
private $refundProcessor;
- private $onboardingState;
+ private $isConnected;
private $transactionUrlProvider;
private $subscriptionHelper;
private $environment;
@@ -63,7 +62,7 @@ class WcGatewayTest extends TestCase
$this->settings = Mockery::mock(Settings::class);
$this->sessionHandler = Mockery::mock(SessionHandler::class);
$this->refundProcessor = Mockery::mock(RefundProcessor::class);
- $this->onboardingState = Mockery::mock(State::class);
+ $this->isConnected = true;
$this->transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class);
$this->subscriptionHelper = Mockery::mock(SubscriptionHelper::class);
$this->environment = Mockery::mock(Environment::class);
@@ -76,8 +75,6 @@ class WcGatewayTest extends TestCase
$this->apiShopCountry = 'DE';
$this->orderEndpoint = Mockery::mock(OrderEndpoint::class);
- $this->onboardingState->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
-
$this->sessionHandler
->shouldReceive('funding_source')
->andReturnUsing(function () {
@@ -108,7 +105,7 @@ class WcGatewayTest extends TestCase
$this->settings,
$this->sessionHandler,
$this->refundProcessor,
- $this->onboardingState,
+ $this->isConnected,
$this->transactionUrlProvider,
$this->subscriptionHelper,
PayPalGateway::ID,
@@ -159,7 +156,7 @@ class WcGatewayTest extends TestCase
->andReturn($wcOrder);
when('wc_get_checkout_url')
- ->justReturn('test');
+ ->justReturn('test');
$woocommerce = Mockery::mock(\WooCommerce::class);
$cart = Mockery::mock(\WC_Cart::class);
@@ -267,23 +264,6 @@ class WcGatewayTest extends TestCase
);
}
- /**
- * @dataProvider dataForTestNeedsSetup
- */
- public function testNeedsSetup($currentState, $needSetup)
- {
- $this->isAdmin = true;
-
- $this->onboardingState = Mockery::mock(State::class);
- $this->onboardingState
- ->expects('current_state')
- ->andReturn($currentState);
-
- $testee = $this->createGateway();
-
- $this->assertSame($needSetup, $testee->needs_setup());
- }
-
/**
* @dataProvider dataForFundingSource
*/
@@ -315,14 +295,6 @@ class WcGatewayTest extends TestCase
];
}
- public function dataForTestNeedsSetup(): array
- {
- return [
- [State::STATE_START, true],
- [State::STATE_ONBOARDED, false]
- ];
- }
-
public function dataForFundingSource(): array
{
return [
diff --git a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php
index 951c3d9af..d7b8c2ed0 100644
--- a/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php
+++ b/tests/PHPUnit/WcGateway/Processor/OrderProcessorTest.php
@@ -37,7 +37,7 @@ class OrderProcessorTest extends TestCase
public function setUp(): void {
parent::setUp();
- $this->environment = new Environment(new ReadOnlyContainer([], [], [], []));
+ $this->environment = new Environment( false );
}
public function testAuthorize() {
diff --git a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php
deleted file mode 100644
index 322b54731..000000000
--- a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php
+++ /dev/null
@@ -1,107 +0,0 @@
-getContainer();
- $handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
-
- // Simulates receiving webhook 1 minute after subscription start.
- $subscription = $this->createSubscription('-1 minute');
-
- $handler->process([$subscription], 'TRANSACTION-ID');
- $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
- $this->assertEquals(count($renewal), 0);
- }
-
- public function test_renewal_order()
- {
- $c = $this->getContainer();
- $handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
-
- // Simulates receiving webhook 9 hours after subscription start.
- $subscription = $this->createSubscription('-9 hour');
-
- $handler->process([$subscription], 'TRANSACTION-ID');
- $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
- $this->assertEquals(count($renewal), 1);
- }
-
- private function createSubscription(string $startDate)
- {
- $args = [
- 'method' => 'POST',
- 'headers' => [
- 'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
- 'Content-Type' => 'application/json',
- ],
- 'body' => wp_json_encode([
- 'customer_id' => 1,
- 'set_paid' => true,
- 'payment_method' => 'ppcp-gateway',
- 'billing' => [
- 'first_name' => 'John',
- 'last_name' => 'Doe',
- 'address_1' => '969 Market',
- 'address_2' => '',
- 'city' => 'San Francisco',
- 'state' => 'CA',
- 'postcode' => '94103',
- 'country' => 'US',
- 'email' => 'john.doe@example.com',
- 'phone' => '(555) 555-5555'
- ],
- 'line_items' => [
- [
- 'product_id' => 156,
- 'quantity' => 1
- ]
- ],
- ]),
- ];
-
- $response = wp_remote_request(
- 'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/orders',
- $args
- );
-
- $body = json_decode( $response['body'] );
-
- $args = [
- 'method' => 'POST',
- 'headers' => [
- 'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
- 'Content-Type' => 'application/json',
- ],
- 'body' => wp_json_encode([
- 'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
- 'parent_id' => $body->id,
- 'customer_id' => 1,
- 'status' => 'active',
- 'billing_period' => 'day',
- 'billing_interval' => 1,
- 'payment_method' => 'ppcp-gateway',
- 'line_items' => [
- [
- 'product_id' => $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'],
- 'quantity' => 1
- ]
- ],
- ]),
- ];
-
- $response = wp_remote_request(
- 'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/subscriptions?per_page=1',
- $args
- );
-
- $body = json_decode( $response['body'] );
-
- return wcs_get_subscription($body->id);
- }
-}
diff --git a/tests/e2e/PHPUnit/SettingsTest.php b/tests/e2e/PHPUnit/SettingsTest.php
deleted file mode 100644
index cee650e01..000000000
--- a/tests/e2e/PHPUnit/SettingsTest.php
+++ /dev/null
@@ -1,70 +0,0 @@
-createMock(AbstractDataModel::class);
- $commonSettingsModel->method('to_array')->willReturn([
- 'use_sandbox' => 'yes',
- 'client_id' => 'abc123',
- 'client_secret' => 'secret123',
- ]);
-
- $generalSettingsModel = $this->createMock(AbstractDataModel::class);
- $generalSettingsModel->method('to_array')->willReturn([
- 'is_sandbox' => 'no',
- 'live_client_id' => 'live_id_123',
- 'live_client_secret' => 'live_secret_123',
- ]);
-
- $settingsMap = [
- new SettingsMap(
- $commonSettingsModel,
- [
- 'client_id' => 'client_id',
- 'client_secret' => 'client_secret',
- ]
- ),
- new SettingsMap(
- $generalSettingsModel,
- [
- 'is_sandbox' => 'sandbox_on',
- 'live_client_id' => 'client_id_production',
- 'live_client_secret' => 'client_secret_production',
- ]
- ),
- ];
-
- $settingsMapHelper = new SettingsMapHelper($settingsMap);
-
- $this->settings = new Settings(
- ['cart', 'checkout'],
- 'PayPal Credit Gateway',
- ['checkout'],
- ['cart'],
- $settingsMapHelper
- );
- }
-
- public function testGetMappedValue() {
- $value = $this->settings->get('sandbox_on');
-
- $this->assertEquals('no', $value);
- }
-
- public function testGetThrowsNotFoundExceptionForInvalidKey() {
- $this->expectException(NotFoundException::class);
-
- $this->settings->get('invalid_key');
- }
-}
-
diff --git a/tests/e2e/PHPUnit/bootstrap.php b/tests/e2e/PHPUnit/bootstrap.php
deleted file mode 100644
index 506b55676..000000000
--- a/tests/e2e/PHPUnit/bootstrap.php
+++ /dev/null
@@ -1,23 +0,0 @@
-load();
-}
-
-if (!isset($_ENV['PPCP_E2E_WP_DIR'])) {
- exit('Copy .env.e2e.example to .env.e2e or define the environment variables.' . PHP_EOL);
-}
-$wpRootDir = str_replace('${ROOT_DIR}', ROOT_DIR, $_ENV['PPCP_E2E_WP_DIR']);
-
-define('WP_ROOT_DIR', $wpRootDir);
-
-$_SERVER['HTTP_HOST'] = ''; // just to avoid a warning
-
-require_once WP_ROOT_DIR . '/wp-load.php';
diff --git a/tests/e2e/PHPUnit/Order/PurchaseUnitTest.php b/tests/integration/PHPUnit/Order/PurchaseUnitTest.php
similarity index 98%
rename from tests/e2e/PHPUnit/Order/PurchaseUnitTest.php
rename to tests/integration/PHPUnit/Order/PurchaseUnitTest.php
index 6a72c4255..80f44e409 100644
--- a/tests/e2e/PHPUnit/Order/PurchaseUnitTest.php
+++ b/tests/integration/PHPUnit/Order/PurchaseUnitTest.php
@@ -1,7 +1,7 @@
getContainer();
+ $handler = new RenewalHandler( $c->get( 'woocommerce.logger.woocommerce' ) );
+
+ // Simulates receiving webhook 1 minute after subscription start.
+ $subscription = $this->createSubscription( '-1 minute' );
+
+ $handler->process( [ $subscription ], 'TRANSACTION-ID' );
+ $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
+ $this->assertEquals( count( $renewal ), 0 );
+ }
+
+ public function test_renewal_order_is_created_when_receiving_webhook_nine_hours_later() {
+ $c = $this->getContainer();
+ $handler = new RenewalHandler( $c->get( 'woocommerce.logger.woocommerce' ) );
+
+ // Simulates receiving webhook 9 hours after subscription start.
+ $subscription = $this->createSubscription( '-9 hour' );
+
+ $handler->process( [ $subscription ], 'TRANSACTION-ID' );
+ $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
+ $this->assertEquals( count( $renewal ), 1 );
+ }
+
+ private function createSubscription( string $startDate ) {
+ $order = wc_create_order( [
+ 'customer_id' => 1,
+ 'set_paid' => true,
+ 'payment_method' => 'ppcp-gateway',
+ 'billing' => [
+ 'first_name' => 'John',
+ 'last_name' => 'Doe',
+ 'address_1' => '969 Market',
+ 'address_2' => '',
+ 'city' => 'San Francisco',
+ 'state' => 'CA',
+ 'postcode' => '94103',
+ 'country' => 'US',
+ 'email' => 'john.doe@example.com',
+ 'phone' => '(555) 555-5555'
+ ],
+ 'line_items' => [
+ [
+ 'product_id' => 42,
+ 'quantity' => 1
+ ]
+ ],
+ ] );
+
+ $product = new WC_Product_Simple();
+ $product->set_props([
+ 'name' => 'Dummy Product',
+ 'regular_price' => 10,
+ 'price' => 10,
+ 'sku' => 'DUMMY SKU',
+ 'manage_stock' => false,
+ 'tax_status' => 'taxable',
+ 'downloadable' => false,
+ 'virtual' => false,
+ 'stock_status' => 'instock',
+ 'weight' => '1.1',
+ ]);
+
+ return wcs_create_subscription([
+ 'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
+ 'parent_id' => $order->get_id(),
+ 'customer_id' => 1,
+ 'status' => 'active',
+ 'billing_period' => 'day',
+ 'billing_interval' => 1,
+ 'payment_method' => 'ppcp-gateway',
+ 'line_items' => [
+ [
+ 'product_id' => $product->get_id(),
+ 'quantity' => 1
+ ]
+ ],
+ ]);
+ }
+}
diff --git a/tests/e2e/PHPUnit/RealTimeAccountUpdaterTest.php b/tests/integration/PHPUnit/RealTimeAccountUpdaterTest.php
similarity index 96%
rename from tests/e2e/PHPUnit/RealTimeAccountUpdaterTest.php
rename to tests/integration/PHPUnit/RealTimeAccountUpdaterTest.php
index 5f582d881..d721144cd 100644
--- a/tests/e2e/PHPUnit/RealTimeAccountUpdaterTest.php
+++ b/tests/integration/PHPUnit/RealTimeAccountUpdaterTest.php
@@ -1,7 +1,7 @@
createMock( AbstractDataModel::class );
+ $commonSettingsModel->method( 'to_array' )->willReturn( [
+ 'use_sandbox' => 'yes',
+ 'client_id' => 'abc123',
+ 'client_secret' => 'secret123',
+ ] );
+
+ $generalSettingsModel = $this->createMock( AbstractDataModel::class );
+ $generalSettingsModel->method( 'to_array' )->willReturn( [
+ 'is_sandbox' => 'no',
+ 'live_client_id' => 'live_id_123',
+ 'live_client_secret' => 'live_secret_123',
+ ] );
+
+ $settingsMap = [
+ new SettingsMap(
+ $commonSettingsModel,
+ [
+ 'client_id' => 'client_id',
+ 'client_secret' => 'client_secret',
+ ]
+ ),
+ new SettingsMap(
+ $generalSettingsModel,
+ [
+ 'is_sandbox' => 'sandbox_on',
+ 'live_client_id' => 'client_id_production',
+ 'live_client_secret' => 'client_secret_production',
+ ]
+ ),
+ ];
+
+ $settingsMapHelper = new SettingsMapHelper(
+ $settingsMap,
+ Mockery::mock( StylingSettingsMapHelper::class ),
+ Mockery::mock( SettingsTabMapHelper::class ),
+ Mockery::mock( SubscriptionSettingsMapHelper::class ),
+ Mockery::mock( GeneralSettingsMapHelper::class ),
+ Mockery::mock( PaymentMethodSettingsMapHelper::class ),
+ true
+ );
+
+ $this->settings = new Settings(
+ [ 'cart', 'checkout' ],
+ 'PayPal Credit Gateway',
+ [ 'checkout' ],
+ [ 'cart' ],
+ $settingsMapHelper
+ );
+ }
+
+ public function testGetMappedValue() {
+ $value = $this->settings->get( 'pay_later_messaging_enabled' );
+
+ $this->assertTrue( $value );
+ }
+
+ public function testGetThrowsNotFoundExceptionForInvalidKey() {
+ $this->expectException( NotFoundException::class );
+
+ $this->settings->get( 'invalid_key' );
+ }
+}
+
diff --git a/tests/e2e/PHPUnit/TestCase.php b/tests/integration/PHPUnit/TestCase.php
similarity index 90%
rename from tests/e2e/PHPUnit/TestCase.php
rename to tests/integration/PHPUnit/TestCase.php
index f12f40690..fcbf3644d 100644
--- a/tests/e2e/PHPUnit/TestCase.php
+++ b/tests/integration/PHPUnit/TestCase.php
@@ -1,7 +1,7 @@
load();
+}
+
+if (!isset($_ENV['PPCP_INTEGRATION_WP_DIR'])) {
+ exit('Copy .env.integration.example to .env.integration or define the environment variables.' . PHP_EOL);
+}
+$wpRootDir = str_replace('${ROOT_DIR}', ROOT_DIR, $_ENV['PPCP_INTEGRATION_WP_DIR']);
+
+define('WP_ROOT_DIR', $wpRootDir);
+
+$_SERVER['HTTP_HOST'] = ''; // just to avoid a warning
+
+require_once WP_ROOT_DIR . '/wp-load.php';
diff --git a/tests/e2e/PHPUnit/setup.php b/tests/integration/PHPUnit/setup.php
similarity index 93%
rename from tests/e2e/PHPUnit/setup.php
rename to tests/integration/PHPUnit/setup.php
index b727f83b8..042d41ca4 100644
--- a/tests/e2e/PHPUnit/setup.php
+++ b/tests/integration/PHPUnit/setup.php
@@ -32,6 +32,6 @@ require WP_ROOT_DIR . '/wp-admin/includes/class-wp-importer.php';
require WP_ROOT_DIR . '/wp-content/plugins/woocommerce/includes/admin/importers/class-wc-tax-rate-importer.php';
$taxImporter = new WC_Tax_Rate_Importer();
-$taxImporter->import(E2E_TESTS_ROOT_DIR . '/data/tax_rates.csv');
+$taxImporter->import(INTEGRATION_TESTS_ROOT_DIR . '/data/tax_rates.csv');
echo PHP_EOL;
diff --git a/tests/e2e/data/tax_rates.csv b/tests/integration/data/tax_rates.csv
similarity index 100%
rename from tests/e2e/data/tax_rates.csv
rename to tests/integration/data/tax_rates.csv
diff --git a/tests/e2e/phpunit.xml.dist b/tests/integration/phpunit.xml.dist
similarity index 100%
rename from tests/e2e/phpunit.xml.dist
rename to tests/integration/phpunit.xml.dist
diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php
index d9e1a2277..66a498798 100644
--- a/woocommerce-paypal-payments.php
+++ b/woocommerce-paypal-payments.php
@@ -3,15 +3,15 @@
* Plugin Name: WooCommerce PayPal Payments
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
- * Version: 2.9.6
- * Author: WooCommerce
- * Author URI: https://woocommerce.com/
+ * Version: 3.0.0
+ * Author: PayPal
+ * Author URI: https://paypal.com/
* License: GPL-2.0
* Requires PHP: 7.4
* Requires Plugins: woocommerce
* Requires at least: 6.5
- * WC requires at least: 9.2
- * WC tested up to: 9.5
+ * WC requires at least: 9.6
+ * WC tested up to: 9.7
* Text Domain: woocommerce-paypal-payments
*
* @package WooCommerce\PayPalCommerce
@@ -27,7 +27,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
define( 'PAYPAL_URL', 'https://www.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' );
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
-define( 'PAYPAL_INTEGRATION_DATE', '2024-12-31' );
+define( 'PAYPAL_INTEGRATION_DATE', '2025-03-13' );
define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );
@@ -229,4 +229,21 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
return class_exists( 'woocommerce' );
}
+ add_action(
+ 'woocommerce_paypal_payments_gateway_migrate',
+ /**
+ * Set new merchant flag on plugin install.
+ *
+ * When installing the plugin for the first time, we direct the user to
+ * the new UI without a data migration, and fully hide the legacy UI.
+ *
+ * @param string|false $version String with previous installed plugin version.
+ * Boolean false on first installation on a new site.
+ */
+ static function ( $version ) {
+ if ( ! $version ) {
+ update_option( 'woocommerce-ppcp-is-new-merchant', '1' );
+ }
+ }
+ );
} )();