Add support for setting-based payment method dependencies

This commit is contained in:
Daniel Dudzic 2025-03-03 14:26:08 +01:00
parent 6fc9aad4ea
commit e8be67d286
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
19 changed files with 563 additions and 201 deletions

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.
.ppcp--method-item--disabled {
position: relative;

View file

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

View file

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

View file

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

View file

@ -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' : '',
]

View file

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

View file

@ -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 = () => {
<TodoSettingsBlock
todosData={ todos }
setActiveModal={ setActiveModal }
setActiveHighlight={ setActiveHighlight }
onDismissTodo={ dismissTodo }
/>
</SettingsCard>

View file

@ -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 }
</a>
</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 { 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 <SpinnerOverlay asModal={ true } />;
}
return (
<SettingsCard
@ -41,25 +56,39 @@ const PaymentMethodCard = ( {
>
<PaymentMethodsBlock
paymentMethods={ methods.map( ( method ) => {
const dependency = dependencyState[ method.id ];
const paymentDependency =
paymentDependencies?.[ method.id ];
const settingDependency =
settingDependencies?.[ method.id ];
const dependencyMessage = dependency ? (
<DependencyMessage
parentId={ dependency.parentId }
parentName={ dependency.parentName }
/>
) : null;
let dependencyMessage = null;
let isMethodDisabled = method.isDisabled || isDisabled;
if ( paymentDependency ) {
dependencyMessage = (
<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 {
...method,
isDisabled:
method.isDisabled ||
isDisabled ||
Boolean( dependency?.isDisabled ),
disabledMessage:
method.disabledMessage ||
dependencyMessage ||
disabledMessage,
isDisabled: isMethodDisabled,
disabledMessage: dependencyMessage,
};
} ) }
onTriggerModal={ onTriggerModal }

View file

@ -0,0 +1,114 @@
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;
// Create the SettingLink element once
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

@ -25,18 +25,18 @@ const SavePaymentMethods = () => {
className="ppcp--save-payment-methods"
>
<ControlToggleButton
id="ppcp-save-paypal-and-venmo"
label={ __(
'Save PayPal and Venmo',
'woocommerce-paypal-payments'
) }
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'
),
'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 }
onChange={ setSavePaypalAndVenmo }

View file

@ -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.
*

View file

@ -51,8 +51,6 @@ const useHooks = () => {
// Transient accessors.
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
const [ activeHighlight, setActiveHighlight ] =
useTransient( 'activeHighlight' );
// Persistent accessors.
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
@ -73,8 +71,6 @@ const useHooks = () => {
return {
activeModal,
setActiveModal,
activeHighlight,
setActiveHighlight,
isSandboxMode,
setSandboxMode: ( state ) => {
return savePersistent( setSandboxMode, state );
@ -226,11 +222,6 @@ export const useActiveModal = () => {
return { activeModal, setActiveModal };
};
export const useActiveHighlight = () => {
const { activeHighlight, setActiveHighlight } = useHooks();
return { activeHighlight, setActiveHighlight };
};
/*
* Busy state management hooks
*/

View file

@ -1,3 +1,6 @@
/**
* Custom hook to handle payment-method-based payment method dependencies
*/
import { useSelect } from '@wordpress/data';
/**
@ -22,36 +25,51 @@ 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.length ) {
return [];
}
const parents = method.depends_on || [];
return parents.filter( ( parentId ) => {
const disabledParents = dependencies.filter( ( parentId ) => {
const parent = methodsMap[ parentId ];
return parent && ! parent.enabled;
} );
return disabledParents;
};
/**
* Custom hook to handle payment method dependencies
* Hook to evaluate payment method dependencies for a set of methods
*
* @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|null} Dependency state object keyed by method ID, or null if not ready
*/
const usePaymentDependencyState = ( methods, methodsMap ) => {
return useSelect(
const dependencyState = useSelect(
( select ) => {
const paymentStore = select( 'wc/paypal/payment' );
if ( ! paymentStore ) {
return {};
return null;
}
// Check if payment methods data is available
if (
! methods ||
! methodsMap ||
Object.keys( methodsMap ).length === 0
) {
return null;
}
const result = {};
methods.forEach( ( method ) => {
if ( ! method || ! method.id ) {
return;
}
const disabledParents = findDisabledParents(
method,
methodsMap
@ -76,6 +94,8 @@ const usePaymentDependencyState = ( methods, methodsMap ) => {
},
[ methods, methodsMap ]
);
return dependencyState;
};
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

@ -173,7 +173,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 {
@ -367,12 +368,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' );

View file

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

View file

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

View file

@ -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();
}