🔀 Merge branch 'trunk’

# Conflicts:
#	modules/ppcp-settings/resources/js/data/common/hooks.js
This commit is contained in:
Philipp Stracker 2025-03-05 17:01:52 +01:00
commit 8ec852952f
No known key found for this signature in database
20 changed files with 567 additions and 225 deletions

View file

@ -1018,6 +1018,10 @@ class ApplePayButton implements ButtonInterface {
* @return void * @return void
*/ */
public function enqueue(): void { public function enqueue(): void {
if ( ! $this->is_enabled() ) {
return;
}
wp_register_script( wp_register_script(
'wc-ppcp-applepay', 'wc-ppcp-applepay',
untrailingslashit( $this->module_url ) . '/assets/js/boot.js', untrailingslashit( $this->module_url ) . '/assets/js/boot.js',

View file

@ -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. // Disabled state styling.
.ppcp--method-item--disabled { .ppcp--method-item--disabled {
position: relative; position: relative;

View file

@ -226,25 +226,91 @@
} }
} }
// Payment Methods .ppcp-r-settings {
.ppcp-highlight {
position: relative;
z-index: 1;
.ppcp-highlight { &::before {
animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1); content: '';
border: 1px solid $color-blueberry; position: absolute;
border-radius: var(--container-border-radius); top: -8px;
position: relative; left: -12px;
z-index: 1; 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 { &::after {
0%, 20% { content: '';
background-color: rgba($color-blueberry, 0.08); position: absolute;
border-color: $color-blueberry; top: -8px;
border-width: 1px; 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; @keyframes ppcp-setting-highlight-bg {
border-color: $color-gray-300; 0%, 15% {
border-width: 1px; 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;
}
}
} }
} }

View file

@ -2,13 +2,14 @@ import { ToggleControl } from '@wordpress/components';
import { Action, Description } from '../Elements'; import { Action, Description } from '../Elements';
const ControlToggleButton = ( { const ControlToggleButton = ( {
id = '',
label, label,
description, description,
value, value,
onChange, onChange,
disabled = false, disabled = false,
} ) => ( } ) => (
<Action> <Action id={ id }>
<ToggleControl <ToggleControl
className="ppcp--control-toggle" className="ppcp--control-toggle"
__nextHasNoMarginBottom __nextHasNoMarginBottom

View file

@ -1,5 +1,7 @@
const Action = ( { children } ) => ( const Action = ( { id, children } ) => (
<div className="ppcp--action">{ children }</div> <div className="ppcp--action" { ...( id ? { id } : {} ) }>
{ children }
</div>
); );
export default Action; export default Action;

View file

@ -1,7 +1,5 @@
import { ToggleControl, Icon, Button } from '@wordpress/components'; import { ToggleControl, Icon, Button } from '@wordpress/components';
import { cog } from '@wordpress/icons'; import { cog } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
import { useActiveHighlight } from '../../../data/common/hooks';
import SettingsBlock from '../SettingsBlock'; import SettingsBlock from '../SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon'; import PaymentMethodIcon from '../PaymentMethodIcon';
@ -16,26 +14,12 @@ const PaymentMethodItemBlock = ( {
disabledMessage, disabledMessage,
warningMessages, warningMessages,
} ) => { } ) => {
const { activeHighlight, setActiveHighlight } = useActiveHighlight();
const isHighlighted = activeHighlight === paymentMethod.id;
const hasWarning = const hasWarning =
warningMessages && Object.keys( warningMessages ).length > 0; 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 // Determine class names based on states
const methodItemClasses = [ const methodItemClasses = [
'ppcp--method-item', 'ppcp--method-item',
isHighlighted ? 'ppcp-highlight' : '',
isDisabled ? 'ppcp--method-item--disabled' : '', isDisabled ? 'ppcp--method-item--disabled' : '',
hasWarning && ! isDisabled ? 'ppcp--method-item--warning' : '', hasWarning && ! isDisabled ? 'ppcp--method-item--warning' : '',
] ]

View file

@ -7,7 +7,6 @@ const TodoSettingsBlock = ( {
todosData, todosData,
className = '', className = '',
setActiveModal, setActiveModal,
setActiveHighlight,
onDismissTodo, onDismissTodo,
} ) => { } ) => {
const [ dismissingIds, setDismissingIds ] = useState( new Set() ); const [ dismissingIds, setDismissingIds ] = useState( new Set() );
@ -58,9 +57,6 @@ const TodoSettingsBlock = ( {
if ( todo.action.modal ) { if ( todo.action.modal ) {
setActiveModal( 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. // Filter out dismissed todos for display and limit to 5.

View file

@ -15,8 +15,7 @@ const Todos = () => {
const [ isResetting, setIsResetting ] = useState( false ); const [ isResetting, setIsResetting ] = useState( false );
const { todos, isReady: areTodosReady, dismissTodo } = useTodos(); const { todos, isReady: areTodosReady, dismissTodo } = useTodos();
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
const { setActiveModal, setActiveHighlight } = const { setActiveModal } = useDispatch( COMMON_STORE_NAME );
useDispatch( COMMON_STORE_NAME );
const { resetDismissedTodos, setDismissedTodos } = const { resetDismissedTodos, setDismissedTodos } =
useDispatch( TODOS_STORE_NAME ); useDispatch( TODOS_STORE_NAME );
const { createSuccessNotice } = useDispatch( noticesStore ); const { createSuccessNotice } = useDispatch( noticesStore );
@ -76,7 +75,6 @@ const Todos = () => {
<TodoSettingsBlock <TodoSettingsBlock
todosData={ todos } todosData={ todos }
setActiveModal={ setActiveModal } setActiveModal={ setActiveModal }
setActiveHighlight={ setActiveHighlight }
onDismissTodo={ dismissTodo } onDismissTodo={ dismissTodo }
/> />
</SettingsCard> </SettingsCard>

View file

@ -10,8 +10,9 @@ import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight';
* @param {string} props.parentName - Display name of the parent payment method * @param {string} props.parentName - Display name of the parent payment method
* @return {JSX.Element} The formatted message with link * @return {JSX.Element} The formatted message with link
*/ */
const DependencyMessage = ( { parentId, parentName } ) => { const PaymentDependencyMessage = ( { parentId, parentName } ) => {
// Using WordPress createInterpolateElement with proper React elements const displayName = parentName || parentId;
return createInterpolateElement( return createInterpolateElement(
/* translators: %s: payment method name */ /* translators: %s: payment method name */
__( __(
@ -28,7 +29,7 @@ const DependencyMessage = ( { parentId, parentName } ) => {
scrollAndHighlight( parentId ); scrollAndHighlight( parentId );
} } } }
> >
{ parentName } { displayName }
</a> </a>
</strong> </strong>
), ),
@ -36,4 +37,4 @@ const DependencyMessage = ( { parentId, parentName } ) => {
); );
}; };
export default DependencyMessage; export default PaymentDependencyMessage;

View file

@ -1,7 +1,11 @@
import SettingsCard from '../../../../ReusableComponents/SettingsCard'; import SettingsCard from '../../../../ReusableComponents/SettingsCard';
import { PaymentMethodsBlock } from '../../../../ReusableComponents/SettingsBlocks'; import { PaymentMethodsBlock } from '../../../../ReusableComponents/SettingsBlocks';
import usePaymentDependencyState from '../../../../../hooks/usePaymentDependencyState'; 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 * Renders a payment method card with dependency handling
@ -27,9 +31,20 @@ const PaymentMethodCard = ( {
methodsMap = {}, methodsMap = {},
onTriggerModal, onTriggerModal,
isDisabled = false, 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 <SpinnerOverlay asModal={ true } />;
}
return ( return (
<SettingsCard <SettingsCard
@ -41,25 +56,39 @@ const PaymentMethodCard = ( {
> >
<PaymentMethodsBlock <PaymentMethodsBlock
paymentMethods={ methods.map( ( method ) => { paymentMethods={ methods.map( ( method ) => {
const dependency = dependencyState[ method.id ]; const paymentDependency =
paymentDependencies?.[ method.id ];
const settingDependency =
settingDependencies?.[ method.id ];
const dependencyMessage = dependency ? ( let dependencyMessage = null;
<DependencyMessage let isMethodDisabled = method.isDisabled || isDisabled;
parentId={ dependency.parentId }
parentName={ dependency.parentName } if ( paymentDependency ) {
/> dependencyMessage = (
) : null; <PaymentDependencyMessage
parentId={ paymentDependency.parentId }
parentName={ paymentDependency.parentName }
/>
);
isMethodDisabled = true;
} else if ( settingDependency?.isDisabled ) {
dependencyMessage = (
<SettingDependencyMessage
settingId={ settingDependency.settingId }
requiredValue={
settingDependency.requiredValue
}
methodId={ method.id }
/>
);
isMethodDisabled = true;
}
return { return {
...method, ...method,
isDisabled: isDisabled: isMethodDisabled,
method.isDisabled || disabledMessage: dependencyMessage,
isDisabled ||
Boolean( dependency?.isDisabled ),
disabledMessage:
method.disabledMessage ||
dependencyMessage ||
disabledMessage,
}; };
} ) } } ) }
onTriggerModal={ onTriggerModal } onTriggerModal={ onTriggerModal }

View file

@ -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 } ) => (
<strong>
<a
href="#"
onClick={ ( e ) => {
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 }
</a>
</strong>
);
/**
* 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 = (
<SettingLink settingName={ settingName } sectionId={ settingId } />
);
const templates = {
true: __(
'This payment method requires <settingLink /> to be enabled.',
'woocommerce-paypal-payments'
),
false: __(
'This payment method requires <settingLink /> 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 <settingLink /> to be set to "%s".',
'woocommerce-paypal-payments'
),
requiredValue
),
{ settingLink }
);
};
export default SettingDependencyMessage;

View file

@ -28,18 +28,18 @@ const SavePaymentMethods = () => {
className="ppcp--save-payment-methods" className="ppcp--save-payment-methods"
> >
<ControlToggleButton <ControlToggleButton
id="ppcp-save-paypal-and-venmo"
label={ __( label={ __(
'Save PayPal and Venmo', 'Save PayPal and Venmo',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
description={ sprintf( description={ sprintf(
/* translators: 1: URL to Pay Later documentation, 2: URL to Alternative Payment Methods documentation */ /* translators: 1: URL to Pay Later documentation */
__( __(
'Securely store your customers\' PayPal accounts for a seamless checkout experience. <br />This will disable all <a target="_blank" rel="noreferrer" href="%1$s">Pay Later</a> features and <a target="_blank" rel="noreferrer" href="%2$s">Alternative Payment Methods</a> on your site.', 'Securely store your customers\' PayPal accounts for a seamless checkout experience. <br />This will disable the <a target="_blank" rel="noreferrer" href="%1$s">Pay Later</a> payment method on your site.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later'
'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
) } ) }
value={ value={
features.save_paypal_and_venmo.enabled features.save_paypal_and_venmo.enabled

View file

@ -74,15 +74,6 @@ export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
export const setActiveModal = ( activeModal ) => export const setActiveModal = ( activeModal ) =>
setTransient( 'activeModal', 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. * Persistent. Sets the sandbox mode on or off.
* *

View file

@ -49,8 +49,6 @@ const useHooks = () => {
// Transient accessors. // Transient accessors.
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' ); const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
const [ activeHighlight, setActiveHighlight ] =
useTransient( 'activeHighlight' );
// Persistent accessors. // Persistent accessors.
const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent( const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
@ -70,8 +68,6 @@ const useHooks = () => {
return { return {
activeModal, activeModal,
setActiveModal, setActiveModal,
activeHighlight,
setActiveHighlight,
isManualConnectionMode, isManualConnectionMode,
setManualConnectionMode: ( state ) => { setManualConnectionMode: ( state ) => {
return savePersistent( setManualConnectionMode, state ); return savePersistent( setManualConnectionMode, state );
@ -227,11 +223,6 @@ export const useActiveModal = () => {
return { activeModal, setActiveModal }; return { activeModal, setActiveModal };
}; };
export const useActiveHighlight = () => {
const { activeHighlight, setActiveHighlight } = useHooks();
return { activeHighlight, setActiveHighlight };
};
/* /*
* Busy state management hooks * Busy state management hooks
*/ */

View file

@ -1,3 +1,6 @@
/**
* Custom hook to handle payment-method-based dependencies
*/
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
/** /**
@ -22,60 +25,54 @@ const getParentMethodName = ( parentId, methodsMap ) => {
* @return {Array} List of disabled parent IDs, empty if none * @return {Array} List of disabled parent IDs, empty if none
*/ */
const findDisabledParents = ( method, methodsMap ) => { const findDisabledParents = ( method, methodsMap ) => {
if ( ! method.depends_on?.length && ! method._disabledByDependency ) { const dependencies = method.depends_on_payment_methods;
if ( ! dependencies || ! Array.isArray( dependencies ) ) {
return []; return [];
} }
const parents = method.depends_on || []; return dependencies.filter( ( parentId ) => {
return parents.filter( ( parentId ) => {
const parent = methodsMap[ parentId ]; const parent = methodsMap[ parentId ];
return parent && ! parent.enabled; 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 {Array} methods - List of payment methods
* @param {Object} methodsMap - Map of payment methods by ID * @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 ) => { const usePaymentDependencyState = ( methods, methodsMap ) => {
return useSelect( return useSelect( () => {
( select ) => { const result = {};
const paymentStore = select( 'wc/paypal/payment' );
if ( ! paymentStore ) {
return {};
}
const result = {};
if ( methods && methodsMap && Object.keys( methodsMap ).length > 0 ) {
methods.forEach( ( method ) => { methods.forEach( ( method ) => {
const disabledParents = findDisabledParents( if ( method && method.id ) {
method, const disabledParents = findDisabledParents(
methodsMap method,
);
if ( disabledParents.length > 0 ) {
const parentId = disabledParents[ 0 ];
const parentName = getParentMethodName(
parentId,
methodsMap methodsMap
); );
result[ method.id ] = { if ( disabledParents.length > 0 ) {
isDisabled: true, const parentId = disabledParents[ 0 ];
parentId, result[ method.id ] = {
parentName, isDisabled: true,
}; parentId,
parentName: getParentMethodName(
parentId,
methodsMap
),
};
}
} }
} ); } );
}
return result; return result;
}, }, [ methods, methodsMap ] );
[ methods, methodsMap ]
);
}; };
export default usePaymentDependencyState; export default usePaymentDependencyState;

View file

@ -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;

View file

@ -181,7 +181,8 @@ return array(
'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
return new PaymentRestEndpoint( return new PaymentRestEndpoint(
$container->get( 'settings.data.payment' ), $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 { 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
@ -380,12 +381,11 @@ return array(
return new PaymentMethodsDefinition( return new PaymentMethodsDefinition(
$container->get( 'settings.data.payment' ), $container->get( 'settings.data.payment' ),
$container->get( 'settings.data.definition.method_dependencies' ),
$axo_notices $axo_notices
); );
}, },
'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition { '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 { 'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
$pay_later_endpoint = $container->get( 'settings.rest.pay_later_messaging' ); $pay_later_endpoint = $container->get( 'settings.rest.pay_later_messaging' );

View file

@ -40,13 +40,6 @@ class PaymentMethodsDefinition {
*/ */
private PaymentSettings $settings; private PaymentSettings $settings;
/**
* Payment method dependencies definition.
*
* @var PaymentMethodsDependenciesDefinition
*/
private PaymentMethodsDependenciesDefinition $dependencies_definition;
/** /**
* Conflict notices for Axo gateway. * Conflict notices for Axo gateway.
* *
@ -64,18 +57,15 @@ class PaymentMethodsDefinition {
/** /**
* Constructor. * Constructor.
* *
* @param PaymentSettings $settings Payment methods data model. * @param PaymentSettings $settings Payment methods data model.
* @param PaymentMethodsDependenciesDefinition $dependencies_definition Payment dependencies definition. * @param array $axo_conflicts_notices Conflicts notices for Axo.
* @param array $axo_conflicts_notices Conflicts notices for Axo.
*/ */
public function __construct( public function __construct(
PaymentSettings $settings, PaymentSettings $settings,
PaymentMethodsDependenciesDefinition $dependencies_definition,
array $axo_conflicts_notices = array() array $axo_conflicts_notices = array()
) { ) {
$this->settings = $settings; $this->settings = $settings;
$this->dependencies_definition = $dependencies_definition; $this->axo_conflicts_notices = $axo_conflicts_notices;
$this->axo_conflicts_notices = $axo_conflicts_notices;
} }
/** /**
@ -94,26 +84,16 @@ class PaymentMethodsDefinition {
$result = array(); $result = array();
foreach ( $all_methods as $method ) { foreach ( $all_methods as $method ) {
$method_id = $method['id']; $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( $result[ $method_id ] = $this->build_method_definition(
$method_id, $method_id,
$method['title'], $method['title'],
$method['description'], $method['description'],
$method['icon'], $method['icon'],
$method['fields'] ?? array(), $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; return $result;
} }
@ -121,14 +101,13 @@ class PaymentMethodsDefinition {
* Returns a new payment method configuration array that contains all * Returns a new payment method configuration array that contains all
* common attributes which must be present in every method definition. * common attributes which must be present in every method definition.
* *
* @param string $gateway_id The payment method ID. * @param string $gateway_id The payment method ID.
* @param string $title Admin-side payment method title. * @param string $title Admin-side payment method title.
* @param string $description Admin-side info about the payment method. * @param string $description Admin-side info about the payment method.
* @param string $icon Admin-side icon of 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. * @param array|false $fields Optional. Additional fields to display in the edit modal.
* Setting this to false omits all fields. * 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 array $warning_messages Optional. Warning messages to display in the UI.
* @return array Payment method definition. * @return array Payment method definition.
*/ */
private function build_method_definition( private function build_method_definition(
@ -136,8 +115,7 @@ class PaymentMethodsDefinition {
string $title, string $title,
string $description, string $description,
string $icon, string $icon,
$fields = array(), $fields = array(),
array $depends_on = array(),
array $warning_messages = array() array $warning_messages = array()
) : array { ) : array {
$gateway = $this->wc_gateways[ $gateway_id ] ?? null; $gateway = $this->wc_gateways[ $gateway_id ] ?? null;
@ -156,11 +134,6 @@ class PaymentMethodsDefinition {
'warningMessages' => $warning_messages, '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 ) ) { if ( is_array( $fields ) ) {
$config['fields'] = array_merge( $config['fields'] = array_merge(
array( array(

View file

@ -9,6 +9,8 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition; 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\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
@ -29,19 +31,35 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGa
/** /**
* Class PaymentMethodsDependenciesDefinition * Class PaymentMethodsDependenciesDefinition
* *
* Defines dependency relationships between payment methods. * Defines dependency relationships between payment methods and settings.
*/ */
class PaymentMethodsDependenciesDefinition { 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. * Maps dependent method ID => array of parent method IDs.
* A dependent method is disabled if ANY of its required parents is disabled. * A dependent method is disabled if ANY of its required parents is disabled.
* *
* @return array The dependency relationships between payment methods * @return array The dependency relationships between payment methods
*/ */
public function get_dependencies(): array { public function get_payment_method_dependencies(): array {
$dependencies = array( $dependencies = array(
CardButtonGateway::ID => array( PayPalGateway::ID ), CardButtonGateway::ID => array( PayPalGateway::ID ),
CreditCardGateway::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 { public function get_setting_dependencies(): array {
$result = array(); $dependencies = array(
$dependencies = $this->get_dependencies(); 'pay-later' => array(
'savePaypalAndVenmo' => false,
),
);
foreach ( $dependencies as $child_id => $parent_ids ) { return apply_filters(
foreach ( $parent_ids as $parent_id ) { 'woocommerce_paypal_payments_setting_dependencies',
if ( ! isset( $result[ $parent_id ] ) ) { $dependencies
$result[ $parent_id ] = array(); );
}
$result[ $parent_id ][] = $child_id;
}
}
return $result;
} }
/** /**
* 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. * @param string $method_id Method ID to check.
* @return array Array of parent method IDs * @return array Array of parent method IDs
*/ */
public function get_parent_methods( string $method_id ): array { public function get_method_payment_method_dependencies( string $method_id ): array {
return $this->get_dependencies()[ $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 unique setting keys that affect payment methods
* @return array Array of dependent method IDs
*/ */
public function get_dependent_methods( string $parent_id ): array { public function get_all_dependent_settings(): array {
return $this->get_dependents_map()[ $parent_id ] ?? 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;
} }
} }

View file

@ -30,6 +30,7 @@ use WP_REST_Request;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition; use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
/** /**
* REST controller for the "Payment Methods" settings tab. * REST controller for the "Payment Methods" settings tab.
@ -50,14 +51,21 @@ class PaymentRestEndpoint extends RestEndpoint {
* *
* @var PaymentSettings * @var PaymentSettings
*/ */
protected PaymentSettings $settings; protected PaymentSettings $payment_settings;
/** /**
* The payment method details. * The payment method details.
* *
* @var PaymentMethodsDefinition * @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. * Field mapping for request to profile transformation.
@ -86,12 +94,18 @@ class PaymentRestEndpoint extends RestEndpoint {
/** /**
* Constructor. * Constructor.
* *
* @param PaymentSettings $settings The settings instance. * @param PaymentSettings $payment_settings The settings instance.
* @param PaymentMethodsDefinition $methods_definition Payment Method details. * @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 ) { public function __construct(
$this->settings = $settings; PaymentSettings $payment_settings,
$this->methods_definition = $methods_definition; 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[] * @return array[]
*/ */
protected function gateways() : 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. * @return WP_REST_Response The current payment methods details.
*/ */
public function get_details() : WP_REST_Response { public function get_details() : WP_REST_Response {
$gateway_settings = array(); $gateway_settings = array();
$all_methods = $this->gateways(); $all_payment_methods = $this->gateways();
// First extract __meta if present. // First extract __meta if present.
if ( isset( $all_methods['__meta'] ) ) { if ( isset( $all_payment_methods['__meta'] ) ) {
$gateway_settings['__meta'] = $all_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. // Skip the __meta key as we've already handled it.
if ( $key === '__meta' ) { if ( $key === '__meta' ) {
continue; continue;
} }
$gateway_settings[ $key ] = array( $gateway_settings[ $key ] = array(
'id' => $method['id'], 'id' => $payment_method['id'],
'title' => $method['title'], 'title' => $payment_method['title'],
'description' => $method['description'], 'description' => $payment_method['description'],
'enabled' => $method['enabled'], 'enabled' => $payment_method['enabled'],
'icon' => $method['icon'], 'icon' => $payment_method['icon'],
'itemTitle' => $method['itemTitle'], 'itemTitle' => $payment_method['itemTitle'],
'itemDescription' => $method['itemDescription'], 'itemDescription' => $payment_method['itemDescription'],
'warningMessages' => $method['warningMessages'], 'warningMessages' => $payment_method['warningMessages'],
); );
if ( isset( $method['fields'] ) ) { if ( isset( $payment_method['fields'] ) ) {
$gateway_settings[ $key ]['fields'] = $method['fields']; $gateway_settings[ $key ]['fields'] = $payment_method['fields'];
} }
// Preserve dependency information. if ( isset( $payment_method['depends_on_payment_methods'] ) ) {
if ( isset( $method['depends_on'] ) ) { $gateway_settings[ $key ]['depends_on_payment_methods'] = $payment_method['depends_on_payment_methods'];
$gateway_settings[ $key ]['depends_on'] = $method['depends_on']; }
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['paypalShowLogo'] = $this->payment_settings->get_paypal_show_logo();
$gateway_settings['threeDSecure'] = $this->settings->get_three_d_secure(); $gateway_settings['threeDSecure'] = $this->payment_settings->get_three_d_secure();
$gateway_settings['fastlaneCardholderName'] = $this->settings->get_fastlane_cardholder_name(); $gateway_settings['fastlaneCardholderName'] = $this->payment_settings->get_fastlane_cardholder_name();
$gateway_settings['fastlaneDisplayWatermark'] = $this->settings->get_fastlane_display_watermark(); $gateway_settings['fastlaneDisplayWatermark'] = $this->payment_settings->get_fastlane_display_watermark();
return $this->return_success( apply_filters( 'woocommerce_paypal_payments_payment_methods', $gateway_settings ) ); 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(); $all_methods = $this->gateways();
foreach ( $all_methods as $key => $value ) { foreach ( $all_methods as $key => $value ) {
$new_data = $request_data[ $key ]; $new_data = $request_data[ $key ] ?? null;
if ( ! $new_data ) { if ( ! $new_data ) {
continue; continue;
} }
if ( isset( $new_data['enabled'] ) ) { 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'] ) ) { 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'] ) ) { 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->field_map
); );
$this->settings->from_array( $wp_data ); $this->payment_settings->from_array( $wp_data );
$this->settings->save(); $this->payment_settings->save();
return $this->get_details(); return $this->get_details();
} }