Merge pull request #2974 from woocommerce/PCP-4092-fix-settings-errors-on-trunk

Fix settings errors on trunk (4092)
This commit is contained in:
Emili Castells 2025-01-10 11:05:29 +01:00 committed by GitHub
commit fa5c325224
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 201 additions and 84 deletions

View file

@ -116,19 +116,13 @@ return array(
return 'WC-'; return 'WC-';
}, },
'api.bearer' => static function ( ContainerInterface $container ): Bearer { 'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$cache = new Cache( 'ppcp-paypal-bearer' );
$key = $container->get( 'api.key' );
$secret = $container->get( 'api.secret' );
$host = $container->get( 'api.host' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$settings = $container->get( 'wcgateway.settings' );
return new PayPalBearer( return new PayPalBearer(
$cache, $container->get( 'api.paypal-bearer-cache' ),
$host, $container->get( 'api.host' ),
$key, $container->get( 'api.key' ),
$secret, $container->get( 'api.secret' ),
$logger, $container->get( 'woocommerce.logger.woocommerce' ),
$settings $container->get( 'wcgateway.settings' )
); );
}, },
'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint { 'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint {
@ -840,6 +834,9 @@ return array(
$container->get( 'wcgateway.settings' ) $container->get( 'wcgateway.settings' )
); );
}, },
'api.paypal-bearer-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-bearer' );
},
'api.client-credentials-cache' => static function( ContainerInterface $container ): Cache { 'api.client-credentials-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-client-credentials-cache' ); return new Cache( 'ppcp-client-credentials-cache' );
}, },

View file

@ -20,6 +20,8 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
/** /**
* Class ApiModule * Class ApiModule
@ -103,6 +105,34 @@ class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
2 2
); );
/**
* Flushes the API client caches.
*/
add_action(
'woocommerce_paypal_payments_flush_api_cache',
static function () use ( $c ) {
$caches = array(
'api.paypal-bearer-cache' => array(
PayPalBearer::CACHE_KEY,
),
'api.client-credentials-cache' => array(
SdkClientToken::CACHE_KEY,
),
);
foreach ( $caches as $cache_id => $keys ) {
$cache = $c->get( $cache_id );
assert( $cache instanceof Cache );
foreach ( $keys as $key ) {
if ( $cache->has( $key ) ) {
$cache->delete( $key );
}
}
}
}
);
return true; return true;
} }
} }

View file

@ -16,7 +16,7 @@ import { todosData } from '../../../data/settings/tab-overview-todos-data';
const TabOverview = () => { const TabOverview = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false ); const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchantFeatures } = useMerchantInfo(); const { merchant, merchantFeatures } = useMerchantInfo();
const { refreshFeatureStatuses, setActiveModal } = const { refreshFeatureStatuses, setActiveModal } =
useDispatch( STORE_NAME ); useDispatch( STORE_NAME );

View file

@ -1,36 +1,46 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { CommonHooks } from '../../../../../../data'; import { CommonHooks } from '../../../../../../data';
import { Title } from '../../../../../ReusableComponents/SettingsBlocks';
const HooksTableBlock = () => { const HooksTableBlock = () => {
const { webhooks } = CommonHooks.useWebhooks(); const { webhooks } = CommonHooks.useWebhooks();
const { url, events } = webhooks;
if ( ! url || ! events?.length ) {
return <div>...</div>;
}
return ( return (
<table className="ppcp-r-table"> <>
<thead> <WebhookUrl url={ url } />
<tr> <WebhookEvents events={ events } />
<th className="ppcp-r-table__hooks-url"> </>
{ __( 'URL', 'woocommerce-paypal-payments' ) } );
</th> };
<th className="ppcp-r-table__hooks-events">
{ __( const WebhookUrl = ( { url } ) => {
'Tracked events', return (
'woocommerce-paypal-payments' <div>
) } <Title>
</th> { __( 'Notification URL', 'woocommerce-paypal-payments' ) }
</tr> </Title>
</thead> <p>{ url }</p>
<tbody> </div>
<tr> );
<td className="ppcp-r-table__hooks-url"> };
{ webhooks?.url }
</td> const WebhookEvents = ( { events } ) => {
<td return (
className="ppcp-r-table__hooks-events" <div>
dangerouslySetInnerHTML={ { __html: webhooks?.events } } <Title>
></td> { __( 'Subscribed Events', 'woocommerce-paypal-payments' ) }
</tr> </Title>
</tbody> <ul>
</table> { events.map( ( event, index ) => (
<li key={ index }>{ event }</li>
) ) }
</ul>
</div>
); );
}; };

View file

@ -42,10 +42,7 @@ const Troubleshooting = ( { updateFormValue, settings } ) => {
<SettingsBlock> <SettingsBlock>
<Header> <Header>
<Title> <Title>
{ __( { __( 'Webhooks', 'woocommerce-paypal-payments' ) }
'Subscribed PayPal webhooks',
'woocommerce-paypal-payments'
) }
</Title> </Title>
<Description> <Description>
{ __( { __(

View file

@ -23,7 +23,9 @@ export const resolvers = {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } ); const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
result.data = { ...result.data, ...webhooks.data }; if ( webhooks.success && webhooks.data ) {
result.webhooks = webhooks.data;
}
yield dispatch( STORE_NAME ).hydrate( result ); yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true ); yield dispatch( STORE_NAME ).setIsReady( true );

View file

@ -156,6 +156,7 @@ return array(
$page_id, $page_id,
$container->get( 'settings.service.onboarding-url-manager' ), $container->get( 'settings.service.onboarding-url-manager' ),
$container->get( 'settings.service.authentication_manager' ), $container->get( 'settings.service.authentication_manager' ),
$container->get( 'http.redirector' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },

View file

@ -9,12 +9,14 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint; namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use stdClass; use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use WP_REST_Response; use WP_REST_Response;
use WP_REST_Server; use WP_REST_Server;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use stdClass;
/** /**
* Class WebhookSettingsEndpoint * Class WebhookSettingsEndpoint
@ -60,8 +62,9 @@ class WebhookSettingsEndpoint extends RestEndpoint {
/** /**
* WebhookSettingsEndpoint constructor. * WebhookSettingsEndpoint constructor.
* *
* @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook endpoint URL. * @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks. * endpoint URL.
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks.
* @param WebhookSimulation $webhook_simulation A service that allows webhook simulations. * @param WebhookSimulation $webhook_simulation A service that allows webhook simulations.
*/ */
public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) { public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) {
@ -73,7 +76,7 @@ class WebhookSettingsEndpoint extends RestEndpoint {
/** /**
* Configure REST API routes. * Configure REST API routes.
*/ */
public function register_routes() { public function register_routes() : void {
register_rest_route( register_rest_route(
$this->namespace, $this->namespace,
'/' . $this->rest_base, '/' . $this->rest_base,
@ -114,27 +117,28 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* *
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function get_webhooks(): WP_REST_Response { public function get_webhooks() : WP_REST_Response {
try { $webhooks = $this->get_webhook_data();
$webhook_list = ( $this->webhook_endpoint->list() )[0]; if ( ! $webhooks ) {
$webhook_events = array_map( return $this->return_error( 'No webhooks found.' );
static function ( stdClass $webhook ) {
return strtolower( $webhook->name );
},
$webhook_list->event_types()
);
return $this->return_success(
array(
'webhooks' => array(
'url' => $webhook_list->url(),
'events' => implode( ', ', $webhook_events ),
),
)
);
} catch ( \Exception $error ) {
return $this->return_error( 'Problem while fetching webhooks data' );
} }
try {
$webhook_url = $webhooks->url();
$webhook_events = array_map(
static fn( stdClass $webhooks ) => strtolower( $webhooks->name ),
$webhooks->event_types()
);
} catch ( Throwable $error ) {
return $this->return_error( $error->getMessage() );
}
return $this->return_success(
array(
'url' => $webhook_url,
'events' => $webhook_events,
)
);
} }
/** /**
@ -142,10 +146,11 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* *
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function resubscribe_webhooks(): WP_REST_Response { public function resubscribe_webhooks() : WP_REST_Response {
if ( ! $this->webhook_registrar->register() ) { if ( ! $this->webhook_registrar->register() ) {
return $this->return_error( 'Webhook subscription failed.' ); return $this->return_error( 'Webhook subscription failed.' );
} }
return $this->get_webhooks(); return $this->get_webhooks();
} }
@ -154,9 +159,10 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* *
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function simulate_webhooks_start(): WP_REST_Response { public function simulate_webhooks_start() : WP_REST_Response {
try { try {
$this->webhook_simulation->start(); $this->webhook_simulation->start();
return $this->return_success( array() ); return $this->return_success( array() );
} catch ( \Exception $error ) { } catch ( \Exception $error ) {
return $this->return_error( $error->getMessage() ); return $this->return_error( $error->getMessage() );
@ -168,18 +174,32 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* *
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function check_simulated_webhook_state(): WP_REST_Response { public function check_simulated_webhook_state() : WP_REST_Response {
try { try {
$state = $this->webhook_simulation->get_state(); $state = $this->webhook_simulation->get_state();
return $this->return_success( return $this->return_success(
array( array( 'state' => $state )
'state' => $state,
)
); );
} catch ( \Exception $error ) { } catch ( \Exception $error ) {
return $this->return_error( $error->getMessage() ); return $this->return_error( $error->getMessage() );
} }
} }
/**
* Retrieves the Webhooks API response object.
*
* @return Webhook|null The webhook data instance, or null.
*/
private function get_webhook_data() : ?Webhook {
try {
$api_response = $this->webhook_endpoint->list();
return $api_response[0] ?? null;
} catch ( Throwable $error ) {
return null;
}
}
} }

View file

@ -15,6 +15,8 @@ use RuntimeException;
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger; use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\Http\RedirectorInterface;
/** /**
* Provides a listener that handles merchant-connection requests. * Provides a listener that handles merchant-connection requests.
@ -46,6 +48,14 @@ class ConnectionListener {
*/ */
private AuthenticationManager $authentication_manager; private AuthenticationManager $authentication_manager;
/**
* A redirector-instance to redirect the merchant after authentication.
*
*
* @var RedirectorInterface
*/
private RedirectorInterface $redirector;
/** /**
* Logger instance, mainly used for debugging purposes. * Logger instance, mainly used for debugging purposes.
* *
@ -66,17 +76,20 @@ class ConnectionListener {
* @param string $settings_page_id Current plugin settings page ID. * @param string $settings_page_id Current plugin settings page ID.
* @param OnboardingUrlManager $url_manager Get OnboardingURL instances. * @param OnboardingUrlManager $url_manager Get OnboardingURL instances.
* @param AuthenticationManager $authentication_manager Authentication manager service. * @param AuthenticationManager $authentication_manager Authentication manager service.
* @param RedirectorInterface $redirector Redirect-handler.
* @param ?LoggerInterface $logger The logger, for debugging purposes. * @param ?LoggerInterface $logger The logger, for debugging purposes.
*/ */
public function __construct( public function __construct(
string $settings_page_id, string $settings_page_id,
OnboardingUrlManager $url_manager, OnboardingUrlManager $url_manager,
AuthenticationManager $authentication_manager, AuthenticationManager $authentication_manager,
RedirectorInterface $redirector,
LoggerInterface $logger = null LoggerInterface $logger = null
) { ) {
$this->settings_page_id = $settings_page_id; $this->settings_page_id = $settings_page_id;
$this->url_manager = $url_manager; $this->url_manager = $url_manager;
$this->authentication_manager = $authentication_manager; $this->authentication_manager = $authentication_manager;
$this->redirector = $redirector;
$this->logger = $logger ?: new NullLogger(); $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().
@ -115,6 +128,8 @@ class ConnectionListener {
} catch ( \Exception $e ) { } catch ( \Exception $e ) {
$this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() ); $this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() );
} }
$this->redirect_after_authentication();
} }
/** /**
@ -125,7 +140,7 @@ class ConnectionListener {
* *
* @return bool True, if the request contains valid connection details. * @return bool True, if the request contains valid connection details.
*/ */
protected function is_valid_request( array $request ) : bool { private function is_valid_request( array $request ) : bool {
if ( $this->user_id < 1 || ! $this->settings_page_id ) { if ( $this->user_id < 1 || ! $this->settings_page_id ) {
return false; return false;
} }
@ -157,7 +172,7 @@ class ConnectionListener {
* @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys, * @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
* or an empty array on failure. * or an empty array on failure.
*/ */
protected function extract_data( array $request ) : array { private function extract_data( array $request ) : array {
$this->logger->info( 'Extracting connection data from request...' ); $this->logger->info( 'Extracting connection data from request...' );
$merchant_id = $this->get_merchant_id_from_request( $request ); $merchant_id = $this->get_merchant_id_from_request( $request );
@ -173,6 +188,17 @@ class ConnectionListener {
); );
} }
/**
* Redirects the browser page at the end of the authentication flow.
*
* @return void
*/
private function redirect_after_authentication() : void {
$redirect_url = $this->get_onboarding_redirect_url();
$this->redirector->redirect( $redirect_url );
}
/** /**
* Returns the sanitized connection token from the incoming request. * Returns the sanitized connection token from the incoming request.
* *
@ -180,7 +206,7 @@ class ConnectionListener {
* *
* @return string The sanitized token, or an empty string. * @return string The sanitized token, or an empty string.
*/ */
protected function get_token_from_request( array $request ) : string { private function get_token_from_request( array $request ) : string {
return $this->sanitize_string( $request['ppcpToken'] ?? '' ); return $this->sanitize_string( $request['ppcpToken'] ?? '' );
} }
@ -191,7 +217,7 @@ class ConnectionListener {
* *
* @return string The sanitized merchant ID, or an empty string. * @return string The sanitized merchant ID, or an empty string.
*/ */
protected function get_merchant_id_from_request( array $request ) : string { private function get_merchant_id_from_request( array $request ) : string {
return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' ); return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' );
} }
@ -206,7 +232,7 @@ class ConnectionListener {
* *
* @return string The sanitized merchant email, or an empty string. * @return string The sanitized merchant email, or an empty string.
*/ */
protected function get_merchant_email_from_request( array $request ) : string { private function get_merchant_email_from_request( array $request ) : string {
return $this->sanitize_merchant_email( $request['merchantId'] ?? '' ); return $this->sanitize_merchant_email( $request['merchantId'] ?? '' );
} }
@ -217,7 +243,7 @@ class ConnectionListener {
* *
* @return string Sanitized value. * @return string Sanitized value.
*/ */
protected function sanitize_string( string $value ) : string { private function sanitize_string( string $value ) : string {
return trim( sanitize_text_field( wp_unslash( $value ) ) ); return trim( sanitize_text_field( wp_unslash( $value ) ) );
} }
@ -228,7 +254,22 @@ class ConnectionListener {
* *
* @return string Sanitized email address. * @return string Sanitized email address.
*/ */
protected function sanitize_merchant_email( string $email ) : string { private function sanitize_merchant_email( string $email ) : string {
return sanitize_text_field( str_replace( ' ', '+', $email ) ); return sanitize_text_field( str_replace( ' ', '+', $email ) );
} }
/**
* Returns the URL opened at the end of onboarding.
*
* @return string
*/
private function get_onboarding_redirect_url() : string {
/**
* The URL opened at the end of onboarding after saving the merchant ID/email.
*/
return apply_filters(
'woocommerce_paypal_payments_onboarding_redirect_url',
admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=' . Settings::CONNECTION_TAB_ID )
);
}
} }

View file

@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger; use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
/** /**
* Class that manages the connection to PayPal. * Class that manages the connection to PayPal.
@ -115,6 +116,12 @@ class AuthenticationManager {
* modules to clean up merchant-related details, such as eligibility flags. * modules to clean up merchant-related details, such as eligibility flags.
*/ */
do_action( 'woocommerce_paypal_payments_merchant_disconnected' ); do_action( 'woocommerce_paypal_payments_merchant_disconnected' );
/**
* Request to flush caches after disconnecting the merchant. While there
* is no need for it here, it's good house-keeping practice to clean up.
*/
do_action( 'woocommerce_paypal_payments_flush_api_cache' );
} }
/** /**
@ -399,12 +406,24 @@ class AuthenticationManager {
if ( $this->common_settings->is_merchant_connected() ) { if ( $this->common_settings->is_merchant_connected() ) {
$this->logger->info( 'Merchant successfully connected to PayPal' ); $this->logger->info( 'Merchant successfully connected to PayPal' );
/**
* Request to flush caches before authenticating the merchant, to
* ensure the new merchant does not use stale data from previous
* connections.
*/
do_action( 'woocommerce_paypal_payments_flush_api_cache' );
/** /**
* Broadcast that the plugin connected to a new PayPal merchant account. * Broadcast that the plugin connected to a new PayPal merchant account.
* This is the right time to initialize merchant relative flags for the * This is the right time to initialize merchant relative flags for the
* first time. * first time.
*/ */
do_action( 'woocommerce_paypal_payments_authenticated_merchant' ); do_action( 'woocommerce_paypal_payments_authenticated_merchant' );
/**
* Subscribe the new merchant to relevant PayPal webhooks.
*/
do_action( WebhookRegistrar::EVENT_HOOK );
} }
} }
} }