diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index cec44ced2..be80943bf 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient; use Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; @@ -39,6 +40,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencySupport; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository; @@ -190,6 +192,13 @@ return array( $subscription_helper ); }, + 'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint { + return new BillingAgreementsEndpoint( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.repository.paypal-request-id' => static function( ContainerInterface $container ) : PayPalRequestIdRepository { return new PayPalRequestIdRepository(); }, @@ -302,4 +311,7 @@ return array( 'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies { return new DccApplies(); }, + 'api.helpers.currency-support' => static function ( ContainerInterface $container ) : CurrencySupport { + return new CurrencySupport(); + }, ); diff --git a/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php b/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php new file mode 100644 index 000000000..3fd590367 --- /dev/null +++ b/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php @@ -0,0 +1,134 @@ +host = $host; + $this->bearer = $bearer; + $this->logger = $logger; + } + + /** + * Creates a billing agreement token. + * + * @param string $description The description. + * @param string $return_url The return URL. + * @param string $cancel_url The cancel URL. + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function create_token( string $description, string $return_url, string $cancel_url ): stdClass { + $data = array( + 'description' => $description, + 'payer' => array( + 'payment_method' => 'PAYPAL', + ), + 'plan' => array( + 'type' => 'MERCHANT_INITIATED_BILLING', + 'merchant_preferences' => array( + 'return_url' => $return_url, + 'cancel_url' => $cancel_url, + 'skip_shipping_address' => true, + ), + ), + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing-agreements/agreement-tokens'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $data ), + ); + $response = $this->request( $url, $args ); + + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to create a billing agreement token.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 201 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $json; + } + + /** + * Checks if reference transactions are enabled in account. + * + * @throws RuntimeException If the request fails (no auth, no connection, etc.). + */ + public function reference_transaction_enabled(): bool { + try { + $this->create_token( + 'Checking if reference transactions are enabled', + 'https://example.com/return', + 'https://example.com/cancel' + ); + + return true; + } catch ( PayPalApiException $exception ) { + return false; + } + } +} diff --git a/modules/ppcp-api-client/src/Helper/CurrencySupport.php b/modules/ppcp-api-client/src/Helper/CurrencySupport.php new file mode 100644 index 000000000..925ee4386 --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/CurrencySupport.php @@ -0,0 +1,70 @@ +supported_currencies, true ); + } + + /** + * Returns whether the current WC currency is supported. + * + * @return bool + */ + public function supports_wc_currency(): bool { + return $this->supports_currency( get_woocommerce_currency() ); + } +} diff --git a/modules/ppcp-button/resources/js/modules/ErrorHandler.js b/modules/ppcp-button/resources/js/modules/ErrorHandler.js index 8c4d7149f..9cd4ac174 100644 --- a/modules/ppcp-button/resources/js/modules/ErrorHandler.js +++ b/modules/ppcp-button/resources/js/modules/ErrorHandler.js @@ -73,11 +73,11 @@ class ErrorHandler { clear() { - if (! this.wrapper.classList.contains('woocommerce-error')) { + if (this.messagesList === null) { return; } - this.wrapper.classList.remove('woocommerce-error'); - this.wrapper.innerText = ''; + + this.messagesList.innerHTML = ''; } } diff --git a/modules/ppcp-compat/src/PPEC/PPECHelper.php b/modules/ppcp-compat/src/PPEC/PPECHelper.php index ec1543b70..09fca3c19 100644 --- a/modules/ppcp-compat/src/PPEC/PPECHelper.php +++ b/modules/ppcp-compat/src/PPEC/PPECHelper.php @@ -29,6 +29,15 @@ class PPECHelper { const PPEC_SETTINGS_OPTION_NAME = 'woocommerce_ppec_paypal_settings'; + /** + * Checks if the PayPal Express Checkout plugin was configured previously. + * + * @return bool + */ + public static function is_plugin_configured() { + return is_array( get_option( self::PPEC_SETTINGS_OPTION_NAME ) ); + } + /** * Checks if the PayPal Express Checkout plugin is active. * diff --git a/modules/ppcp-status-report/src/Renderer.php b/modules/ppcp-status-report/src/Renderer.php index 680006bcd..eefa70eea 100644 --- a/modules/ppcp-status-report/src/Renderer.php +++ b/modules/ppcp-status-report/src/Renderer.php @@ -37,9 +37,11 @@ class Renderer { foreach ( $items as $item ) { ?> - + + + - + get( 'wcgateway.settings' ); + assert( $settings instanceof ContainerInterface ); /* @var State $state The state. */ $state = $c->get( 'onboarding.state' ); @@ -51,44 +57,102 @@ class StatusReportModule implements ModuleInterface { /* @var Bearer $bearer The bearer. */ $bearer = $c->get( 'api.bearer' ); + $currency_support = $c->get( 'api.helpers.currency-support' ); + assert( $currency_support instanceof CurrencySupport ); + /* @var DccApplies $dcc_applies The ddc applies. */ $dcc_applies = $c->get( 'api.helpers.dccapplies' ); /* @var MessagesApply $messages_apply The messages apply. */ $messages_apply = $c->get( 'button.helper.messages-apply' ); + $last_webhook_storage = $c->get( 'webhook.last-webhook-storage' ); + assert( $last_webhook_storage instanceof WebhookInfoStorage ); + + $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); + assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + /* @var Renderer $renderer The renderer. */ $renderer = $c->get( 'status-report.renderer' ); + $had_ppec_plugin = PPECHelper::is_plugin_configured(); + $items = array( array( - 'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ), - 'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ), - 'value' => $this->onboarded( $bearer, $state ), + 'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Onboarded', + 'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $this->onboarded( $bearer, $state ) + ), ), array( - 'label' => esc_html__( 'Shop country code', 'woocommerce-paypal-payments' ), - 'description' => esc_html__( 'Country / State value on Settings / General / Store Address.', 'woocommerce-paypal-payments' ), - 'value' => wc_get_base_location()['country'], + 'label' => esc_html__( 'Shop country code', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Shop country code', + 'description' => esc_html__( 'Country / State value on Settings / General / Store Address.', 'woocommerce-paypal-payments' ), + 'value' => wc_get_base_location()['country'], ), array( - 'label' => esc_html__( 'PayPal card processing available in country', 'woocommerce-paypal-payments' ), - 'description' => esc_html__( 'Whether PayPal card processing is available in country or not.', 'woocommerce-paypal-payments' ), - 'value' => $dcc_applies->for_country_currency() - ? esc_html__( 'Yes', 'woocommerce-paypal-payments' ) - : esc_html__( 'No', 'woocommerce-paypal-payments' ), + 'label' => esc_html__( 'WooCommerce currency supported', 'woocommerce-paypal-payments' ), + 'exported_label' => 'WooCommerce currency supported', + 'description' => esc_html__( 'Whether PayPal supports the default store currency or not.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $currency_support->supports_wc_currency() + ), ), array( - 'label' => esc_html__( 'Pay Later messaging available in country', 'woocommerce-paypal-payments' ), - 'description' => esc_html__( 'Whether Pay Later is available in country or not.', 'woocommerce-paypal-payments' ), - 'value' => $messages_apply->for_country() - ? esc_html__( 'Yes', 'woocommerce-paypal-payments' ) - : esc_html__( 'No', 'woocommerce-paypal-payments' ), + 'label' => esc_html__( 'PayPal card processing available in country', 'woocommerce-paypal-payments' ), + 'exported_label' => 'PayPal card processing available in country', + 'description' => esc_html__( 'Whether PayPal card processing is available in country or not.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $dcc_applies->for_country_currency() + ), ), array( - 'label' => esc_html__( 'Vault enabled', 'woocommerce-paypal-payments' ), - 'description' => esc_html__( 'Whether vaulting is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ), - 'value' => $this->vault_enabled( $bearer ), + 'label' => esc_html__( 'Pay Later messaging available in country', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Pay Later messaging available in country', + 'description' => esc_html__( 'Whether Pay Later is available in country or not.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $messages_apply->for_country() + ), + ), + array( + 'label' => esc_html__( 'Webhook status', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Webhook status', + 'description' => esc_html__( 'Whether we received webhooks successfully.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( ! $last_webhook_storage->is_empty() ), + ), + array( + 'label' => esc_html__( 'Vault enabled', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Vault enabled', + 'description' => esc_html__( 'Whether vaulting is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $this->vault_enabled( $bearer ) + ), + ), + array( + 'label' => esc_html__( 'Logging enabled', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Logging enabled', + 'description' => esc_html__( 'Whether logging of plugin events and errors is enabled.', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $settings->has( 'logging_enabled' ) && $settings->get( 'logging_enabled' ) + ), + ), + array( + 'label' => esc_html__( 'Reference Transactions', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Reference Transactions', + 'description' => esc_html__( 'Whether Reference Transactions are enabled for the connected account', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $this->reference_transaction_enabled( $billing_agreements_endpoint ) + ), + ), + array( + 'label' => esc_html__( 'Used PayPal Checkout plugin', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Used PayPal Checkout plugin', + 'description' => esc_html__( 'Whether the PayPal Checkout Gateway plugin was configured previously or not', 'woocommerce-paypal-payments' ), + 'value' => $this->bool_to_html( + $had_ppec_plugin + ), ), ); @@ -112,37 +176,56 @@ class StatusReportModule implements ModuleInterface { * * @param Bearer $bearer The bearer. * @param State $state The state. - * @return string + * @return bool */ - private function onboarded( $bearer, $state ): string { + private function onboarded( Bearer $bearer, State $state ): bool { try { $token = $bearer->bearer(); } catch ( RuntimeException $exception ) { - return esc_html__( 'No', 'woocommerce-paypal-payments' ); + return false; } $current_state = $state->current_state(); - if ( $token->is_valid() && $current_state === $state::STATE_ONBOARDED ) { - return esc_html__( 'Yes', 'woocommerce-paypal-payments' ); - } - - return esc_html__( 'No', 'woocommerce-paypal-payments' ); + return $token->is_valid() && $current_state === $state::STATE_ONBOARDED; } /** * It returns whether vaulting is enabled or not. * * @param Bearer $bearer The bearer. - * @return string + * @return bool */ - private function vault_enabled( $bearer ) { + private function vault_enabled( Bearer $bearer ): bool { try { $token = $bearer->bearer(); - return $token->vaulting_available() - ? esc_html__( 'Yes', 'woocommerce-paypal-payments' ) - : esc_html__( 'No', 'woocommerce-paypal-payments' ); + return $token->vaulting_available(); } catch ( RuntimeException $exception ) { - return esc_html__( 'No', 'woocommerce-paypal-payments' ); + return false; } } + + /** + * Checks if reference transactions are enabled in account. + * + * @param BillingAgreementsEndpoint $billing_agreements_endpoint The endpoint. + */ + private function reference_transaction_enabled( BillingAgreementsEndpoint $billing_agreements_endpoint ): bool { + try { + return $billing_agreements_endpoint->reference_transaction_enabled(); + } catch ( RuntimeException $exception ) { + return false; + } + } + + /** + * Converts the bool value to "yes" icon or dash. + * + * @param bool $value The value. + * @return string + */ + private function bool_to_html( bool $value ): string { + return $value + ? '' + : ''; + } } diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index 8282ef3f6..13b0b5e54 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -32,11 +32,13 @@ return array( $factory = $container->get( 'api.factory.webhook' ); $endpoint = $container->get( 'api.endpoint.webhook' ); $rest_endpoint = $container->get( 'webhook.endpoint.controller' ); + $last_webhook_storage = $container->get( 'webhook.last-webhook-storage' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new WebhookRegistrar( $factory, $endpoint, $rest_endpoint, + $last_webhook_storage, $logger ); }, @@ -48,6 +50,7 @@ return array( $verify_request = ! defined( 'PAYPAL_WEBHOOK_REQUEST_VERIFICATION' ) || PAYPAL_WEBHOOK_REQUEST_VERIFICATION; $webhook_event_factory = $container->get( 'api.factory.webhook-event' ); $simulation = $container->get( 'webhook.status.simulation' ); + $last_webhook_storage = $container->get( 'webhook.last-webhook-storage' ); return new IncomingWebhookEndpoint( $webhook_endpoint, @@ -56,6 +59,7 @@ return array( $verify_request, $webhook_event_factory, $simulation, + $last_webhook_storage, ... $handler ); }, @@ -183,6 +187,13 @@ return array( ); }, + 'webhook.last-webhook-storage' => static function ( ContainerInterface $container ): WebhookInfoStorage { + return new WebhookInfoStorage( $container->get( 'webhook.last-webhook-storage.key' ) ); + }, + 'webhook.last-webhook-storage.key' => static function ( ContainerInterface $container ): string { + return 'ppcp-last-webhook'; + }, + 'webhook.module-url' => static function ( ContainerInterface $container ): string { return plugins_url( '/modules/ppcp-webhooks/', diff --git a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php index ed96e6974..9f8bbcf7a 100644 --- a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php +++ b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php @@ -76,6 +76,13 @@ class IncomingWebhookEndpoint { */ private $simulation; + /** + * The last webhook info storage. + * + * @var WebhookInfoStorage + */ + private $last_webhook_storage; + /** * IncomingWebhookEndpoint constructor. * @@ -85,6 +92,7 @@ class IncomingWebhookEndpoint { * @param bool $verify_request Whether requests need to be verified or not. * @param WebhookEventFactory $webhook_event_factory The webhook event factory. * @param WebhookSimulation $simulation The simulation handler. + * @param WebhookInfoStorage $last_webhook_storage The last webhook info storage. * @param RequestHandler ...$handlers The handlers, which process a request in the end. */ public function __construct( @@ -94,6 +102,7 @@ class IncomingWebhookEndpoint { bool $verify_request, WebhookEventFactory $webhook_event_factory, WebhookSimulation $simulation, + WebhookInfoStorage $last_webhook_storage, RequestHandler ...$handlers ) { @@ -103,6 +112,7 @@ class IncomingWebhookEndpoint { $this->logger = $logger; $this->verify_request = $verify_request; $this->webhook_event_factory = $webhook_event_factory; + $this->last_webhook_storage = $last_webhook_storage; $this->simulation = $simulation; } @@ -176,6 +186,8 @@ class IncomingWebhookEndpoint { public function handle_request( \WP_REST_Request $request ): \WP_REST_Response { $event = $this->event_from_request( $request ); + $this->last_webhook_storage->save( $event ); + if ( $this->simulation->is_simulation_event( $event ) ) { $this->logger->info( 'Received simulated webhook.' ); $this->simulation->receive( $event ); diff --git a/modules/ppcp-webhooks/src/WebhookInfoStorage.php b/modules/ppcp-webhooks/src/WebhookInfoStorage.php new file mode 100644 index 000000000..9fb9865f6 --- /dev/null +++ b/modules/ppcp-webhooks/src/WebhookInfoStorage.php @@ -0,0 +1,76 @@ +key = $key; + } + + /** + * Saves the info about webhook event. + * + * @param WebhookEvent $webhook_event The webhook event to save. + */ + public function save( WebhookEvent $webhook_event ): void { + $data = array( + 'id' => $webhook_event->id(), + 'received_time' => time(), + ); + + update_option( $this->key, $data ); + } + + /** + * Returns the stored data or null. + */ + public function get_data(): ?array { + $data = get_option( $this->key ); + if ( ! $data || ! is_array( $data ) ) { + return null; + } + return $data; + } + + /** + * Checks if there is any stored data. + */ + public function is_empty(): bool { + $data = get_option( $this->key ); + return ! $data || ! is_array( $data ); + } + + /** + * Removes the stored data. + */ + public function clear(): void { + delete_option( $this->key ); + } +} diff --git a/modules/ppcp-webhooks/src/WebhookRegistrar.php b/modules/ppcp-webhooks/src/WebhookRegistrar.php index e737e293e..c16aa9acd 100644 --- a/modules/ppcp-webhooks/src/WebhookRegistrar.php +++ b/modules/ppcp-webhooks/src/WebhookRegistrar.php @@ -44,6 +44,13 @@ class WebhookRegistrar { */ private $rest_endpoint; + /** + * The last webhook info storage. + * + * @var WebhookInfoStorage + */ + private $last_webhook_storage; + /** * The logger. * @@ -57,19 +64,22 @@ class WebhookRegistrar { * @param WebhookFactory $webhook_factory The Webhook factory. * @param WebhookEndpoint $endpoint The Webhook endpoint. * @param IncomingWebhookEndpoint $rest_endpoint The WordPress Rest API endpoint. + * @param WebhookInfoStorage $last_webhook_storage The last webhook info storage. * @param LoggerInterface $logger The logger. */ public function __construct( WebhookFactory $webhook_factory, WebhookEndpoint $endpoint, IncomingWebhookEndpoint $rest_endpoint, + WebhookInfoStorage $last_webhook_storage, LoggerInterface $logger ) { - $this->webhook_factory = $webhook_factory; - $this->endpoint = $endpoint; - $this->rest_endpoint = $rest_endpoint; - $this->logger = $logger; + $this->webhook_factory = $webhook_factory; + $this->endpoint = $endpoint; + $this->rest_endpoint = $rest_endpoint; + $this->last_webhook_storage = $last_webhook_storage; + $this->logger = $logger; } /** @@ -92,6 +102,7 @@ class WebhookRegistrar { self::KEY, $created->to_array() ); + $this->last_webhook_storage->clear(); $this->logger->info( 'Webhooks subscribed.' ); return true; } catch ( RuntimeException $error ) { @@ -120,6 +131,7 @@ class WebhookRegistrar { if ( $success ) { delete_option( self::KEY ); + $this->last_webhook_storage->clear(); $this->logger->info( 'Webhooks deleted.' ); } return $success; diff --git a/tests/PHPUnit/StatusReport/RendererTest.php b/tests/PHPUnit/StatusReport/RendererTest.php index 0b83d86ff..4be0c713f 100644 --- a/tests/PHPUnit/StatusReport/RendererTest.php +++ b/tests/PHPUnit/StatusReport/RendererTest.php @@ -12,20 +12,23 @@ class RendererTest extends TestCase { $items = [ [ - 'label' => 'Foo', + 'label' => 'Translated Foo', + 'exported_label' => 'Foo', 'description' => 'Bar', 'value' => 'Baz' ], ]; when('esc_attr')->returnArg(); + when('wp_kses_post')->returnArg(); when('wc_help_tip')->returnArg(); $testee = new Renderer(); $result = $testee->render('Some title here', $items); self::assertStringContainsString('

Some title here

', $result); - self::assertStringContainsString('Foo', $result); + self::assertStringContainsString('data-export-label="Foo"', $result); + self::assertStringContainsString('Translated Foo', $result); self::assertStringContainsString('Bar', $result); self::assertStringContainsString('Baz', $result); }