From 31bd95487095f73ba5a8a0478f2a1abd3a808443 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 20 Aug 2021 17:13:37 +0300 Subject: [PATCH 001/101] Add 3d secure contingency settings --- modules/ppcp-wc-gateway/services.php | 47 +++++++++++++++++++ .../src/Settings/class-settingsrenderer.php | 41 ---------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f072e347c..c05de6dcb 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -1804,6 +1804,53 @@ return array( ), 'gateway' => 'dcc', ), + '3d_secure_heading' => array( + 'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-heading', + 'description' => wp_kses_post( + sprintf( + // translators: %1$s and %2$s is a link tag. + __( + '3D Secure benefits cardholders and merchants by providing + an additional layer of verification using Verified by Visa, + MasterCard SecureCode and American Express SafeKey. + %1$sLearn more about 3D Secure.%2$s', + 'woocommerce-paypal-payments' + ), + '', + '' + ) + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array( + 'dcc', + ), + 'gateway' => 'dcc', + ), + '3d_secure_contingency' => array( + 'title' => __( 'Contingency for 3D Secure', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'SCA_WHEN_REQUIRED', + 'desc_tip' => false, + 'options' => array( + 'SCA_WHEN_REQUIRED' => __( 'When required', 'woocommerce-paypal-payments' ), + '3D_SECURE' => __( 'Always', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array( + 'dcc', + ), + 'gateway' => 'dcc', + ), ); if ( ! defined( 'PPCP_FLAG_SUBSCRIPTION' ) || ! PPCP_FLAG_SUBSCRIPTION ) { unset( $fields['vault_enabled'] ); diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php index b08533e05..f4e35499a 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php @@ -399,8 +399,6 @@ class SettingsRenderer { if ( $this->dcc_applies->for_country_currency() ) { if ( State::STATE_ONBOARDED > $this->state->current_state() ) { $this->render_dcc_onboarding_info(); - } elseif ( State::STATE_ONBOARDED === $this->state->current_state() && $this->dcc_product_status->dcc_is_active() ) { - $this->render_3d_secure_info(); } elseif ( ! $this->dcc_product_status->dcc_is_active() ) { $this->render_dcc_not_active_yet(); } @@ -450,45 +448,6 @@ class SettingsRenderer { - - - -

- ', - '' - ) - ); - ?> -

- - - Date: Fri, 20 Aug 2021 17:47:32 +0300 Subject: [PATCH 002/101] Send 3ds contingency in js --- .../js/modules/Renderer/CreditCardRenderer.js | 2 +- .../ppcp-button/src/Assets/class-smartbutton.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index dc5035383..a4663ac29 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -104,7 +104,7 @@ class CreditCardRenderer { const vault = document.getElementById('ppcp-credit-card-vault') ? document.getElementById('ppcp-credit-card-vault').checked : save_card; hostedFields.submit({ - contingencies: ['SCA_WHEN_REQUIRED'], + contingencies: [this.defaultConfig.hosted_fields.contingency], vault: vault }).then((payload) => { payload.orderID = payload.orderId; diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 5f3fc0244..a3585567d 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -601,6 +601,19 @@ class SmartButton implements SmartButtonInterface { return $this->subscription_helper->cart_contains_subscription(); } + /** + * Retrieves the 3D Secure contingency settings. + * + * @return string + */ + private function get_3ds_contingency(): string { + if ( $this->settings->has( '3d_secure_contingency' ) ) { + return $this->settings->get( '3d_secure_contingency' ); + } + + return 'SCA_WHEN_REQUIRED'; + } + /** * The localized data for the smart button. * @@ -677,6 +690,7 @@ class SmartButton implements SmartButtonInterface { ), ), 'valid_cards' => $this->dcc_applies->valid_cards(), + 'contingency' => $this->get_3ds_contingency(), ), 'messages' => $this->message_values(), 'labels' => array( From b988e08c660684bb5e43f2a6b8fbb7ecbe08d053 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 23 Aug 2021 12:27:12 +0300 Subject: [PATCH 003/101] Add "No 3DS" option --- .../js/modules/Renderer/CreditCardRenderer.js | 10 +++++++--- modules/ppcp-wc-gateway/services.php | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index a4663ac29..b65e033aa 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -103,10 +103,14 @@ class CreditCardRenderer { const save_card = this.defaultConfig.save_card ? true : false; const vault = document.getElementById('ppcp-credit-card-vault') ? document.getElementById('ppcp-credit-card-vault').checked : save_card; - hostedFields.submit({ - contingencies: [this.defaultConfig.hosted_fields.contingency], + const contingency = this.defaultConfig.hosted_fields.contingency; + const hostedFieldsData = { vault: vault - }).then((payload) => { + }; + if (contingency !== 'NO_3D_SECURE') { + hostedFieldsData.contingencies = [contingency]; + } + hostedFields.submit(hostedFieldsData).then((payload) => { payload.orderID = payload.orderId; this.spinner.unblock(); return contextConfig.onApprove(payload); diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index c05de6dcb..538363d32 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -1840,8 +1840,9 @@ return array( 'default' => 'SCA_WHEN_REQUIRED', 'desc_tip' => false, 'options' => array( - 'SCA_WHEN_REQUIRED' => __( 'When required', 'woocommerce-paypal-payments' ), - '3D_SECURE' => __( 'Always', 'woocommerce-paypal-payments' ), + 'NO_3D_SECURE' => __( 'No 3D Secure (transaction will be denied if 3D Secure is required)', 'woocommerce-paypal-payments' ), + 'SCA_WHEN_REQUIRED' => __( '3D Secure when required', 'woocommerce-paypal-payments' ), + '3D_SECURE' => __( 'Always trigger 3D Secure', 'woocommerce-paypal-payments' ), ), 'screens' => array( State::STATE_ONBOARDED, From ac482cc44d3382eb041d4123d9520598d4133a52 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 23 Aug 2021 18:39:07 +0300 Subject: [PATCH 004/101] Do not send payee email if have id --- modules/ppcp-api-client/src/Entity/class-payee.php | 6 +++--- .../ppcp-api-client/src/Factory/class-payeefactory.php | 9 ++------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/class-payee.php b/modules/ppcp-api-client/src/Entity/class-payee.php index a719b10be..c49f13dfb 100644 --- a/modules/ppcp-api-client/src/Entity/class-payee.php +++ b/modules/ppcp-api-client/src/Entity/class-payee.php @@ -68,11 +68,11 @@ class Payee { * @return array */ public function to_array(): array { - $data = array( - 'email_address' => $this->email(), - ); + $data = array(); if ( $this->merchant_id ) { $data['merchant_id'] = $this->merchant_id(); + } else { + $data['email_address'] = $this->email(); } return $data; } diff --git a/modules/ppcp-api-client/src/Factory/class-payeefactory.php b/modules/ppcp-api-client/src/Factory/class-payeefactory.php index ddd489e1d..bb89d07b0 100644 --- a/modules/ppcp-api-client/src/Factory/class-payeefactory.php +++ b/modules/ppcp-api-client/src/Factory/class-payeefactory.php @@ -26,13 +26,8 @@ class PayeeFactory { * @throws RuntimeException When JSON object is malformed. */ public function from_paypal_response( \stdClass $data ) { - if ( ! isset( $data->email_address ) ) { - throw new RuntimeException( - __( 'No email for payee given.', 'woocommerce-paypal-payments' ) - ); - } - + $email = ( isset( $data->email_address ) ) ? $data->email_address : ''; $merchant_id = ( isset( $data->merchant_id ) ) ? $data->merchant_id : ''; - return new Payee( $data->email_address, $merchant_id ); + return new Payee( $email, $merchant_id ); } } From bc141ae7fbacc04a43c37bc03d7c78be41ad3e80 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 13:43:00 +0300 Subject: [PATCH 005/101] Refactor gateway page check --- modules/ppcp-wc-gateway/services.php | 44 ++++++++++++++++--- .../src/Gateway/class-paypalgateway.php | 18 +++++--- .../src/Settings/class-sectionsrenderer.php | 24 +++++++--- .../src/Settings/class-settingslistener.php | 31 +++++++------ .../src/Settings/class-settingsrenderer.php | 33 ++++++-------- 5 files changed, 99 insertions(+), 51 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f072e347c..10b10e838 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -5,6 +5,8 @@ * @package WooCommerce\PayPalCommerce\WcGateway */ +// phpcs:disable WordPress.Security.NonceVerification.Recommended + declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway; @@ -48,6 +50,7 @@ return array( $state = $container->get( 'onboarding.state' ); $transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' ); $subscription_helper = $container->get( 'subscription.helper' ); + $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); return new PayPalGateway( $settings_renderer, $order_processor, @@ -58,7 +61,8 @@ return array( $refund_processor, $state, $transaction_url_provider, - $subscription_helper + $subscription_helper, + $page_id ); }, 'wcgateway.credit-card-gateway' => static function ( $container ): CreditCardGateway { @@ -100,6 +104,33 @@ return array( $settings = $container->get( 'wcgateway.settings' ); return new DisableGateways( $session_handler, $settings ); }, + + 'wcgateway.is-wc-payments-page' => static function ( $container ): bool { + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; + $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; + return 'wc-settings' === $page && 'checkout' === $tab; + }, + + 'wcgateway.is-ppcp-settings-page' => static function ( $container ): bool { + if ( ! $container->get( 'wcgateway.is-wc-payments-page' ) ) { + return false; + } + + $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : ''; + return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID ), true ); + }, + + 'wcgateway.current-ppcp-settings-page-id' => static function ( $container ): string { + if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) { + return ''; + } + + $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : ''; + $ppcp_tab = isset( $_GET[ SectionsRenderer::KEY ] ) ? sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) : ''; + + return $ppcp_tab ? $ppcp_tab : $section; + }, + 'wcgateway.settings' => static function ( $container ): Settings { return new Settings(); }, @@ -113,7 +144,7 @@ return array( return new AuthorizeOrderActionNotice(); }, 'wcgateway.settings.sections-renderer' => static function ( $container ): SectionsRenderer { - return new SectionsRenderer(); + return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ) ); }, 'wcgateway.settings.status' => static function ( $container ): SettingsStatus { $settings = $container->get( 'wcgateway.settings' ); @@ -127,6 +158,7 @@ return array( $messages_apply = $container->get( 'button.helper.messages-apply' ); $dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' ); $settings_status = $container->get( 'wcgateway.settings.status' ); + $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); return new SettingsRenderer( $settings, $state, @@ -134,7 +166,8 @@ return array( $dcc_applies, $messages_apply, $dcc_product_status, - $settings_status + $settings_status, + $page_id ); }, 'wcgateway.settings.listener' => static function ( $container ): SettingsListener { @@ -144,7 +177,8 @@ return array( $state = $container->get( 'onboarding.state' ); $cache = new Cache( 'ppcp-paypal-bearer' ); $bearer = $container->get( 'api.bearer' ); - return new SettingsListener( $settings, $fields, $webhook_registrar, $cache, $state, $bearer ); + $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); + return new SettingsListener( $settings, $fields, $webhook_registrar, $cache, $state, $bearer, $page_id ); }, 'wcgateway.order-processor' => static function ( $container ): OrderProcessor { @@ -646,7 +680,7 @@ return array( 'label' => sprintf( // translators: %1$s and %2$s are the opening and closing of HTML tag. __( 'Enable saved cards and subscription features on your store. To use vaulting features, you must %1$senable vaulting on your account%2$s.', 'woocommerce-paypal-payments' ), - '', diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php index e1aadeb83..4fae00409 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php @@ -103,6 +103,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ private $onboarded; + /** + * ID of the current PPCP gateway settings page, or empty if it is not such page. + * + * @var string + */ + protected $page_id; + /** * PayPalGateway constructor. * @@ -116,6 +123,7 @@ class PayPalGateway extends \WC_Payment_Gateway { * @param State $state The state. * @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order. * @param SubscriptionHelper $subscription_helper The subscription helper. + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. */ public function __construct( SettingsRenderer $settings_renderer, @@ -127,7 +135,8 @@ class PayPalGateway extends \WC_Payment_Gateway { RefundProcessor $refund_processor, State $state, TransactionUrlProvider $transaction_url_provider, - SubscriptionHelper $subscription_helper + SubscriptionHelper $subscription_helper, + string $page_id ) { $this->id = self::ID; @@ -139,6 +148,7 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->session_handler = $session_handler; $this->refund_processor = $refund_processor; $this->transaction_url_provider = $transaction_url_provider; + $this->page_id = $page_id; $this->onboarded = $state->current_state() === State::STATE_ONBOARDED; if ( $this->onboarded ) { @@ -332,8 +342,7 @@ class PayPalGateway extends \WC_Payment_Gateway { */ private function is_credit_card_tab() : bool { return is_admin() - && isset( $_GET[ SectionsRenderer::KEY ] ) - && CreditCardGateway::ID === sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ); + && CreditCardGateway::ID === $this->page_id; } @@ -345,8 +354,7 @@ class PayPalGateway extends \WC_Payment_Gateway { private function is_paypal_tab() : bool { return ! $this->is_credit_card_tab() && is_admin() - && isset( $_GET['section'] ) - && self::ID === sanitize_text_field( wp_unslash( $_GET['section'] ) ); + && self::ID === $this->page_id; } // phpcs:enable WordPress.Security.NonceVerification.Recommended diff --git a/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php index ee000f03d..ea9e693f5 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php @@ -19,15 +19,29 @@ class SectionsRenderer { const KEY = 'ppcp-tab'; + /** + * ID of the current PPCP gateway settings page, or empty if it is not such page. + * + * @var string + */ + protected $page_id; + + /** + * SectionsRenderer constructor. + * + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. + */ + public function __construct( string $page_id ) { + $this->page_id = $page_id; + } + /** * Whether the sections tab should be rendered. * * @return bool */ public function should_render() : bool { - - global $current_section; - return PayPalGateway::ID === $current_section; + return ! empty( $this->page_id ); } /** @@ -38,8 +52,6 @@ class SectionsRenderer { return; } - //phpcs:ignore WordPress.Security.NonceVerification.Recommended - $current = ! isset( $_GET[ self::KEY ] ) ? PayPalGateway::ID : sanitize_text_field( wp_unslash( $_GET[ self::KEY ] ) ); $sections = array( PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), @@ -51,7 +63,7 @@ class SectionsRenderer { foreach ( $sections as $id => $label ) { $url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&' . self::KEY . '=' . $id ); - echo '
  • ' . esc_html( $label ) . ' ' . ( end( $array_keys ) === $id ? '' : '|' ) . '
  • '; + echo '
  • ' . esc_html( $label ) . ' ' . ( end( $array_keys ) === $id ? '' : '|' ) . '
  • '; } echo '
    '; diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php index 6c0f9463f..dd3e89aa1 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php @@ -67,6 +67,13 @@ class SettingsListener { */ private $bearer; + /** + * ID of the current PPCP gateway settings page, or empty if it is not such page. + * + * @var string + */ + protected $page_id; + /** * SettingsListener constructor. * @@ -76,6 +83,7 @@ class SettingsListener { * @param Cache $cache The Cache. * @param State $state The state. * @param Bearer $bearer The bearer. + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. */ public function __construct( Settings $settings, @@ -83,7 +91,8 @@ class SettingsListener { WebhookRegistrar $webhook_registrar, Cache $cache, State $state, - Bearer $bearer + Bearer $bearer, + string $page_id ) { $this->settings = $settings; @@ -92,6 +101,7 @@ class SettingsListener { $this->cache = $cache; $this->state = $state; $this->bearer = $bearer; + $this->page_id = $page_id; } /** @@ -218,7 +228,7 @@ class SettingsListener { $settings = $this->read_active_credentials_from_settings( $settings ); - if ( ! isset( $_GET[ SectionsRenderer::KEY ] ) || PayPalGateway::ID === $_GET[ SectionsRenderer::KEY ] ) { + if ( PayPalGateway::ID === $this->page_id ) { $settings['enabled'] = isset( $_POST['woocommerce_ppcp-gateway_enabled'] ) && 1 === absint( $_POST['woocommerce_ppcp-gateway_enabled'] ); $this->maybe_register_webhooks( $settings ); @@ -313,17 +323,13 @@ class SettingsListener { } if ( 'dcc' === $config['gateway'] - && ( - ! isset( $_GET[ SectionsRenderer::KEY ] ) - || sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) !== CreditCardGateway::ID - ) + && CreditCardGateway::ID !== $this->page_id ) { continue; } if ( 'paypal' === $config['gateway'] - && isset( $_GET[ SectionsRenderer::KEY ] ) - && sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) !== PayPalGateway::ID + && PayPalGateway::ID !== $this->page_id ) { continue; } @@ -406,14 +412,7 @@ class SettingsListener { * phpcs:disable WordPress.Security.NonceVerification.Missing * phpcs:disable WordPress.Security.NonceVerification.Recommended */ - if ( - ! isset( $_REQUEST['section'] ) - || ! in_array( - sanitize_text_field( wp_unslash( $_REQUEST['section'] ) ), - array( 'ppcp-gateway', 'ppcp-credit-card-gateway' ), - true - ) - ) { + if ( empty( $this->page_id ) ) { return false; } diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php index b08533e05..73dcde5d6 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use Woocommerce\PayPalCommerce\WcGateway\Helper\DccProductStatus; use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; @@ -72,6 +73,13 @@ class SettingsRenderer { */ private $dcc_product_status; + /** + * ID of the current PPCP gateway settings page, or empty if it is not such page. + * + * @var string + */ + protected $page_id; + /** * SettingsRenderer constructor. * @@ -82,6 +90,7 @@ class SettingsRenderer { * @param MessagesApply $messages_apply Whether messages can be shown. * @param DccProductStatus $dcc_product_status The product status. * @param SettingsStatus $settings_status The Settings status helper. + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. */ public function __construct( ContainerInterface $settings, @@ -90,7 +99,8 @@ class SettingsRenderer { DccApplies $dcc_applies, MessagesApply $messages_apply, DccProductStatus $dcc_product_status, - SettingsStatus $settings_status + SettingsStatus $settings_status, + string $page_id ) { $this->settings = $settings; @@ -100,6 +110,7 @@ class SettingsRenderer { $this->messages_apply = $messages_apply; $this->dcc_product_status = $dcc_product_status; $this->settings_status = $settings_status; + $this->page_id = $page_id; } /** @@ -166,21 +177,7 @@ class SettingsRenderer { * @return bool Whether is PayPal checkout screen or not. */ private function is_paypal_checkout_screen(): bool { - $current_screen = get_current_screen(); - //phpcs:disable WordPress.Security.NonceVerification.Recommended - //phpcs:disable WordPress.Security.NonceVerification.Missing - if ( isset( $current_screen->id ) && 'woocommerce_page_wc-settings' === $current_screen->id - && isset( $_GET['section'] ) && 'ppcp-gateway' === $_GET['section'] ) { - - if ( isset( $_GET['ppcp-tab'] ) && 'ppcp-gateway' !== $_GET['ppcp-tab'] ) { - return false; - } - - return true; - } - //phpcs:enable - - return false; + return PayPalGateway::ID === $this->page_id; } /** @@ -317,9 +314,7 @@ class SettingsRenderer { */ public function render() { - //phpcs:disable WordPress.Security.NonceVerification.Recommended - //phpcs:disable WordPress.Security.NonceVerification.Missing - $is_dcc = isset( $_GET[ SectionsRenderer::KEY ] ) && CreditCardGateway::ID === sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ); + $is_dcc = CreditCardGateway::ID === $this->page_id; //phpcs:enable WordPress.Security.NonceVerification.Recommended //phpcs:enable WordPress.Security.NonceVerification.Missing $nonce = wp_create_nonce( SettingsListener::NONCE ); From 45bef752fdfbfb2a760b17c4b1030967a9e0818c Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 13:43:25 +0300 Subject: [PATCH 006/101] Simplify if --- .../ppcp-wc-gateway/src/Settings/class-settingsrenderer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php index 73dcde5d6..a08591379 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php @@ -542,8 +542,8 @@ class SettingsRenderer { return false; } - return $this->is_paypal_checkout_screen() && $this->paypal_vaulting_is_enabled() - || $this->is_paypal_checkout_screen() && $this->settings_status->pay_later_messaging_is_enabled(); + return $this->is_paypal_checkout_screen() + && ( $this->paypal_vaulting_is_enabled() || $this->settings_status->pay_later_messaging_is_enabled() ); } } From 3ebd3b29bda646b9e22ff0a6492f8fce07dc55f8 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 13:46:54 +0300 Subject: [PATCH 007/101] Fix return value --- .../ppcp-wc-gateway/src/Gateway/class-paypalgateway.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php index 4fae00409..da7597abb 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php @@ -395,14 +395,18 @@ class PayPalGateway extends \WC_Payment_Gateway { * * @param string $key The option key. * @param string $value The option value. - * @return bool|void + * @return bool was anything saved? */ public function update_option( $key, $value = '' ) { - parent::update_option( $key, $value ); + $ret = parent::update_option( $key, $value ); if ( 'enabled' === $key ) { $this->config->set( 'enabled', 'yes' === $value ); $this->config->persist(); + + return true; } + + return $ret; } } From f60f6b0556da3d7573e04efdbc154a9fa265fb87 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 13:50:31 +0300 Subject: [PATCH 008/101] Show CC gateway in admin payments list allow changing order, enabling/disabling, opening settings via it --- .../src/Gateway/class-creditcardgateway.php | 80 +++++++++++++++++-- .../src/class-wcgatewaymodule.php | 51 +----------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php index 86604af4b..a49d0ed39 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-creditcardgateway.php @@ -197,13 +197,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ public function init_form_fields() { $this->form_fields = array( - 'enabled' => array( - 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), - 'type' => 'checkbox', - 'label' => __( 'Enable Credit Card Payments', 'woocommerce-paypal-payments' ), - 'default' => 'no', - ), - 'ppcp' => array( + 'ppcp' => array( 'type' => 'ppcp', ), ); @@ -218,6 +212,20 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { remove_action( 'gettext', 'replace_credit_card_cvv_label' ); } + /** + * Renders the settings. + * + * @return string + */ + public function generate_ppcp_html(): string { + + ob_start(); + $this->settings_renderer->render(); + $content = ob_get_contents(); + ob_end_clean(); + return $content; + } + /** * Replace WooCommerce credit card field label. * @@ -314,7 +322,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { * @return bool */ public function is_available() : bool { - return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ); + return $this->is_enabled(); } @@ -349,4 +357,60 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { return parent::get_transaction_url( $order ); } + + /** + * Initialize settings for WC. + * + * @return void + */ + public function init_settings() { + parent::init_settings(); + + // looks like in some cases WC uses this field instead of get_option. + $this->enabled = $this->is_enabled(); + } + + /** + * Get the option value for WC. + * + * @param string $key The option key. + * @param mixed $empty_value Value when empty. + * @return mixed + */ + public function get_option( $key, $empty_value = null ) { + if ( 'enabled' === $key ) { + return $this->is_enabled(); + } + + return parent::get_option( $key, $empty_value ); + } + + /** + * Handle update of WC settings. + * + * @param string $key The option key. + * @param string $value The option value. + * @return bool was anything saved? + */ + public function update_option( $key, $value = '' ) { + $ret = parent::update_option( $key, $value ); + + if ( 'enabled' === $key ) { + $this->config->set( 'dcc_enabled', 'yes' === $value ); + $this->config->persist(); + + return true; + } + + return $ret; + } + + /** + * Returns if the gateway is enabled. + * + * @return bool + */ + private function is_enabled(): bool { + return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ); + } } diff --git a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php index 981491351..1dfffc810 100644 --- a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php +++ b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php @@ -58,7 +58,6 @@ class WcGatewayModule implements ModuleInterface { $this->register_order_functionality( $container ); $this->register_columns( $container ); $this->register_checkout_paypal_address_preset( $container ); - $this->ajax_gateway_enabler( $container ); add_action( 'woocommerce_sections_checkout', @@ -149,50 +148,6 @@ class WcGatewayModule implements ModuleInterface { ); } - /** - * Adds the functionality to listen to the ajax enable gateway switch. - * - * @param ContainerInterface $container The container. - */ - private function ajax_gateway_enabler( ContainerInterface $container ) { - add_action( - 'wp_ajax_woocommerce_toggle_gateway_enabled', - static function () use ( $container ) { - if ( - ! current_user_can( 'manage_woocommerce' ) - || ! check_ajax_referer( - 'woocommerce-toggle-payment-gateway-enabled', - 'security' - ) - || ! isset( $_POST['gateway_id'] ) - ) { - return; - } - - /** - * The settings. - * - * @var Settings $settings - */ - $settings = $container->get( 'wcgateway.settings' ); - $key = PayPalGateway::ID === $_POST['gateway_id'] ? 'enabled' : ''; - if ( CreditCardGateway::ID === $_POST['gateway_id'] ) { - $key = 'dcc_enabled'; - } - if ( ! $key ) { - return; - } - $enabled = $settings->has( $key ) ? $settings->get( $key ) : false; - if ( ! $enabled ) { - return; - } - $settings->set( $key, false ); - $settings->persist(); - }, - 9 - ); - } - /** * Registers the payment gateways. * @@ -206,16 +161,12 @@ class WcGatewayModule implements ModuleInterface { $methods[] = $container->get( 'wcgateway.paypal-gateway' ); $dcc_applies = $container->get( 'api.helpers.dccapplies' ); - $screen = ! function_exists( 'get_current_screen' ) ? (object) array( 'id' => 'front' ) : get_current_screen(); - if ( ! $screen ) { - $screen = (object) array( 'id' => 'front' ); - } /** * The DCC Applies object. * * @var DccApplies $dcc_applies */ - if ( 'woocommerce_page_wc-settings' !== $screen->id && $dcc_applies->for_country_currency() ) { + if ( $dcc_applies->for_country_currency() ) { $methods[] = $container->get( 'wcgateway.credit-card-gateway' ); } return (array) $methods; From e6c0182dd41b70a161025a3a5532e43881943b04 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 17:28:15 +0300 Subject: [PATCH 009/101] Update tests --- .../WcGateway/Gateway/WcGatewayTest.php | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 9cef49c38..65ed9776c 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -68,7 +68,8 @@ class WcGatewayTest extends TestCase $refundProcessor, $state, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); expect('wc_get_order') @@ -116,7 +117,8 @@ class WcGatewayTest extends TestCase $refundProcessor, $state, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); expect('wc_get_order') @@ -181,7 +183,8 @@ class WcGatewayTest extends TestCase $refundProcessor, $state, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); expect('wc_get_order') @@ -254,7 +257,8 @@ class WcGatewayTest extends TestCase $refundProcessor, $state, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); $this->assertTrue($testee->capture_authorized_payment($wcOrder)); @@ -311,7 +315,8 @@ class WcGatewayTest extends TestCase $refundProcessor, $state, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); $this->assertTrue($testee->capture_authorized_payment($wcOrder)); @@ -362,12 +367,13 @@ class WcGatewayTest extends TestCase $refundProcessor, $state, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); $this->assertFalse($testee->capture_authorized_payment($wcOrder)); } - + /** * @dataProvider dataForTestNeedsSetup */ @@ -390,7 +396,7 @@ class WcGatewayTest extends TestCase ->andReturn($currentState); $transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class); $subscriptionHelper = Mockery::mock(SubscriptionHelper::class); - + $testee = new PayPalGateway( $settingsRenderer, $orderProcessor, @@ -401,9 +407,10 @@ class WcGatewayTest extends TestCase $refundProcessor, $onboardingState, $transactionUrlProvider, - $subscriptionHelper + $subscriptionHelper, + PayPalGateway::ID ); - + $this->assertSame($needSetup, $testee->needs_setup()); } @@ -424,7 +431,7 @@ class WcGatewayTest extends TestCase ], ]; } - + public function dataForTestNeedsSetup(): array { return [ From 510e2b05b5d6411577336043221b31debf57163f Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 20:10:57 +0300 Subject: [PATCH 010/101] Simplify mock --- tests/PHPUnit/TestCase.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index 4f4e6a532..e51ff6828 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -3,9 +3,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce; +use function Brain\Monkey\Functions\when; use function Brain\Monkey\setUp; use function Brain\Monkey\tearDown; -use function Brain\Monkey\Functions\expect; use Mockery; class TestCase extends \PHPUnit\Framework\TestCase @@ -13,9 +13,9 @@ class TestCase extends \PHPUnit\Framework\TestCase public function setUp(): void { parent::setUp(); - expect('__')->andReturnUsing(function (string $text) { - return $text; - }); + + when('__')->returnArg(); + setUp(); } From f71b18f358872c64b55d50e18742bd835b174ba0 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 20:40:09 +0300 Subject: [PATCH 011/101] Extract bootstrap of modules --- bootstrap.php | 53 +++++++++++++++++++++++++++++++++ modules.php | 29 ++++++++++++++++++ phpcs.xml.dist | 2 ++ woocommerce-paypal-payments.php | 32 ++++---------------- 4 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 bootstrap.php create mode 100644 modules.php diff --git a/bootstrap.php b/bootstrap.php new file mode 100644 index 000000000..c3d706960 --- /dev/null +++ b/bootstrap.php @@ -0,0 +1,53 @@ +setup(); + }, + $modules + ); + + $provider = new CompositeCachingServiceProvider( $providers ); + $proxy_container = new ProxyContainer(); + $container = new DelegatingContainer( $provider, $proxy_container ); + $app_container = new CachingContainer( + new CompositeContainer( + array_merge( + $additional_containers, + array( $container ) + ) + ) + ); + $proxy_container->setInnerContainer( $app_container ); + + foreach ( $modules as $module ) { + /* @var $module ModuleInterface module */ + $module->run( $app_container ); + } + + return $app_container; +}; diff --git a/modules.php b/modules.php new file mode 100644 index 000000000..8bffd9d34 --- /dev/null +++ b/modules.php @@ -0,0 +1,29 @@ +./src ./modules ./woocommerce-paypal-payments.php + ./modules.php + ./bootstrap.php */node_modules/* */vendor/* diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index cfdef7375..9a7b910d0 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -19,12 +19,6 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce; -use Dhii\Container\CachingContainer; -use Dhii\Container\CompositeCachingServiceProvider; -use Dhii\Container\DelegatingContainer; -use Dhii\Container\ProxyContainer; -use Dhii\Modular\Module\ModuleInterface; - define( 'PAYPAL_API_URL', 'https://api.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' ); define( 'PAYPAL_INTEGRATION_DATE', '2020-10-15' ); @@ -45,6 +39,8 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); * Initialize the plugin and its modules. */ function init() { + $root_dir = __DIR__; + if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } @@ -72,30 +68,12 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); static $initialized; if ( ! $initialized ) { - $modules = array( new PluginModule() ); - foreach ( glob( plugin_dir_path( __FILE__ ) . 'modules/*/module.php' ) as $module_file ) { - $modules[] = ( require $module_file )(); - } - $providers = array(); + $bootstrap = require "$root_dir/bootstrap.php"; - // Use this filter to add custom module or remove some of existing ones. - // Modules able to access container, add services and modify existing ones. - $modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules ); + $app_container = $bootstrap( $root_dir ); - foreach ( $modules as $module ) { - /* @var $module ModuleInterface module */ - $providers[] = $module->setup(); - } - $proxy = new ProxyContainer(); - $provider = new CompositeCachingServiceProvider( $providers ); - $container = new CachingContainer( new DelegatingContainer( $provider ) ); - $proxy->setInnerContainer( $container ); - foreach ( $modules as $module ) { - /* @var $module ModuleInterface module */ - $module->run( $container ); - } $initialized = true; - do_action( 'woocommerce_paypal_payments_built_container', $proxy ); + do_action( 'woocommerce_paypal_payments_built_container', $app_container ); } } From 0851560bc2cc8a1396edea52f5ceeb4d1b24b4fc Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 20:41:06 +0300 Subject: [PATCH 012/101] Allow to use module services in tests --- tests/PHPUnit/ModularTestCase.php | 70 +++++++++++++++++++++++++++++++ tests/PHPUnit/TestCase.php | 9 ++++ tests/PHPUnit/bootstrap.php | 12 ++++-- 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 tests/PHPUnit/ModularTestCase.php diff --git a/tests/PHPUnit/ModularTestCase.php b/tests/PHPUnit/ModularTestCase.php new file mode 100644 index 000000000..be813085f --- /dev/null +++ b/tests/PHPUnit/ModularTestCase.php @@ -0,0 +1,70 @@ +justReturn(null); + when('plugins_url')->returnArg(); + when('plugin_dir_path')->alias(function ($file) { return trailingslashit(dirname($file)); }); + when('get_current_blog_id')->justReturn(42); + when('get_site_url')->justReturn('example.com'); + when('get_bloginfo')->justReturn('My Shop'); + when('wc_get_base_location')->justReturn(['country' => 'US']); + when('get_woocommerce_currency')->justReturn('USD'); + when('WC')->justReturn((object) [ + 'session' => null, + ]); + + global $wpdb; + $wpdb = \Mockery::mock(\stdClass::class); + $wpdb->shouldReceive('get_var')->andReturn(null); + $wpdb->shouldReceive('prepare')->andReturn(null); + $wpdb->posts = ''; + $wpdb->postmeta = ''; + + !defined('PAYPAL_API_URL') && define('PAYPAL_API_URL', 'https://api.paypal.com'); + !defined('PAYPAL_SANDBOX_API_URL') && define('PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com'); + !defined('PAYPAL_INTEGRATION_DATE') && define('PAYPAL_INTEGRATION_DATE', '2020-10-15'); + + !defined('PPCP_FLAG_SUBSCRIPTION') && define('PPCP_FLAG_SUBSCRIPTION', true); + + !defined('CONNECT_WOO_CLIENT_ID') && define('CONNECT_WOO_CLIENT_ID', 'woo-id'); + !defined('CONNECT_WOO_SANDBOX_CLIENT_ID') && define('CONNECT_WOO_SANDBOX_CLIENT_ID', 'woo-id2'); + !defined('CONNECT_WOO_MERCHANT_ID') && define('CONNECT_WOO_MERCHANT_ID', 'merchant-id'); + !defined('CONNECT_WOO_SANDBOX_MERCHANT_ID') && define('CONNECT_WOO_SANDBOX_MERCHANT_ID', 'merchant-id2'); + !defined('CONNECT_WOO_URL') && define('CONNECT_WOO_URL', 'https://connect.woocommerce.com/ppc'); + !defined('CONNECT_WOO_SANDBOX_URL') && define('CONNECT_WOO_SANDBOX_URL', 'https://connect.woocommerce.com/ppcsandbox'); + } + + /** + * @param array $overriddenServices + * @return ContainerInterface + */ + protected function bootstrapModule(array $overriddenServices = []): ContainerInterface + { + $overridingContainer = new DelegatingContainer(new CompositeCachingServiceProvider([ + new ServiceProvider($overriddenServices, []), + ])); + + $rootDir = ROOT_DIR; + $bootstrap = require ("$rootDir/bootstrap.php"); + $appContainer = $bootstrap($rootDir, $overridingContainer); + + return $appContainer; + } +} diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index e51ff6828..d399e44c7 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -15,6 +15,15 @@ class TestCase extends \PHPUnit\Framework\TestCase parent::setUp(); when('__')->returnArg(); + when('_x')->returnArg(); + when('esc_url')->returnArg(); + when('esc_attr')->returnArg(); + when('esc_attr__')->returnArg(); + when('esc_html')->returnArg(); + when('esc_html__')->returnArg(); + when('esc_textarea')->returnArg(); + when('sanitize_text_field')->returnArg(); + when('wp_unslash')->returnArg(); setUp(); } diff --git a/tests/PHPUnit/bootstrap.php b/tests/PHPUnit/bootstrap.php index ba6ffb319..2e2b3350e 100644 --- a/tests/PHPUnit/bootstrap.php +++ b/tests/PHPUnit/bootstrap.php @@ -1,6 +1,10 @@ Date: Fri, 27 Aug 2021 20:41:30 +0300 Subject: [PATCH 013/101] Fix/update test --- .../Settings/SettingsListenerTest.php | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index 11c48bcd5..076fa8f50 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -4,44 +4,59 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ModularTestCase; use WooCommerce\PayPalCommerce\Onboarding\State; -use WooCommerce\PayPalCommerce\TestCase; use Mockery; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; use function Brain\Monkey\Functions\when; -use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; -class SettingsListenerTest extends TestCase +class SettingsListenerTest extends ModularTestCase { - use MockeryPHPUnitIntegration; + private $appContainer; - public function testListen() + public function setUp(): void + { + parent::setUp(); + + $this->appContainer = $this->bootstrapModule(); + } + + public function testListen() { $settings = Mockery::mock(Settings::class); - $setting_fields = []; + $settings->shouldReceive('set'); + + $setting_fields = $this->appContainer->get('wcgateway.settings.fields'); + $webhook_registrar = Mockery::mock(WebhookRegistrar::class); + $webhook_registrar->shouldReceive('unregister')->andReturnTrue(); + $webhook_registrar->shouldReceive('register')->andReturnTrue(); + $cache = Mockery::mock(Cache::class); + $state = Mockery::mock(State::class); + $state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); $bearer = Mockery::mock(Bearer::class); + $testee = new SettingsListener( $settings, $setting_fields, $webhook_registrar, $cache, $state, - $bearer + $bearer, + PayPalGateway::ID ); - $_REQUEST['section'] = 'ppcp-gateway'; + $_GET['section'] = PayPalGateway::ID; $_POST['ppcp-nonce'] = 'foo'; $_POST['ppcp'] = [ 'client_id' => 'client_id', ]; - $_GET['ppcp-tab'] = 'just-a-tab'; + $_GET['ppcp-tab'] = PayPalGateway::ID; - when('sanitize_text_field')->justReturn('ppcp-gateway'); - when('wp_unslash')->justReturn('ppcp-gateway'); when('current_user_can')->justReturn(true); when('wp_verify_nonce')->justReturn(true); From dcd9bfffdd97b34433efa6b2db56857d94e7f61a Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 20:41:52 +0300 Subject: [PATCH 014/101] Fix test warning --- tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php index 2f4efbe8e..fa6f2f9e9 100644 --- a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php @@ -32,7 +32,7 @@ class CreateOrderEndpointTest extends TestCase { list($payer_factory, $testee) = $this->mockTestee(); - $method = $this->testPrivateMethod(CreateOrderEndpoint::class, 'payer'); + $method = $this->makePrivateMethod(CreateOrderEndpoint::class, 'payer'); $dataString = wp_json_encode($expectedResult['payer']); $dataObj = json_decode(wp_json_encode($expectedResult['payer'])); @@ -173,11 +173,11 @@ class CreateOrderEndpointTest extends TestCase * @return \ReflectionMethod * @throws \ReflectionException */ - protected function testPrivateMethod($class, $method) + protected function makePrivateMethod($class, $method) { $reflector = new ReflectionClass($class); $method = $reflector->getMethod($method); $method->setAccessible(true); return $method; } -} \ No newline at end of file +} From 65c048438c9ea002f0b35795bccd3efa9c552090 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Aug 2021 20:42:55 +0300 Subject: [PATCH 015/101] Fix indent --- .../Settings/SettingsListenerTest.php | 93 +++++++++---------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index 076fa8f50..050b87e40 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -13,9 +13,9 @@ use function Brain\Monkey\Functions\when; class SettingsListenerTest extends ModularTestCase { - private $appContainer; + private $appContainer; - public function setUp(): void + public function setUp(): void { parent::setUp(); @@ -23,59 +23,58 @@ class SettingsListenerTest extends ModularTestCase } public function testListen() - { - $settings = Mockery::mock(Settings::class); - $settings->shouldReceive('set'); + { + $settings = Mockery::mock(Settings::class); + $settings->shouldReceive('set'); - $setting_fields = $this->appContainer->get('wcgateway.settings.fields'); + $setting_fields = $this->appContainer->get('wcgateway.settings.fields'); - $webhook_registrar = Mockery::mock(WebhookRegistrar::class); - $webhook_registrar->shouldReceive('unregister')->andReturnTrue(); - $webhook_registrar->shouldReceive('register')->andReturnTrue(); + $webhook_registrar = Mockery::mock(WebhookRegistrar::class); + $webhook_registrar->shouldReceive('unregister')->andReturnTrue(); + $webhook_registrar->shouldReceive('register')->andReturnTrue(); - $cache = Mockery::mock(Cache::class); + $cache = Mockery::mock(Cache::class); - $state = Mockery::mock(State::class); - $state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); - $bearer = Mockery::mock(Bearer::class); + $state = Mockery::mock(State::class); + $state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); + $bearer = Mockery::mock(Bearer::class); - - $testee = new SettingsListener( - $settings, - $setting_fields, - $webhook_registrar, - $cache, - $state, - $bearer, + $testee = new SettingsListener( + $settings, + $setting_fields, + $webhook_registrar, + $cache, + $state, + $bearer, PayPalGateway::ID - ); + ); - $_GET['section'] = PayPalGateway::ID; - $_POST['ppcp-nonce'] = 'foo'; - $_POST['ppcp'] = [ - 'client_id' => 'client_id', - ]; - $_GET['ppcp-tab'] = PayPalGateway::ID; + $_GET['section'] = PayPalGateway::ID; + $_POST['ppcp-nonce'] = 'foo'; + $_POST['ppcp'] = [ + 'client_id' => 'client_id', + ]; + $_GET['ppcp-tab'] = PayPalGateway::ID; - when('current_user_can')->justReturn(true); - when('wp_verify_nonce')->justReturn(true); + when('current_user_can')->justReturn(true); + when('wp_verify_nonce')->justReturn(true); - $settings->shouldReceive('has') - ->with('client_id') - ->andReturn('client_id'); - $settings->shouldReceive('get') - ->with('client_id') - ->andReturn('client_id'); - $settings->shouldReceive('has') - ->with('client_secret') - ->andReturn('client_secret'); - $settings->shouldReceive('get') - ->with('client_secret') - ->andReturn('client_secret'); - $settings->shouldReceive('persist'); - $cache->shouldReceive('has') - ->andReturn(false); + $settings->shouldReceive('has') + ->with('client_id') + ->andReturn('client_id'); + $settings->shouldReceive('get') + ->with('client_id') + ->andReturn('client_id'); + $settings->shouldReceive('has') + ->with('client_secret') + ->andReturn('client_secret'); + $settings->shouldReceive('get') + ->with('client_secret') + ->andReturn('client_secret'); + $settings->shouldReceive('persist'); + $cache->shouldReceive('has') + ->andReturn(false); - $testee->listen(); - } + $testee->listen(); + } } From 33e6280aa3c006dd4e953d250bfd1b1a5f5d50af Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 31 Aug 2021 10:20:11 +0300 Subject: [PATCH 016/101] Add notice DCC enabled without the main gateway --- modules/ppcp-wc-gateway/services.php | 6 ++ .../class-dccwithoutpaypaladminnotice.php | 77 +++++++++++++++++++ .../src/class-wcgatewaymodule.php | 9 +++ 3 files changed, 92 insertions(+) create mode 100644 modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 10b10e838..1595b76b3 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -30,6 +30,7 @@ use Woocommerce\PayPalCommerce\WcGateway\Helper\DccProductStatus; use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -139,6 +140,11 @@ return array( $settings = $container->get( 'wcgateway.settings' ); return new ConnectAdminNotice( $state, $settings ); }, + 'wcgateway.notice.dcc-without-paypal' => static function ( $container ): DccWithoutPayPalAdminNotice { + $state = $container->get( 'onboarding.state' ); + $settings = $container->get( 'wcgateway.settings' ); + return new DccWithoutPayPalAdminNotice( $state, $settings ); + }, 'wcgateway.notice.authorize-order-action' => static function ( $container ): AuthorizeOrderActionNotice { return new AuthorizeOrderActionNotice(); diff --git a/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php b/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php new file mode 100644 index 000000000..58ee3ac38 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php @@ -0,0 +1,77 @@ +state = $state; + $this->settings = $settings; + } + + /** + * Returns the message. + * + * @return Message|null + */ + public function message(): ?Message { + if ( ! $this->should_display() ) { + return null; + } + + $message = sprintf( + /* translators: %1$s the gateway name. */ + __( + 'PayPal Card Processing cannot be used without the PayPal gateway. Enable the PayPal Gateway.', + 'woocommerce-paypal-payments' + ), + admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) + ); + return new Message( $message, 'warning' ); + } + + /** + * Whether the message should be displayed. + * + * @return bool + */ + protected function should_display(): bool { + return State::STATE_ONBOARDED === $this->state->current_state() + && ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) + && ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) ); + } +} diff --git a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php index 1dfffc810..2e88f3002 100644 --- a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php +++ b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php @@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; @@ -94,6 +95,14 @@ class WcGatewayModule implements ModuleInterface { if ( $connect_message ) { $notices[] = $connect_message; } + + $dcc_without_paypal_notice = $container->get( 'wcgateway.notice.dcc-without-paypal' ); + assert( $dcc_without_paypal_notice instanceof DccWithoutPayPalAdminNotice ); + $dcc_without_paypal_message = $dcc_without_paypal_notice->message(); + if ( $dcc_without_paypal_message ) { + $notices[] = $dcc_without_paypal_message; + } + $authorize_order_action = $container->get( 'wcgateway.notice.authorize-order-action' ); $authorized_message = $authorize_order_action->message(); if ( $authorized_message ) { From 527d14eb4a734eda7c339a0167c08bd0bca654c8 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 31 Aug 2021 10:20:34 +0300 Subject: [PATCH 017/101] Simplify type hint --- .../ppcp-wc-gateway/src/class-wcgatewaymodule.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php index 2e88f3002..60ec5d7c1 100644 --- a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php +++ b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php @@ -86,11 +86,7 @@ class WcGatewayModule implements ModuleInterface { Repository::NOTICES_FILTER, static function ( $notices ) use ( $container ): array { $notice = $container->get( 'wcgateway.notice.connect' ); - /** - * The Connect Admin Notice object. - * - * @var ConnectAdminNotice $notice - */ + assert( $notice instanceof ConnectAdminNotice ); $connect_message = $notice->connect_message(); if ( $connect_message ) { $notices[] = $connect_message; @@ -110,11 +106,7 @@ class WcGatewayModule implements ModuleInterface { } $settings_renderer = $container->get( 'wcgateway.settings.render' ); - /** - * The settings renderer. - * - * @var SettingsRenderer $settings_renderer - */ + assert( $settings_renderer instanceof SettingsRenderer ); $messages = $settings_renderer->messages(); $notices = array_merge( $notices, $messages ); From 93cb005b4d805d825d73fc56d9a3e2b0ee00a371 Mon Sep 17 00:00:00 2001 From: Alex Pantechovskis Date: Tue, 31 Aug 2021 10:29:53 +0300 Subject: [PATCH 018/101] Remove travis We don't need it, GitHub Actions do the same and more. And it used PHP 7.0. --- .travis.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 794e0b310..000000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: php -os: linux -dist: xenial - -notifications: - email: false - -php: - - 7.0 - -branches: - only: - - master - - trunk - - compat/ppxo - -script: | - CHANGED_FILES=`git diff --name-only --diff-filter=ACMR $TRAVIS_COMMIT_RANGE | grep \\\\.php | awk '{print}' ORS=' '` - - if [ "$CHANGED_FILES" != "" ]; then - composer global require woocommerce/woocommerce-sniffs --update-with-all-dependencies - $HOME/.config/composer/vendor/bin/phpcs -p $CHANGED_FILES - fi From 9408d1a9e5e6687f9797644efb19fd834dbef1df Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 1 Sep 2021 16:01:07 +0300 Subject: [PATCH 019/101] Fix text --- .../src/Notice/class-dccwithoutpaypaladminnotice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php b/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php index 58ee3ac38..cd88d292a 100644 --- a/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php @@ -56,7 +56,7 @@ class DccWithoutPayPalAdminNotice { $message = sprintf( /* translators: %1$s the gateway name. */ __( - 'PayPal Card Processing cannot be used without the PayPal gateway. Enable the PayPal Gateway.', + 'PayPal Card Processing cannot be used without the PayPal gateway. Enable the PayPal Gateway.', 'woocommerce-paypal-payments' ), admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) From 34ae2ee30b97ec773901a2e72881e77e5969bfce Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 1 Sep 2021 16:28:17 +0300 Subject: [PATCH 020/101] Show notice only on ppcp and payments page --- modules/ppcp-wc-gateway/services.php | 4 ++- .../class-dccwithoutpaypaladminnotice.php | 30 +++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 1595b76b3..428caf750 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -143,7 +143,9 @@ return array( 'wcgateway.notice.dcc-without-paypal' => static function ( $container ): DccWithoutPayPalAdminNotice { $state = $container->get( 'onboarding.state' ); $settings = $container->get( 'wcgateway.settings' ); - return new DccWithoutPayPalAdminNotice( $state, $settings ); + $is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' ); + $is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' ); + return new DccWithoutPayPalAdminNotice( $state, $settings, $is_payments_page, $is_ppcp_settings_page ); }, 'wcgateway.notice.authorize-order-action' => static function ( $container ): AuthorizeOrderActionNotice { diff --git a/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php b/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php index cd88d292a..2343c9e00 100644 --- a/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/class-dccwithoutpaypaladminnotice.php @@ -32,15 +32,38 @@ class DccWithoutPayPalAdminNotice { */ private $settings; + /** + * Whether the current page is the WC payment page. + * + * @var bool + */ + private $is_payments_page; + + /** + * Whether the current page is the PPCP settings page. + * + * @var bool + */ + private $is_ppcp_settings_page; + /** * ConnectAdminNotice constructor. * * @param State $state The state. * @param ContainerInterface $settings The settings. + * @param bool $is_payments_page Whether the current page is the WC payment page. + * @param bool $is_ppcp_settings_page Whether the current page is the PPCP settings page. */ - public function __construct( State $state, ContainerInterface $settings ) { - $this->state = $state; - $this->settings = $settings; + public function __construct( + State $state, + ContainerInterface $settings, + bool $is_payments_page, + bool $is_ppcp_settings_page + ) { + $this->state = $state; + $this->settings = $settings; + $this->is_payments_page = $is_payments_page; + $this->is_ppcp_settings_page = $is_ppcp_settings_page; } /** @@ -71,6 +94,7 @@ class DccWithoutPayPalAdminNotice { */ protected function should_display(): bool { return State::STATE_ONBOARDED === $this->state->current_state() + && ( $this->is_payments_page || $this->is_ppcp_settings_page ) && ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) && ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) ); } From 10d7574e22dd728fd5f01f0fa7946f3a9eb0ab42 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 7 Sep 2021 17:55:39 +0300 Subject: [PATCH 021/101] Teardown hosted fields on re-render --- .../resources/js/modules/Renderer/CreditCardRenderer.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index dc5035383..4fec241f1 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -8,6 +8,7 @@ class CreditCardRenderer { this.spinner = spinner; this.cardValid = false; this.formValid = false; + this.currentHostedFieldsInstance = null; } render(wrapper, contextConfig) { @@ -31,6 +32,12 @@ class CreditCardRenderer { return; } + if (this.currentHostedFieldsInstance) { + this.currentHostedFieldsInstance.teardown() + .catch(err => console.error(`Hosted fields teardown error: ${err}`)); + this.currentHostedFieldsInstance = null; + } + const gateWayBox = document.querySelector('.payment_box.payment_method_ppcp-credit-card-gateway'); const oldDisplayStyle = gateWayBox.style.display; gateWayBox.style.display = 'block'; @@ -92,6 +99,7 @@ class CreditCardRenderer { } } }).then(hostedFields => { + this.currentHostedFieldsInstance = hostedFields; const submitEvent = (event) => { this.spinner.block(); if (event) { From a41f678ac6c54a4272f6c6f50b43477c4fcafc65 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 7 Sep 2021 17:58:17 +0300 Subject: [PATCH 022/101] Subscribe to form button click only once --- .../js/modules/Renderer/CreditCardRenderer.js | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index 4fec241f1..72b9756d3 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -9,6 +9,7 @@ class CreditCardRenderer { this.cardValid = false; this.formValid = false; this.currentHostedFieldsInstance = null; + this.formSubmissionSubscribed = false; } render(wrapper, contextConfig) { @@ -100,36 +101,9 @@ class CreditCardRenderer { } }).then(hostedFields => { this.currentHostedFieldsInstance = hostedFields; - const submitEvent = (event) => { - this.spinner.block(); - if (event) { - event.preventDefault(); - } - this.errorHandler.clear(); - if (this.formValid && this.cardValid) { - const save_card = this.defaultConfig.save_card ? true : false; - const vault = document.getElementById('ppcp-credit-card-vault') ? - document.getElementById('ppcp-credit-card-vault').checked : save_card; - hostedFields.submit({ - contingencies: ['SCA_WHEN_REQUIRED'], - vault: vault - }).then((payload) => { - payload.orderID = payload.orderId; - this.spinner.unblock(); - return contextConfig.onApprove(payload); - }).catch(() => { - this.errorHandler.genericError(); - this.spinner.unblock(); - }); - } else { - this.spinner.unblock(); - const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; - this.errorHandler.message(message); - } - } - hostedFields.on('inputSubmitRequest', function () { - submitEvent(null); + hostedFields.on('inputSubmitRequest', () => { + this._submit(contextConfig); }); hostedFields.on('cardTypeChange', (event) => { if ( ! event.cards.length ) { @@ -145,11 +119,18 @@ class CreditCardRenderer { }); this.formValid = formValid; - }) - document.querySelector(wrapper + ' button').addEventListener( - 'click', - submitEvent - ); + }); + + if (!this.formSubmissionSubscribed) { + document.querySelector(wrapper + ' button').addEventListener( + 'click', + event => { + event.preventDefault(); + this._submit(contextConfig); + } + ); + this.formSubmissionSubscribed = true; + } }); document.querySelector('#payment_method_ppcp-credit-card-gateway').addEventListener( @@ -159,5 +140,32 @@ class CreditCardRenderer { } ) } + + _submit(contextConfig) { + this.spinner.block(); + this.errorHandler.clear(); + + if (this.formValid && this.cardValid) { + const save_card = this.defaultConfig.save_card ? true : false; + const vault = document.getElementById('ppcp-credit-card-vault') ? + document.getElementById('ppcp-credit-card-vault').checked : save_card; + this.currentHostedFieldsInstance.submit({ + contingencies: ['SCA_WHEN_REQUIRED'], + vault: vault + }).then((payload) => { + payload.orderID = payload.orderId; + this.spinner.unblock(); + return contextConfig.onApprove(payload); + }).catch(err => { + console.error(err); + this.errorHandler.genericError(); + this.spinner.unblock(); + }); + } else { + this.spinner.unblock(); + const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; + this.errorHandler.message(message); + } + } } export default CreditCardRenderer; From 6e0f739a0048e5b4eef537255247ee76f4d376d5 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 8 Sep 2021 11:40:13 +0200 Subject: [PATCH 023/101] Remove disabling card for UK --- modules/ppcp-wc-gateway/services.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f072e347c..ca76a5478 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -646,7 +646,7 @@ return array( 'label' => sprintf( // translators: %1$s and %2$s are the opening and closing of HTML tag. __( 'Enable saved cards and subscription features on your store. To use vaulting features, you must %1$senable vaulting on your account%2$s.', 'woocommerce-paypal-payments' ), - '', @@ -1820,15 +1820,6 @@ return array( unset( $fields['ppcp_disconnect_sandbox'] ); } - /** - * Disable card for UK. - */ - $region = wc_get_base_location(); - $country = $region['country']; - if ( 'GB' === $country ) { - unset( $fields['disable_funding']['options']['card'] ); - } - /** * Depending on your store location, some credit cards can't be used. * Here, we filter them out. From 3580462efc101ba7ecd35469b6a2faa23f360f8a Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 8 Sep 2021 14:39:56 +0200 Subject: [PATCH 024/101] Add tooltip description --- modules/ppcp-wc-gateway/services.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 538363d32..2e8eee082 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -1835,10 +1835,18 @@ return array( '3d_secure_contingency' => array( 'title' => __( 'Contingency for 3D Secure', 'woocommerce-paypal-payments' ), 'type' => 'select', + 'description' => sprintf( + // translators: %1$s and %2$s opening and closing ul tag, %3$s and %4$s opening and closing li tag. + __( '%1$s%3$sNo 3D Secure will cause transactions to be denied if 3D Secure is required by the bank of the cardholder.%4$s%3$sSCA_WHEN_REQUIRED returns a 3D Secure contingency when it is a mandate in the region where you operate.%4$s%3$sSCA_ALWAYS triggers 3D Secure for every transaction, regardless of SCA requirements.%4$s%2$s', 'woocommerce-paypal-payments' ), + '
      ', + '
    ', + '
  • ', + '
  • ' + ), 'class' => array(), 'input_class' => array( 'wc-enhanced-select' ), 'default' => 'SCA_WHEN_REQUIRED', - 'desc_tip' => false, + 'desc_tip' => true, 'options' => array( 'NO_3D_SECURE' => __( 'No 3D Secure (transaction will be denied if 3D Secure is required)', 'woocommerce-paypal-payments' ), 'SCA_WHEN_REQUIRED' => __( '3D Secure when required', 'woocommerce-paypal-payments' ), From 085523c5dafeaedbcb3abfb58e4405b6381844b5 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 9 Sep 2021 12:14:50 +0300 Subject: [PATCH 025/101] Fix test --- tests/PHPUnit/TestCase.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index d399e44c7..8d7d821bd 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -10,12 +10,12 @@ use Mockery; class TestCase extends \PHPUnit\Framework\TestCase { - public function setUp(): void - { - parent::setUp(); + public function setUp(): void + { + parent::setUp(); - when('__')->returnArg(); - when('_x')->returnArg(); + when('__')->returnArg(); + when('_x')->returnArg(); when('esc_url')->returnArg(); when('esc_attr')->returnArg(); when('esc_attr__')->returnArg(); @@ -23,15 +23,16 @@ class TestCase extends \PHPUnit\Framework\TestCase when('esc_html__')->returnArg(); when('esc_textarea')->returnArg(); when('sanitize_text_field')->returnArg(); + when('wp_kses_post')->returnArg(); when('wp_unslash')->returnArg(); - setUp(); - } + setUp(); + } - public function tearDown(): void - { - tearDown(); - Mockery::close(); - parent::tearDown(); - } + public function tearDown(): void + { + tearDown(); + Mockery::close(); + parent::tearDown(); + } } From 5ca32d0a0a5162e2f521f2784f39489dcdedb9e6 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 9 Sep 2021 12:33:24 +0300 Subject: [PATCH 026/101] Revert container cache fix --- bootstrap.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bootstrap.php b/bootstrap.php index c3d706960..a1f527a01 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -33,8 +33,11 @@ return function ( $provider = new CompositeCachingServiceProvider( $providers ); $proxy_container = new ProxyContainer(); - $container = new DelegatingContainer( $provider, $proxy_container ); - $app_container = new CachingContainer( + // TODO: caching does not work currently, + // may want to consider fixing it later (pass proxy as parent to DelegatingContainer) + // for now not fixed since we were using this behavior for long time and fixing it now may break things. + $container = new DelegatingContainer( $provider ); + $app_container = new CachingContainer( new CompositeContainer( array_merge( $additional_containers, From b1ce9191f9885d5f93f3e4f134186704c3129c23 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Thu, 9 Sep 2021 12:20:43 +0200 Subject: [PATCH 027/101] Ignore disable funding card if card processing is enabled --- .../ppcp-button/src/Assets/class-smartbutton.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-button/src/Assets/class-smartbutton.php b/modules/ppcp-button/src/Assets/class-smartbutton.php index 5f3fc0244..c55decdac 100644 --- a/modules/ppcp-button/src/Assets/class-smartbutton.php +++ b/modules/ppcp-button/src/Assets/class-smartbutton.php @@ -729,8 +729,7 @@ class SmartButton implements SmartButtonInterface { 'currency' => get_woocommerce_currency(), 'integration-date' => PAYPAL_INTEGRATION_DATE, 'components' => implode( ',', $this->components() ), - 'vault' => $this->can_save_vault_token() ? - 'true' : 'false', + 'vault' => $this->can_save_vault_token() ? 'true' : 'false', 'commit' => is_checkout() ? 'true' : 'false', 'intent' => ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture', @@ -743,11 +742,20 @@ class SmartButton implements SmartButtonInterface { ) { $params['buyer-country'] = WC()->customer->get_billing_country(); } - $disable_funding = $this->settings->has( 'disable_funding' ) ? - $this->settings->get( 'disable_funding' ) : array(); + + $disable_funding = $this->settings->has( 'disable_funding' ) + ? $this->settings->get( 'disable_funding' ) + : array(); + if ( ! is_checkout() ) { $disable_funding[] = 'card'; } + if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) { + $key = array_search( 'card', $disable_funding, true ); + if ( false !== $key ) { + unset( $disable_funding[ $key ] ); + } + } if ( count( $disable_funding ) > 0 ) { $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); From 0034e4dd5163783821c0ff153ab058e5284ee86b Mon Sep 17 00:00:00 2001 From: dinamiko Date: Thu, 9 Sep 2021 15:35:26 +0200 Subject: [PATCH 028/101] Do not throw error if customer does not have payment tokens --- .../Endpoint/class-paymenttokenendpoint.php | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php index f76f419f5..e3c8ed903 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-paymenttokenendpoint.php @@ -139,24 +139,7 @@ class PaymentTokenEndpoint { foreach ( $json->payment_tokens as $token_value ) { $tokens[] = $this->factory->from_paypal_response( $token_value ); } - if ( empty( $tokens ) ) { - $error = new RuntimeException( - sprintf( - // translators: %d is the customer id. - __( 'No token stored for customer %d.', 'woocommerce-paypal-payments' ), - $id - ) - ); - $this->logger->log( - 'warning', - $error->getMessage(), - array( - 'args' => $args, - 'response' => $response, - ) - ); - throw $error; - } + return $tokens; } From 12f1a2e40bc4f869d5623c8b340cb073e89ce6c4 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Thu, 9 Sep 2021 17:28:16 +0200 Subject: [PATCH 029/101] Disable credit card fields when selecting saved payment --- .../ContextBootstrap/CheckoutBootstap.js | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index e4558e34a..a240f8270 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -24,9 +24,11 @@ class CheckoutBootstap { }) - jQuery('#saved-credit-card').on('change', () => { - this.displayPlaceOrderButtonForSavedCreditCards() - }) + setTimeout(() => { + jQuery('#saved-credit-card').on('change', () => { + this.displayPlaceOrderButtonForSavedCreditCards() + }) + }, 3000) this.switchBetweenPayPalandOrderButton() this.displayPlaceOrderButtonForSavedCreditCards() @@ -100,13 +102,45 @@ class CheckoutBootstap { this.renderer.hideButtons(this.gateway.messages.wrapper) this.renderer.hideButtons(this.gateway.hosted_fields.wrapper) jQuery('#place_order').show() + this.disableCreditCardFields() } else { jQuery('#place_order').hide() this.renderer.hideButtons(this.gateway.button.wrapper) this.renderer.hideButtons(this.gateway.messages.wrapper) this.renderer.showButtons(this.gateway.hosted_fields.wrapper) + this.enableCreditCardFields() } } + + disableCreditCardFields() { + jQuery('label[for="ppcp-credit-card-gateway-card-number"]').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-gateway-card-number').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", true); + jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-gateway-card-expiry').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", true) + jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-gateway-card-cvc').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-gateway-card-cvc').attr("disabled", true) + jQuery('label[for="vault"]').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-vault').css({ opacity: 0.4 }) + jQuery('#ppcp-credit-card-vault').attr("disabled", true) + } + + enableCreditCardFields() { + jQuery('label[for="ppcp-credit-card-gateway-card-number"]').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-gateway-card-number').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", false) + jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-gateway-card-expiry').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", false); + jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-gateway-card-cvc').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-gateway-card-cvc').attr("disabled", false) + jQuery('label[for="vault"]').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-vault').css({ opacity: 1 }) + jQuery('#ppcp-credit-card-vault').attr("disabled", false) + } } export default CheckoutBootstap From 05dcf5c4f4732751becaa1b6728b6ffd44aa2d65 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Fri, 10 Sep 2021 12:16:33 +0200 Subject: [PATCH 030/101] Refactoring --- .../class-paymenttokenrepository.php | 35 ++++++---- .../Endpoint/PaymentTokenEndpointTest.php | 21 ------ .../Repository/PaymentTokenRepositoryTest.php | 68 +++++++++++++++++++ 3 files changed, 89 insertions(+), 35 deletions(-) diff --git a/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php b/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php index 53f121d6c..d143d1931 100644 --- a/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php +++ b/modules/ppcp-subscription/src/Repository/class-paymenttokenrepository.php @@ -107,13 +107,8 @@ class PaymentTokenRepository { * @param PaymentToken[] $tokens The tokens. * @return bool Whether tokens contains card or not. */ - public function tokens_contains_card( $tokens ): bool { - foreach ( $tokens as $token ) { - if ( isset( $token->source()->card ) ) { - return true; - } - } - return false; + public function tokens_contains_card( array $tokens ): bool { + return $this->token_contains_source( $tokens, 'card' ); } /** @@ -122,13 +117,8 @@ class PaymentTokenRepository { * @param PaymentToken[] $tokens The tokens. * @return bool Whether tokens contains card or not. */ - public function tokens_contains_paypal( $tokens ): bool { - foreach ( $tokens as $token ) { - if ( isset( $token->source()->paypal ) ) { - return true; - } - } - return false; + public function tokens_contains_paypal( array $tokens ): bool { + return $this->token_contains_source( $tokens, 'paypal' ); } /** @@ -145,4 +135,21 @@ class PaymentTokenRepository { update_user_meta( $id, self::USER_META, $token_array ); return $token; } + + /** + * Checks if tokens has the given source. + * + * @param array $tokens Payment tokens. + * @param string $source_type Payment token source type. + * @return bool Whether tokens contains source or not. + */ + private function token_contains_source( array $tokens, string $source_type ): bool { + foreach ( $tokens as $token ) { + if ( isset( $token->source()->card ) && 'card' === $source_type || isset( $token->source()->paypal ) && 'paypal' === $source_type ) { + return true; + } + } + + return false; + } } diff --git a/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php index 8254049cd..67f566631 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php @@ -110,27 +110,6 @@ class PaymentTokenEndpointTest extends TestCase $this->sut->for_user($id); } - public function testForUserFailBecauseEmptyTokens() - { - $id = 1; - $token = Mockery::mock(Token::class); - $rawResponse = ['body' => '{"payment_tokens":[]}']; - $this->bearer->shouldReceive('bearer') - ->andReturn($token); - $token->shouldReceive('token') - ->andReturn('bearer'); - $this->ensureRequestForUser($rawResponse, $id); - - - expect('wp_remote_get')->andReturn($rawResponse); - expect('is_wp_error')->with($rawResponse)->andReturn(false); - expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200); - $this->logger->shouldReceive('log'); - - $this->expectException(RuntimeException::class); - $this->sut->for_user($id); - } - public function testDeleteToken() { $paymentToken = $paymentToken = Mockery::mock(PaymentToken::class); diff --git a/tests/PHPUnit/Subscription/Repository/PaymentTokenRepositoryTest.php b/tests/PHPUnit/Subscription/Repository/PaymentTokenRepositoryTest.php index f585cea88..d5adba52e 100644 --- a/tests/PHPUnit/Subscription/Repository/PaymentTokenRepositoryTest.php +++ b/tests/PHPUnit/Subscription/Repository/PaymentTokenRepositoryTest.php @@ -90,4 +90,72 @@ class PaymentTokenRepositoryTest extends TestCase $this->sut->delete_token($id, $paymentToken); } + + public function testAllForUserId() + { + $id = 1; + $tokens = []; + + $this->endpoint->shouldReceive('for_user') + ->with($id) + ->andReturn($tokens); + expect('update_user_meta')->with($id, $this->sut::USER_META, $tokens); + + $result = $this->sut->all_for_user_id($id); + $this->assertSame($tokens, $result); + } + + public function test_AllForUserIdReturnsEmptyArrayIfGettingTokenFails() + { + $id = 1; + $tokens = []; + + $this->endpoint + ->expects('for_user') + ->with($id) + ->andThrow(RuntimeException::class); + + $result = $this->sut->all_for_user_id($id); + $this->assertSame($tokens, $result); + } + + public function testTokensContainCardReturnsTrue() + { + $source = new \stdClass(); + $card = new \stdClass(); + $source->card = $card; + $token = Mockery::mock(PaymentToken::class); + $tokens = [$token]; + + $token->shouldReceive('source')->andReturn($source); + + $this->assertTrue($this->sut->tokens_contains_card($tokens)); + } + + public function testTokensContainCardReturnsFalse() + { + $tokens = []; + $this->assertFalse($this->sut->tokens_contains_card($tokens)); + } + + public function testTokensContainPayPalReturnsTrue() + { + $source = new \stdClass(); + $paypal = new \stdClass(); + $source->paypal = $paypal; + $token = Mockery::mock(PaymentToken::class); + $tokens = [$token]; + + $token->shouldReceive('source')->andReturn($source); + + $this->assertTrue($this->sut->tokens_contains_paypal($tokens)); + } + + public function testTokensContainPayPalReturnsFalse() + { + $tokens = []; + $this->assertFalse($this->sut->tokens_contains_paypal($tokens)); + } + + } From 1eb2087bfbb4e8d0fbd63a195f0f6b11f9c56aad Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 13 Sep 2021 13:38:58 +0200 Subject: [PATCH 031/101] Log request and response information --- .../src/Endpoint/class-requesttrait.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index 7066f13b1..73dc2df94 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -36,6 +36,26 @@ trait RequestTrait { $args['headers']['PayPal-Partner-Attribution-Id'] = 'Woo_PPCP'; } - return wp_remote_get( $url, $args ); + $response = wp_remote_get( $url, $args ); + + $this->logger->log( 'info', '--------------------------------------------------------------------' ); + $this->logger->log( 'info', 'URL: ' . wc_print_r( $url, true ) ); + if ( isset( $args['method'] ) ) { + $this->logger->log( 'info', 'Method: ' . wc_print_r( $args['method'], true ) ); + } + if ( isset( $args['headers']['body'] ) ) { + $this->logger->log( 'info', 'Request Body: ' . wc_print_r( $args['headers']['body'], true ) ); + } + if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { + $this->logger->log( 'info', 'Response Debug ID: ' . wc_print_r( $response['headers']->getAll()['paypal-debug-id'], true ) ); + } + if ( isset( $response['response'] ) ) { + $this->logger->log( 'info', 'Response: ' . wc_print_r( $response['response'], true ) ); + } + if ( isset( $response['body'] ) ) { + $this->logger->log( 'info', 'Response Body: ' . wc_print_r( $response['body'], true ) ); + } + + return $response; } } From ba66cf6c2eda00f883752ec8828ab6bf8d065dfa Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 13 Sep 2021 15:38:34 +0200 Subject: [PATCH 032/101] Log request body and wp error --- .../src/Endpoint/class-requesttrait.php | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index 73dc2df94..e8e00f772 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -43,17 +43,22 @@ trait RequestTrait { if ( isset( $args['method'] ) ) { $this->logger->log( 'info', 'Method: ' . wc_print_r( $args['method'], true ) ); } - if ( isset( $args['headers']['body'] ) ) { - $this->logger->log( 'info', 'Request Body: ' . wc_print_r( $args['headers']['body'], true ) ); + if ( isset( $args['body'] ) ) { + $this->logger->log( 'info', 'Request Body: ' . wc_print_r( $args['body'], true ) ); } - if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { - $this->logger->log( 'info', 'Response Debug ID: ' . wc_print_r( $response['headers']->getAll()['paypal-debug-id'], true ) ); - } - if ( isset( $response['response'] ) ) { - $this->logger->log( 'info', 'Response: ' . wc_print_r( $response['response'], true ) ); - } - if ( isset( $response['body'] ) ) { - $this->logger->log( 'info', 'Response Body: ' . wc_print_r( $response['body'], true ) ); + + if ( ! is_wp_error( $response ) ) { + if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { + $this->logger->log( 'info', 'Response Debug ID: ' . wc_print_r( $response['headers']->getAll()['paypal-debug-id'], true ) ); + } + if ( isset( $response['response'] ) ) { + $this->logger->log( 'info', 'Response: ' . wc_print_r( $response['response'], true ) ); + } + if ( isset( $response['body'] ) ) { + $this->logger->log( 'info', 'Response Body: ' . wc_print_r( $response['body'], true ) ); + } + } else { + $this->logger->log( 'error', 'WP Error: ' . wc_print_r( $response->get_error_code() . ' ' . $response->get_error_message(), true ) ); } return $response; From a40c073bb9a5f99ca7a20351091e490dbc1f8934 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 13 Sep 2021 15:59:01 +0200 Subject: [PATCH 033/101] Refactoring --- .../src/Endpoint/class-requesttrait.php | 27 +++--------- .../src/Logger/class-woocommercelogger.php | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index e8e00f772..25c97c050 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; +use WooCommerce\WooCommerce\Logging\Logger\WooCommerceLogger; + /** * Trait RequestTrait */ @@ -38,29 +40,12 @@ trait RequestTrait { $response = wp_remote_get( $url, $args ); - $this->logger->log( 'info', '--------------------------------------------------------------------' ); - $this->logger->log( 'info', 'URL: ' . wc_print_r( $url, true ) ); - if ( isset( $args['method'] ) ) { - $this->logger->log( 'info', 'Method: ' . wc_print_r( $args['method'], true ) ); - } - if ( isset( $args['body'] ) ) { - $this->logger->log( 'info', 'Request Body: ' . wc_print_r( $args['body'], true ) ); - } - - if ( ! is_wp_error( $response ) ) { - if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { - $this->logger->log( 'info', 'Response Debug ID: ' . wc_print_r( $response['headers']->getAll()['paypal-debug-id'], true ) ); - } - if ( isset( $response['response'] ) ) { - $this->logger->log( 'info', 'Response: ' . wc_print_r( $response['response'], true ) ); - } - if ( isset( $response['body'] ) ) { - $this->logger->log( 'info', 'Response Body: ' . wc_print_r( $response['body'], true ) ); - } - } else { - $this->logger->log( 'error', 'WP Error: ' . wc_print_r( $response->get_error_code() . ' ' . $response->get_error_message(), true ) ); + if ( $this->logger instanceof WooCommerceLogger ) { + $this->logger->logRequestResponse( $url, $args, $response ); } return $response; } + + } diff --git a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php index bee7e7774..037ee1be5 100644 --- a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php +++ b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php @@ -14,6 +14,7 @@ namespace WooCommerce\WooCommerce\Logging\Logger; use Psr\Log\LoggerInterface; use Psr\Log\LoggerTrait; +use WP_Error; /** * Class WooCommerceLogger @@ -61,4 +62,46 @@ class WooCommerceLogger implements LoggerInterface { } $this->wc_logger->log( $level, $message, $context ); } + + /** + * Logs request and response information. + * + * @param string $url The request URL. + * @param array $args The request arguments. + * @param array|WP_Error $response The response or WP_Error on failure. + * @return void + */ + public function logRequestResponse( string $url, array $args, $response ) { + $this->log( 'info', '--------------------------------------------------------------------' ); + $this->log( 'info', 'URL: ' . wc_print_r( $url, true ) ); + if ( isset( $args['method'] ) ) { + $this->log( 'info', 'Method: ' . wc_print_r( $args['method'], true ) ); + } + if ( isset( $args['body'] ) ) { + $this->log( 'info', 'Request Body: ' . wc_print_r( $args['body'], true ) ); + } + + if ( ! is_wp_error( $response ) ) { + if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { + $this->log( + 'info', + 'Response Debug ID: ' . wc_print_r( + $response['headers']->getAll()['paypal-debug-id'], + true + ) + ); + } + if ( isset( $response['response'] ) ) { + $this->log( 'info', 'Response: ' . wc_print_r( $response['response'], true ) ); + } + if ( isset( $response['body'] ) ) { + $this->log( 'info', 'Response Body: ' . wc_print_r( $response['body'], true ) ); + } + } else { + $this->log( + 'error', + 'WP Error: ' . $response->get_error_code() . ' ' . $response->get_error_message() + ); + } + } } From 24859e6bc9a4f57d40089a2067197078036799a2 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Mon, 13 Sep 2021 17:06:50 +0200 Subject: [PATCH 034/101] Log into single entry --- .../src/Logger/class-woocommercelogger.php | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php index 037ee1be5..bef6f1631 100644 --- a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php +++ b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php @@ -72,36 +72,30 @@ class WooCommerceLogger implements LoggerInterface { * @return void */ public function logRequestResponse( string $url, array $args, $response ) { - $this->log( 'info', '--------------------------------------------------------------------' ); - $this->log( 'info', 'URL: ' . wc_print_r( $url, true ) ); - if ( isset( $args['method'] ) ) { - $this->log( 'info', 'Method: ' . wc_print_r( $args['method'], true ) ); - } - if ( isset( $args['body'] ) ) { - $this->log( 'info', 'Request Body: ' . wc_print_r( $args['body'], true ) ); + + if ( is_wp_error( $response ) ) { + $this->error( $response->get_error_code() . ' ' . $response->get_error_message() ); + return; } - if ( ! is_wp_error( $response ) ) { + $method = $args['method'] ?? ''; + $output = $method . ' ' . $url . "\n"; + if ( isset( $args['body'] ) ) { + $output .= 'Request Body: ' . wc_print_r( $args['body'], true ) . "\n"; + } + + if ( is_array( $response ) ) { if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { - $this->log( - 'info', - 'Response Debug ID: ' . wc_print_r( - $response['headers']->getAll()['paypal-debug-id'], - true - ) - ); + $output .= 'Response Debug ID: ' . $response['headers']->getAll()['paypal-debug-id'] . "\n"; } if ( isset( $response['response'] ) ) { - $this->log( 'info', 'Response: ' . wc_print_r( $response['response'], true ) ); + $output .= 'Response: ' . wc_print_r( $response['response'], true ) . "\n"; } if ( isset( $response['body'] ) ) { - $this->log( 'info', 'Response Body: ' . wc_print_r( $response['body'], true ) ); + $output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n"; } - } else { - $this->log( - 'error', - 'WP Error: ' . $response->get_error_code() . ' ' . $response->get_error_message() - ); } + + $this->info( $output ); } } From b853beb3b1c82c6b9dff04d8f0e8315b93686dc4 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 13 Sep 2021 11:37:21 +0300 Subject: [PATCH 035/101] Refactor webhook registration errors logging Logging was useless because it was just repeating the exception message. Context args were not used. --- .../src/Endpoint/class-webhookendpoint.php | 39 +++---------------- modules/ppcp-webhooks/services.php | 4 +- .../src/class-webhookregistrar.php | 15 ++++++- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php index c6e64af5d..7f08587d9 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php @@ -79,14 +79,14 @@ class WebhookEndpoint { * * @return Webhook * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. */ public function create( Webhook $hook ): Webhook { - /** - * An hook, which has an ID has already been created. - */ + // The hook was already created. if ( $hook->id() ) { return $hook; } + $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v1/notifications/webhooks'; $args = array( @@ -100,36 +100,18 @@ class WebhookEndpoint { $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { - $error = new RuntimeException( + throw new RuntimeException( __( 'Not able to create a webhook.', 'woocommerce-paypal-payments' ) ); - $this->logger->log( - 'warning', - $error->getMessage(), - array( - 'args' => $args, - 'response' => $response, - ) - ); - throw $error; } $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 201 !== $status_code ) { - $error = new PayPalApiException( + throw new PayPalApiException( $json, $status_code ); - $this->logger->log( - 'warning', - $error->getMessage(), - array( - 'args' => $args, - 'response' => $response, - ) - ); - throw $error; } $hook = $this->webhook_factory->from_paypal_response( $json ); @@ -160,18 +142,9 @@ class WebhookEndpoint { $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { - $error = new RuntimeException( + throw new RuntimeException( __( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' ) ); - $this->logger->log( - 'warning', - $error->getMessage(), - array( - 'args' => $args, - 'response' => $response, - ) - ); - throw $error; } return wp_remote_retrieve_response_code( $response ) === 204; } diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index 3b19bf7a1..0da653f50 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -22,10 +22,12 @@ return array( $factory = $container->get( 'api.factory.webhook' ); $endpoint = $container->get( 'api.endpoint.webhook' ); $rest_endpoint = $container->get( 'webhook.endpoint.controller' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new WebhookRegistrar( $factory, $endpoint, - $rest_endpoint + $rest_endpoint, + $logger ); }, 'webhook.endpoint.controller' => function( $container ) : IncomingWebhookEndpoint { diff --git a/modules/ppcp-webhooks/src/class-webhookregistrar.php b/modules/ppcp-webhooks/src/class-webhookregistrar.php index 5bf11d190..d2cb6c66a 100644 --- a/modules/ppcp-webhooks/src/class-webhookregistrar.php +++ b/modules/ppcp-webhooks/src/class-webhookregistrar.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks; +use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; @@ -43,22 +44,32 @@ class WebhookRegistrar { */ private $rest_endpoint; + /** + * The logger. + * + * @var LoggerInterface + */ + private $logger; + /** * WebhookRegistrar constructor. * * @param WebhookFactory $webhook_factory The Webhook factory. * @param WebhookEndpoint $endpoint The Webhook endpoint. * @param IncomingWebhookEndpoint $rest_endpoint The WordPress Rest API endpoint. + * @param LoggerInterface $logger The logger. */ public function __construct( WebhookFactory $webhook_factory, WebhookEndpoint $endpoint, - IncomingWebhookEndpoint $rest_endpoint + IncomingWebhookEndpoint $rest_endpoint, + LoggerInterface $logger ) { $this->webhook_factory = $webhook_factory; $this->endpoint = $endpoint; $this->rest_endpoint = $rest_endpoint; + $this->logger = $logger; } /** @@ -83,6 +94,7 @@ class WebhookRegistrar { ); return true; } catch ( RuntimeException $error ) { + $this->logger->error( 'Failed to register webhooks: ' . $error->getMessage() ); return false; } } @@ -101,6 +113,7 @@ class WebhookRegistrar { $webhook = $this->webhook_factory->from_array( $data ); $success = $this->endpoint->delete( $webhook ); } catch ( RuntimeException $error ) { + $this->logger->error( 'Failed to delete webhooks: ' . $error->getMessage() ); return false; } From 09cef78577b9dfe1256717b5ee54fa63f81c5e4b Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Sep 2021 10:16:16 +0300 Subject: [PATCH 036/101] Fix and refactor credential change handling --- .../src/Settings/class-settingslistener.php | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php index dd3e89aa1..b8ef0a937 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php @@ -25,6 +25,11 @@ class SettingsListener { const NONCE = 'ppcp-settings'; + private const CREDENTIALS_ADDED = 'credentials_added'; + private const CREDENTIALS_REMOVED = 'credentials_removed'; + private const CREDENTIALS_CHANGED = 'credentials_changed'; + private const CREDENTIALS_UNCHANGED = 'credentials_unchanged'; + /** * The Settings. * @@ -228,18 +233,40 @@ class SettingsListener { $settings = $this->read_active_credentials_from_settings( $settings ); + $credentials_change_status = $this->determine_credentials_change_status( $settings ); + if ( PayPalGateway::ID === $this->page_id ) { $settings['enabled'] = isset( $_POST['woocommerce_ppcp-gateway_enabled'] ) && 1 === absint( $_POST['woocommerce_ppcp-gateway_enabled'] ); - $this->maybe_register_webhooks( $settings ); } // phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing + if ( self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) { + $this->settings->set( 'products_dcc_enabled', null ); + } + + if ( in_array( + $credentials_change_status, + array( self::CREDENTIALS_REMOVED, self::CREDENTIALS_CHANGED ), + true + ) ) { + $this->webhook_registrar->unregister(); + } + foreach ( $settings as $id => $value ) { $this->settings->set( $id, $value ); } $this->settings->persist(); + + if ( in_array( + $credentials_change_status, + array( self::CREDENTIALS_ADDED, self::CREDENTIALS_CHANGED ), + true + ) ) { + $this->webhook_registrar->register(); + } + if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) { $this->cache->delete( PayPalBearer::CACHE_KEY ); } @@ -275,30 +302,36 @@ class SettingsListener { } /** - * Depending on the settings change, we might need to register or unregister the Webhooks at PayPal. + * Checks whether on the credentials changed. * - * @param array $settings The settings. - * - * @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException If a setting hasn't been found. + * @param array $new_settings New settings. + * @return string One of the CREDENTIALS_ constants. */ - private function maybe_register_webhooks( array $settings ) { + private function determine_credentials_change_status( array $new_settings ): string { + $current_id = $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : ''; + $current_secret = $this->settings->has( 'client_secret' ) ? $this->settings->get( 'client_secret' ) : ''; + $new_id = $new_settings['client_id'] ?? ''; + $new_secret = $new_settings['client_secret'] ?? ''; - if ( ! $this->settings->has( 'client_id' ) && $settings['client_id'] ) { - $this->settings->set( 'products_dcc_enabled', null ); - $this->webhook_registrar->register(); + $had_credentials = $current_id && $current_secret; + $submitted_credentials = $new_id && $new_secret; + + if ( ! $had_credentials && $submitted_credentials ) { + return self::CREDENTIALS_ADDED; } - if ( $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) ) { - $current_secret = $this->settings->has( 'client_secret' ) ? - $this->settings->get( 'client_secret' ) : ''; + if ( $had_credentials ) { + if ( ! $submitted_credentials ) { + return self::CREDENTIALS_REMOVED; + } + if ( - $settings['client_id'] !== $this->settings->get( 'client_id' ) - || $settings['client_secret'] !== $current_secret + $current_id !== $new_id + || $current_secret !== $new_secret ) { - $this->settings->set( 'products_dcc_enabled', null ); - $this->webhook_registrar->unregister(); - $this->webhook_registrar->register(); + return self::CREDENTIALS_CHANGED; } } + return self::CREDENTIALS_UNCHANGED; } /** From 803bc64d349814ddda6217c691d0f99182bc395a Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Sep 2021 10:16:49 +0300 Subject: [PATCH 037/101] Add logging --- modules/ppcp-webhooks/src/class-webhookregistrar.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ppcp-webhooks/src/class-webhookregistrar.php b/modules/ppcp-webhooks/src/class-webhookregistrar.php index d2cb6c66a..bf587dd40 100644 --- a/modules/ppcp-webhooks/src/class-webhookregistrar.php +++ b/modules/ppcp-webhooks/src/class-webhookregistrar.php @@ -92,6 +92,7 @@ class WebhookRegistrar { self::KEY, $created->to_array() ); + $this->logger->info( 'Webhooks registered.' ); return true; } catch ( RuntimeException $error ) { $this->logger->error( 'Failed to register webhooks: ' . $error->getMessage() ); @@ -119,6 +120,7 @@ class WebhookRegistrar { if ( $success ) { delete_option( self::KEY ); + $this->logger->info( 'Webhooks deleted.' ); } return $success; } From 4c02326d6355481b26b0e787f2f7b0c9c90b6025 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Sep 2021 10:20:32 +0300 Subject: [PATCH 038/101] Register webhooks a bit later Otherwise it uses old state (and cannot choose the correct host, etc.), at least with ALTERNATE_WP_CRON --- .../ppcp-onboarding/src/Endpoint/class-loginsellerendpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-onboarding/src/Endpoint/class-loginsellerendpoint.php b/modules/ppcp-onboarding/src/Endpoint/class-loginsellerendpoint.php index 7e1afc61f..3b006413f 100644 --- a/modules/ppcp-onboarding/src/Endpoint/class-loginsellerendpoint.php +++ b/modules/ppcp-onboarding/src/Endpoint/class-loginsellerendpoint.php @@ -136,7 +136,7 @@ class LoginSellerEndpoint implements EndpointInterface { $this->cache->delete( PayPalBearer::CACHE_KEY ); } wp_schedule_single_event( - time() - 1, + time() + 5, WebhookRegistrar::EVENT_HOOK ); wp_send_json_success(); From ab32a20f68634eb13d241fc8495109b47c3ad64c Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Sep 2021 09:28:48 +0200 Subject: [PATCH 039/101] Add and remove class for css opacity --- .../resources/css/hosted-fields.scss | 4 +++ .../ContextBootstrap/CheckoutBootstap.js | 36 +++++++++---------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/modules/ppcp-button/resources/css/hosted-fields.scss b/modules/ppcp-button/resources/css/hosted-fields.scss index 5b90939b4..10f29f523 100644 --- a/modules/ppcp-button/resources/css/hosted-fields.scss +++ b/modules/ppcp-button/resources/css/hosted-fields.scss @@ -7,3 +7,7 @@ .payments-sdk-contingency-handler { z-index: 1000 !important; } + +.ppcp-credit-card-gateway-form-field { + opacity: .5 !important; +} diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index a240f8270..22ec9a2d8 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -113,32 +113,32 @@ class CheckoutBootstap { } disableCreditCardFields() { - jQuery('label[for="ppcp-credit-card-gateway-card-number"]').css({ opacity: 0.4 }) - jQuery('#ppcp-credit-card-gateway-card-number').css({ opacity: 0.4 }) - jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", true); - jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').css({ opacity: 0.4 }) - jQuery('#ppcp-credit-card-gateway-card-expiry').css({ opacity: 0.4 }) + jQuery('label[for="ppcp-credit-card-gateway-card-number"]').addClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-number').addClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", true) + jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').addClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-expiry').addClass('ppcp-credit-card-gateway-form-field') jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", true) - jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').css({ opacity: 0.4 }) - jQuery('#ppcp-credit-card-gateway-card-cvc').css({ opacity: 0.4 }) + jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').addClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-cvc').addClass('ppcp-credit-card-gateway-form-field') jQuery('#ppcp-credit-card-gateway-card-cvc').attr("disabled", true) - jQuery('label[for="vault"]').css({ opacity: 0.4 }) - jQuery('#ppcp-credit-card-vault').css({ opacity: 0.4 }) + jQuery('label[for="vault"]').addClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-vault').addClass('ppcp-credit-card-gateway-form-field') jQuery('#ppcp-credit-card-vault').attr("disabled", true) } enableCreditCardFields() { - jQuery('label[for="ppcp-credit-card-gateway-card-number"]').css({ opacity: 1 }) - jQuery('#ppcp-credit-card-gateway-card-number').css({ opacity: 1 }) + jQuery('label[for="ppcp-credit-card-gateway-card-number"]').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-number').removeClass('ppcp-credit-card-gateway-form-field') jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", false) - jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').css({ opacity: 1 }) - jQuery('#ppcp-credit-card-gateway-card-expiry').css({ opacity: 1 }) - jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", false); - jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').css({ opacity: 1 }) - jQuery('#ppcp-credit-card-gateway-card-cvc').css({ opacity: 1 }) + jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-expiry').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", false) + jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-gateway-card-cvc').removeClass('ppcp-credit-card-gateway-form-field') jQuery('#ppcp-credit-card-gateway-card-cvc').attr("disabled", false) - jQuery('label[for="vault"]').css({ opacity: 1 }) - jQuery('#ppcp-credit-card-vault').css({ opacity: 1 }) + jQuery('label[for="vault"]').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('#ppcp-credit-card-vault').removeClass('ppcp-credit-card-gateway-form-field') jQuery('#ppcp-credit-card-vault').attr("disabled", false) } } From da7450ccdfe957b6655e20b12cab6784bc1d486b Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Sep 2021 11:24:27 +0200 Subject: [PATCH 040/101] Dispatch hosted fields loaded event --- .../resources/js/modules/ContextBootstrap/CheckoutBootstap.js | 4 ++-- .../resources/js/modules/Renderer/CreditCardRenderer.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 22ec9a2d8..b724248ba 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -24,11 +24,11 @@ class CheckoutBootstap { }) - setTimeout(() => { + jQuery(document).on('hosted_fields_loaded', () => { jQuery('#saved-credit-card').on('change', () => { this.displayPlaceOrderButtonForSavedCreditCards() }) - }, 3000) + }); this.switchBetweenPayPalandOrderButton() this.displayPlaceOrderButtonForSavedCreditCards() diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index d506fadce..7005afa6a 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -100,6 +100,7 @@ class CreditCardRenderer { } } }).then(hostedFields => { + document.dispatchEvent(new CustomEvent("hosted_fields_loaded")); this.currentHostedFieldsInstance = hostedFields; hostedFields.on('inputSubmitRequest', () => { From 6896265ca3d32854cfc6d7c76dfd683057c96b4b Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Sep 2021 11:44:49 +0200 Subject: [PATCH 041/101] Add `-disabled` to css class name --- .../resources/css/hosted-fields.scss | 2 +- .../ContextBootstrap/CheckoutBootstap.js | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-button/resources/css/hosted-fields.scss b/modules/ppcp-button/resources/css/hosted-fields.scss index 10f29f523..ba6328001 100644 --- a/modules/ppcp-button/resources/css/hosted-fields.scss +++ b/modules/ppcp-button/resources/css/hosted-fields.scss @@ -8,6 +8,6 @@ z-index: 1000 !important; } -.ppcp-credit-card-gateway-form-field { +.ppcp-credit-card-gateway-form-field-disabled { opacity: .5 !important; } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index b724248ba..ccbedc849 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -113,32 +113,32 @@ class CheckoutBootstap { } disableCreditCardFields() { - jQuery('label[for="ppcp-credit-card-gateway-card-number"]').addClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-gateway-card-number').addClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="ppcp-credit-card-gateway-card-number"]').addClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-gateway-card-number').addClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", true) - jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').addClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-gateway-card-expiry').addClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').addClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-gateway-card-expiry').addClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", true) - jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').addClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-gateway-card-cvc').addClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').addClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-gateway-card-cvc').addClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-cvc').attr("disabled", true) - jQuery('label[for="vault"]').addClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-vault').addClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="vault"]').addClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-vault').addClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-vault').attr("disabled", true) } enableCreditCardFields() { - jQuery('label[for="ppcp-credit-card-gateway-card-number"]').removeClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-gateway-card-number').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="ppcp-credit-card-gateway-card-number"]').removeClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-gateway-card-number').removeClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-number').attr("disabled", false) - jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').removeClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-gateway-card-expiry').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').removeClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-gateway-card-expiry').removeClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-expiry').attr("disabled", false) - jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').removeClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-gateway-card-cvc').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').removeClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-gateway-card-cvc').removeClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-cvc').attr("disabled", false) - jQuery('label[for="vault"]').removeClass('ppcp-credit-card-gateway-form-field') - jQuery('#ppcp-credit-card-vault').removeClass('ppcp-credit-card-gateway-form-field') + jQuery('label[for="vault"]').removeClass('ppcp-credit-card-gateway-form-field-disabled') + jQuery('#ppcp-credit-card-vault').removeClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-vault').attr("disabled", false) } } From 80d56f293c49659b7eca1299aa82e77f777c827e Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Sep 2021 10:51:35 +0300 Subject: [PATCH 042/101] Do not detect credentials change on DCC settings page --- .../src/Settings/class-settingslistener.php | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php index b8ef0a937..1e3447f97 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php @@ -233,25 +233,29 @@ class SettingsListener { $settings = $this->read_active_credentials_from_settings( $settings ); - $credentials_change_status = $this->determine_credentials_change_status( $settings ); + $credentials_change_status = null; // Cannot detect on Card Processing page. if ( PayPalGateway::ID === $this->page_id ) { $settings['enabled'] = isset( $_POST['woocommerce_ppcp-gateway_enabled'] ) && 1 === absint( $_POST['woocommerce_ppcp-gateway_enabled'] ); + + $credentials_change_status = $this->determine_credentials_change_status( $settings ); } // phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing - if ( self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) { - $this->settings->set( 'products_dcc_enabled', null ); - } + if ( $credentials_change_status ) { + if ( self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) { + $this->settings->set( 'products_dcc_enabled', null ); + } - if ( in_array( - $credentials_change_status, - array( self::CREDENTIALS_REMOVED, self::CREDENTIALS_CHANGED ), - true - ) ) { - $this->webhook_registrar->unregister(); + if ( in_array( + $credentials_change_status, + array( self::CREDENTIALS_REMOVED, self::CREDENTIALS_CHANGED ), + true + ) ) { + $this->webhook_registrar->unregister(); + } } foreach ( $settings as $id => $value ) { @@ -259,12 +263,14 @@ class SettingsListener { } $this->settings->persist(); - if ( in_array( - $credentials_change_status, - array( self::CREDENTIALS_ADDED, self::CREDENTIALS_CHANGED ), - true - ) ) { - $this->webhook_registrar->register(); + if ( $credentials_change_status ) { + if ( in_array( + $credentials_change_status, + array( self::CREDENTIALS_ADDED, self::CREDENTIALS_CHANGED ), + true + ) ) { + $this->webhook_registrar->register(); + } } if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) { From 9f348ed411ee7c3f3eb5485c3bf02008c3e66f2e Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Sep 2021 12:46:41 +0200 Subject: [PATCH 043/101] Do not log sensible body data --- .../src/Endpoint/class-requesttrait.php | 2 +- .../src/Logger/class-woocommercelogger.php | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index 25c97c050..c43166b7d 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -41,7 +41,7 @@ trait RequestTrait { $response = wp_remote_get( $url, $args ); if ( $this->logger instanceof WooCommerceLogger ) { - $this->logger->logRequestResponse( $url, $args, $response ); + $this->logger->logRequestResponse( $url, $args, $response, $this->host ); } return $response; diff --git a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php index bef6f1631..1ae4f2f76 100644 --- a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php +++ b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php @@ -69,9 +69,10 @@ class WooCommerceLogger implements LoggerInterface { * @param string $url The request URL. * @param array $args The request arguments. * @param array|WP_Error $response The response or WP_Error on failure. + * @param string $host The host. * @return void */ - public function logRequestResponse( string $url, array $args, $response ) { + public function logRequestResponse( string $url, array $args, $response, string $host ) { if ( is_wp_error( $response ) ) { $this->error( $response->get_error_code() . ' ' . $response->get_error_message() ); @@ -81,7 +82,16 @@ class WooCommerceLogger implements LoggerInterface { $method = $args['method'] ?? ''; $output = $method . ' ' . $url . "\n"; if ( isset( $args['body'] ) ) { - $output .= 'Request Body: ' . wc_print_r( $args['body'], true ) . "\n"; + if ( ! in_array( + $url, + array( + trailingslashit( $host ) . 'v1/oauth2/token/', + trailingslashit( $host ) . 'v1/oauth2/token?grant_type=client_credentials', + ), + true + ) ) { + $output .= 'Request Body: ' . wc_print_r( $args['body'], true ) . "\n"; + } } if ( is_array( $response ) ) { @@ -90,9 +100,12 @@ class WooCommerceLogger implements LoggerInterface { } if ( isset( $response['response'] ) ) { $output .= 'Response: ' . wc_print_r( $response['response'], true ) . "\n"; - } - if ( isset( $response['body'] ) ) { - $output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n"; + + if ( isset( $response['body'] ) + && isset( $response['response']['code'] ) + && ! in_array( $response['response']['code'], array( 200, 201, 202, 204 ), true ) ) { + $output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n"; + } } } From 5ca2f9d69fd378dfadd7a25c264ddb7ed65bd34f Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Sep 2021 16:23:33 +0200 Subject: [PATCH 044/101] Move request response information from logger to request trait and return it as string --- .../src/Endpoint/class-requesttrait.php | 45 ++++++++++++++++- .../src/Logger/class-woocommercelogger.php | 49 ------------------- 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index c43166b7d..de49dd8fa 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use WooCommerce\WooCommerce\Logging\Logger\WooCommerceLogger; +use WP_Error; /** * Trait RequestTrait @@ -40,12 +41,52 @@ trait RequestTrait { $response = wp_remote_get( $url, $args ); - if ( $this->logger instanceof WooCommerceLogger ) { - $this->logger->logRequestResponse( $url, $args, $response, $this->host ); + if ( is_wp_error( $response ) ) { + $this->logger->error( $response->get_error_code() . ' ' . $response->get_error_message() ); + return $response; } + $this->logger->info( $this->request_response_string( $url, $args, $response ) ); return $response; } + /** + * Returns request and response information as string. + * + * @param string $url The request URL. + * @param array $args The request arguments. + * @param array $response The response. + * @return string + */ + private function request_response_string( string $url, array $args, array $response ): string { + $method = $args['method'] ?? ''; + $output = $method . ' ' . $url . "\n"; + if ( isset( $args['body'] ) ) { + if ( ! in_array( + $url, + array( + trailingslashit( $this->host ) . 'v1/oauth2/token/', + trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials', + ), + true + ) ) { + $output .= 'Request Body: ' . wc_print_r( $args['body'], true ) . "\n"; + } + } + if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { + $output .= 'Response Debug ID: ' . $response['headers']->getAll()['paypal-debug-id'] . "\n"; + } + if ( isset( $response['response'] ) ) { + $output .= 'Response: ' . wc_print_r( $response['response'], true ) . "\n"; + + if ( isset( $response['body'] ) + && isset( $response['response']['code'] ) + && ! in_array( $response['response']['code'], array( 200, 201, 202, 204 ), true ) ) { + $output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n"; + } + } + + return $output; + } } diff --git a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php index 1ae4f2f76..247f1aa55 100644 --- a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php +++ b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php @@ -62,53 +62,4 @@ class WooCommerceLogger implements LoggerInterface { } $this->wc_logger->log( $level, $message, $context ); } - - /** - * Logs request and response information. - * - * @param string $url The request URL. - * @param array $args The request arguments. - * @param array|WP_Error $response The response or WP_Error on failure. - * @param string $host The host. - * @return void - */ - public function logRequestResponse( string $url, array $args, $response, string $host ) { - - if ( is_wp_error( $response ) ) { - $this->error( $response->get_error_code() . ' ' . $response->get_error_message() ); - return; - } - - $method = $args['method'] ?? ''; - $output = $method . ' ' . $url . "\n"; - if ( isset( $args['body'] ) ) { - if ( ! in_array( - $url, - array( - trailingslashit( $host ) . 'v1/oauth2/token/', - trailingslashit( $host ) . 'v1/oauth2/token?grant_type=client_credentials', - ), - true - ) ) { - $output .= 'Request Body: ' . wc_print_r( $args['body'], true ) . "\n"; - } - } - - if ( is_array( $response ) ) { - if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) { - $output .= 'Response Debug ID: ' . $response['headers']->getAll()['paypal-debug-id'] . "\n"; - } - if ( isset( $response['response'] ) ) { - $output .= 'Response: ' . wc_print_r( $response['response'], true ) . "\n"; - - if ( isset( $response['body'] ) - && isset( $response['response']['code'] ) - && ! in_array( $response['response']['code'], array( 200, 201, 202, 204 ), true ) ) { - $output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n"; - } - } - } - - $this->info( $output ); - } } From 12b9905c604c9580d55dd619f49dc8576c4db553 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Tue, 14 Sep 2021 17:46:33 +0200 Subject: [PATCH 045/101] Fix unit tests (WIP) --- .../src/Endpoint/class-requesttrait.php | 8 +---- .../Authentication/PayPalBearerTest.php | 27 +++++++++++++---- .../ApiClient/Endpoint/IdentityTokenTest.php | 30 ++++++++++++++++--- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index de49dd8fa..5e7674a4a 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -40,13 +40,7 @@ trait RequestTrait { } $response = wp_remote_get( $url, $args ); - - if ( is_wp_error( $response ) ) { - $this->logger->error( $response->get_error_code() . ' ' . $response->get_error_message() ); - return $response; - } - - $this->logger->info( $this->request_response_string( $url, $args, $response ) ); + $this->logger->debug( $this->request_response_string( $url, $args, $response ) ); return $response; } diff --git a/tests/PHPUnit/ApiClient/Authentication/PayPalBearerTest.php b/tests/PHPUnit/ApiClient/Authentication/PayPalBearerTest.php index 469c337d5..9b423866b 100644 --- a/tests/PHPUnit/ApiClient/Authentication/PayPalBearerTest.php +++ b/tests/PHPUnit/ApiClient/Authentication/PayPalBearerTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Authentication; +use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\TestCase; @@ -28,10 +29,12 @@ class PayPalBearerTest extends TestCase $key = 'key'; $secret = 'secret'; $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $settings = Mockery::mock(Settings::class); $settings->shouldReceive('has')->andReturn(true); $settings->shouldReceive('get')->andReturn(''); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings); @@ -40,7 +43,7 @@ class PayPalBearerTest extends TestCase ->andReturn($host . '/'); expect('wp_remote_get') ->andReturnUsing( - function ($url, $args) use ($json, $key, $secret, $host) { + function ($url, $args) use ($json, $key, $secret, $host, $headers) { if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { return false; } @@ -53,6 +56,7 @@ class PayPalBearerTest extends TestCase return [ 'body' => $json, + 'headers' => $headers ]; } ); @@ -80,10 +84,12 @@ class PayPalBearerTest extends TestCase $key = 'key'; $secret = 'secret'; $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $settings = Mockery::mock(Settings::class); $settings->shouldReceive('has')->andReturn(true); $settings->shouldReceive('get')->andReturn(''); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings); @@ -92,7 +98,7 @@ class PayPalBearerTest extends TestCase ->andReturn($host . '/'); expect('wp_remote_get') ->andReturnUsing( - function ($url, $args) use ($json, $key, $secret, $host) { + function ($url, $args) use ($json, $key, $secret, $host, $headers) { if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { return false; } @@ -105,6 +111,7 @@ class PayPalBearerTest extends TestCase return [ 'body' => $json, + 'headers' => $headers, ]; } ); @@ -153,9 +160,12 @@ class PayPalBearerTest extends TestCase $secret = 'secret'; $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $settings = Mockery::mock(Settings::class); $settings->shouldReceive('has')->andReturn(true); $settings->shouldReceive('get')->andReturn(''); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings); @@ -164,7 +174,7 @@ class PayPalBearerTest extends TestCase ->andReturn($host . '/'); expect('wp_remote_get') ->andReturnUsing( - function ($url, $args) use ($json, $key, $secret, $host) { + function ($url, $args) use ($json, $key, $secret, $host, $headers) { if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { return false; } @@ -177,6 +187,7 @@ class PayPalBearerTest extends TestCase return [ 'body' => $json, + 'headers' => $headers, ]; } ); @@ -199,9 +210,12 @@ class PayPalBearerTest extends TestCase $secret = 'secret'; $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $settings = Mockery::mock(Settings::class); $settings->shouldReceive('has')->andReturn(true); $settings->shouldReceive('get')->andReturn(''); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings); @@ -210,7 +224,7 @@ class PayPalBearerTest extends TestCase ->andReturn($host . '/'); expect('wp_remote_get') ->andReturnUsing( - function ($url, $args) use ($json, $key, $secret, $host) { + function ($url, $args) use ($json, $key, $secret, $host, $headers) { if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { return false; } @@ -223,6 +237,7 @@ class PayPalBearerTest extends TestCase return [ 'body' => $json, + 'headers' => $headers, ]; } ); diff --git a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php index 0e3371b64..1ff901070 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use Psr\Log\LoggerInterface; +use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Token; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; @@ -11,6 +12,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\TestCase; use Mockery; use function Brain\Monkey\Functions\expect; +use function Brain\Monkey\Functions\when; class IdentityTokenTest extends TestCase { @@ -40,10 +42,18 @@ class IdentityTokenTest extends TestCase $this->bearer ->expects('bearer')->andReturn($token); - $rawResponse = ['body' => '{"client_token":"abc123", "expires_in":3600}']; $host = $this->host; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $this->logger->shouldReceive('debug'); + + $rawResponse = [ + 'body' => '{"client_token":"abc123", "expires_in":3600}', + 'headers' => $headers, + ]; + expect('wp_remote_get') - ->andReturnUsing(function ($url, $args) use ($rawResponse, $host) { + ->andReturnUsing(function ($url, $args) use ($rawResponse, $host, $headers) { if ($url !== $host . 'v1/identity/generate-token') { return false; } @@ -65,6 +75,7 @@ class IdentityTokenTest extends TestCase expect('is_wp_error')->with($rawResponse)->andReturn(false); expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200); + when('wc_print_r')->returnArg(); $result = $this->sut->generate_for_customer(1); $this->assertInstanceOf(Token::class, $result); @@ -78,9 +89,13 @@ class IdentityTokenTest extends TestCase $this->bearer ->expects('bearer')->andReturn($token); - expect('wp_remote_get')->andReturn(); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + expect('wp_remote_get')->andReturn(['headers' => $headers,]); expect('is_wp_error')->andReturn(true); + when('wc_print_r')->returnArg(); $this->logger->shouldReceive('log'); + $this->logger->shouldReceive('debug'); $this->expectException(RuntimeException::class); $this->sut->generate_for_customer(1); @@ -94,10 +109,17 @@ class IdentityTokenTest extends TestCase $this->bearer ->expects('bearer')->andReturn($token); - expect('wp_remote_get')->andReturn(['body' => '',]); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + expect('wp_remote_get')->andReturn([ + 'body' => '', + 'headers' => $headers, + ]); expect('is_wp_error')->andReturn(false); expect('wp_remote_retrieve_response_code')->andReturn(500); + when('wc_print_r')->returnArg(); $this->logger->shouldReceive('log'); + $this->logger->shouldReceive('debug'); $this->expectException(PayPalApiException::class); $this->sut->generate_for_customer(1); From 46308b504432595ec1f63a99a5012e4a4bc3c057 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Tue, 14 Sep 2021 15:42:33 -0300 Subject: [PATCH 046/101] Update wp.org assets --- wordpress_org_assets/banner-1544x500.png | Bin 0 -> 47740 bytes wordpress_org_assets/banner-772x250.png | Bin 0 -> 23653 bytes wordpress_org_assets/icon-128x128.png | Bin 15835 -> 3899 bytes wordpress_org_assets/icon-256x256.png | Bin 35452 -> 7591 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 wordpress_org_assets/banner-1544x500.png create mode 100644 wordpress_org_assets/banner-772x250.png diff --git a/wordpress_org_assets/banner-1544x500.png b/wordpress_org_assets/banner-1544x500.png new file mode 100644 index 0000000000000000000000000000000000000000..c1799a1183edc90a32b9a92e457ef0516da7016b GIT binary patch literal 47740 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 literal 0 HcmV?d00001 diff --git a/wordpress_org_assets/banner-772x250.png b/wordpress_org_assets/banner-772x250.png new file mode 100644 index 0000000000000000000000000000000000000000..9dc7d6f3c3b5884e522e16bb587c10b4f7f9db7d GIT binary patch literal 23653 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ŋ>!L1KxV-C!6W1$%eDf! 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 diff --git a/wordpress_org_assets/icon-256x256.png b/wordpress_org_assets/icon-256x256.png index 05e79e17deb690758d3926113e6e804414902bbc..8a19ca4e7a579be89d54a457eafbb62af6ddcf4d 100644 GIT binary patch literal 7591 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 literal 35452 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 From 804ff8e526403a09dc8ae23331d58961dedc74ec Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 15 Sep 2021 11:28:32 +0200 Subject: [PATCH 047/101] Fix unit tests --- .../ApiClient/Endpoint/OrderEndpointTest.php | 131 +++++++++++++++--- .../Endpoint/PaymentTokenEndpointTest.php | 47 ++++++- .../Endpoint/PaymentsEndpointTest.php | 49 ++++++- 3 files changed, 192 insertions(+), 35 deletions(-) diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php index 17ebf857b..d44471319 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use Hamcrest\Matchers; +use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture; @@ -22,14 +23,20 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; use WooCommerce\PayPalCommerce\ApiClient\TestCase; use Mockery; - use Psr\Log\LoggerInterface; use function Brain\Monkey\Functions\expect; +use function Brain\Monkey\Functions\when; class OrderEndpointTest extends TestCase { - public function testOrderDefault() + public function setUp(): void + { + parent::setUp(); + when('wc_print_r')->returnArg(); + } + + public function testOrderDefault() { expect('wp_json_encode')->andReturnUsing('json_encode'); $orderId = 'id'; @@ -51,10 +58,14 @@ class OrderEndpointTest extends TestCase $intent = 'CAPTURE'; $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository ->expects('get_for_order_id')->with($orderId)->andReturn('uniqueRequestId'); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $testee = new OrderEndpoint( $host, $bearer, @@ -66,7 +77,10 @@ class OrderEndpointTest extends TestCase $paypalRequestIdRepository ); - $rawResponse = ['body' => '{"is_correct":true}']; + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; expect('wp_remote_get') ->andReturnUsing(function ($url, $args) use ($rawResponse, $host, $orderId) { if ($url !== $host . 'v2/checkout/orders/' . $orderId) { @@ -103,10 +117,14 @@ class OrderEndpointTest extends TestCase $intent = 'CAPTURE'; $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository ->expects('get_for_order_id')->with($orderId)->andReturn('uniqueRequestId'); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $testee = new OrderEndpoint( $host, $bearer, @@ -118,7 +136,10 @@ class OrderEndpointTest extends TestCase $paypalRequestIdRepository ); - $rawResponse = ['body' => '{"is_correct":true}']; + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; expect('wp_remote_get')->andReturn($rawResponse); expect('is_wp_error')->with($rawResponse)->andReturn(true); @@ -140,9 +161,15 @@ class OrderEndpointTest extends TestCase $orderFactory = Mockery::mock(OrderFactory::class); $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); $intent = 'CAPTURE'; - $rawResponse = ['body' => '{"some_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"some_error":true}', + 'headers' => $headers, + ]; $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -176,8 +203,12 @@ class OrderEndpointTest extends TestCase $orderToCapture = Mockery::mock(Order::class); $orderToCapture->expects('status')->andReturn($orderToCaptureStatus); $orderToCapture->expects('id')->andReturn($orderId); - - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $expectedOrder = Mockery::mock(Order::class); $host = 'https://example.com/'; $token = Mockery::mock(Token::class); @@ -202,6 +233,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -307,6 +339,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -321,8 +354,12 @@ class OrderEndpointTest extends TestCase $applicationContextRepository, $paypalRequestIdRepository ); - - $rawResponse = ['body' => '{"is_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_error":true}', + 'headers' => $headers, + ]; expect('wp_remote_get')->andReturn($rawResponse); expect('is_wp_error')->with($rawResponse)->andReturn(true); $this->expectException(RuntimeException::class); @@ -351,6 +388,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -366,7 +404,12 @@ class OrderEndpointTest extends TestCase $paypalRequestIdRepository ); - $rawResponse = ['body' => '{"some_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"some_error":true}', + 'headers' => $headers + ]; expect('wp_remote_get')->andReturn($rawResponse); expect('is_wp_error')->with($rawResponse)->andReturn(false); expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); @@ -396,6 +439,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -415,8 +459,12 @@ class OrderEndpointTest extends TestCase )->makePartial(); $orderToExpect = Mockery::mock(Order::class); $testee->expects('order')->with($orderId)->andReturn($orderToExpect); - - $rawResponse = ['body' => '{"some_error": "' . ErrorResponse::ORDER_ALREADY_CAPTURED . '"}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"some_error": "' . ErrorResponse::ORDER_ALREADY_CAPTURED . '"}', + 'headers' => $headers, + ]; expect('wp_remote_get')->andReturn($rawResponse); expect('is_wp_error')->with($rawResponse)->andReturn(false); expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); @@ -436,8 +484,12 @@ class OrderEndpointTest extends TestCase ->shouldReceive('purchase_units') ->andReturn([]); $orderToCompare = Mockery::mock(Order::class); - - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $expectedOrder = Mockery::mock(Order::class); $host = 'https://example.com/'; $token = Mockery::mock(Token::class); @@ -464,6 +516,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -533,8 +586,12 @@ class OrderEndpointTest extends TestCase ->shouldReceive('purchase_units') ->andReturn([]); $orderToCompare = Mockery::mock(Order::class); - - $rawResponse = ['body' => '{"has_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"has_error":true}', + 'headers' => $headers, + ]; $expectedOrder = Mockery::mock(Order::class); $host = 'https://example.com/'; $token = Mockery::mock(Token::class); @@ -561,6 +618,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); $paypalRequestIdRepository @@ -624,8 +682,12 @@ class OrderEndpointTest extends TestCase ->shouldReceive('purchase_units') ->andReturn([]); $orderToCompare = Mockery::mock(Order::class); - - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $expectedOrder = Mockery::mock(Order::class); $host = 'https://example.com/'; $token = Mockery::mock(Token::class); @@ -652,6 +714,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); @@ -748,7 +811,12 @@ class OrderEndpointTest extends TestCase public function testCreateForPurchaseUnitsDefault() { expect('wp_json_encode')->andReturnUsing('json_encode'); - $rawResponse = ['body' => '{"success":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"success":true}', + 'headers' => $headers, + ]; $host = 'https://example.com/'; $bearer = Mockery::mock(Bearer::class); $token = Mockery::mock(Token::class); @@ -772,6 +840,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $applicationContext = Mockery::mock(ApplicationContext::class); $applicationContext ->expects('to_array') @@ -844,7 +913,12 @@ class OrderEndpointTest extends TestCase public function testCreateForPurchaseUnitsWithPayer() { expect('wp_json_encode')->andReturnUsing('json_encode'); - $rawResponse = ['body' => '{"success":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"success":true}', + 'headers' => $headers, + ]; $host = 'https://example.com/'; $token = Mockery::mock(Token::class); $token @@ -868,6 +942,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); $applicationContext = Mockery::mock(ApplicationContext::class); $applicationContext ->expects('to_array') @@ -928,7 +1003,12 @@ class OrderEndpointTest extends TestCase public function testCreateForPurchaseUnitsIsWpError() { expect('wp_json_encode')->andReturnUsing('json_encode'); - $rawResponse = ['body' => '{"success":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"success":true}', + 'headers' => $headers, + ]; $host = 'https://example.com/'; $token = Mockery::mock(Token::class); $token @@ -943,6 +1023,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContext = Mockery::mock(ApplicationContext::class); $applicationContext ->expects('to_array') @@ -1006,7 +1087,12 @@ class OrderEndpointTest extends TestCase public function testCreateForPurchaseUnitsIsNot201() { expect('wp_json_encode')->andReturnUsing('json_encode'); - $rawResponse = ['body' => '{"has_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"has_error":true}', + 'headers' => $headers, + ]; $host = 'https://example.com/'; $token = Mockery::mock(Token::class); $token @@ -1021,6 +1107,7 @@ class OrderEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $applicationContext = Mockery::mock(ApplicationContext::class); $applicationContext ->expects('to_array') diff --git a/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php index 8254049cd..e14288534 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use Psr\Log\LoggerInterface; +use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\Token; @@ -48,7 +49,12 @@ class PaymentTokenEndpointTest extends TestCase { $id = 1; $token = Mockery::mock(Token::class); - $rawResponse = ['body' => '{"payment_tokens":[{"id": "123abc"}]}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"payment_tokens":[{"id": "123abc"}]}', + 'headers' => $headers, + ]; $paymentToken = Mockery::mock(PaymentToken::class); $paymentToken->shouldReceive('id') ->andReturn('foo'); @@ -65,6 +71,8 @@ class PaymentTokenEndpointTest extends TestCase $this->factory->shouldReceive('from_paypal_response') ->andReturn($paymentToken); + $this->logger->shouldReceive('debug'); + $result = $this->sut->for_user($id); $this->assertInstanceOf(PaymentToken::class, $result[0]); @@ -74,7 +82,9 @@ class PaymentTokenEndpointTest extends TestCase { $id = 1; $token = Mockery::mock(Token::class); - $rawResponse = []; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = ['headers' => $headers,]; $this->bearer->shouldReceive('bearer') ->andReturn($token); $token->shouldReceive('token') @@ -84,6 +94,7 @@ class PaymentTokenEndpointTest extends TestCase expect('wp_remote_get')->andReturn($rawResponse); expect('is_wp_error')->with($rawResponse)->andReturn(true); $this->logger->shouldReceive('log'); + $this->logger->shouldReceive('debug'); $this->expectException(RuntimeException::class); $this->sut->for_user($id); @@ -93,7 +104,12 @@ class PaymentTokenEndpointTest extends TestCase { $id = 1; $token = Mockery::mock(Token::class); - $rawResponse = ['body' => '{"some_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"some_error":true}', + 'headers' => $headers, + ]; $this->bearer->shouldReceive('bearer') ->andReturn($token); $token->shouldReceive('token') @@ -105,6 +121,7 @@ class PaymentTokenEndpointTest extends TestCase expect('is_wp_error')->with($rawResponse)->andReturn(false); expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); $this->logger->shouldReceive('log'); + $this->logger->shouldReceive('debug'); $this->expectException(PayPalApiException::class); $this->sut->for_user($id); @@ -114,7 +131,12 @@ class PaymentTokenEndpointTest extends TestCase { $id = 1; $token = Mockery::mock(Token::class); - $rawResponse = ['body' => '{"payment_tokens":[]}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"payment_tokens":[]}', + 'headers' => $headers, + ]; $this->bearer->shouldReceive('bearer') ->andReturn($token); $token->shouldReceive('token') @@ -126,6 +148,7 @@ class PaymentTokenEndpointTest extends TestCase expect('is_wp_error')->with($rawResponse)->andReturn(false); expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200); $this->logger->shouldReceive('log'); + $this->logger->shouldReceive('debug'); $this->expectException(RuntimeException::class); $this->sut->for_user($id); @@ -133,7 +156,7 @@ class PaymentTokenEndpointTest extends TestCase public function testDeleteToken() { - $paymentToken = $paymentToken = Mockery::mock(PaymentToken::class); + $paymentToken = Mockery::mock(PaymentToken::class); $paymentToken->shouldReceive('id') ->andReturn('foo'); $token = Mockery::mock(Token::class); @@ -142,9 +165,14 @@ class PaymentTokenEndpointTest extends TestCase $token->shouldReceive('token') ->andReturn('bearer'); - expect('wp_remote_get')->andReturn(); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + expect('wp_remote_get')->andReturn([ + 'headers' => $headers, + ]); expect('is_wp_error')->andReturn(false); expect('wp_remote_retrieve_response_code')->andReturn(204); + $this->logger->shouldReceive('debug'); $this->sut->delete_token($paymentToken); } @@ -160,9 +188,14 @@ class PaymentTokenEndpointTest extends TestCase $token->shouldReceive('token') ->andReturn('bearer'); - expect('wp_remote_get')->andReturn(); + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + expect('wp_remote_get')->andReturn([ + 'headers' => $headers, + ]); expect('is_wp_error')->andReturn(true); $this->logger->shouldReceive('log'); + $this->logger->shouldReceive('debug'); $this->expectException(RuntimeException::class); $this->sut->delete_token($paymentToken); diff --git a/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php index 2e0433bcf..87b2ca9e9 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; +use Requests_Utility_CaseInsensitiveDictionary; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\ErrorResponseCollection; @@ -40,8 +41,14 @@ class PaymentsEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $testee = new PaymentsEndpoint( $host, @@ -88,8 +95,14 @@ class PaymentsEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $testee = new PaymentsEndpoint( $host, @@ -119,10 +132,16 @@ class PaymentsEndpointTest extends TestCase $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $rawResponse = ['body' => '{"some_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"some_error":true}', + 'headers' => $headers, + ]; $logger = Mockery::mock(LoggerInterface::class); $logger->shouldReceive('log'); + $logger->shouldReceive('debug'); $testee = new PaymentsEndpoint( $host, @@ -161,8 +180,14 @@ class PaymentsEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->shouldNotReceive('log'); + $logger->shouldReceive('debug'); - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $testee = new PaymentsEndpoint( $host, @@ -212,8 +237,14 @@ class PaymentsEndpointTest extends TestCase $logger = Mockery::mock(LoggerInterface::class); $logger->expects('log'); + $logger->expects('debug'); - $rawResponse = ['body' => '{"is_correct":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"is_correct":true}', + 'headers' => $headers, + ]; $testee = new PaymentsEndpoint( $host, @@ -243,10 +274,16 @@ class PaymentsEndpointTest extends TestCase $authorizationFactory = Mockery::mock(AuthorizationFactory::class); - $rawResponse = ['body' => '{"some_error":true}']; + $headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class); + $headers->shouldReceive('getAll'); + $rawResponse = [ + 'body' => '{"some_error":true}', + 'headers' => $headers, + ]; $logger = Mockery::mock(LoggerInterface::class); $logger->expects('log'); + $logger->expects('debug'); $testee = new PaymentsEndpoint( $host, From 26e65dc3b6b92e154227e7f392d95cd154cd280a Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 15 Sep 2021 11:40:26 +0200 Subject: [PATCH 048/101] Remove unused class --- .../woocommerce-logging/src/Logger/class-woocommercelogger.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php index 247f1aa55..bee7e7774 100644 --- a/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php +++ b/modules/woocommerce-logging/src/Logger/class-woocommercelogger.php @@ -14,7 +14,6 @@ namespace WooCommerce\WooCommerce\Logging\Logger; use Psr\Log\LoggerInterface; use Psr\Log\LoggerTrait; -use WP_Error; /** * Class WooCommerceLogger From c77baafd688ec73fce1e2d2149e00bb18185c978 Mon Sep 17 00:00:00 2001 From: dinamiko Date: Wed, 15 Sep 2021 11:40:58 +0200 Subject: [PATCH 049/101] Remove unused class --- modules/ppcp-api-client/src/Endpoint/class-requesttrait.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php index 5e7674a4a..187f7c89a 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php +++ b/modules/ppcp-api-client/src/Endpoint/class-requesttrait.php @@ -9,9 +9,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; -use WooCommerce\WooCommerce\Logging\Logger\WooCommerceLogger; -use WP_Error; - /** * Trait RequestTrait */ From 08ab8ef4040511ee4b0f76f75496b6b49a8fb62a Mon Sep 17 00:00:00 2001 From: dinamiko Date: Thu, 16 Sep 2021 10:21:31 +0200 Subject: [PATCH 050/101] Add vaulting module --- modules.php | 1 + modules/ppcp-vaulting/extensions.php | 12 ++++ modules/ppcp-vaulting/module.php | 16 +++++ modules/ppcp-vaulting/services.php | 12 ++++ .../src/class-vaultingmodule.php | 64 +++++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 modules/ppcp-vaulting/extensions.php create mode 100644 modules/ppcp-vaulting/module.php create mode 100644 modules/ppcp-vaulting/services.php create mode 100644 modules/ppcp-vaulting/src/class-vaultingmodule.php diff --git a/modules.php b/modules.php index 8bffd9d34..f60b190dd 100644 --- a/modules.php +++ b/modules.php @@ -23,6 +23,7 @@ return function ( string $root_dir ): iterable { ( require "$modules_dir/ppcp-subscription/module.php" )(), ( require "$modules_dir/ppcp-wc-gateway/module.php" )(), ( require "$modules_dir/ppcp-webhooks/module.php" )(), + ( require "$modules_dir/ppcp-vaulting/module.php" )(), ); return $modules; diff --git a/modules/ppcp-vaulting/extensions.php b/modules/ppcp-vaulting/extensions.php new file mode 100644 index 000000000..35e431f57 --- /dev/null +++ b/modules/ppcp-vaulting/extensions.php @@ -0,0 +1,12 @@ + 'PayPal payments' ) + + array_slice( $menu_links, 5, NULL, true ); + + return $menu_links; + }, 40 ); + + add_action( 'init', function() { + add_rewrite_endpoint( 'ppcp-paypal-payment-tokens', EP_PAGES ); + } ); + + add_action( 'woocommerce_account_ppcp-paypal-payment-tokens_endpoint', function() use ($container){ + + $repo = $container->get('subscription.repository.payment-token'); + $tokens = $repo->all_for_user_id( get_current_user_id() ); + $a = 1; + }); + + } + + /** + * {@inheritDoc} + */ + public function getKey() { } +} From b2904504f4a4b53cc0defacd163d20a7876826a7 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 16 Sep 2021 12:37:05 +0300 Subject: [PATCH 051/101] Implement webhook list retrieval --- .../src/Endpoint/class-webhookendpoint.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php b/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php index 7f08587d9..7e5f02315 100644 --- a/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/class-webhookendpoint.php @@ -118,6 +118,46 @@ class WebhookEndpoint { return $hook; } + /** + * Loads the webhooks list for the current auth token. + * + * @return Webhook[] + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function list(): array { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/notifications/webhooks'; + $args = array( + 'method' => 'GET', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + ); + $response = $this->request( $url, $args ); + + if ( is_wp_error( $response ) ) { + throw new RuntimeException( + __( 'Not able to load webhooks list.', 'woocommerce-paypal-payments' ) + ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return array_map( + array( $this->webhook_factory, 'from_paypal_response' ), + $json->webhooks + ); + } + /** * Deletes a webhook. * From 620f25d88dab6adc152a7ad3fd173b692f0435bd Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 16 Sep 2021 12:40:00 +0300 Subject: [PATCH 052/101] Refactor field page checking Reduce code duplication and support more than 2 pages. Also remove 'all' --- modules/ppcp-wc-gateway/services.php | 2 +- .../src/Settings/class-pagematchertrait.php | 49 +++++++++++++++++++ .../src/Settings/class-settingslistener.php | 13 ++--- .../src/Settings/class-settingsrenderer.php | 7 ++- .../src/Status/class-webhooksstatuspage.php | 18 +++++++ 5 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 modules/ppcp-wc-gateway/src/Settings/class-pagematchertrait.php create mode 100644 modules/ppcp-webhooks/src/Status/class-webhooksstatuspage.php diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index c977e588e..91485d040 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -700,7 +700,7 @@ return array( State::STATE_ONBOARDED, ), 'requirements' => array(), - 'gateway' => 'all', + 'gateway' => array( 'paypal', 'dcc' ), ), 'logging_enabled' => array( 'title' => __( 'Logging', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-wc-gateway/src/Settings/class-pagematchertrait.php b/modules/ppcp-wc-gateway/src/Settings/class-pagematchertrait.php new file mode 100644 index 000000000..f47388c85 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Settings/class-pagematchertrait.php @@ -0,0 +1,49 @@ + 'paypal', + CreditCardGateway::ID => 'dcc', // TODO: consider using just the gateway ID for PayPal and DCC too. + WebhooksStatusPage::ID => WebhooksStatusPage::ID, + ); + return array_key_exists( $current_page_id, $gateway_page_id_map ) + && in_array( $gateway_page_id_map[ $current_page_id ], $allowed_gateways, true ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php index 1e3447f97..3155c79df 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingslistener.php @@ -23,6 +23,8 @@ use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; */ class SettingsListener { + use PageMatcherTrait; + const NONCE = 'ppcp-settings'; private const CREDENTIALS_ADDED = 'credentials_added'; @@ -360,16 +362,7 @@ class SettingsListener { if ( ! in_array( $this->state->current_state(), $config['screens'], true ) ) { continue; } - if ( - 'dcc' === $config['gateway'] - && CreditCardGateway::ID !== $this->page_id - ) { - continue; - } - if ( - 'paypal' === $config['gateway'] - && PayPalGateway::ID !== $this->page_id - ) { + if ( ! $this->field_matches_page( $config, $this->page_id ) ) { continue; } switch ( $config['type'] ) { diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php index 690c5d91f..df86aad14 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php @@ -24,6 +24,8 @@ use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; */ class SettingsRenderer { + use PageMatcherTrait; + /** * The Settings status helper. * @@ -325,10 +327,7 @@ class SettingsRenderer { if ( ! in_array( $this->state->current_state(), $config['screens'], true ) ) { continue; } - if ( $is_dcc && ! in_array( $config['gateway'], array( 'all', 'dcc' ), true ) ) { - continue; - } - if ( ! $is_dcc && ! in_array( $config['gateway'], array( 'all', 'paypal' ), true ) ) { + if ( ! $this->field_matches_page( $config, $this->page_id ) ) { continue; } if ( diff --git a/modules/ppcp-webhooks/src/Status/class-webhooksstatuspage.php b/modules/ppcp-webhooks/src/Status/class-webhooksstatuspage.php new file mode 100644 index 000000000..54dd56c1c --- /dev/null +++ b/modules/ppcp-webhooks/src/Status/class-webhooksstatuspage.php @@ -0,0 +1,18 @@ + Date: Thu, 16 Sep 2021 12:41:08 +0300 Subject: [PATCH 053/101] Add table field renderer --- .../src/Settings/class-settingsrenderer.php | 54 +++++++++++++++++++ .../src/class-wcgatewaymodule.php | 1 + 2 files changed, 55 insertions(+) diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php index df86aad14..65308f31b 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php @@ -311,6 +311,60 @@ class SettingsRenderer { return $html; } + /** + * Renders the table row. + * + * @param array $data Values of the row cells. + * @param string $tag HTML tag ('td', 'th'). + * @return string + */ + public function render_table_row( array $data, string $tag = 'td' ): string { + $cells = array_map( + function ( $value ) use ( $tag ): string { + return "<$tag>" . (string) $value . ""; + }, + $data + ); + return '' . implode( $cells ) . ''; + } + + /** + * Renders the table field. + * + * @param string $field The current field HTML. + * @param string $key The key. + * @param array $config The configuration of the field. + * @param array $value The current value. + * + * @return string HTML. + */ + public function render_table( $field, $key, $config, $value ): string { + if ( 'ppcp-table' !== $config['type'] ) { + return $field; + } + + $data = $value['data']; + if ( empty( $data ) ) { + $empty_placeholder = $value['empty_placeholder'] ?? ( $config['empty_placeholder'] ?? null ); + if ( $empty_placeholder ) { + return $empty_placeholder; + } + } + + $header_row_html = $this->render_table_row( $value['headers'], 'th' ); + $data_rows_html = implode( + array_map( + array( $this, 'render_table_row' ), + $data + ) + ); + + return " +$header_row_html +$data_rows_html +
    "; + } + /** * Renders the settings. */ diff --git a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php index 60ec5d7c1..7fd14cdb8 100644 --- a/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php +++ b/modules/ppcp-wc-gateway/src/class-wcgatewaymodule.php @@ -214,6 +214,7 @@ class WcGatewayModule implements ModuleInterface { $field = $renderer->render_password( $field, $key, $args, $value ); $field = $renderer->render_text_input( $field, $key, $args, $value ); $field = $renderer->render_heading( $field, $key, $args, $value ); + $field = $renderer->render_table( $field, $key, $args, $value ); return $field; }, 10, From fa6f9eb188d38dfbb7701a5b34f4f4503a40531a Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 16 Sep 2021 12:42:30 +0300 Subject: [PATCH 054/101] Support passing field value without settings Pass as function to not perform e.g. HTTP requests if the field is not needed. --- modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php index 65308f31b..40fbad8ee 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-settingsrenderer.php @@ -402,7 +402,7 @@ $data_rows_html ) { continue; } - $value = $this->settings->has( $field ) ? $this->settings->get( $field ) : null; + $value = $this->settings->has( $field ) ? $this->settings->get( $field ) : ( isset( $config['value'] ) ? $config['value']() : null ); $key = 'ppcp[' . $field . ']'; $id = 'ppcp-' . $field; $config['id'] = $id; From 0b029a9392fa8dda48127ababa5cb72a06a8c2fe Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 16 Sep 2021 12:44:29 +0300 Subject: [PATCH 055/101] Add webhooks status page --- modules/ppcp-wc-gateway/services.php | 3 ++- .../src/Gateway/class-paypalgateway.php | 22 ++++++++++++++++++- .../src/Settings/class-sectionsrenderer.php | 6 +++-- .../ppcp-webhooks/src/class-webhookmodule.php | 5 +++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 91485d040..ab228be00 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -38,6 +38,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; +use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; return array( 'wcgateway.paypal-gateway' => static function ( $container ): PayPalGateway { @@ -118,7 +119,7 @@ return array( } $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : ''; - return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID ), true ); + return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID ), true ); }, 'wcgateway.current-ppcp-settings-page-id' => static function ( $container ): string { diff --git a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php index da7597abb..4ca2f947b 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/class-paypalgateway.php @@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; /** * Class PayPalGateway @@ -224,7 +225,7 @@ class PayPalGateway extends \WC_Payment_Gateway { ), ); - $should_show_enabled_checkbox = ! $this->is_credit_card_tab() && ( $this->config->has( 'merchant_email' ) && $this->config->get( 'merchant_email' ) ); + $should_show_enabled_checkbox = $this->is_paypal_tab() && ( $this->config->has( 'merchant_email' ) && $this->config->get( 'merchant_email' ) ); if ( ! $should_show_enabled_checkbox ) { unset( $this->form_fields['enabled'] ); } @@ -308,6 +309,9 @@ class PayPalGateway extends \WC_Payment_Gateway { if ( $this->is_credit_card_tab() ) { return __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ); } + if ( $this->is_webhooks_tab() ) { + return __( 'Webhooks Status', 'woocommerce-paypal-payments' ); + } if ( $this->is_paypal_tab() ) { return __( 'PayPal Checkout', 'woocommerce-paypal-payments' ); } @@ -326,6 +330,12 @@ class PayPalGateway extends \WC_Payment_Gateway { 'woocommerce-paypal-payments' ); } + if ( $this->is_webhooks_tab() ) { + return __( + 'Status of the webhooks subscription.', + 'woocommerce-paypal-payments' + ); + } return __( 'Accept PayPal, Pay Later and alternative payment types.', @@ -346,6 +356,16 @@ class PayPalGateway extends \WC_Payment_Gateway { } + /** + * Whether we are on the Webhooks Status tab. + * + * @return bool + */ + private function is_webhooks_tab() : bool { + return is_admin() + && WebhooksStatusPage::ID === $this->page_id; + } + /** * Whether we are on the PayPal settings tab. * diff --git a/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php b/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php index ea9e693f5..2bddabe1b 100644 --- a/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/class-sectionsrenderer.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; /** * Class SectionsRenderer @@ -53,8 +54,9 @@ class SectionsRenderer { } $sections = array( - PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), - CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), + PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), + CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), + WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ), ); echo '