From 6738f80efcb1d38c13b4c40f940363524888a403 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Wed, 21 May 2025 12:45:15 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=87=B2=F0=9F=87=BD=20Add=20Mexico-specifi?= =?UTF-8?q?c=20logic=20for=20the=20new=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-card-fields/services.php | 1 + .../services.php | 3 +- .../ppcp-settings/images/icon-button-oxxo.svg | 16 ++++ .../Onboarding/Components/PaymentFlow.js | 3 +- .../AlternativePaymentMethods.js | 21 +++-- .../Onboarding/Steps/StepPaymentMethods.js | 14 ++-- .../Screens/Onboarding/Steps/StepWelcome.js | 3 +- .../Onboarding/hooks/usePaymentConfig.js | 16 +++- .../Components/Overview/Features/Features.js | 15 +++- .../Components/Payment/PaymentMethodCard.js | 44 ++++------ .../PaymentMethodValueDependencyMessage.js | 49 +++++++++++ .../Settings/Tabs/TabPaymentMethods.js | 27 +++++- .../resources/js/data/onboarding/hooks.js | 7 +- .../resources/js/data/onboarding/selectors.js | 9 +- .../js/hooks/useDependencyMessages.js | 83 +++++++++++++++++++ .../js/hooks/useHandleConnections.js | 8 +- .../js/hooks/usePaymentDependencyState.js | 83 ++++++++++++++++--- modules/ppcp-settings/services.php | 13 +-- .../PaymentMethodsDependenciesDefinition.php | 30 +++++++ .../src/Endpoint/PaymentRestEndpoint.php | 4 + .../src/Service/SettingsDataManager.php | 12 ++- modules/ppcp-settings/src/SettingsModule.php | 82 ++++++++++++++++-- 22 files changed, 456 insertions(+), 87 deletions(-) create mode 100644 modules/ppcp-settings/images/icon-button-oxxo.svg create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodValueDependencyMessage.js create mode 100644 modules/ppcp-settings/resources/js/hooks/useDependencyMessages.js diff --git a/modules/ppcp-card-fields/services.php b/modules/ppcp-card-fields/services.php index 7d8427c68..005b4d07e 100644 --- a/modules/ppcp-card-fields/services.php +++ b/modules/ppcp-card-fields/services.php @@ -61,6 +61,7 @@ return array( 'LT', 'LU', 'MT', + 'MX', 'NL', 'PL', 'PT', diff --git a/modules/ppcp-local-alternative-payment-methods/services.php b/modules/ppcp-local-alternative-payment-methods/services.php index 5408288c0..8a1eef931 100644 --- a/modules/ppcp-local-alternative-payment-methods/services.php +++ b/modules/ppcp-local-alternative-payment-methods/services.php @@ -73,7 +73,8 @@ return array( $container->get( 'wcgateway.settings' ), $container->get( 'api.endpoint.partners' ), $container->get( 'settings.flag.is-connected' ), - $container->get( 'api.helper.failure-registry' ) + $container->get( 'api.helper.failure-registry' ), + $container->get( 'woocommerce.logger.woocommerce' ) ); }, 'ppcp-local-apms.bancontact.wc-gateway' => static function ( ContainerInterface $container ): BancontactGateway { diff --git a/modules/ppcp-settings/images/icon-button-oxxo.svg b/modules/ppcp-settings/images/icon-button-oxxo.svg new file mode 100644 index 000000000..c271de3c0 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-oxxo.svg @@ -0,0 +1,16 @@ + + + OXXO + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/PaymentFlow.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/PaymentFlow.js index 24ee7b33d..ce1125bbc 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/PaymentFlow.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/PaymentFlow.js @@ -42,7 +42,8 @@ const PaymentFlow = ( { /> ); } -const description = useAcdc ? optionalDescription : ''; + const description = + useAcdc && 'MX' !== storeCountry ? optionalDescription : ''; return (
{ + const { storeCountry } = useWooSettings(); + + // Determine which icons to display based on the country code. + const imageBadges = + storeCountry === 'MX' + ? [ 'icon-button-oxxo.svg' ] + : [ + // 'icon-button-sepa.svg', // Enable this when the SEPA-Gateway is ready. + 'icon-button-ideal.svg', + 'icon-button-blik.svg', + 'icon-button-bancontact.svg', + ]; + return ( } description={ __( 'Seamless payments for customers across the globe using their preferred payment methods.', diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js index 5d052c03f..1863944bf 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js @@ -10,13 +10,13 @@ import PaymentFlow from '../Components/PaymentFlow'; const StepPaymentMethods = () => { const { optionalMethods, setOptionalMethods } = OnboardingHooks.useOptionalPaymentMethods(); - const { ownBrandOnly } = CommonHooks.useWooSettings(); + const { ownBrandOnly, storeCountry } = CommonHooks.useWooSettings(); const { isCasualSeller } = OnboardingHooks.useBusiness(); const { canUseCardPayments } = OnboardingHooks.useFlags(); const optionalMethodTitle = useMemo( () => { - // The BCDC flow does not show a title. !acdc does not show a title. - if ( isCasualSeller || ! canUseCardPayments ) { + // The BCDC flow does not show a title. !acdc does not show a title. Mexico does not show a title. + if ( isCasualSeller || ! canUseCardPayments || 'MX' === storeCountry ) { return null; } @@ -24,7 +24,7 @@ const StepPaymentMethods = () => { 'Available with additional application', 'woocommerce-paypal-payments' ); - }, [ isCasualSeller, canUseCardPayments ] ); + }, [ isCasualSeller, canUseCardPayments, storeCountry ] ); const methodChoices = [ { @@ -34,7 +34,7 @@ const StepPaymentMethods = () => { }, { title: - ownBrandOnly || ! canUseCardPayments + ownBrandOnly || ! canUseCardPayments || 'MX' === storeCountry ? __( 'No thanks, I prefer to use a different provider for local payment methods', 'woocommerce-paypal-payments' @@ -87,7 +87,9 @@ const OptionalMethodDescription = () => { return ( { const { storeCountry, ownBrandOnly } = CommonHooks.useWooSettings(); const { canUseCardPayments, canUseFastlane } = OnboardingHooks.useFlags(); + const { isCasualSeller } = OnboardingHooks.useBusiness(); const { icons } = usePaymentConfig( storeCountry, @@ -23,7 +24,7 @@ const StepWelcome = ( { setStep, currentStep } ) => { ); const onboardingHeaderDescription = - canUseCardPayments && ! ownBrandOnly + canUseCardPayments && ! ownBrandOnly && 'MX' !== storeCountry ? __( 'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.', 'woocommerce-paypal-payments' diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js index 7bb5f4af4..eb08ba4d2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/hooks/usePaymentConfig.js @@ -15,6 +15,8 @@ import { CreditDebitCards, } from '../Components/PaymentOptions'; +import { useWooSettings } from '../../../../data/common/hooks'; + // List of all payment icons and which requirements they have. const PAYMENT_ICONS = [ { name: 'paypal', always: true }, @@ -28,6 +30,7 @@ const PAYMENT_ICONS = [ { name: 'blik', isOwnBrand: true, onlyAcdc: true }, { name: 'ideal', isOwnBrand: true, onlyAcdc: true }, { name: 'bancontact', isOwnBrand: true, onlyAcdc: true }, + { name: 'oxxo', isOwnBrand: true, onlyAcdc: false, countries: [ 'MX' ] }, ]; // Default configuration, used for all countries, unless they override individual attributes below. @@ -211,6 +214,11 @@ const getRelevantIcons = ( country, includeAcdc, onlyBranded ) => return true; } + // If we're in Mexico, only show OXXO from the APMs. + if ( country === 'MX' && onlyAcdc ) { + return false; + } + if ( onlyBranded && ! isOwnBrand ) { return false; } @@ -251,6 +259,7 @@ export const usePaymentConfig = ( hasFastlane, ownBrandOnly ) => { + const { countryCode } = useWooSettings(); return useMemo( () => { // eslint-disable-next-line no-console console.log( '[Payment Config]', { @@ -277,8 +286,11 @@ export const usePaymentConfig = ( const availableOptionalMethods = filterMethods( config.extendedMethods, [ - // Either include Acdc or non-Acdc methods. - ( method ) => method.isAcdc === canUseCardPayments, + // Either include Acdc or non-Acdc methods except for Mexico. + ( method ) => + countryCode === 'MX' + ? ! method.isAcdc || canUseCardPayments + : method.isAcdc === canUseCardPayments, // Only include own-brand methods when ownBrandOnly is true. ( method ) => ! ownBrandOnly || method.isOwnBrand === true, // Only include Fastlane when hasFastlane is true. diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js index cd5215b0f..709d1fb64 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js @@ -6,7 +6,10 @@ import FeatureItem from './FeatureItem'; import FeatureDescription from './FeatureDescription'; import { ContentWrapper } from '../../../../../ReusableComponents/Elements'; import SettingsCard from '../../../../../ReusableComponents/SettingsCard'; -import { useMerchantInfo } from '../../../../../../data/common/hooks'; +import { + useMerchantInfo, + useWooSettings, +} from '../../../../../../data/common/hooks'; import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common'; import { NOTIFICATION_ERROR, @@ -17,6 +20,7 @@ import { useFeatures } from '../../../../../../data/features/hooks'; const Features = () => { const [ isRefreshing, setIsRefreshing ] = useState( false ); const { merchant } = useMerchantInfo(); + const { storeCountry } = useWooSettings(); const { features, fetchFeatures } = useFeatures(); const { refreshFeatureStatuses } = useDispatch( COMMON_STORE_NAME ); const { createSuccessNotice, createErrorNotice } = @@ -26,6 +30,13 @@ const Features = () => { return null; } + // Filter out ACDC for Mexico (when disabled). + const filteredFeatures = features.filter( + ( feature ) => + feature.id !== 'advanced_credit_and_debit_cards' || + storeCountry !== 'MX' + ); + const refreshHandler = async () => { setIsRefreshing( true ); try { @@ -86,7 +97,7 @@ const Features = () => { aria-busy={ isRefreshing } > - { features.map( ( { id, enabled, ...feature } ) => ( + { filteredFeatures.map( ( { id, enabled, ...feature } ) => ( ; } - // Process methods with dependencies. + // Process methods with dependencies from the pre-computed map. const processedMethods = methods.map( ( method ) => { - const paymentDependency = paymentDependencies?.[ method.id ]; - const settingDependency = settingDependencies?.[ method.id ]; - - let dependencyMessage = null; - let isMethodDisabled = method.isDisabled || isDisabled; - - if ( paymentDependency ) { - dependencyMessage = ( - - ); - isMethodDisabled = true; - } else if ( settingDependency?.isDisabled ) { - dependencyMessage = ( - - ); - isMethodDisabled = true; - } + const dependencyInfo = dependencyMessagesMap[ method.id ] || {}; return { ...method, - isDisabled: isMethodDisabled, - disabledMessage: dependencyMessage, + isDisabled: + dependencyInfo.isMethodDisabled || + method.isDisabled || + isDisabled, + disabledMessage: dependencyInfo.dependencyMessage, }; } ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodValueDependencyMessage.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodValueDependencyMessage.js new file mode 100644 index 000000000..899e16338 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Payment/PaymentMethodValueDependencyMessage.js @@ -0,0 +1,49 @@ +import { createInterpolateElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight'; + +/** + * Component to display a payment method value dependency message + * + * @param {Object} props - Component props + * @param {string} props.dependentMethodId - ID of the dependent payment method + * @param {string} props.dependentMethodName - Display name of the dependent payment method + * @param {boolean} props.requiredValue - Required value (enabled/disabled state) for the dependent method + * @return {JSX.Element} The formatted message with link + */ +const PaymentMethodValueDependencyMessage = ( { + dependentMethodId, + dependentMethodName, + requiredValue, +} ) => { + const displayName = dependentMethodName || dependentMethodId; + + // Determine appropriate message template based on the required value + const template = requiredValue + ? __( + 'Enable to use this method.', + 'woocommerce-paypal-payments' + ) + : __( + 'Disable to use this method.', + 'woocommerce-paypal-payments' + ); + + return createInterpolateElement( template, { + methodLink: ( + + { + e.preventDefault(); + scrollAndHighlight( dependentMethodId ); + } } + > + { displayName } + + + ), + } ); +}; + +export default PaymentMethodValueDependencyMessage; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js index 8cc92e017..f93c4040f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabPaymentMethods.js @@ -2,15 +2,17 @@ import { __ } from '@wordpress/i18n'; import { useCallback } from '@wordpress/element'; import { CommonHooks, OnboardingHooks, PaymentHooks } from '../../../../data'; -import { useActiveModal } from '../../../../data/common/hooks'; +import { useActiveModal, useWooSettings } from '../../../../data/common/hooks'; import Modal from '../Components/Payment/Modal'; import PaymentMethodCard from '../Components/Payment/PaymentMethodCard'; +import { useFeatures } from '../../../../data/features/hooks'; const TabPaymentMethods = () => { const methods = PaymentHooks.usePaymentMethods(); const store = PaymentHooks.useStore(); const { setPersistent, changePaymentSettings } = store; const { activeModal, setActiveModal } = useActiveModal(); + const { features } = useFeatures(); // Get all methods as a map for dependency checking const methodsMap = {}; @@ -52,12 +54,31 @@ const TabPaymentMethods = () => { ); const merchant = CommonHooks.useMerchant(); + const { storeCountry } = useWooSettings(); const { canUseCardPayments } = OnboardingHooks.useFlags(); const showCardPayments = methods.cardPayment.length > 0 && merchant.isBusinessSeller && - canUseCardPayments; + canUseCardPayments && + // Show ACDC if the merchant has the feature enabled in PayPal account. + features.some( + ( feature ) => + feature.id === 'advanced_credit_and_debit_cards' && + feature.enabled + ); + + // Hide BCDC for all countries except Mexico when ACDC is turned on. + const filteredPayPalMethods = methods.paypal.filter( + ( method ) => + method.id !== 'ppcp-card-button-gateway' || + storeCountry === 'MX' || + ! features.some( + ( feature ) => + feature.id === 'advanced_credit_and_debit_cards' && + feature.enabled === true + ) + ); const showApms = methods.apm.length > 0 && merchant.isBusinessSeller; return ( @@ -70,7 +91,7 @@ const TabPaymentMethods = () => { 'woocommerce-paypal-payments' ) } icon="icon-checkout-standard.svg" - methods={ methods.paypal } + methods={ filteredPayPalMethods } onTriggerModal={ setActiveModal } methodsMap={ methodsMap } /> diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index c7efe1d6b..042835c57 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -162,14 +162,15 @@ export const useNavigationState = () => { }; }; -export const useDetermineProducts = ( ownBrandOnly ) => { +export const useDetermineProducts = ( ownBrandOnly, storeCountry ) => { return useSelect( ( select ) => { return select( STORE_NAME ).determineProductsAndCaps( - ownBrandOnly + ownBrandOnly, + storeCountry ); }, - [ ownBrandOnly ] + [ ownBrandOnly, storeCountry ] ); }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index 0ea630072..452ba9614 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -35,9 +35,14 @@ export const flags = ( state ) => { * * @param {{}} state * @param {boolean} ownBrandOnly + * @param {string} storeCountry * @return {{products:string[], options:{}}} The ISU products, based on choices made in the onboarding wizard. */ -export const determineProductsAndCaps = ( state, ownBrandOnly ) => { +export const determineProductsAndCaps = ( + state, + ownBrandOnly, + storeCountry +) => { /** * An array of product-names that are used to build an onboarding URL via the * PartnerReferrals API. To avoid confusion with the "products" property from the @@ -80,7 +85,7 @@ export const determineProductsAndCaps = ( state, ownBrandOnly ) => { if ( canUseVaulting ) { apiModules.push( PAYPAL_PRODUCTS.VAULTING ); } - } else if ( isCasualSeller ) { + } else if ( isCasualSeller || 'MX' === storeCountry ) { /** * Branch 2: Merchant has no business. * The store uses the Express-checkout product. diff --git a/modules/ppcp-settings/resources/js/hooks/useDependencyMessages.js b/modules/ppcp-settings/resources/js/hooks/useDependencyMessages.js new file mode 100644 index 000000000..9789e4184 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useDependencyMessages.js @@ -0,0 +1,83 @@ +// hooks/useDependencyMessages.js +import { useMemo } from '@wordpress/element'; +import PaymentDependencyMessage from '../Components/Screens/Settings/Components/Payment/PaymentDependencyMessage'; +import PaymentMethodValueDependencyMessage from '../Components/Screens/Settings/Components/Payment/PaymentMethodValueDependencyMessage'; +import SettingDependencyMessage from '../Components/Screens/Settings/Components/Payment/SettingDependencyMessage'; + +/** + * Hook to process dependency messages for all methods + * + * @param {Array} methods - List of payment methods + * @param {Object} paymentDependencies - Payment method dependencies + * @param {Object} settingDependencies - Setting dependencies + * @param {boolean} isDisabled - Whether methods are globally disabled + * @return {Object} Map of method IDs to their dependency messages and disabled states + */ +const useDependencyMessages = ( + methods, + paymentDependencies, + settingDependencies, + isDisabled = false +) => { + return useMemo( () => { + const result = {}; + + if ( ! methods || ! methods.length ) { + return result; + } + + // Process each method once to create their dependency messages. + methods.forEach( ( method ) => { + if ( ! method || ! method.id ) { + return; + } + + let dependencyMessage = null; + let isMethodDisabled = method.isDisabled || isDisabled; + + // Check payment dependencies + const dependency = paymentDependencies?.[ method.id ]; + if ( dependency ) { + if ( dependency.type === 'parent' ) { + dependencyMessage = ( + + ); + } else if ( dependency.type === 'value' ) { + dependencyMessage = ( + + ); + } + isMethodDisabled = true; + } + // Check setting dependencies + else if ( settingDependencies?.[ method.id ]?.isDisabled ) { + const settingDependency = settingDependencies[ method.id ]; + dependencyMessage = ( + + ); + isMethodDisabled = true; + } + + // Store the results for this method + result[ method.id ] = { + dependencyMessage, + isMethodDisabled, + }; + } ); + + return result; + }, [ methods, paymentDependencies, settingDependencies, isDisabled ] ); +}; + +export default useDependencyMessages; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index d174c26a7..cdfbc5846 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -31,9 +31,11 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const { onboardingUrl } = isSandbox ? CommonHooks.useSandbox() : CommonHooks.useProduction(); - const { ownBrandOnly } = CommonHooks.useWooSettings(); - const { products, options } = - OnboardingHooks.useDetermineProducts( ownBrandOnly ); + const { ownBrandOnly, storeCountry } = CommonHooks.useWooSettings(); + const { products, options } = OnboardingHooks.useDetermineProducts( + ownBrandOnly, + storeCountry + ); const { startActivity } = CommonHooks.useBusyState(); const { authenticateWithOAuth } = CommonHooks.useAuthentication(); const [ onboardingUrlState, setOnboardingUrl ] = useState( '' ); diff --git a/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js b/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js index 3fb087c1f..8bbca2098 100644 --- a/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js +++ b/modules/ppcp-settings/resources/js/hooks/usePaymentDependencyState.js @@ -6,15 +6,13 @@ import { useSelect } from '@wordpress/data'; /** * Gets the display name for a parent payment method * - * @param {string} parentId - ID of the parent payment method + * @param {string} methodId - ID of the payment method * @param {Object} methodsMap - Map of all payment methods by ID - * @return {string} The display name to use for the parent method + * @return {string} The display name of the method */ -const getParentMethodName = ( parentId, methodsMap ) => { - const parentMethod = methodsMap[ parentId ]; - return parentMethod - ? parentMethod.itemTitle || parentMethod.title || '' - : ''; +const getMethodName = ( methodId, methodsMap ) => { + const method = methodsMap[ methodId ]; + return method ? method.itemTitle || method.title || '' : ''; }; /** @@ -38,7 +36,51 @@ const findDisabledParents = ( method, methodsMap ) => { }; /** - * Hook to evaluate payment method dependencies + * Checks if method should be disabled due to value dependencies + * + * @param {Object} method - The payment method to check + * @param {Object} methodsMap - Map of all payment methods by ID + * @return {Object|null} Value dependency info if should be disabled, null otherwise + */ +const checkValueDependencies = ( method, methodsMap ) => { + const valueDependencies = method.depends_on_payment_methods_values; + + if ( ! valueDependencies ) { + return null; + } + + // Check each dependency against the actual state of the dependent method. + for ( const [ dependentId, requiredValue ] of Object.entries( + valueDependencies + ) ) { + const dependent = methodsMap[ dependentId ]; + + if ( ! dependent ) { + continue; + } + + // Example: card-button-gateway depends on credit-card-gateway being FALSE. + // So if credit-card-gateway is TRUE (enabled), card-button-gateway should be disabled. + + // If the dependency requires a method to be false but it's enabled (or vice versa). + if ( + typeof requiredValue === 'boolean' && + dependent.enabled !== requiredValue + ) { + // This dependency is violated - the dependent method is in the wrong state. + return { + dependentId, + dependentName: getMethodName( dependentId, methodsMap ), + requiredValue, + }; + } + } + + return null; +}; + +/** + * Hook to evaluate all payment method dependencies * * @param {Array} methods - List of payment methods * @param {Object} methodsMap - Map of payment methods by ID @@ -51,6 +93,7 @@ const usePaymentDependencyState = ( methods, methodsMap ) => { if ( methods && methodsMap && Object.keys( methodsMap ).length > 0 ) { methods.forEach( ( method ) => { if ( method && method.id ) { + // Check regular parent-child dependencies first. const disabledParents = findDisabledParents( method, methodsMap @@ -59,18 +102,32 @@ const usePaymentDependencyState = ( methods, methodsMap ) => { if ( disabledParents.length > 0 ) { const parentId = disabledParents[ 0 ]; result[ method.id ] = { + type: 'parent', isDisabled: true, parentId, - parentName: getParentMethodName( - parentId, - methodsMap - ), + parentName: getMethodName( parentId, methodsMap ), + }; + return; // Skip other checks if already disabled. + } + + // Check value dependencies. + const valueDependency = checkValueDependencies( + method, + methodsMap + ); + + if ( valueDependency ) { + result[ method.id ] = { + type: 'value', + isDisabled: true, + dependentId: valueDependency.dependentId, + dependentName: valueDependency.dependentName, + requiredValue: valueDependency.requiredValue, }; } } } ); } - return result; }, [ methods, methodsMap ] ); }; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 10e257cda..08d326b25 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -568,16 +568,17 @@ return array( ); // Merchant capabilities, serve to show active or inactive badge and buttons. $capabilities = array( - 'apple_pay' => $features['apple_pay']['enabled'] ?? false, - 'google_pay' => $features['google_pay']['enabled'] ?? false, - 'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false, - 'save_paypal' => $features['save_paypal_and_venmo']['enabled'] ?? false, + 'apple_pay' => $features['apple_pay']['enabled'] ?? false, + 'google_pay' => $features['google_pay']['enabled'] ?? false, + 'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false, + 'save_paypal' => $features['save_paypal_and_venmo']['enabled'] ?? false, + 'alternative_payment_methods' => $features['alternative_payment_methods']['enabled'] ?? false, ); $merchant_capabilities = array( 'save_paypal' => $capabilities['save_paypal'], // Save PayPal and Venmo eligibility. - 'acdc' => $capabilities['acdc'] && ! $gateways['card-button'], // Advanced credit and debit cards eligibility. - 'apm' => ! $gateways['card-button'], // Alternative payment methods eligibility. + 'acdc' => $capabilities['acdc'], // Advanced credit and debit cards eligibility. + 'apm' => $capabilities['alternative_payment_methods'], // Alternative payment methods eligibility. 'google_pay' => $capabilities['acdc'] && $capabilities['google_pay'], // Google Pay eligibility. 'apple_pay' => $capabilities['acdc'] && $capabilities['apple_pay'], // Apple Pay eligibility. 'pay_later' => $capabilities['acdc'] && ! $gateways['card-button'], // Pay Later eligibility. diff --git a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php index 9c7771be9..9bec748a9 100644 --- a/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php +++ b/modules/ppcp-settings/src/Data/Definition/PaymentMethodsDependenciesDefinition.php @@ -107,6 +107,27 @@ class PaymentMethodsDependenciesDefinition { ); } + /** + * Get payment method value dependencies for a specific method + * + * @return array Value dependencies for the method or empty array if none exist + */ + public function get_payment_method_value_dependencies(): array { + $dependencies = array( + CardButtonGateway::ID => array( + CreditCardGateway::ID => false, + ), + CreditCardGateway::ID => array( + CardButtonGateway::ID => false, + ), + ); + + return apply_filters( + 'woocommerce_paypal_payments_method_value_dependencies', + $dependencies + ); + } + /** * Get method setting dependencies * @@ -139,6 +160,15 @@ class PaymentMethodsDependenciesDefinition { $method['depends_on_payment_methods'] = $payment_method_dependencies; } + // Add payment method value dependency info if applicable. + $all_payment_method_value_dependencies = $this->get_payment_method_value_dependencies(); + + // Only add dependencies that directly apply to this method. + if ( isset( $all_payment_method_value_dependencies[ $method_id ] ) ) { + // Direct dependencies on other method values. + $method['depends_on_payment_methods_values'] = $all_payment_method_value_dependencies[ $method_id ]; + } + // Check if this method has setting dependencies. $method_setting_dependencies = $this->get_method_setting_dependencies( $method_id ); if ( ! empty( $method_setting_dependencies ) ) { diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index 428e05c9a..189179910 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -197,6 +197,10 @@ class PaymentRestEndpoint extends RestEndpoint { $gateway_settings[ $key ]['depends_on_payment_methods'] = $payment_method['depends_on_payment_methods']; } + if ( isset( $payment_method['depends_on_payment_methods_values'] ) ) { + $gateway_settings[ $key ]['depends_on_payment_methods_values'] = $payment_method['depends_on_payment_methods_values']; + } + if ( isset( $payment_method['depends_on_settings'] ) ) { $gateway_settings[ $key ]['depends_on_settings'] = $payment_method['depends_on_settings']; } diff --git a/modules/ppcp-settings/src/Service/SettingsDataManager.php b/modules/ppcp-settings/src/Service/SettingsDataManager.php index d2d49f67a..b0ecdcc71 100644 --- a/modules/ppcp-settings/src/Service/SettingsDataManager.php +++ b/modules/ppcp-settings/src/Service/SettingsDataManager.php @@ -270,10 +270,14 @@ class SettingsDataManager { $this->payment_methods->toggle_method_state( CardButtonGateway::ID, true ); } - // Enable all APM methods. - foreach ( $methods_apm as $method ) { - $this->payment_methods->toggle_method_state( $method['id'], true ); - } + /** + * Allow plugins to modify apm payment gateway states before saving. + * + * @param PaymentSettings $payment_methods The payment methods object. + * @param PaymentSettings $methods_apm List of APM methods. + * @param ConfigurationFlagsDTO $flags Configuration flags that determine which gateways to enable. + */ + do_action( 'woocommerce_paypal_payments_toggle_payment_gateways_apms', $this->payment_methods, $methods_apm, $flags ); } /** diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 621961bec..0cbb0fa54 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -13,8 +13,10 @@ use WC_Payment_Gateway; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway; @@ -321,11 +323,13 @@ class SettingsModule implements ServiceModule, ExecutableModule { // Unset BCDC if merchant is eligible for ACDC and country is eligible for card fields. $card_fields_eligible = $container->get( 'card-fields.eligible' ); - if ( $dcc_product_status->is_active() && $card_fields_eligible ) { - unset( $payment_methods[ CardButtonGateway::ID ] ); - } else { - // For non-ACDC regions unset ACDC. - unset( $payment_methods[ CreditCardGateway::ID ] ); + if ( 'MX' !== $container->get( 'api.shop.country' ) ) { + if ( $dcc_product_status->is_active() && $card_fields_eligible ) { + unset( $payment_methods[ CardButtonGateway::ID ] ); + } else { + // For non-ACDC regions unset ACDC. + unset( $payment_methods[ CreditCardGateway::ID ] ); + } } // Unset Venmo when store location is not United States. @@ -358,6 +362,18 @@ class SettingsModule implements ServiceModule, ExecutableModule { unset( $payment_methods[ PayUponInvoiceGateway::ID ] ); } + // Unset all APMs other than OXXO for Mexico. + if ( 'MX' === $merchant_country ) { + unset( $payment_methods[ BancontactGateway::ID ] ); + unset( $payment_methods[ BlikGateway::ID ] ); + unset( $payment_methods[ EPSGateway::ID ] ); + unset( $payment_methods[ IDealGateway::ID ] ); + unset( $payment_methods[ MyBankGateway::ID ] ); + unset( $payment_methods[ P24Gateway::ID ] ); + unset( $payment_methods[ TrustlyGateway::ID ] ); + unset( $payment_methods[ MultibancoGateway::ID ] ); + } + return $payment_methods; } ); @@ -539,6 +555,62 @@ class SettingsModule implements ServiceModule, ExecutableModule { $payment_methods->toggle_method_state( AxoGateway::ID, true ); } } + + $general_settings = $container->get( 'settings.data.general' ); + assert( $general_settings instanceof GeneralSettings ); + + $merchant_data = $general_settings->get_merchant_data(); + $merchant_country = $merchant_data->merchant_country; + + // Disable all extended checkout card methods if the store is in Mexico. + if ( 'MX' === $merchant_country ) { + $payment_methods->toggle_method_state( CreditCardGateway::ID, false ); + $payment_methods->toggle_method_state( ApplePayGateway::ID, false ); + $payment_methods->toggle_method_state( GooglePayGateway::ID, false ); + } + }, + 10, + 2 + ); + + // Enable APMs after onboarding if the country is compatible. + add_action( + 'woocommerce_paypal_payments_toggle_payment_gateways_apms', + function ( PaymentSettings $payment_methods, array $methods_apm ) use ( $container ) { + + $general_settings = $container->get( 'settings.data.general' ); + assert( $general_settings instanceof GeneralSettings ); + + $merchant_data = $general_settings->get_merchant_data(); + $merchant_country = $merchant_data->merchant_country; + + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $logger->info( 'Merchant country: ' . $merchant_country ); + $logger->info( 'Merchant data: ' . json_encode( $merchant_data ) ); + $logger->info( '$methods_apm: ' . json_encode( $methods_apm ) ); + + // Enable all APM methods. + foreach ( $methods_apm as $method ) { + // Skip PayUponInvoice if merchant is not in Germany. + if ( PayUponInvoiceGateway::ID === $method['id'] && 'DE' !== $merchant_country ) { + continue; + } + + // For OXXO: enable ONLY if merchant is in Mexico. + if ( OXXO::ID === $method['id'] ) { + if ( 'MX' === $merchant_country ) { + $payment_methods->toggle_method_state( $method['id'], true ); + } + continue; + } + + // For all other APMs: enable only if merchant is NOT in Mexico. + if ( 'MX' !== $merchant_country ) { + $payment_methods->toggle_method_state( $method['id'], true ); + } + } }, 10, 2