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
strategy:
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 }}
steps:
@ -30,6 +30,7 @@ jobs:
run: vendor/bin/phpunit
- name: Psalm
if: ${{ matrix.php-versions == '7.4' }}
run: ./vendor/bin/psalm --show-info=false --threads=8 --diff
- name: Run PHPCS

View file

@ -1,6 +1,6 @@
*** 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 - 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

View file

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

6
composer.lock generated
View file

@ -5541,8 +5541,8 @@
"aliases": [],
"minimum-stability": "dev",
"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-lowest": false,
@ -5550,7 +5550,7 @@
"php": "^7.4 | ^8.0",
"ext-json": "*"
},
"platform-dev": [],
"platform-dev": {},
"platform-overrides": {
"php": "7.4"
},

View file

@ -5,66 +5,153 @@
* @package WooCommerce\PayPalCommerce\Compat
*/
declare(strict_types=1);
declare( strict_types = 1 );
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 {
/**
* A list of mapped settings.
* A list of settings maps containing mapping definitions.
*
* @var SettingsMap[]
*/
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.
*
* @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 ) {
$this->validate_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.
* @return ?mixed the mapped value or Null if it doesn't exist.
* @param SettingsMap[] $settings_map The settings map to validate.
* @throws RuntimeException When an old key has multiple mappings.
*/
public function mapped_value( string $key ) {
if ( ! $this->has_mapped_key( $key ) ) {
return null;
}
protected function validate_settings_map( array $settings_map ) : void {
$seen_keys = array();
foreach ( $this->settings_map as $settings_map ) {
$mapped_key = array_search( $key, $settings_map->get_map(), true );
$new_settings = $settings_map->get_model()->to_array();
if ( ! empty( $new_settings[ $mapped_key ] ) ) {
return $new_settings[ $mapped_key ];
foreach ( $settings_map as $settings_map_instance ) {
foreach ( $settings_map_instance->get_map() as $old_key => $new_key ) {
if ( isset( $seen_keys[ $old_key ] ) ) {
throw new RuntimeException( "Duplicate mapping for legacy key '$old_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.
* @return bool true if the given key exists in the new settings, otherwise false.
* @param string $old_key The key from the legacy settings.
*
* @return mixed|null The value of the mapped setting, or null if not found.
*/
public function has_mapped_key( string $key ) : bool {
foreach ( $this->settings_map as $settings_map ) {
if ( in_array( $key, $settings_map->get_map(), true ) ) {
return true;
}
public function mapped_value( string $old_key ) {
$this->ensure_map_initialized();
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 {
display: flex;
position: relative;
pointer-events: none;
*:not(a){
pointer-events: none;
}
a {
pointer-events: all;
}
}
&__title {

View file

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

View file

@ -11,7 +11,6 @@ const PaymentMethodIcons = ( props ) => {
<PaymentMethodIcon type="discover" icons={ props.icons } />
<PaymentMethodIcon type="apple-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="bancontact" icons={ props.icons } />
</div>

View file

@ -66,7 +66,7 @@ const AcdcFlow = ( {
description={ sprintf(
// 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'
),
'https://www.paypal.com/us/business/paypal-business-fees'
@ -256,7 +256,7 @@ const AcdcFlow = ( {
description={ sprintf(
// 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'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '

View file

@ -60,7 +60,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// 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'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -158,7 +158,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// 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'
),
'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 BcdcFlow from './BcdcFlow';
import { Button } from '@wordpress/components';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import { pricesBasedDescription } from './pricesBasedDescription';
const WelcomeDocs = ( {
useAcdc,
@ -10,15 +11,6 @@ const WelcomeDocs = ( {
storeCountry,
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 (
<div className="ppcp-r-welcome-docs">
<h2 className="ppcp-r-welcome-docs__title">
@ -41,10 +33,14 @@ const WelcomeDocs = ( {
storeCurrency={ storeCurrency }
/>
) }
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ { __html: pricesBasedDescription } }
></p>
{ storeCountry in countryPriceInfo && (
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ {
__html: pricesBasedDescription,
} }
></p>
) }
</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 Navigation from './Components/Navigation';
import { useEffect } from '@wordpress/element';
const Onboarding = () => {
const { step, setStep, flags } = OnboardingHooks.useSteps();
const Steps = getSteps( flags );
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 handlePrev = () => setStep( currentStep.prevStep );
const handleExit = () => {

View file

@ -1,10 +1,12 @@
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox';
import { CommonHooks, OnboardingHooks } from '../../../data';
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';
@ -16,15 +18,6 @@ const StepPaymentMethods = ( {} ) => {
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 (
<div className="ppcp-r-page-optional-payment-methods">
<OnboardingHeader
@ -67,12 +60,14 @@ const StepPaymentMethods = ( {} ) => {
type="radio"
></SelectBox>
</SelectBoxWrapper>
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ {
__html: pricesBasedDescription,
} }
></p>
{ storeCountry in countryPriceInfo && (
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ {
__html: pricesBasedDescription,
} }
></p>
) }
</div>
</div>
);

View file

@ -1,4 +1,4 @@
import { useMemo } from '@wordpress/element';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
@ -11,6 +11,20 @@ import SettingsScreen from './SettingsScreen';
const Settings = () => {
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', {
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\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
@ -138,11 +139,24 @@ return array(
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 {
return new Cache( 'ppcp-paypal-signup-link' );
},
'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 {
// Define available environments.
@ -162,7 +176,6 @@ return array(
$generators[ $environment ] = new ConnectionUrlGenerator(
$config['partner_referrals'],
$container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'settings.service.signup-link-cache' ),
$environment,
$container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'woocommerce.logger.woocommerce' )

View file

@ -206,12 +206,6 @@ class CommonRestEndpoint extends RestEndpoint {
$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;
}

View file

@ -12,8 +12,18 @@ namespace WooCommerce\PayPalCommerce\Settings\Handler;
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
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.
*
@ -35,6 +45,13 @@ class ConnectionHandler {
*/
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.
*
@ -48,11 +65,13 @@ class ConnectionHandler {
* @param string $settings_page_id Current plugin settings page ID.
* @param CommonSettings $settings Access to saved connection details.
* @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 = $settings;
$this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger();
// Initialize as "guest", the real ID is provided via process().
$this->user_id = 0;
@ -71,9 +90,20 @@ class ConnectionHandler {
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 );
if ( ! $data ) {
return;
}
$this->logger->info( 'Found merchant data in request', $data );
$this->store_data(
$data['use_sandbox'],
$data['is_sandbox'],
$data['merchant_id'],
$data['merchant_email']
);
@ -96,32 +126,38 @@ class ConnectionHandler {
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;
}
/**
* 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
* not possible to verify the same token twice.
* @param array $request The full request details.
*
* @param string $token The token to verify.
*
* @return bool True, if the token is valid.
* @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
* or an empty array on failure.
*/
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 {
// $merchant_id: $request['merchantIdInPayPal'] (!), sanitize: sanitize_text_field( wp_unslash() )
// $merchant_email: $request['merchantId'] (!), sanitize: $this->sanitize_merchant_email()
$this->logger->info( 'Extracting connection data from request...' );
$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(
'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.
*
* @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 ) );
}
/**
* 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;
/**
* The cache
*
* @var Cache
*/
protected Cache $cache;
/**
* Manages access to OnboardingUrl instances
*
@ -63,7 +56,7 @@ class ConnectionUrlGenerator {
*
* @var LoggerInterface
*/
private $logger;
private LoggerInterface $logger;
/**
* Constructor for the ConnectionUrlGenerator class.
@ -72,8 +65,6 @@ class ConnectionUrlGenerator {
*
* @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation.
* @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.
* ['production'|'sandbox'].
* @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances.
@ -82,14 +73,12 @@ class ConnectionUrlGenerator {
public function __construct(
PartnerReferrals $partner_referrals,
PartnerReferralsData $referrals_data,
Cache $cache,
string $environment,
OnboardingUrlManager $url_manager,
?LoggerInterface $logger = null
) {
$this->partner_referrals = $partner_referrals;
$this->referrals_data = $referrals_data;
$this->cache = $cache;
$this->environment = $environment;
$this->url_manager = $url_manager;
$this->logger = $logger ?: new NullLogger();
@ -119,7 +108,7 @@ class ConnectionUrlGenerator {
public function generate( array $products = array() ) : string {
$cache_key = $this->cache_key( $products );
$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 );
if ( $cached_url ) {

View file

@ -9,7 +9,9 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
// TODO: Replace the OnboardingUrl with a new implementation for this module.
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.
*/
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.
*
* @param Cache $cache The cache object to store the URL.
* @param string $cache_key_prefix The prefix for the cache entry.
* @param int $user_id User ID to associate the link with.
*
* @return OnboardingUrl
*/
public function get( Cache $cache, string $cache_key_prefix, int $user_id ) : OnboardingUrl {
return new OnboardingUrl( $cache, $cache_key_prefix, $user_id );
public function get( string $cache_key_prefix, int $user_id ) : OnboardingUrl {
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\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
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 );
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;
}

View file

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

View file

@ -21,9 +21,14 @@ class TaskRegistrar implements TaskRegistrarInterface {
*
* @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 ) {
$added_task = TaskLists::add_task( 'extended', $task );
$added_task = TaskLists::add_task( $list_id, $task );
if ( $added_task instanceof WP_Error ) {
throw new RuntimeException( $added_task->get_error_message() );
}

View file

@ -15,11 +15,12 @@ use RuntimeException;
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.
* @return void
* @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' );
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 {
$listener->listen_for_vaulting_enabled();
@ -880,10 +883,11 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
if ( empty( $simple_redirect_tasks ) ) {
return;
}
$task_registrar = $container->get( 'wcgateway.settings.wc-tasks.task-registrar' );
assert( $task_registrar instanceof TaskRegistrarInterface );
$task_registrar->register( $simple_redirect_tasks );
$task_registrar->register( 'extended', $simple_redirect_tasks );
} catch ( Exception $exception ) {
$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 ==
= 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 - 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