diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 22663d605..045c234cb 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -1018,6 +1018,10 @@ class ApplePayButton implements ButtonInterface { * @return void */ public function enqueue(): void { + if ( ! $this->is_enabled() ) { + return; + } + wp_register_script( 'wc-ppcp-applepay', untrailingslashit( $this->module_url ) . '/assets/js/boot.js', diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss index 9ae5ee92e..72633ce3d 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss @@ -93,6 +93,29 @@ } } +.ppcp-r-payment-methods { + .ppcp-highlight { + animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid $color-blueberry; + border-radius: var(--container-border-radius); + position: relative; + z-index: 1; + } + + @keyframes ppcp-highlight-fade { + 0%, 20% { + background-color: rgba($color-blueberry, 0.08); + border-color: $color-blueberry; + border-width: 1px; + } + 100% { + background-color: transparent; + border-color: $color-gray-300; + border-width: 1px; + } + } +} + // Disabled state styling. .ppcp--method-item--disabled { position: relative; diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 901009e6c..63952ed1f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -226,25 +226,91 @@ } } -// Payment Methods +.ppcp-r-settings { + .ppcp-highlight { + position: relative; + z-index: 1; -.ppcp-highlight { - animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1); - border: 1px solid $color-blueberry; - border-radius: var(--container-border-radius); - position: relative; - z-index: 1; -} + &::before { + content: ''; + position: absolute; + top: -8px; + left: -12px; + right: -12px; + bottom: -8px; + border: 1px solid $color-blueberry; + border-radius: 4px; + z-index: -1; + pointer-events: none; + animation: ppcp-setting-highlight-bg 2s cubic-bezier(0.4, 0, 0.2, 1); + animation-fill-mode: forwards; + } -@keyframes ppcp-highlight-fade { - 0%, 20% { - background-color: rgba($color-blueberry, 0.08); - border-color: $color-blueberry; - border-width: 1px; + &::after { + content: ''; + position: absolute; + top: -8px; + left: -12px; + width: 4px; + bottom: -8px; + background-color: $color-blueberry; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + z-index: -1; + pointer-events: none; + animation: ppcp-setting-highlight-accent 2s cubic-bezier(0.4, 0, 0.2, 1); + animation-fill-mode: forwards; + } } - 100% { - background-color: transparent; - border-color: $color-gray-300; - border-width: 1px; + + @keyframes ppcp-setting-highlight-bg { + 0%, 15% { + background-color: rgba($color-blueberry, 0.08); + border-color: $color-blueberry; + } + 70% { + background-color: transparent; + border-color: transparent; + } + 100% { + background-color: transparent; + border-color: transparent; + } + } + + @keyframes ppcp-setting-highlight-accent { + 0%, 15% { + opacity: 1; + } + 70% { + opacity: 0; + } + 100% { + opacity: 0; + } + } + + .ppcp-r-settings-section { + .ppcp--setting-row { + position: relative; + padding: 12px; + margin: 0 -12px; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba($color-gray-100, 0.5); + } + } + } + + // RTL support + html[dir="rtl"] { + .ppcp-highlight { + &::after { + left: auto; + right: -12px; + border-radius: 0 4px 4px 0; + } + } } } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js index cbef51f5d..8c07544d9 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js @@ -2,13 +2,14 @@ import { ToggleControl } from '@wordpress/components'; import { Action, Description } from '../Elements'; const ControlToggleButton = ( { + id = '', label, description, value, onChange, disabled = false, } ) => ( - + ( -
{ children }
+const Action = ( { id, children } ) => ( +
+ { children } +
); export default Action; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js index 6855a2738..2a2b52b41 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js @@ -1,7 +1,5 @@ import { ToggleControl, Icon, Button } from '@wordpress/components'; import { cog } from '@wordpress/icons'; -import { useEffect } from '@wordpress/element'; -import { useActiveHighlight } from '../../../data/common/hooks'; import SettingsBlock from '../SettingsBlock'; import PaymentMethodIcon from '../PaymentMethodIcon'; @@ -16,26 +14,12 @@ const PaymentMethodItemBlock = ( { disabledMessage, warningMessages, } ) => { - const { activeHighlight, setActiveHighlight } = useActiveHighlight(); - const isHighlighted = activeHighlight === paymentMethod.id; const hasWarning = warningMessages && Object.keys( warningMessages ).length > 0; - // Reset the active highlight after 2 seconds - useEffect( () => { - if ( isHighlighted ) { - const timer = setTimeout( () => { - setActiveHighlight( null ); - }, 2000 ); - - return () => clearTimeout( timer ); - } - }, [ isHighlighted, setActiveHighlight ] ); - // Determine class names based on states const methodItemClasses = [ 'ppcp--method-item', - isHighlighted ? 'ppcp-highlight' : '', isDisabled ? 'ppcp--method-item--disabled' : '', hasWarning && ! isDisabled ? 'ppcp--method-item--warning' : '', ] diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js index 1716891fb..aa6773ff0 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js @@ -7,7 +7,6 @@ const TodoSettingsBlock = ( { todosData, className = '', setActiveModal, - setActiveHighlight, onDismissTodo, } ) => { const [ dismissingIds, setDismissingIds ] = useState( new Set() ); @@ -58,9 +57,6 @@ const TodoSettingsBlock = ( { if ( todo.action.modal ) { setActiveModal( todo.action.modal ); } - if ( todo.action.highlight ) { - setActiveHighlight( todo.action.highlight ); - } }; // Filter out dismissed todos for display and limit to 5. 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 index e7ff5fb4b..6be06ab6b 100644 --- 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 @@ -15,8 +15,7 @@ const Todos = () => { const [ isResetting, setIsResetting ] = useState( false ); const { todos, isReady: areTodosReady, dismissTodo } = useTodos(); // eslint-disable-next-line no-shadow - const { setActiveModal, setActiveHighlight } = - useDispatch( COMMON_STORE_NAME ); + const { setActiveModal } = useDispatch( COMMON_STORE_NAME ); const { resetDismissedTodos, setDismissedTodos } = useDispatch( TODOS_STORE_NAME ); const { createSuccessNotice } = useDispatch( noticesStore ); @@ -76,7 +75,6 @@ const Todos = () => { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/DependencyMessage.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentDependencyMessage.js similarity index 82% rename from modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/DependencyMessage.js rename to modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentDependencyMessage.js index f32d90f80..ef835569b 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/DependencyMessage.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentDependencyMessage.js @@ -10,8 +10,9 @@ import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight'; * @param {string} props.parentName - Display name of the parent payment method * @return {JSX.Element} The formatted message with link */ -const DependencyMessage = ( { parentId, parentName } ) => { - // Using WordPress createInterpolateElement with proper React elements +const PaymentDependencyMessage = ( { parentId, parentName } ) => { + const displayName = parentName || parentId; + return createInterpolateElement( /* translators: %s: payment method name */ __( @@ -28,7 +29,7 @@ const DependencyMessage = ( { parentId, parentName } ) => { scrollAndHighlight( parentId ); } } > - { parentName } + { displayName } ), @@ -36,4 +37,4 @@ const DependencyMessage = ( { parentId, parentName } ) => { ); }; -export default DependencyMessage; +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 index d7689f950..b385ad2d4 100644 --- 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 @@ -1,7 +1,11 @@ import SettingsCard from '../../../../ReusableComponents/SettingsCard'; import { PaymentMethodsBlock } from '../../../../ReusableComponents/SettingsBlocks'; import usePaymentDependencyState from '../../../../../hooks/usePaymentDependencyState'; -import DependencyMessage from './DependencyMessage'; +import useSettingDependencyState from '../../../../../hooks/useSettingDependencyState'; +import PaymentDependencyMessage from './PaymentDependencyMessage'; +import SettingDependencyMessage from './SettingDependencyMessage'; +import SpinnerOverlay from '../../../../ReusableComponents/SpinnerOverlay'; +import { PaymentHooks, SettingsHooks } from '../../../../../data'; /** * Renders a payment method card with dependency handling @@ -27,9 +31,20 @@ const PaymentMethodCard = ( { methodsMap = {}, onTriggerModal, isDisabled = false, - disabledMessage, } ) => { - const dependencyState = usePaymentDependencyState( methods, methodsMap ); + const { isReady: isPaymentStoreReady } = PaymentHooks.useStore(); + const { isReady: isSettingsStoreReady } = SettingsHooks.useStore(); + + const paymentDependencies = usePaymentDependencyState( + methods, + methodsMap + ); + + const settingDependencies = useSettingDependencyState( methods ); + + if ( ! isPaymentStoreReady || ! isSettingsStoreReady ) { + return ; + } return ( { - const dependency = dependencyState[ method.id ]; + const paymentDependency = + paymentDependencies?.[ method.id ]; + const settingDependency = + settingDependencies?.[ method.id ]; - const dependencyMessage = dependency ? ( - - ) : null; + let dependencyMessage = null; + let isMethodDisabled = method.isDisabled || isDisabled; + + if ( paymentDependency ) { + dependencyMessage = ( + + ); + isMethodDisabled = true; + } else if ( settingDependency?.isDisabled ) { + dependencyMessage = ( + + ); + isMethodDisabled = true; + } return { ...method, - isDisabled: - method.isDisabled || - isDisabled || - Boolean( dependency?.isDisabled ), - disabledMessage: - method.disabledMessage || - dependencyMessage || - disabledMessage, + isDisabled: isMethodDisabled, + disabledMessage: dependencyMessage, }; } ) } onTriggerModal={ onTriggerModal } 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/Settings/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js index e1e15670a..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 @@ -28,18 +28,18 @@ const SavePaymentMethods = () => { 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={ features.save_paypal_and_venmo.enabled diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index fbbe022de..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. * diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 506881345..cb6dc9974 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -49,8 +49,6 @@ const useHooks = () => { // Transient accessors. const [ activeModal, setActiveModal ] = useTransient( 'activeModal' ); - const [ activeHighlight, setActiveHighlight ] = - useTransient( 'activeHighlight' ); // Persistent accessors. const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent( @@ -70,8 +68,6 @@ const useHooks = () => { return { activeModal, setActiveModal, - activeHighlight, - setActiveHighlight, isManualConnectionMode, setManualConnectionMode: ( state ) => { return savePersistent( setManualConnectionMode, state ); @@ -227,11 +223,6 @@ export const useActiveModal = () => { return { activeModal, setActiveModal }; }; -export const useActiveHighlight = () => { - const { activeHighlight, setActiveHighlight } = useHooks(); - return { activeHighlight, setActiveHighlight }; -}; - /* * Busy state management hooks */ diff --git a/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js b/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js index e3bf06cec..3fb087c1f 100644 --- a/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js +++ b/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js @@ -1,3 +1,6 @@ +/** + * Custom hook to handle payment-method-based dependencies + */ import { useSelect } from '@wordpress/data'; /** @@ -22,60 +25,54 @@ const getParentMethodName = ( parentId, methodsMap ) => { * @return {Array} List of disabled parent IDs, empty if none */ const findDisabledParents = ( method, methodsMap ) => { - if ( ! method.depends_on?.length && ! method._disabledByDependency ) { + const dependencies = method.depends_on_payment_methods; + + if ( ! dependencies || ! Array.isArray( dependencies ) ) { return []; } - const parents = method.depends_on || []; - - return parents.filter( ( parentId ) => { + return dependencies.filter( ( parentId ) => { const parent = methodsMap[ parentId ]; return parent && ! parent.enabled; } ); }; /** - * Custom hook to handle payment method dependencies + * 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 with methods that should be disabled + * @return {Object} Dependency state object keyed by method ID */ const usePaymentDependencyState = ( methods, methodsMap ) => { - return useSelect( - ( select ) => { - const paymentStore = select( 'wc/paypal/payment' ); - if ( ! paymentStore ) { - return {}; - } - - const result = {}; + return useSelect( () => { + const result = {}; + if ( methods && methodsMap && Object.keys( methodsMap ).length > 0 ) { methods.forEach( ( method ) => { - const disabledParents = findDisabledParents( - method, - methodsMap - ); - - if ( disabledParents.length > 0 ) { - const parentId = disabledParents[ 0 ]; - const parentName = getParentMethodName( - parentId, + if ( method && method.id ) { + const disabledParents = findDisabledParents( + method, methodsMap ); - result[ method.id ] = { - isDisabled: true, - parentId, - parentName, - }; + if ( disabledParents.length > 0 ) { + const parentId = disabledParents[ 0 ]; + result[ method.id ] = { + isDisabled: true, + parentId, + parentName: getParentMethodName( + parentId, + methodsMap + ), + }; + } } } ); + } - return result; - }, - [ methods, methodsMap ] - ); + return result; + }, [ methods, methodsMap ] ); }; export default usePaymentDependencyState; 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/services.php b/modules/ppcp-settings/services.php index fa32bdebf..17009fd1e 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -181,7 +181,8 @@ return array( '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.methods' ), + $container->get( 'settings.data.definition.method_dependencies' ) ); }, 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint { @@ -380,12 +381,11 @@ return array( return new PaymentMethodsDefinition( $container->get( 'settings.data.payment' ), - $container->get( 'settings.data.definition.method_dependencies' ), $axo_notices ); }, 'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition { - return new 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' ); diff --git a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php index c9d08c0a9..02d552a54 100644 --- a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php +++ b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDefinition.php @@ -40,13 +40,6 @@ class PaymentMethodsDefinition { */ private PaymentSettings $settings; - /** - * Payment method dependencies definition. - * - * @var PaymentMethodsDependenciesDefinition - */ - private PaymentMethodsDependenciesDefinition $dependencies_definition; - /** * Conflict notices for Axo gateway. * @@ -64,18 +57,15 @@ class PaymentMethodsDefinition { /** * Constructor. * - * @param PaymentSettings $settings Payment methods data model. - * @param PaymentMethodsDependenciesDefinition $dependencies_definition Payment dependencies definition. - * @param array $axo_conflicts_notices Conflicts notices for Axo. + * @param PaymentSettings $settings Payment methods data model. + * @param array $axo_conflicts_notices Conflicts notices for Axo. */ public function __construct( PaymentSettings $settings, - PaymentMethodsDependenciesDefinition $dependencies_definition, array $axo_conflicts_notices = array() ) { - $this->settings = $settings; - $this->dependencies_definition = $dependencies_definition; - $this->axo_conflicts_notices = $axo_conflicts_notices; + $this->settings = $settings; + $this->axo_conflicts_notices = $axo_conflicts_notices; } /** @@ -94,26 +84,16 @@ class PaymentMethodsDefinition { $result = array(); foreach ( $all_methods as $method ) { $method_id = $method['id']; - // Add dependency info if applicable. - $depends_on = $this->dependencies_definition->get_parent_methods( $method_id ); - if ( ! empty( $depends_on ) ) { - $method['depends_on'] = $depends_on; - } + $result[ $method_id ] = $this->build_method_definition( $method_id, $method['title'], $method['description'], $method['icon'], $method['fields'] ?? array(), - $depends_on, - $method['warningMessages'] ?? array() + $method['warningMessages'] ?? array(), ); } - // Add dependency maps to metadata. - $result['__meta'] = array( - 'dependencies' => $this->dependencies_definition->get_dependencies(), - 'dependents' => $this->dependencies_definition->get_dependents_map(), - ); return $result; } @@ -121,14 +101,13 @@ class PaymentMethodsDefinition { * 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 $depends_on Optional. IDs of payment methods that this depends on. - * @param array $warning_messages Optional. Warning messages to display in the UI. + * @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( @@ -136,8 +115,7 @@ class PaymentMethodsDefinition { string $title, string $description, string $icon, - $fields = array(), - array $depends_on = array(), + $fields = array(), array $warning_messages = array() ) : array { $gateway = $this->wc_gateways[ $gateway_id ] ?? null; @@ -156,11 +134,6 @@ class PaymentMethodsDefinition { 'warningMessages' => $warning_messages, ); - // Add dependency information if provided - ensure it's included directly in the config. - if ( ! empty( $depends_on ) ) { - $config['depends_on'] = $depends_on; - } - if ( is_array( $fields ) ) { $config['fields'] = array_merge( array( diff --git a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php index 38b43e16f..9c7771be9 100644 --- a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php +++ b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php @@ -9,6 +9,8 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Data\Definition; +use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; @@ -29,19 +31,35 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGa /** * Class PaymentMethodsDependenciesDefinition * - * Defines dependency relationships between payment methods. + * Defines dependency relationships between payment methods and settings. */ class PaymentMethodsDependenciesDefinition { /** - * Get all payment method dependencies + * Current settings values + * + * @var Settings + */ + private Settings $settings; + + /** + * Constructor + * + * @param Settings $settings Settings instance. + */ + public function __construct( Settings $settings ) { + $this->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_dependencies(): array { + public function get_payment_method_dependencies(): array { $dependencies = array( CardButtonGateway::ID => array( PayPalGateway::ID ), CreditCardGateway::ID => array( PayPalGateway::ID ), @@ -69,43 +87,114 @@ class PaymentMethodsDependenciesDefinition { } /** - * Create a mapping from parent methods to their dependent methods + * Get setting to payment method dependencies. * - * @return array Parent-to-child dependency map + * 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_dependents_map(): array { - $result = array(); - $dependencies = $this->get_dependencies(); + public function get_setting_dependencies(): array { + $dependencies = array( + 'pay-later' => array( + 'savePaypalAndVenmo' => false, + ), + ); - foreach ( $dependencies as $child_id => $parent_ids ) { - foreach ( $parent_ids as $parent_id ) { - if ( ! isset( $result[ $parent_id ] ) ) { - $result[ $parent_id ] = array(); - } - $result[ $parent_id ][] = $child_id; - } - } - - return $result; + return apply_filters( + 'woocommerce_paypal_payments_setting_dependencies', + $dependencies + ); } /** - * Get all parent methods that a method depends on + * 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_parent_methods( string $method_id ): array { - return $this->get_dependencies()[ $method_id ] ?? array(); + public function get_method_payment_method_dependencies( string $method_id ): array { + return $this->get_payment_method_dependencies()[ $method_id ] ?? array(); } /** - * Get methods that depend on a parent method + * Get all settings that affect payment methods * - * @param string $parent_id Parent method ID. - * @return array Array of dependent method IDs + * @return array Array of unique setting keys that affect payment methods */ - public function get_dependent_methods( string $parent_id ): array { - return $this->get_dependents_map()[ $parent_id ] ?? array(); + 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/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index f2d813065..428e05c9a 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -30,6 +30,7 @@ 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. @@ -50,14 +51,21 @@ class PaymentRestEndpoint extends RestEndpoint { * * @var PaymentSettings */ - protected PaymentSettings $settings; + protected PaymentSettings $payment_settings; /** * The payment method details. * * @var PaymentMethodsDefinition */ - protected PaymentMethodsDefinition $methods_definition; + protected PaymentMethodsDefinition $payment_methods_definition; + + /** + * The payment method dependencies. + * + * @var PaymentMethodsDependenciesDefinition + */ + protected PaymentMethodsDependenciesDefinition $payment_methods_dependencies; /** * Field mapping for request to profile transformation. @@ -86,12 +94,18 @@ class PaymentRestEndpoint extends RestEndpoint { /** * Constructor. * - * @param PaymentSettings $settings The settings instance. - * @param PaymentMethodsDefinition $methods_definition Payment Method details. + * @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, PaymentMethodsDefinition $methods_definition ) { - $this->settings = $settings; - $this->methods_definition = $methods_definition; + 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; } /** @@ -100,7 +114,10 @@ class PaymentRestEndpoint extends RestEndpoint { * @return array[] */ protected function gateways() : array { - return $this->methods_definition->get_definitions(); + $methods = $this->payment_methods_definition->get_definitions(); + + // Add dependency information to the methods. + return $this->payment_methods_dependencies->add_dependency_info_to_methods( $methods ); } /** @@ -147,45 +164,48 @@ class PaymentRestEndpoint extends RestEndpoint { * @return WP_REST_Response The current payment methods details. */ public function get_details() : WP_REST_Response { - $gateway_settings = array(); - $all_methods = $this->gateways(); + $gateway_settings = array(); + $all_payment_methods = $this->gateways(); // First extract __meta if present. - if ( isset( $all_methods['__meta'] ) ) { - $gateway_settings['__meta'] = $all_methods['__meta']; + if ( isset( $all_payment_methods['__meta'] ) ) { + $gateway_settings['__meta'] = $all_payment_methods['__meta']; } - foreach ( $all_methods as $key => $method ) { + foreach ( $all_payment_methods as $key => $payment_method ) { // Skip the __meta key as we've already handled it. if ( $key === '__meta' ) { continue; } $gateway_settings[ $key ] = array( - 'id' => $method['id'], - 'title' => $method['title'], - 'description' => $method['description'], - 'enabled' => $method['enabled'], - 'icon' => $method['icon'], - 'itemTitle' => $method['itemTitle'], - 'itemDescription' => $method['itemDescription'], - 'warningMessages' => $method['warningMessages'], + '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( $method['fields'] ) ) { - $gateway_settings[ $key ]['fields'] = $method['fields']; + if ( isset( $payment_method['fields'] ) ) { + $gateway_settings[ $key ]['fields'] = $payment_method['fields']; } - // Preserve dependency information. - if ( isset( $method['depends_on'] ) ) { - $gateway_settings[ $key ]['depends_on'] = $method['depends_on']; + 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 ) ); } @@ -202,21 +222,21 @@ class PaymentRestEndpoint extends RestEndpoint { $all_methods = $this->gateways(); foreach ( $all_methods as $key => $value ) { - $new_data = $request_data[ $key ]; + $new_data = $request_data[ $key ] ?? null; if ( ! $new_data ) { continue; } if ( isset( $new_data['enabled'] ) ) { - $this->settings->toggle_method_state( $key, $new_data['enabled'] ); + $this->payment_settings->toggle_method_state( $key, $new_data['enabled'] ); } if ( isset( $new_data['title'] ) ) { - $this->settings->set_method_title( $key, sanitize_text_field( $new_data['title'] ) ); + $this->payment_settings->set_method_title( $key, sanitize_text_field( $new_data['title'] ) ); } if ( isset( $new_data['description'] ) ) { - $this->settings->set_method_description( $key, wp_kses_post( $new_data['description'] ) ); + $this->payment_settings->set_method_description( $key, wp_kses_post( $new_data['description'] ) ); } } @@ -225,8 +245,8 @@ 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(); }