Merge remote-tracking branch 'origin/trunk' into PCP-3930-Make-the-webhook-resubscribe/simulate-logic-usable-in-React-application

This commit is contained in:
inpsyde-maticluznar 2024-12-13 08:47:28 +01:00
commit 0c8de4900c
No known key found for this signature in database
GPG key ID: D005973F231309F6
26 changed files with 441 additions and 171 deletions

View file

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
name: PHP ${{ matrix.php-versions }} name: PHP ${{ matrix.php-versions }}
steps: steps:
@ -30,6 +30,7 @@ jobs:
run: vendor/bin/phpunit run: vendor/bin/phpunit
- name: Psalm - name: Psalm
if: ${{ matrix.php-versions == '7.4' }}
run: ./vendor/bin/psalm --show-info=false --threads=8 --diff run: ./vendor/bin/psalm --show-info=false --threads=8 --diff
- name: Run PHPCS - name: Run PHPCS

View file

@ -1,6 +1,6 @@
*** Changelog *** *** Changelog ***
= 2.9.5 - xxxx-xx-xx = = 2.9.5 - 2024-12-10 =
Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745

View file

@ -2,7 +2,7 @@
"name": "woocommerce/woocommerce-paypal-payments", "name": "woocommerce/woocommerce-paypal-payments",
"type": "wordpress-plugin", "type": "wordpress-plugin",
"description": "PayPal Commerce Platform for WooCommerce", "description": "PayPal Commerce Platform for WooCommerce",
"license": "GPL-2.0", "license": "GPL-2.0-or-later",
"require": { "require": {
"php": "^7.4 | ^8.0", "php": "^7.4 | ^8.0",
"ext-json": "*", "ext-json": "*",

6
composer.lock generated
View file

@ -5541,8 +5541,8 @@
"aliases": [], "aliases": [],
"minimum-stability": "dev", "minimum-stability": "dev",
"stability-flags": { "stability-flags": {
"php-stubs/wordpress-stubs": 0, "php-stubs/woocommerce-stubs": 0,
"php-stubs/woocommerce-stubs": 0 "php-stubs/wordpress-stubs": 0
}, },
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
@ -5550,7 +5550,7 @@
"php": "^7.4 | ^8.0", "php": "^7.4 | ^8.0",
"ext-json": "*" "ext-json": "*"
}, },
"platform-dev": [], "platform-dev": {},
"platform-overrides": { "platform-overrides": {
"php": "7.4" "php": "7.4"
}, },

View file

@ -5,66 +5,153 @@
* @package WooCommerce\PayPalCommerce\Compat * @package WooCommerce\PayPalCommerce\Compat
*/ */
declare(strict_types=1); declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Compat; namespace WooCommerce\PayPalCommerce\Compat;
use RuntimeException;
/** /**
* A helper for mapping the new/old settings. * A helper class to manage the transition between legacy and new settings.
*
* This utility provides mapping from old setting keys to new ones and retrieves
* their corresponding values from the appropriate models. The class uses lazy
* loading and caching to optimize performance during runtime.
*/ */
class SettingsMapHelper { class SettingsMapHelper {
/** /**
* A list of mapped settings. * A list of settings maps containing mapping definitions.
* *
* @var SettingsMap[] * @var SettingsMap[]
*/ */
protected array $settings_map; protected array $settings_map;
/**
* Indexed map for faster lookups, initialized lazily.
*
* @var array|null Associative array where old keys map to metadata.
*/
protected ?array $key_to_model = null;
/**
* Cache for results of `to_array()` calls on models.
*
* @var array Associative array where keys are model IDs.
*/
protected array $model_cache = array();
/** /**
* Constructor. * Constructor.
* *
* @param SettingsMap[] $settings_map A list of mapped settings. * @param SettingsMap[] $settings_map A list of settings maps containing key definitions.
* @throws RuntimeException When an old key has multiple mappings.
*/ */
public function __construct( array $settings_map ) { public function __construct( array $settings_map ) {
$this->validate_settings_map( $settings_map );
$this->settings_map = $settings_map; $this->settings_map = $settings_map;
} }
/** /**
* Retrieves the mapped value from the new settings. * Validates the settings map for duplicate keys.
* *
* @param string $key The key. * @param SettingsMap[] $settings_map The settings map to validate.
* @return ?mixed the mapped value or Null if it doesn't exist. * @throws RuntimeException When an old key has multiple mappings.
*/ */
public function mapped_value( string $key ) { protected function validate_settings_map( array $settings_map ) : void {
if ( ! $this->has_mapped_key( $key ) ) { $seen_keys = array();
return null;
}
foreach ( $this->settings_map as $settings_map ) { foreach ( $settings_map as $settings_map_instance ) {
$mapped_key = array_search( $key, $settings_map->get_map(), true ); foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
$new_settings = $settings_map->get_model()->to_array(); if ( isset( $seen_keys[ $old_key ] ) ) {
if ( ! empty( $new_settings[ $mapped_key ] ) ) { throw new RuntimeException( "Duplicate mapping for legacy key '$old_key'." );
return $new_settings[ $mapped_key ]; }
$seen_keys[ $old_key ] = true;
} }
} }
return null;
} }
/** /**
* Checks if the given key exists in the new settings. * Retrieves the value of a mapped key from the new settings.
* *
* @param string $key The key. * @param string $old_key The key from the legacy settings.
* @return bool true if the given key exists in the new settings, otherwise false. *
* @return mixed|null The value of the mapped setting, or null if not found.
*/ */
public function has_mapped_key( string $key ) : bool { public function mapped_value( string $old_key ) {
foreach ( $this->settings_map as $settings_map ) { $this->ensure_map_initialized();
if ( in_array( $key, $settings_map->get_map(), true ) ) {
return true; if ( ! isset( $this->key_to_model[ $old_key ] ) ) {
} return null;
} }
return false; $mapping = $this->key_to_model[ $old_key ];
$model_id = spl_object_id( $mapping['model'] );
return $this->get_cached_model_value( $model_id, $mapping['new_key'], $mapping['model'] );
}
/**
* Determines if a given legacy key exists in the new settings.
*
* @param string $old_key The key from the legacy settings.
*
* @return bool True if the key exists in the new settings, false otherwise.
*/
public function has_mapped_key( string $old_key ) : bool {
$this->ensure_map_initialized();
return isset( $this->key_to_model[ $old_key ] );
}
/**
* Retrieves a cached model value or caches it if not already cached.
*
* @param int $model_id The unique identifier for the model object.
* @param string $new_key The key in the new settings structure.
* @param object $model The model object.
*
* @return mixed|null The value of the key in the model, or null if not found.
*/
protected function get_cached_model_value( int $model_id, string $new_key, object $model ) {
if ( ! isset( $this->model_cache[ $model_id ] ) ) {
$this->model_cache[ $model_id ] = $model->to_array();
}
return $this->model_cache[ $model_id ][ $new_key ] ?? null;
}
/**
* Ensures the map of old-to-new settings is initialized.
*
* This method initializes the `key_to_model` array lazily to improve performance.
*
* @return void
*/
protected function ensure_map_initialized() : void {
if ( $this->key_to_model === null ) {
$this->initialize_key_map();
}
}
/**
* Initializes the indexed map of old-to-new settings keys.
*
* This method processes the provided settings maps and indexes the legacy
* keys to their corresponding metadata for efficient lookup.
*
* @return void
*/
protected function initialize_key_map() : void {
$this->key_to_model = array();
foreach ( $this->settings_map as $settings_map_instance ) {
foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
$this->key_to_model[ $old_key ] = array(
'new_key' => $new_key,
'model' => $settings_map_instance->get_model(),
);
}
}
} }
} }

View file

@ -57,6 +57,14 @@
&__content { &__content {
display: flex; display: flex;
position: relative;
pointer-events: none;
*:not(a){
pointer-events: none;
}
a {
pointer-events: all;
}
} }
&__title { &__title {

View file

@ -69,7 +69,6 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
imageBadge={ [ imageBadge={ [
'icon-button-sepa.svg',
'icon-button-ideal.svg', 'icon-button-ideal.svg',
'icon-button-blik.svg', 'icon-button-blik.svg',
'icon-button-bancontact.svg', 'icon-button-bancontact.svg',

View file

@ -11,7 +11,6 @@ const PaymentMethodIcons = ( props ) => {
<PaymentMethodIcon type="discover" icons={ props.icons } /> <PaymentMethodIcon type="discover" icons={ props.icons } />
<PaymentMethodIcon type="apple-pay" icons={ props.icons } /> <PaymentMethodIcon type="apple-pay" icons={ props.icons } />
<PaymentMethodIcon type="google-pay" icons={ props.icons } /> <PaymentMethodIcon type="google-pay" icons={ props.icons } />
<PaymentMethodIcon type="sepa" icons={ props.icons } />
<PaymentMethodIcon type="ideal" icons={ props.icons } /> <PaymentMethodIcon type="ideal" icons={ props.icons } />
<PaymentMethodIcon type="bancontact" icons={ props.icons } /> <PaymentMethodIcon type="bancontact" icons={ props.icons } />
</div> </div>

View file

@ -66,7 +66,7 @@ const AcdcFlow = ( {
description={ sprintf( description={ sprintf(
// translators: %s: Link to PayPal business fees guide // translators: %s: Link to PayPal business fees guide
__( __(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>', 'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'https://www.paypal.com/us/business/paypal-business-fees' 'https://www.paypal.com/us/business/paypal-business-fees'
@ -256,7 +256,7 @@ const AcdcFlow = ( {
description={ sprintf( description={ sprintf(
// translators: %s: Link to PayPal REST application guide // translators: %s: Link to PayPal REST application guide
__( __(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>', 'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '

View file

@ -60,7 +60,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf( description={ sprintf(
// translators: %s: Link to PayPal REST application guide // translators: %s: Link to PayPal REST application guide
__( __(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>', 'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -158,7 +158,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf( description={ sprintf(
// translators: %s: Link to PayPal REST application guide // translators: %s: Link to PayPal REST application guide
__( __(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>', 'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '

View file

@ -1,7 +1,8 @@
import { __, sprintf } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import AcdcFlow from './AcdcFlow'; import AcdcFlow from './AcdcFlow';
import BcdcFlow from './BcdcFlow'; import BcdcFlow from './BcdcFlow';
import { Button } from '@wordpress/components'; import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import { pricesBasedDescription } from './pricesBasedDescription';
const WelcomeDocs = ( { const WelcomeDocs = ( {
useAcdc, useAcdc,
@ -10,15 +11,6 @@ const WelcomeDocs = ( {
storeCountry, storeCountry,
storeCurrency, storeCurrency,
} ) => { } ) => {
const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
return ( return (
<div className="ppcp-r-welcome-docs"> <div className="ppcp-r-welcome-docs">
<h2 className="ppcp-r-welcome-docs__title"> <h2 className="ppcp-r-welcome-docs__title">
@ -41,10 +33,14 @@ const WelcomeDocs = ( {
storeCurrency={ storeCurrency } storeCurrency={ storeCurrency }
/> />
) } ) }
<p { storeCountry in countryPriceInfo && (
className="ppcp-r-optional-payment-methods__description" <p
dangerouslySetInnerHTML={ { __html: pricesBasedDescription } } className="ppcp-r-optional-payment-methods__description"
></p> dangerouslySetInnerHTML={ {
__html: pricesBasedDescription,
} }
></p>
) }
</div> </div>
); );
}; };

View file

@ -0,0 +1,10 @@
import { __, sprintf } from '@wordpress/i18n';
export const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);

View file

@ -3,27 +3,12 @@ import { OnboardingHooks } from '../../../data';
import { getSteps, getCurrentStep } from './availableSteps'; import { getSteps, getCurrentStep } from './availableSteps';
import Navigation from './Components/Navigation'; import Navigation from './Components/Navigation';
import { useEffect } from '@wordpress/element';
const Onboarding = () => { const Onboarding = () => {
const { step, setStep, flags } = OnboardingHooks.useSteps(); const { step, setStep, flags } = OnboardingHooks.useSteps();
const Steps = getSteps( flags ); const Steps = getSteps( flags );
const currentStep = getCurrentStep( step, Steps ); const currentStep = getCurrentStep( step, Steps );
// Disable the "Changes you made might not be saved" browser warning.
useEffect( () => {
const suppressBeforeUnload = ( event ) => {
event.stopImmediatePropagation();
return undefined;
};
window.addEventListener( 'beforeunload', suppressBeforeUnload );
return () => {
window.removeEventListener( 'beforeunload', suppressBeforeUnload );
};
}, [] );
const handleNext = () => setStep( currentStep.nextStep ); const handleNext = () => setStep( currentStep.nextStep );
const handlePrev = () => setStep( currentStep.prevStep ); const handlePrev = () => setStep( currentStep.prevStep );
const handleExit = () => { const handleExit = () => {

View file

@ -1,10 +1,12 @@
import { __, sprintf } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBox from '../../ReusableComponents/SelectBox';
import { CommonHooks, OnboardingHooks } from '../../../data'; import { CommonHooks, OnboardingHooks } from '../../../data';
import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods'; import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
import { pricesBasedDescription } from '../../ReusableComponents/WelcomeDocs/pricesBasedDescription';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods'; const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
@ -16,15 +18,6 @@ const StepPaymentMethods = ( {} ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
return ( return (
<div className="ppcp-r-page-optional-payment-methods"> <div className="ppcp-r-page-optional-payment-methods">
<OnboardingHeader <OnboardingHeader
@ -67,12 +60,14 @@ const StepPaymentMethods = ( {} ) => {
type="radio" type="radio"
></SelectBox> ></SelectBox>
</SelectBoxWrapper> </SelectBoxWrapper>
<p { storeCountry in countryPriceInfo && (
className="ppcp-r-optional-payment-methods__description" <p
dangerouslySetInnerHTML={ { className="ppcp-r-optional-payment-methods__description"
__html: pricesBasedDescription, dangerouslySetInnerHTML={ {
} } __html: pricesBasedDescription,
></p> } }
></p>
) }
</div> </div>
</div> </div>
); );

View file

@ -1,4 +1,4 @@
import { useMemo } from '@wordpress/element'; import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import classNames from 'classnames'; import classNames from 'classnames';
@ -11,6 +11,20 @@ import SettingsScreen from './SettingsScreen';
const Settings = () => { const Settings = () => {
const onboardingProgress = OnboardingHooks.useSteps(); const onboardingProgress = OnboardingHooks.useSteps();
// Disable the "Changes you made might not be saved" browser warning.
useEffect( () => {
const suppressBeforeUnload = ( event ) => {
event.stopImmediatePropagation();
return undefined;
};
window.addEventListener( 'beforeunload', suppressBeforeUnload );
return () => {
window.removeEventListener( 'beforeunload', suppressBeforeUnload );
};
}, [] );
const wrapperClass = classNames( 'ppcp-r-app', { const wrapperClass = classNames( 'ppcp-r-app', {
loading: ! onboardingProgress.isReady, loading: ! onboardingProgress.isReady,
} ); } );

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
return array( return array(
'settings.url' => static function ( ContainerInterface $container ) : string { 'settings.url' => static function ( ContainerInterface $container ) : string {
@ -138,11 +139,24 @@ return array(
return in_array( $country, $eligible_countries, true ); return in_array( $country, $eligible_countries, true );
}, },
'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
$page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : '';
return new ConnectionListener(
$page_id,
$container->get( 'settings.data.common' ),
$container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache { 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
return new Cache( 'ppcp-paypal-signup-link' ); return new Cache( 'ppcp-paypal-signup-link' );
}, },
'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager { 'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
return new OnboardingUrlManager(); return new OnboardingUrlManager(
$container->get( 'settings.service.signup-link-cache' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
}, },
'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array { 'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array {
// Define available environments. // Define available environments.
@ -162,7 +176,6 @@ return array(
$generators[ $environment ] = new ConnectionUrlGenerator( $generators[ $environment ] = new ConnectionUrlGenerator(
$config['partner_referrals'], $config['partner_referrals'],
$container->get( 'api.repository.partner-referrals-data' ), $container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'settings.service.signup-link-cache' ),
$environment, $environment,
$container->get( 'settings.service.onboarding-url-manager' ), $container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )

View file

@ -206,12 +206,6 @@ class CommonRestEndpoint extends RestEndpoint {
$this->merchant_info_map $this->merchant_info_map
); );
// TEMP for demonstration.
$extra_data['merchant']['isConnected'] = true;
$extra_data['merchant']['isSandbox'] = true;
$extra_data['merchant']['id'] = '1234567890';
$extra_data['merchant']['email'] = 'example@example.com';
return $extra_data; return $extra_data;
} }

View file

@ -12,8 +12,18 @@ namespace WooCommerce\PayPalCommerce\Settings\Handler;
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use Psr\Log\LoggerInterface;
class ConnectionHandler { /**
* Provides a listener that handles merchant-connection requests.
*
* Those connection requests are made after the merchant logs into their PayPal
* account (inside the login popup). At the last step, they see a "Return to
* Store" button.
* Clicking that button triggers the merchant-connection request.
*/
class ConnectionListener {
/** /**
* ID of the current settings page; empty if not on a PayPal settings page. * ID of the current settings page; empty if not on a PayPal settings page.
* *
@ -35,6 +45,13 @@ class ConnectionHandler {
*/ */
private OnboardingUrlManager $url_manager; private OnboardingUrlManager $url_manager;
/**
* Logger instance, mainly used for debugging purposes.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;
/** /**
* ID of the current user, set by the process() method. * ID of the current user, set by the process() method.
* *
@ -48,11 +65,13 @@ class ConnectionHandler {
* @param string $settings_page_id Current plugin settings page ID. * @param string $settings_page_id Current plugin settings page ID.
* @param CommonSettings $settings Access to saved connection details. * @param CommonSettings $settings Access to saved connection details.
* @param OnboardingUrlManager $url_manager Get OnboardingURL instances. * @param OnboardingUrlManager $url_manager Get OnboardingURL instances.
* @param ?LoggerInterface $logger The logger, for debugging purposes.
*/ */
public function __construct( string $settings_page_id, CommonSettings $settings, OnboardingUrlManager $url_manager ) { public function __construct( string $settings_page_id, CommonSettings $settings, OnboardingUrlManager $url_manager, LoggerInterface $logger = null ) {
$this->settings_page_id = $settings_page_id; $this->settings_page_id = $settings_page_id;
$this->settings = $settings; $this->settings = $settings;
$this->url_manager = $url_manager; $this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger();
// Initialize as "guest", the real ID is provided via process(). // Initialize as "guest", the real ID is provided via process().
$this->user_id = 0; $this->user_id = 0;
@ -71,9 +90,20 @@ class ConnectionHandler {
return; return;
} }
$token = $this->get_token_from_request( $request );
if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) {
return;
}
$data = $this->extract_data( $request ); $data = $this->extract_data( $request );
if ( ! $data ) {
return;
}
$this->logger->info( 'Found merchant data in request', $data );
$this->store_data( $this->store_data(
$data['use_sandbox'], $data['is_sandbox'],
$data['merchant_id'], $data['merchant_id'],
$data['merchant_email'] $data['merchant_email']
); );
@ -96,32 +126,38 @@ class ConnectionHandler {
return false; return false;
} }
// Requirement 3: The params are present and not empty - 'merchantIdInPayPal' - 'merchantId' - 'ppcpToken' $required_params = array(
'merchantIdInPayPal',
'merchantId',
'ppcpToken',
);
foreach ( $required_params as $param ) {
if ( empty( $request[ $param ] ) ) {
return false;
}
}
return true; return true;
} }
/** /**
* Checks, if the connection token is valid. * Extract the merchant details (ID & email) from the request details.
* *
* If the token is valid, it is *instantly invalidated* by this check: It's * @param array $request The full request details.
* not possible to verify the same token twice.
* *
* @param string $token The token to verify. * @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
* * or an empty array on failure.
* @return bool True, if the token is valid.
*/ */
protected function is_token_valid( string $token ) : bool {
// $valid = OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id )
// OR OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id )
return true;
}
protected function extract_data( array $request ) : array { protected function extract_data( array $request ) : array {
// $merchant_id: $request['merchantIdInPayPal'] (!), sanitize: sanitize_text_field( wp_unslash() ) $this->logger->info( 'Extracting connection data from request...' );
// $merchant_email: $request['merchantId'] (!), sanitize: $this->sanitize_merchant_email()
$merchant_id = $this->get_merchant_id_from_request( $request );
$merchant_email = $this->get_merchant_email_from_request( $request );
if ( ! $merchant_id || ! $merchant_email ) {
return array();
}
return array( return array(
'is_sandbox' => $this->settings->get_sandbox(), 'is_sandbox' => $this->settings->get_sandbox(),
@ -130,26 +166,76 @@ class ConnectionHandler {
); );
} }
/**
* Persist the merchant details to the database.
*
* @param bool $is_sandbox Whether the details are for a sandbox account.
* @param string $merchant_id The anonymized merchant ID.
* @param string $merchant_email The merchant's email.
*/
protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
$this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" );
$this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email );
$this->settings->save();
}
/**
* Returns the sanitized connection token from the incoming request.
*
* @param array $request Full request details.
*
* @return string The sanitized token, or an empty string.
*/
protected function get_token_from_request( array $request ) : string {
return $this->sanitize_string( $request['ppcpToken'] ?? '' );
}
/**
* Returns the sanitized merchant ID from the incoming request.
*
* @param array $request Full request details.
*
* @return string The sanitized merchant ID, or an empty string.
*/
protected function get_merchant_id_from_request( array $request ) : string {
return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' );
}
/**
* Returns the sanitized merchant email from the incoming request.
*
* Note that the email is provided via the argument "merchantId", which
* looks incorrect at first, but PayPal uses the email address as merchant
* IDm and offers a more anonymous ID via the "merchantIdInPayPal" argument.
*
* @param array $request Full request details.
*
* @return string The sanitized merchant email, or an empty string.
*/
protected function get_merchant_email_from_request( array $request ) : string {
return $this->sanitize_merchant_email( $request['merchantId'] ?? '' );
}
/**
* Sanitizes a request-argument for processing.
*
* @param string $value Value from the request argument.
*
* @return string Sanitized value.
*/
protected function sanitize_string( string $value ) : string {
return trim( sanitize_text_field( wp_unslash( $value ) ) );
}
/** /**
* Sanitizes the merchant's email address for processing. * Sanitizes the merchant's email address for processing.
* *
* @param string $email The plain email. * @param string $email The plain email.
* *
* @return string * @return string Sanitized email address.
*/ */
private function sanitize_merchant_email( string $email ) : string { protected function sanitize_merchant_email( string $email ) : string {
return sanitize_text_field( str_replace( ' ', '+', $email ) ); return sanitize_text_field( str_replace( ' ', '+', $email ) );
} }
/**
* Persist the merchant details to the database.
*
* @param bool $is_sandbox
* @param string $merchant_id
* @param string $merchant_email
*/
protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void {
$this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email );
$this->settings->save();
}
} }

View file

@ -37,13 +37,6 @@ class ConnectionUrlGenerator {
*/ */
protected PartnerReferralsData $referrals_data; protected PartnerReferralsData $referrals_data;
/**
* The cache
*
* @var Cache
*/
protected Cache $cache;
/** /**
* Manages access to OnboardingUrl instances * Manages access to OnboardingUrl instances
* *
@ -63,7 +56,7 @@ class ConnectionUrlGenerator {
* *
* @var LoggerInterface * @var LoggerInterface
*/ */
private $logger; private LoggerInterface $logger;
/** /**
* Constructor for the ConnectionUrlGenerator class. * Constructor for the ConnectionUrlGenerator class.
@ -72,8 +65,6 @@ class ConnectionUrlGenerator {
* *
* @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation. * @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation.
* @param PartnerReferralsData $referrals_data Default partner referrals data. * @param PartnerReferralsData $referrals_data Default partner referrals data.
* @param Cache $cache The cache object used for storing and
* retrieving data.
* @param string $environment Environment that is used to generate the URL. * @param string $environment Environment that is used to generate the URL.
* ['production'|'sandbox']. * ['production'|'sandbox'].
* @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances. * @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances.
@ -82,14 +73,12 @@ class ConnectionUrlGenerator {
public function __construct( public function __construct(
PartnerReferrals $partner_referrals, PartnerReferrals $partner_referrals,
PartnerReferralsData $referrals_data, PartnerReferralsData $referrals_data,
Cache $cache,
string $environment, string $environment,
OnboardingUrlManager $url_manager, OnboardingUrlManager $url_manager,
?LoggerInterface $logger = null ?LoggerInterface $logger = null
) { ) {
$this->partner_referrals = $partner_referrals; $this->partner_referrals = $partner_referrals;
$this->referrals_data = $referrals_data; $this->referrals_data = $referrals_data;
$this->cache = $cache;
$this->environment = $environment; $this->environment = $environment;
$this->url_manager = $url_manager; $this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
@ -119,7 +108,7 @@ class ConnectionUrlGenerator {
public function generate( array $products = array() ) : string { public function generate( array $products = array() ) : string {
$cache_key = $this->cache_key( $products ); $cache_key = $this->cache_key( $products );
$user_id = get_current_user_id(); $user_id = get_current_user_id();
$onboarding_url = $this->url_manager->get( $this->cache, $cache_key, $user_id ); $onboarding_url = $this->url_manager->get( $cache_key, $user_id );
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key ); $cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
if ( $cached_url ) { if ( $cached_url ) {

View file

@ -9,7 +9,9 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service; namespace WooCommerce\PayPalCommerce\Settings\Service;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
// TODO: Replace the OnboardingUrl with a new implementation for this module. // TODO: Replace the OnboardingUrl with a new implementation for this module.
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
@ -25,16 +27,75 @@ use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
* without having to re-write all token-related details just yet. * without having to re-write all token-related details just yet.
*/ */
class OnboardingUrlManager { class OnboardingUrlManager {
/**
* Cache instance for onboarding token.
*
* @var Cache
*/
private Cache $cache;
/**
* Logger instance, mainly used for debugging purposes.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* Constructor.
*
* @param Cache $cache Cache instance for onboarding token.
* @param ?LoggerInterface $logger The logger, for debugging purposes.
*/
public function __construct( Cache $cache, LoggerInterface $logger = null ) {
$this->cache = $cache;
$this->logger = $logger ?: new NullLogger();
}
/** /**
* Returns a new Onboarding Url instance. * Returns a new Onboarding Url instance.
* *
* @param Cache $cache The cache object to store the URL.
* @param string $cache_key_prefix The prefix for the cache entry. * @param string $cache_key_prefix The prefix for the cache entry.
* @param int $user_id User ID to associate the link with. * @param int $user_id User ID to associate the link with.
* *
* @return OnboardingUrl * @return OnboardingUrl
*/ */
public function get( Cache $cache, string $cache_key_prefix, int $user_id ) : OnboardingUrl { public function get( string $cache_key_prefix, int $user_id ) : OnboardingUrl {
return new OnboardingUrl( $cache, $cache_key_prefix, $user_id ); return new OnboardingUrl( $this->cache, $cache_key_prefix, $user_id );
}
/**
* Validates the authentication token; if it's valid, the token is instantly
* invalidated (deleted), so it cannot be validated again.
*
* @param string $token The token to validate.
* @param int $user_id User ID who generated the token.
*
* @return bool True, if the token is valid. False otherwise.
*/
public function validate_token_and_delete( string $token, int $user_id ) : bool {
if ( $user_id < 1 || strlen( $token ) < 10 ) {
return false;
}
$log_token = ( (string) substr( $token, 0, 2 ) ) . '...' . ( (string) substr( $token, - 6 ) );
$this->logger->debug( 'Validating onboarding ppcpToken: ' . $log_token );
if ( OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id ) ) {
$this->logger->info( 'Validated onboarding ppcpToken: ' . $log_token );
return true;
}
if ( OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id ) ) {
// TODO: Do we need this here? Previous logic was to reload the page without doing anything in this case.
$this->logger->info( 'Validated previous token, silently redirecting: ' . $log_token );
return true;
}
$this->logger->error( 'Failed to validate onboarding ppcpToken: ' . $log_token );
return false;
} }
} }

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
@ -85,7 +86,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
} }
); );
$endpoint = $container->get( 'settings.switch-ui.endpoint' ); $endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null;
assert( $endpoint instanceof SwitchSettingsUiEndpoint ); assert( $endpoint instanceof SwitchSettingsUiEndpoint );
add_action( add_action(
@ -189,6 +190,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
} }
); );
add_action(
'admin_init',
static function () use ( $container ) : void {
$connection_handler = $container->get( 'settings.handler.connection-listener' );
assert( $connection_handler instanceof ConnectionListener );
// @phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce; sanitation done by the handler
$connection_handler->process( get_current_user_id(), $_GET );
}
);
return true; return true;
} }

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\WcGateway\Settings * @package WooCommerce\PayPalCommerce\WcGateway\Settings
*/ */
declare(strict_types=1); declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Settings; namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
@ -18,44 +18,46 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
*/ */
class Settings implements ContainerInterface { class Settings implements ContainerInterface {
const KEY = 'woocommerce-ppcp-settings'; const KEY = 'woocommerce-ppcp-settings';
const CONNECTION_TAB_ID = 'ppcp-connection'; const CONNECTION_TAB_ID = 'ppcp-connection';
const PAY_LATER_TAB_ID = 'ppcp-pay-later';
const PAY_LATER_TAB_ID = 'ppcp-pay-later';
/** /**
* The settings. * The settings.
* *
* @var array * @var array
*/ */
private $settings = array(); private array $settings = array();
/** /**
* The list of selected default button locations. * The list of selected default button locations.
* *
* @var string[] * @var string[]
*/ */
protected $default_button_locations; protected array $default_button_locations;
/** /**
* The list of selected default pay later button locations. * The list of selected default pay later button locations.
* *
* @var string[] * @var string[]
*/ */
protected $default_pay_later_button_locations; protected array $default_pay_later_button_locations;
/** /**
* The list of selected default pay later messaging locations. * The list of selected default pay later messaging locations.
* *
* @var string[] * @var string[]
*/ */
protected $default_pay_later_messaging_locations; protected array $default_pay_later_messaging_locations;
/** /**
* The default ACDC gateway title. * The default ACDC gateway title.
* *
* @var string * @var string
*/ */
protected $default_dcc_gateway_title; protected string $default_dcc_gateway_title;
/** /**
* A helper for mapping the new/old settings. * A helper for mapping the new/old settings.
@ -67,11 +69,17 @@ class Settings implements ContainerInterface {
/** /**
* Settings constructor. * Settings constructor.
* *
* @param string[] $default_button_locations The list of selected default button locations. * @param string[] $default_button_locations The list of selected default
* @param string $default_dcc_gateway_title The default ACDC gateway title. * button locations.
* @param string[] $default_pay_later_button_locations The list of selected default pay later button locations. * @param string $default_dcc_gateway_title The default ACDC gateway
* @param string[] $default_pay_later_messaging_locations The list of selected default pay later messaging locations. * title.
* @param SettingsMapHelper $settings_map_helper A helper for mapping the new/old settings. * @param string[] $default_pay_later_button_locations The list of selected default
* pay later button locations.
* @param string[] $default_pay_later_messaging_locations The list of selected default
* pay later messaging
* locations.
* @param SettingsMapHelper $settings_map_helper A helper for mapping the
* new/old settings.
*/ */
public function __construct( public function __construct(
array $default_button_locations, array $default_button_locations,
@ -90,10 +98,11 @@ class Settings implements ContainerInterface {
/** /**
* Returns the value for an id. * Returns the value for an id.
* *
* @param string $id The value identificator. * @throws NotFoundException When nothing was found.
*
* @param string $id The value identifier.
* *
* @return mixed * @return mixed
* @throws NotFoundException When nothing was found.
*/ */
public function get( $id ) { public function get( $id ) {
if ( ! $this->has( $id ) ) { if ( ! $this->has( $id ) ) {
@ -106,23 +115,24 @@ class Settings implements ContainerInterface {
/** /**
* Whether a value exists. * Whether a value exists.
* *
* @param string $id The value identificator. * @param string $id The value identifier.
* *
* @return bool * @return bool
*/ */
public function has( $id ) { public function has( string $id ) {
if ( $this->settings_map_helper->has_mapped_key( $id ) ) { if ( $this->settings_map_helper->has_mapped_key( $id ) ) {
return true; return true;
} }
$this->load(); $this->load();
return array_key_exists( $id, $this->settings ); return array_key_exists( $id, $this->settings );
} }
/** /**
* Sets a value. * Sets a value.
* *
* @param string $id The value identificator. * @param string $id The value identifier.
* @param mixed $value The value. * @param mixed $value The value.
*/ */
public function set( $id, $value ) { public function set( $id, $value ) {
@ -142,7 +152,7 @@ class Settings implements ContainerInterface {
* *
* @return bool * @return bool
*/ */
private function load(): bool { private function load() : bool {
if ( $this->settings ) { if ( $this->settings ) {
return false; return false;
} }
@ -175,6 +185,7 @@ class Settings implements ContainerInterface {
} }
$this->settings[ $key ] = $value; $this->settings[ $key ] = $value;
} }
return true; return true;
} }
} }

View file

@ -21,9 +21,14 @@ class TaskRegistrar implements TaskRegistrarInterface {
* *
* @throws RuntimeException If problem registering. * @throws RuntimeException If problem registering.
*/ */
public function register( array $tasks ): void { public function register( string $list_id, array $tasks ): void {
$task_lists = TaskLists::get_lists();
if ( ! isset( $task_lists[ $list_id ] ) ) {
return;
}
foreach ( $tasks as $task ) { foreach ( $tasks as $task ) {
$added_task = TaskLists::add_task( 'extended', $task ); $added_task = TaskLists::add_task( $list_id, $task );
if ( $added_task instanceof WP_Error ) { if ( $added_task instanceof WP_Error ) {
throw new RuntimeException( $added_task->get_error_message() ); throw new RuntimeException( $added_task->get_error_message() );
} }

View file

@ -15,11 +15,12 @@ use RuntimeException;
interface TaskRegistrarInterface { interface TaskRegistrarInterface {
/** /**
* Registers the tasks inside "Things to do next" WC section. * Registers the tasks inside the section with given list ID.
* *
* @param string $list_id The list ID.
* @param Task[] $tasks The list of tasks. * @param Task[] $tasks The list of tasks.
* @return void * @return void
* @throws RuntimeException If problem registering. * @throws RuntimeException If problem registering.
*/ */
public function register( array $tasks ): void; public function register( string $list_id, array $tasks ): void;
} }

View file

@ -653,7 +653,10 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$listener = $container->get( 'wcgateway.settings.listener' ); $listener = $container->get( 'wcgateway.settings.listener' );
assert( $listener instanceof SettingsListener ); assert( $listener instanceof SettingsListener );
$listener->listen_for_merchant_id(); $use_new_ui = $container->get( 'wcgateway.settings.admin-settings-enabled' );
if ( ! $use_new_ui ) {
$listener->listen_for_merchant_id();
}
try { try {
$listener->listen_for_vaulting_enabled(); $listener->listen_for_vaulting_enabled();
@ -880,10 +883,11 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
if ( empty( $simple_redirect_tasks ) ) { if ( empty( $simple_redirect_tasks ) ) {
return; return;
} }
$task_registrar = $container->get( 'wcgateway.settings.wc-tasks.task-registrar' ); $task_registrar = $container->get( 'wcgateway.settings.wc-tasks.task-registrar' );
assert( $task_registrar instanceof TaskRegistrarInterface ); assert( $task_registrar instanceof TaskRegistrarInterface );
$task_registrar->register( $simple_redirect_tasks ); $task_registrar->register( 'extended', $simple_redirect_tasks );
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
$logger->error( "Failed to create a task in the 'Things to do next' section of WC. " . $exception->getMessage() ); $logger->error( "Failed to create a task in the 'Things to do next' section of WC. " . $exception->getMessage() );
} }

View file

@ -179,7 +179,7 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog == == Changelog ==
= 2.9.5 - xxxx-xx-xx = = 2.9.5 - 2024-12-10 =
Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816
Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852
Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745