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-';
},
'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(
$cache,
$host,
$key,
$secret,
$logger,
$settings
$container->get( 'api.paypal-bearer-cache' ),
$container->get( 'api.host' ),
$container->get( 'api.key' ),
$container->get( 'api.secret' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.settings' )
);
},
'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint {
@ -840,6 +834,9 @@ return array(
$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 {
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\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
/**
* Class ApiModule
@ -103,6 +105,34 @@ class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
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;
}
}

View file

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

View file

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

View file

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

View file

@ -23,7 +23,9 @@ export const resolvers = {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
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 ).setIsReady( true );

View file

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

View file

@ -9,12 +9,14 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use Throwable;
use WP_REST_Response;
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
@ -60,8 +62,9 @@ class WebhookSettingsEndpoint extends RestEndpoint {
/**
* WebhookSettingsEndpoint constructor.
*
* @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook endpoint URL.
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks.
* @param WebhookEndpoint $webhook_endpoint A list of subscribed webhooks and a webhook
* endpoint URL.
* @param WebhookRegistrar $webhook_registrar A service that allows resubscribing webhooks.
* @param WebhookSimulation $webhook_simulation A service that allows webhook simulations.
*/
public function __construct( WebhookEndpoint $webhook_endpoint, WebhookRegistrar $webhook_registrar, WebhookSimulation $webhook_simulation ) {
@ -73,7 +76,7 @@ class WebhookSettingsEndpoint extends RestEndpoint {
/**
* Configure REST API routes.
*/
public function register_routes() {
public function register_routes() : void {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
@ -114,27 +117,28 @@ class WebhookSettingsEndpoint extends RestEndpoint {
*
* @return WP_REST_Response
*/
public function get_webhooks(): WP_REST_Response {
try {
$webhook_list = ( $this->webhook_endpoint->list() )[0];
$webhook_events = array_map(
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' );
public function get_webhooks() : WP_REST_Response {
$webhooks = $this->get_webhook_data();
if ( ! $webhooks ) {
return $this->return_error( 'No webhooks found.' );
}
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
*/
public function resubscribe_webhooks(): WP_REST_Response {
public function resubscribe_webhooks() : WP_REST_Response {
if ( ! $this->webhook_registrar->register() ) {
return $this->return_error( 'Webhook subscription failed.' );
}
return $this->get_webhooks();
}
@ -154,9 +159,10 @@ class WebhookSettingsEndpoint extends RestEndpoint {
*
* @return WP_REST_Response
*/
public function simulate_webhooks_start(): WP_REST_Response {
public function simulate_webhooks_start() : WP_REST_Response {
try {
$this->webhook_simulation->start();
return $this->return_success( array() );
} catch ( \Exception $error ) {
return $this->return_error( $error->getMessage() );
@ -168,18 +174,32 @@ class WebhookSettingsEndpoint extends RestEndpoint {
*
* @return WP_REST_Response
*/
public function check_simulated_webhook_state(): WP_REST_Response {
public function check_simulated_webhook_state() : WP_REST_Response {
try {
$state = $this->webhook_simulation->get_state();
return $this->return_success(
array(
'state' => $state,
)
array( 'state' => $state )
);
} catch ( \Exception $error ) {
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\OnboardingUrlManager;
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.
@ -46,6 +48,14 @@ class ConnectionListener {
*/
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.
*
@ -66,17 +76,20 @@ class ConnectionListener {
* @param string $settings_page_id Current plugin settings page ID.
* @param OnboardingUrlManager $url_manager Get OnboardingURL instances.
* @param AuthenticationManager $authentication_manager Authentication manager service.
* @param RedirectorInterface $redirector Redirect-handler.
* @param ?LoggerInterface $logger The logger, for debugging purposes.
*/
public function __construct(
string $settings_page_id,
OnboardingUrlManager $url_manager,
AuthenticationManager $authentication_manager,
RedirectorInterface $redirector,
LoggerInterface $logger = null
) {
$this->settings_page_id = $settings_page_id;
$this->url_manager = $url_manager;
$this->authentication_manager = $authentication_manager;
$this->redirector = $redirector;
$this->logger = $logger ?: new NullLogger();
// Initialize as "guest", the real ID is provided via process().
@ -115,6 +128,8 @@ class ConnectionListener {
} catch ( \Exception $e ) {
$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.
*/
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 ) {
return false;
}
@ -157,7 +172,7 @@ class ConnectionListener {
* @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys,
* 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...' );
$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.
*
@ -180,7 +206,7 @@ class ConnectionListener {
*
* @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'] ?? '' );
}
@ -191,7 +217,7 @@ class ConnectionListener {
*
* @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'] ?? '' );
}
@ -206,7 +232,7 @@ class ConnectionListener {
*
* @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'] ?? '' );
}
@ -217,7 +243,7 @@ class ConnectionListener {
*
* @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 ) ) );
}
@ -228,7 +254,22 @@ class ConnectionListener {
*
* @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 ) );
}
/**
* 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\WooCommerce\Logging\Logger\NullLogger;
use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
/**
* Class that manages the connection to PayPal.
@ -115,6 +116,12 @@ class AuthenticationManager {
* modules to clean up merchant-related details, such as eligibility flags.
*/
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() ) {
$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.
* This is the right time to initialize merchant relative flags for the
* first time.
*/
do_action( 'woocommerce_paypal_payments_authenticated_merchant' );
/**
* Subscribe the new merchant to relevant PayPal webhooks.
*/
do_action( WebhookRegistrar::EVENT_HOOK );
}
}
}