diff --git a/changelog.txt b/changelog.txt index 3022adc75..d57f6ff85 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,15 @@ *** Changelog *** -= 1.6.3 - TBD = += 1.6.4 - TBD = +* Fix - Non admin user cannot save changes to the plugin settings #278 +* Fix - Empty space in invoice prefix causes smart buttons to not load #390 +* Fix - woocommerce_payment_complete action not triggered for payments completed via webhook #399 +* Fix - Paying with Venmo - Change funding source on checkout page and receipt to Venmo #394 +* Fix - Internal server error on checkout when selected saved card but then switched to paypal #403 +* Enhancement - Allow formatted text for the Description field #407 +* Enhancement - Remove filter to prevent On-Hold emails #411 + += 1.6.3 - 2021-12-14 = * Fix - Payments fail when using custom order numbers #354 * Fix - Do not display saved payments on PayPal buttons if vault option is disabled #358 * Fix - Double "Place Order" button #362 diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 424b691a7..49fd98b52 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -14,7 +14,10 @@ const bootstrap = () => { const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic); const spinner = new Spinner(); const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner); - const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway); + const onSmartButtonClick = data => { + window.ppcpFundingSource = data.fundingSource; + }; + const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const context = PayPalCommerceGateway.context; if (context === 'mini-cart' || context === 'product') { diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index 579ff8100..696884be2 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -4,7 +4,8 @@ const onApprove = (context, errorHandler) => { method: 'POST', body: JSON.stringify({ nonce: context.config.ajax.approve_order.nonce, - order_id:data.orderID + order_id:data.orderID, + funding_source: window.ppcpFundingSource, }) }).then((res)=>{ return res.json(); @@ -13,7 +14,7 @@ const onApprove = (context, errorHandler) => { errorHandler.genericError(); return actions.restart().catch(err => { errorHandler.genericError(); - });; + }); } location.href = context.config.redirect; }); diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js index 36a2eed90..dbe0eba5a 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForPayNow.js @@ -7,7 +7,8 @@ const onApprove = (context, errorHandler, spinner) => { method: 'POST', body: JSON.stringify({ nonce: context.config.ajax.approve_order.nonce, - order_id:data.orderID + order_id:data.orderID, + funding_source: window.ppcpFundingSource, }) }).then((res)=>{ return res.json(); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 5b884cc9e..43660c30c 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -1,7 +1,8 @@ class Renderer { - constructor(creditCardRenderer, defaultConfig) { + constructor(creditCardRenderer, defaultConfig, onSmartButtonClick) { this.defaultConfig = defaultConfig; this.creditCardRenderer = creditCardRenderer; + this.onSmartButtonClick = onSmartButtonClick; } render(wrapper, hostedFieldsWrapper, contextConfig) { @@ -19,6 +20,7 @@ class Renderer { paypal.Buttons({ style, ...contextConfig, + onClick: this.onSmartButtonClick, }).render(wrapper); } diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index f20eb21b8..bfbde9923 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -184,6 +184,9 @@ class ApproveOrderEndpoint implements EndpointInterface { throw new RuntimeException( $message ); } + $funding_source = $data['funding_source'] ?? null; + $this->session_handler->replace_funding_source( $funding_source ); + $this->session_handler->replace_order( $order ); wp_send_json_success( $order ); return true; diff --git a/modules/ppcp-onboarding/assets/css/onboarding.css b/modules/ppcp-onboarding/assets/css/onboarding.css index 9fadc2b3f..affc68415 100644 --- a/modules/ppcp-onboarding/assets/css/onboarding.css +++ b/modules/ppcp-onboarding/assets/css/onboarding.css @@ -88,3 +88,7 @@ .woocommerce_page_wc-settings h3.ppcp-subheading { font-size: 1.1em; } + +.input-text[pattern]:invalid { + border: red solid 2px; +} diff --git a/modules/ppcp-session/services.php b/modules/ppcp-session/services.php index b83a1a6a5..b5d76e82b 100644 --- a/modules/ppcp-session/services.php +++ b/modules/ppcp-session/services.php @@ -28,7 +28,10 @@ return array( return $session_handler; }, 'session.cancellation.view' => function ( ContainerInterface $container ) : CancelView { - return new CancelView(); + return new CancelView( + $container->get( 'wcgateway.settings' ), + $container->get( 'wcgateway.funding-source.renderer' ) + ); }, 'session.cancellation.controller' => function ( ContainerInterface $container ) : CancelController { return new CancelController( diff --git a/modules/ppcp-session/src/Cancellation/CancelController.php b/modules/ppcp-session/src/Cancellation/CancelController.php index 6de8a2262..54eb13cb2 100644 --- a/modules/ppcp-session/src/Cancellation/CancelController.php +++ b/modules/ppcp-session/src/Cancellation/CancelController.php @@ -67,7 +67,7 @@ class CancelController { add_action( 'woocommerce_review_order_after_submit', function () use ( $url ) { - $this->view->render_session_cancellation( $url ); + $this->view->render_session_cancellation( $url, $this->session_handler->funding_source() ); } ); } diff --git a/modules/ppcp-session/src/Cancellation/CancelView.php b/modules/ppcp-session/src/Cancellation/CancelView.php index 2c4e1356b..4727e6b0e 100644 --- a/modules/ppcp-session/src/Cancellation/CancelView.php +++ b/modules/ppcp-session/src/Cancellation/CancelView.php @@ -9,31 +9,66 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Session\Cancellation; +use Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; + /** * Class CancelView */ class CancelView { + /** + * The settings. + * + * @var ContainerInterface + */ + protected $settings; + + /** + * The funding source renderer. + * + * @var FundingSourceRenderer + */ + protected $funding_source_renderer; + + /** + * CancelView constructor. + * + * @param ContainerInterface $settings The settings. + * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. + */ + public function __construct( + ContainerInterface $settings, + FundingSourceRenderer $funding_source_renderer + ) { + $this->settings = $settings; + $this->funding_source_renderer = $funding_source_renderer; + } /** * Renders the cancel link. * - * @param string $url The URL. + * @param string $url The URL. + * @param string|null $funding_source The ID of the funding source, such as 'venmo'. */ - public function render_session_cancellation( string $url ) { + public function render_session_cancellation( string $url, ?string $funding_source ) { ?>

funding_source_renderer->render_name( $funding_source ) + : ( $this->settings->has( 'title' ) ? $this->settings->get( 'title' ) : __( 'PayPal', 'woocommerce-paypal-payments' ) ); printf( - // translators: the placeholders are html tags for a link. + // translators: %3$ is funding source like "PayPal" or "Venmo", other placeholders are html tags for a link. esc_html__( - 'You are currently paying with PayPal. If you want to cancel + 'You are currently paying with %3$s. If you want to cancel this process, please click %1$shere%2$s.', 'woocommerce-paypal-payments' ), '', - '' + '', + esc_html( $name ) ); ?>

diff --git a/modules/ppcp-session/src/SessionHandler.php b/modules/ppcp-session/src/SessionHandler.php index 6d65160f6..7d3f7546b 100644 --- a/modules/ppcp-session/src/SessionHandler.php +++ b/modules/ppcp-session/src/SessionHandler.php @@ -40,6 +40,13 @@ class SessionHandler { */ private $insufficient_funding_tries = 0; + /** + * The funding source of the current checkout (venmo, ...) or null. + * + * @var string|null + */ + private $funding_source = null; + /** * Returns the order. * @@ -84,6 +91,28 @@ class SessionHandler { return $this; } + /** + * Returns the funding source of the current checkout (venmo, ...) or null. + * + * @return string|null + */ + public function funding_source(): ?string { + return $this->funding_source; + } + + /** + * Replaces the funding source of the current checkout. + * + * @param string|null $funding_source The funding source. + * + * @return SessionHandler + */ + public function replace_funding_source( ?string $funding_source ): SessionHandler { + $this->funding_source = $funding_source; + $this->store_session(); + return $this; + } + /** * Returns how many times the customer tried to use the PayPal Gateway in this session. * @@ -113,6 +142,7 @@ class SessionHandler { $this->order = null; $this->bn_code = ''; $this->insufficient_funding_tries = 0; + $this->funding_source = null; $this->store_session(); return $this; } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 7368b9706..f53ffd6e3 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction; use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; @@ -45,6 +46,7 @@ return array( 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); + $funding_source_renderer = $container->get( 'wcgateway.funding-source.renderer' ); $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); $settings = $container->get( 'wcgateway.settings' ); $session_handler = $container->get( 'session.handler' ); @@ -60,6 +62,7 @@ return array( $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new PayPalGateway( $settings_renderer, + $funding_source_renderer, $order_processor, $authorized_payments, $settings, @@ -746,22 +749,26 @@ return array( 'gateway' => 'paypal', ), 'prefix' => array( - 'title' => __( 'Invoice prefix', 'woocommerce-paypal-payments' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => __( 'If you use your PayPal account with more than one installation, please use a distinct prefix to separate those installations. Please do not use numbers in your prefix.', 'woocommerce-paypal-payments' ), - 'default' => ( static function (): string { + 'title' => __( 'Invoice prefix', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'If you use your PayPal account with more than one installation, please use a distinct prefix to separate those installations. Please use only English letters and "-", "_" characters.', 'woocommerce-paypal-payments' ), + 'maxlength' => 15, + 'custom_attributes' => array( + 'pattern' => '[a-zA-Z_-]+', + ), + 'default' => ( static function (): string { $site_url = get_site_url( get_current_blog_id() ); $hash = md5( $site_url ); $letters = preg_replace( '~\d~', '', $hash ); return substr( $letters, 0, 6 ) . '-'; } )(), - 'screens' => array( + 'screens' => array( State::STATE_PROGRESSIVE, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => 'paypal', + 'requirements' => array(), + 'gateway' => 'paypal', ), // General button styles. @@ -2039,4 +2046,10 @@ return array( $container->get( 'api.shop.country' ) ); }, + + 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { + return new FundingSourceRenderer( + $container->get( 'wcgateway.settings' ) + ); + }, ); diff --git a/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php b/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php new file mode 100644 index 000000000..3d7ad050f --- /dev/null +++ b/modules/ppcp-wc-gateway/src/FundingSource/FundingSourceRenderer.php @@ -0,0 +1,61 @@ +settings = $settings; + } + + /** + * Returns name of the funding source (suitable for displaying to user). + * + * @param string $id The ID of the funding source, such as 'venmo'. + */ + public function render_name( string $id ): string { + if ( 'venmo' === $id ) { + return __( 'Venmo', 'woocommerce-paypal-payments' ); + } + return $this->settings->has( 'title' ) ? + $this->settings->get( 'title' ) + : __( 'PayPal', 'woocommerce-paypal-payments' ); + } + + /** + * Returns description of the funding source (for checkout). + * + * @param string $id The ID of the funding source, such as 'venmo'. + */ + public function render_description( string $id ): string { + if ( 'venmo' === $id ) { + return __( 'Pay via Venmo.', 'woocommerce-paypal-payments' ); + } + return $this->settings->has( 'description' ) ? + $this->settings->get( 'description' ) + : __( 'Pay via PayPal.', 'woocommerce-paypal-payments' ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index cb4446e77..162b2934d 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -44,6 +45,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $settings_renderer; + /** + * The funding source renderer. + * + * @var FundingSourceRenderer + */ + protected $funding_source_renderer; + /** * The processor for orders. * @@ -153,6 +161,7 @@ class PayPalGateway extends \WC_Payment_Gateway { * PayPalGateway constructor. * * @param SettingsRenderer $settings_renderer The Settings Renderer. + * @param FundingSourceRenderer $funding_source_renderer The funding source renderer. * @param OrderProcessor $order_processor The Order Processor. * @param AuthorizedPaymentsProcessor $authorized_payments_processor The Authorized Payments Processor. * @param ContainerInterface $config The settings. @@ -170,6 +179,7 @@ class PayPalGateway extends \WC_Payment_Gateway { */ public function __construct( SettingsRenderer $settings_renderer, + FundingSourceRenderer $funding_source_renderer, OrderProcessor $order_processor, AuthorizedPaymentsProcessor $authorized_payments_processor, ContainerInterface $config, @@ -190,6 +200,7 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->order_processor = $order_processor; $this->authorized_payments_processor = $authorized_payments_processor; $this->settings_renderer = $settings_renderer; + $this->funding_source_renderer = $funding_source_renderer; $this->config = $config; $this->session_handler = $session_handler; $this->refund_processor = $refund_processor; @@ -241,6 +252,12 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->description = $this->config->has( 'description' ) ? $this->config->get( 'description' ) : $this->method_description; + $funding_source = $this->session_handler->funding_source(); + if ( $funding_source ) { + $this->title = $this->funding_source_renderer->render_name( $funding_source ); + $this->description = $this->funding_source_renderer->render_description( $funding_source ); + } + $this->init_form_fields(); $this->init_settings(); @@ -344,8 +361,15 @@ class PayPalGateway extends \WC_Payment_Gateway { ); } + if ( is_admin() ) { + return __( + 'Accept PayPal, Pay Later and alternative payment types.', + 'woocommerce-paypal-payments' + ); + } + return __( - 'Accept PayPal, Pay Later and alternative payment types.', + 'Pay via PayPal.', 'woocommerce-paypal-payments' ); } diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index aaca3c829..5f8130299 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -54,12 +54,14 @@ trait ProcessPaymentTrait { return $failure_data; } + $payment_method = filter_input( INPUT_POST, 'payment_method', FILTER_SANITIZE_STRING ); + /** * If customer has chosen a saved credit card payment. */ $saved_credit_card = filter_input( INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING ); $change_payment = filter_input( INPUT_POST, 'woocommerce_change_payment', FILTER_SANITIZE_STRING ); - if ( $saved_credit_card && ! isset( $change_payment ) ) { + if ( CreditCardGateway::ID === $payment_method && $saved_credit_card && ! isset( $change_payment ) ) { $user_id = (int) $wc_order->get_customer_id(); $customer = new \WC_Customer( $user_id ); diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index 3155c79df..b3db8d3a6 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -372,14 +372,11 @@ class SettingsListener { case 'text': case 'number': case 'ppcp-text-input': - case 'ppcp-password': - $settings[ $key ] = isset( $raw_data[ $key ] ) ? sanitize_text_field( $raw_data[ $key ] ) : ''; + $settings[ $key ] = isset( $raw_data[ $key ] ) ? wp_kses_post( $raw_data[ $key ] ) : ''; break; + case 'ppcp-password': case 'password': - if ( empty( $raw_data[ $key ] ) ) { - break; - } - $settings[ $key ] = sanitize_text_field( $raw_data[ $key ] ); + $settings[ $key ] = $raw_data[ $key ] ?? ''; break; case 'ppcp-multiselect': $values = isset( $raw_data[ $key ] ) ? (array) $raw_data[ $key ] : array(); @@ -451,7 +448,7 @@ class SettingsListener { // phpcs:enable WordPress.Security.NonceVerification.Missing // phpcs:enable WordPress.Security.NonceVerification.Recommended - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { return false; } return true; diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 7e90955d6..5de6680d6 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -132,18 +132,6 @@ class WCGatewayModule implements ModuleInterface { $endpoint->handle_request(); } ); - - add_filter( - 'woocommerce_email_recipient_customer_on_hold_order', - function( $recipient, $order ) { - if ( $order->get_payment_method() === PayPalGateway::ID || $order->get_payment_method() === CreditCardGateway::ID ) { - $recipient = ''; - } - return $recipient; - }, - 10, - 2 - ); } /** diff --git a/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php b/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php index 01f3375d5..45d49762b 100644 --- a/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php +++ b/modules/ppcp-webhooks/src/Handler/CheckoutOrderApproved.php @@ -136,7 +136,7 @@ class CheckoutOrderApproved implements RequestHandler { } if ( $order->intent() === 'CAPTURE' ) { - $this->order_endpoint->capture( $order ); + $order = $this->order_endpoint->capture( $order ); } } catch ( RuntimeException $error ) { $message = sprintf( @@ -187,23 +187,18 @@ class CheckoutOrderApproved implements RequestHandler { return rest_ensure_response( $response ); } - $new_status = $order->intent() === 'CAPTURE' ? 'processing' : 'on-hold'; - $status_message = $order->intent() === 'CAPTURE' ? - __( 'Payment received.', 'woocommerce-paypal-payments' ) - : __( 'Payment can be captured.', 'woocommerce-paypal-payments' ); foreach ( $wc_orders as $wc_order ) { if ( ! in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) { continue; } - /** - * The WooCommerce order. - * - * @var \WC_Order $wc_order - */ - $wc_order->update_status( - $new_status, - $status_message - ); + if ( $order->intent() === 'CAPTURE' ) { + $wc_order->payment_complete(); + } else { + $wc_order->update_status( + 'on-hold', + __( 'Payment can be captured.', 'woocommerce-paypal-payments' ) + ); + } $this->logger->log( 'info', sprintf( diff --git a/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php b/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php index 6f950f9fd..4b1eea7b7 100644 --- a/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/CheckoutOrderCompleted.php @@ -134,15 +134,7 @@ class CheckoutOrderCompleted implements RequestHandler { if ( ! in_array( $wc_order->get_status(), array( 'pending', 'on-hold' ), true ) ) { continue; } - /** - * The WooCommerce order. - * - * @var \WC_Order $wc_order - */ - $wc_order->update_status( - 'processing', - __( 'Payment received.', 'woocommerce-paypal-payments' ) - ); + $wc_order->payment_complete(); $this->logger->log( 'info', sprintf( diff --git a/package.json b/package.json index 014145147..d9240a0f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "1.6.3", + "version": "1.6.4", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index 26f0f3761..1611198ce 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell, Requires at least: 5.3 Tested up to: 5.8 Requires PHP: 7.1 -Stable tag: 1.6.3 +Stable tag: 1.6.4 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,15 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == += 1.6.4 = +* Fix - Non admin user cannot save changes to the plugin settings #278 +* Fix - Empty space in invoice prefix causes smart buttons to not load #390 +* Fix - woocommerce_payment_complete action not triggered for payments completed via webhook #399 +* Fix - Paying with Venmo - Change funding source on checkout page and receipt to Venmo #394 +* Fix - Internal server error on checkout when selected saved card but then switched to paypal #403 +* Enhancement - Allow formatted text for the Description field #407 +* Enhancement - Remove filter to prevent On-Hold emails #411 + = 1.6.3 = * Fix - Payments fail when using custom order numbers #354 * Fix - Do not display saved payments on PayPal buttons if vault option is disabled #358 diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 42300db66..0df1b0e99 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; +use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; @@ -28,75 +29,107 @@ use function Brain\Monkey\Functions\when; class WcGatewayTest extends TestCase { + private $isAdmin = false; + private $sessionHandler; + private $fundingSource = null; + + private $settingsRenderer; + private $funding_source_renderer; + private $orderProcessor; + private $authorizedOrdersProcessor; + private $settings; + private $refundProcessor; + private $onboardingState; + private $transactionUrlProvider; + private $subscriptionHelper; private $environment; + private $paymentTokenRepository; + private $logger; + private $paymentsEndpoint; + private $orderEndpoint; public function setUp(): void { parent::setUp(); + expect('is_admin')->andReturnUsing(function () { + return $this->isAdmin; + }); + + $this->settingsRenderer = Mockery::mock(SettingsRenderer::class); + $this->orderProcessor = Mockery::mock(OrderProcessor::class); + $this->authorizedOrdersProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); + $this->settings = Mockery::mock(Settings::class); + $this->sessionHandler = Mockery::mock(SessionHandler::class); + $this->refundProcessor = Mockery::mock(RefundProcessor::class); + $this->onboardingState = Mockery::mock(State::class); + $this->transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class); + $this->subscriptionHelper = Mockery::mock(SubscriptionHelper::class); $this->environment = Mockery::mock(Environment::class); + $this->paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); + $this->logger = Mockery::mock(LoggerInterface::class); + $this->paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); + $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); + $this->funding_source_renderer = new FundingSourceRenderer($this->settings); + + $this->onboardingState->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); + + $this->sessionHandler + ->shouldReceive('funding_source') + ->andReturnUsing(function () { + return $this->fundingSource; + }); + + $this->settings->shouldReceive('has')->andReturnFalse(); + + $this->logger->shouldReceive('info'); + } + + private function createGateway() + { + return new PayPalGateway( + $this->settingsRenderer, + $this->funding_source_renderer, + $this->orderProcessor, + $this->authorizedOrdersProcessor, + $this->settings, + $this->sessionHandler, + $this->refundProcessor, + $this->onboardingState, + $this->transactionUrlProvider, + $this->subscriptionHelper, + PayPalGateway::ID, + $this->environment, + $this->paymentTokenRepository, + $this->logger, + $this->paymentsEndpoint, + $this->orderEndpoint + ); } public function testProcessPaymentSuccess() { - expect('is_admin')->andReturn(false); - $orderId = 1; $wcOrder = Mockery::mock(\WC_Order::class); $wcOrder->shouldReceive('get_customer_id')->andReturn(1); $wcOrder->shouldReceive('get_meta')->andReturn(''); - $settingsRenderer = Mockery::mock(SettingsRenderer::class); - $orderProcessor = Mockery::mock(OrderProcessor::class); - $orderProcessor + $this->orderProcessor ->expects('process') ->andReturnUsing( function(\WC_Order $order) use ($wcOrder) : bool { return $order === $wcOrder; } ); - $authorizedPaymentsProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); - $settings = Mockery::mock(Settings::class); - $sessionHandler = Mockery::mock(SessionHandler::class); - $sessionHandler + $this->sessionHandler ->shouldReceive('destroy_session_data'); - $settings - ->shouldReceive('has')->andReturnFalse(); - $refundProcessor = Mockery::mock(RefundProcessor::class); - $transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class); - $state = Mockery::mock(State::class); - $state - ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); - $subscriptionHelper = Mockery::mock(SubscriptionHelper::class); - $subscriptionHelper + $this->subscriptionHelper ->shouldReceive('has_subscription') ->with($orderId) ->andReturn(true) ->andReturn(false); - $subscriptionHelper + $this->subscriptionHelper ->shouldReceive('is_subscription_change_payment') ->andReturn(true); - $paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); - $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldReceive('info'); - $paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); - $orderEndpoint = Mockery::mock(OrderEndpoint::class); - - $testee = new PayPalGateway( - $settingsRenderer, - $orderProcessor, - $authorizedPaymentsProcessor, - $settings, - $sessionHandler, - $refundProcessor, - $state, - $transactionUrlProvider, - $subscriptionHelper, - PayPalGateway::ID, - $this->environment, - $paymentTokenRepository, - $logger, - $paymentsEndpoint, - $orderEndpoint - ); + $testee = $this->createGateway(); expect('wc_get_order') ->with($orderId) @@ -121,45 +154,9 @@ class WcGatewayTest extends TestCase } public function testProcessPaymentOrderNotFound() { - expect('is_admin')->andReturn(false); - $orderId = 1; - $settingsRenderer = Mockery::mock(SettingsRenderer::class); - $orderProcessor = Mockery::mock(OrderProcessor::class); - $authorizedPaymentsProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); - $settings = Mockery::mock(Settings::class); - $settings - ->shouldReceive('has')->andReturnFalse(); - $sessionHandler = Mockery::mock(SessionHandler::class); - $refundProcessor = Mockery::mock(RefundProcessor::class); - $state = Mockery::mock(State::class); - $transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class); - $state - ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); - $subscriptionHelper = Mockery::mock(SubscriptionHelper::class); - $paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); - $logger = Mockery::mock(LoggerInterface::class); - $paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); - $orderEndpoint = Mockery::mock(OrderEndpoint::class); - - $testee = new PayPalGateway( - $settingsRenderer, - $orderProcessor, - $authorizedPaymentsProcessor, - $settings, - $sessionHandler, - $refundProcessor, - $state, - $transactionUrlProvider, - $subscriptionHelper, - PayPalGateway::ID, - $this->environment, - $paymentTokenRepository, - $logger, - $paymentsEndpoint, - $orderEndpoint - ); + $testee = $this->createGateway(); expect('wc_get_order') ->with($orderId) @@ -184,56 +181,20 @@ class WcGatewayTest extends TestCase public function testProcessPaymentFails() { - expect('is_admin')->andReturn(false); - $orderId = 1; $wcOrder = Mockery::mock(\WC_Order::class); $lastError = 'some-error'; - $settingsRenderer = Mockery::mock(SettingsRenderer::class); - $orderProcessor = Mockery::mock(OrderProcessor::class); - $orderProcessor + $this->orderProcessor ->expects('process') ->andReturnFalse(); - $orderProcessor + $this->orderProcessor ->expects('last_error') ->andReturn($lastError); - $authorizedPaymentsProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); - $settings = Mockery::mock(Settings::class); - $settings - ->shouldReceive('has')->andReturnFalse(); - $sessionHandler = Mockery::mock(SessionHandler::class); - $refundProcessor = Mockery::mock(RefundProcessor::class); - $state = Mockery::mock(State::class); - $transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class); - $state - ->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); - $subscriptionHelper = Mockery::mock(SubscriptionHelper::class); - $subscriptionHelper->shouldReceive('has_subscription')->with($orderId)->andReturn(true); - $subscriptionHelper->shouldReceive('is_subscription_change_payment')->andReturn(true); + $this->subscriptionHelper->shouldReceive('has_subscription')->with($orderId)->andReturn(true); + $this->subscriptionHelper->shouldReceive('is_subscription_change_payment')->andReturn(true); $wcOrder->shouldReceive('update_status')->andReturn(true); - $paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); - $logger = Mockery::mock(LoggerInterface::class); - $paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); - $orderEndpoint = Mockery::mock(OrderEndpoint::class); - - $testee = new PayPalGateway( - $settingsRenderer, - $orderProcessor, - $authorizedPaymentsProcessor, - $settings, - $sessionHandler, - $refundProcessor, - $state, - $transactionUrlProvider, - $subscriptionHelper, - PayPalGateway::ID, - $this->environment, - $paymentTokenRepository, - $logger, - $paymentsEndpoint, - $orderEndpoint - ); + $testee = $this->createGateway(); expect('wc_get_order') ->with($orderId) @@ -261,49 +222,31 @@ class WcGatewayTest extends TestCase */ public function testNeedsSetup($currentState, $needSetup) { - expect('is_admin')->andReturn(true); - $settingsRenderer = Mockery::mock(SettingsRenderer::class); - $orderProcessor = Mockery::mock(OrderProcessor::class); - $authorizedOrdersProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); - $config = Mockery::mock(ContainerInterface::class); - $config - ->shouldReceive('has') - ->andReturn(false); - $sessionHandler = Mockery::mock(SessionHandler::class); - $refundProcessor = Mockery::mock(RefundProcessor::class); - $onboardingState = Mockery::mock(State::class); - $onboardingState + $this->isAdmin = true; + + $this->onboardingState = Mockery::mock(State::class); + $this->onboardingState ->expects('current_state') ->andReturn($currentState); - $transactionUrlProvider = Mockery::mock(TransactionUrlProvider::class); - $subscriptionHelper = Mockery::mock(SubscriptionHelper::class); - $paymentTokenRepository = Mockery::mock(PaymentTokenRepository::class); - $logger = Mockery::mock(LoggerInterface::class); - $paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class); - $orderEndpoint = Mockery::mock(OrderEndpoint::class); - - $testee = new PayPalGateway( - $settingsRenderer, - $orderProcessor, - $authorizedOrdersProcessor, - $config, - $sessionHandler, - $refundProcessor, - $onboardingState, - $transactionUrlProvider, - $subscriptionHelper, - PayPalGateway::ID, - $this->environment, - $paymentTokenRepository, - $logger, - $paymentsEndpoint, - $orderEndpoint - ); + $testee = $this->createGateway(); $this->assertSame($needSetup, $testee->needs_setup()); } + /** + * @dataProvider dataForFundingSource + */ + public function testFundingSource($fundingSource, $title, $description) + { + $this->fundingSource = $fundingSource; + + $testee = $this->createGateway(); + + self::assertEquals($title, $testee->title); + self::assertEquals($description, $testee->description); + } + public function dataForTestCaptureAuthorizedPaymentNoActionableFailures() : array { return [ @@ -330,4 +273,13 @@ class WcGatewayTest extends TestCase [State::STATE_ONBOARDED, false] ]; } + + public function dataForFundingSource(): array + { + return [ + [null, 'PayPal', 'Pay via PayPal.'], + ['venmo', 'Venmo', 'Pay via Venmo.'], + ['qwerty', 'PayPal', 'Pay via PayPal.'], + ]; + } } diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 820697fb1..a4cf1ca41 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,13 +3,13 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 1.6.3 + * Version: 1.6.4 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.1 * WC requires at least: 3.9 - * WC tested up to: 5.9 + * WC tested up to: 6.0 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce