diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 27e045297..494fa4594 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -112,7 +112,7 @@ class PurchaseUnitFactory { $items = array_filter( $this->item_factory->from_wc_order( $order ), function ( Item $item ): bool { - return $item->unit_amount()->value() > 0; + return $item->unit_amount()->value() >= 0; } ); $shipping = $this->shipping_factory->from_wc_order( $order ); @@ -168,7 +168,7 @@ class PurchaseUnitFactory { $items = array_filter( $this->item_factory->from_wc_cart( $cart ), function ( Item $item ): bool { - return $item->unit_amount()->value() > 0; + return $item->unit_amount()->value() >= 0; } ); diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index f0654c4f0..f528f289e 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -102,6 +102,9 @@ class CheckoutActionHandler { } else { errorHandler.message(data.data.message); } + + // fire WC event for other plugins + jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] ); } throw {type: 'create-order-error', data: data.data}; diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js index 878e20207..5486576ff 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/FreeTrialHandler.js @@ -40,6 +40,10 @@ class FreeTrialHandler { if (errors.length > 0) { this.spinner.unblock(); this.errorHandler.messages(errors); + + // fire WC event for other plugins + jQuery( document.body ).trigger( 'checkout_error' , [ this.errorHandler.currentHtml() ] ); + return; } } catch (error) { diff --git a/modules/ppcp-button/resources/js/modules/ErrorHandler.js b/modules/ppcp-button/resources/js/modules/ErrorHandler.js index 726ef2074..6048360b2 100644 --- a/modules/ppcp-button/resources/js/modules/ErrorHandler.js +++ b/modules/ppcp-button/resources/js/modules/ErrorHandler.js @@ -40,6 +40,15 @@ class ErrorHandler { this._scrollToMessages(); } + /** + * @returns {String} + */ + currentHtml() + { + const messageContainer = this._getMessageContainer(); + return messageContainer.outerHTML; + } + /** * @private * @param {String} text diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index 0ddafdd15..fcd20a609 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -10,6 +10,7 @@ class CreditCardRenderer { this.spinner = spinner; this.cardValid = false; this.formValid = false; + this.emptyFields = new Set(['number', 'cvv', 'expirationDate']); this.currentHostedFieldsInstance = null; } @@ -130,7 +131,7 @@ class CreditCardRenderer { return event.fields[key].isValid; }); - const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type); + const className = event.cards.length ? this._cardNumberFiledCLassNameByCardType(event.cards[0].type) : ''; event.fields.number.isValid ? cardNumber.classList.add(className) : this._recreateElementClassAttribute(cardNumber, cardNumberField.className); @@ -138,6 +139,12 @@ class CreditCardRenderer { this.formValid = formValid; }); + hostedFields.on('empty', (event) => { + this.emptyFields.add(event.emittedBy); + }); + hostedFields.on('notEmpty', (event) => { + this.emptyFields.delete(event.emittedBy); + }); show(buttonSelector); @@ -249,7 +256,16 @@ class CreditCardRenderer { }); } else { this.spinner.unblock(); - const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid; + + let message = this.defaultConfig.labels.error.generic; + if (this.emptyFields.size > 0) { + message = this.defaultConfig.hosted_fields.labels.fields_empty; + } else if (!this.cardValid) { + message = this.defaultConfig.hosted_fields.labels.card_not_supported; + } else if (!this.formValid) { + message = this.defaultConfig.hosted_fields.labels.fields_not_valid; + } + this.errorHandler.message(message); } } diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index eb7a020dd..708b04717 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Assets; use Exception; use Psr\Log\LoggerInterface; +use WC_Order; use WC_Product; use WC_Product_Variation; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; @@ -806,8 +807,6 @@ class SmartButton implements SmartButtonInterface { * @return array */ public function script_data(): array { - global $wp; - $is_free_trial_cart = $this->is_free_trial_cart(); $url_params = $this->url_params(); @@ -903,6 +902,10 @@ class SmartButton implements SmartButtonInterface { 'credit_card_number' => '', 'cvv' => '', 'mm_yy' => __( 'MM/YY', 'woocommerce-paypal-payments' ), + 'fields_empty' => __( + 'Card payment details are missing. Please fill in all required fields.', + 'woocommerce-paypal-payments' + ), 'fields_not_valid' => __( 'Unfortunately, your credit card details are not valid.', 'woocommerce-paypal-payments' @@ -944,7 +947,7 @@ class SmartButton implements SmartButtonInterface { // phpcs:ignore WordPress.WP.I18n 'shipping_field' => _x( 'Shipping %s', 'checkout-validation', 'woocommerce' ), ), - 'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0, + 'order_id' => 'pay-now' === $this->context() ? $this->get_order_pay_id() : 0, 'single_product_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'product' ), 'mini_cart_buttons_enabled' => $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ), 'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled, @@ -1018,6 +1021,19 @@ class SmartButton implements SmartButtonInterface { $params['buyer-country'] = WC()->customer->get_billing_country(); } + if ( 'pay-now' === $this->context() ) { + $wc_order_id = $this->get_order_pay_id(); + if ( $wc_order_id ) { + $wc_order = wc_get_order( $wc_order_id ); + if ( $wc_order instanceof WC_Order ) { + $currency = $wc_order->get_currency(); + if ( $currency ) { + $params['currency'] = $currency; + } + } + } + } + $disable_funding = $this->settings->has( 'disable_funding' ) ? $this->settings->get( 'disable_funding' ) : array(); @@ -1444,4 +1460,19 @@ class SmartButton implements SmartButtonInterface { return $this->context() === 'product' ? $product_intent : $other_context_intent; } + + /** + * Returns the ID of WC order on the order-pay page, or 0. + * + * @return int + */ + protected function get_order_pay_id(): int { + global $wp; + + if ( ! isset( $wp->query_vars['order-pay'] ) ) { + return 0; + } + + return absint( $wp->query_vars['order-pay'] ); + } } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index c2244f348..478cc35e0 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -57,6 +57,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\UnsupportedCurrencyAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -208,6 +209,12 @@ return array( $settings = $container->get( 'wcgateway.settings' ); return new ConnectAdminNotice( $state, $settings ); }, + 'wcgateway.notice.currency-unsupported' => static function ( ContainerInterface $container ): UnsupportedCurrencyAdminNotice { + $state = $container->get( 'onboarding.state' ); + $shop_currency = $container->get( 'api.shop.currency' ); + $supported_currencies = $container->get( 'api.supported-currencies' ); + return new UnsupportedCurrencyAdminNotice( $state, $shop_currency, $supported_currencies ); + }, 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice { return new GatewayWithoutPayPalAdminNotice( CreditCardGateway::ID, @@ -223,7 +230,8 @@ return array( $container->get( 'onboarding.state' ), $container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.is-wc-payments-page' ), - $container->get( 'wcgateway.is-ppcp-settings-page' ) + $container->get( 'wcgateway.is-ppcp-settings-page' ), + $container->get( 'wcgateway.settings.status' ) ); }, 'wcgateway.notice.authorize-order-action' => diff --git a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php index 1d297c4e9..08e263541 100644 --- a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php +++ b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php @@ -84,8 +84,11 @@ class DisableGateways { unset( $methods[ CreditCardGateway::ID ] ); } - if ( ! $this->settings_status->is_smart_button_enabled_for_location( 'checkout' ) && ! $this->session_handler->order() && is_checkout() ) { - unset( $methods[ PayPalGateway::ID ] ); + if ( ! $this->settings_status->is_smart_button_enabled_for_location( 'checkout' ) ) { + unset( $methods[ CardButtonGateway::ID ] ); + if ( ! $this->session_handler->order() && is_checkout() ) { + unset( $methods[ PayPalGateway::ID ] ); + } } if ( ! $this->needs_to_disable_gateways() ) { diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 71634f07a..ce6f43f58 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -277,6 +277,27 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->order_endpoint = $order_endpoint; } + /** + * Return the gateway's title. + * + * @return string + */ + public function get_title() { + if ( is_admin() ) { + // $theorder and other things for retrieving the order or post info are not available + // in the constructor, so must do it here. + global $theorder; + if ( $theorder instanceof WC_Order ) { + $payment_method_title = $theorder->get_payment_method_title(); + if ( $payment_method_title ) { + $this->title = $payment_method_title; + } + } + } + + return parent::get_title(); + } + /** * Whether the Gateway needs to be setup. * diff --git a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php index 08a20651b..81b057423 100644 --- a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php @@ -13,11 +13,16 @@ use WC_Payment_Gateway; use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; /** * Creates the admin message about the gateway being enabled without the PayPal gateway. */ class GatewayWithoutPayPalAdminNotice { + private const NOTICE_OK = ''; + private const NOTICE_DISABLED_GATEWAY = 'disabled_gateway'; + private const NOTICE_DISABLED_LOCATION = 'disabled_location'; + /** * The gateway ID. * @@ -53,27 +58,37 @@ class GatewayWithoutPayPalAdminNotice { */ private $is_ppcp_settings_page; + /** + * The Settings status helper. + * + * @var SettingsStatus|null + */ + protected $settings_status; + /** * ConnectAdminNotice constructor. * - * @param string $id The gateway ID. - * @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. + * @param string $id The gateway ID. + * @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. + * @param SettingsStatus|null $settings_status The Settings status helper. */ public function __construct( string $id, State $state, ContainerInterface $settings, bool $is_payments_page, - bool $is_ppcp_settings_page + bool $is_ppcp_settings_page, + ?SettingsStatus $settings_status = null ) { $this->id = $id; $this->state = $state; $this->settings = $settings; $this->is_payments_page = $is_payments_page; $this->is_ppcp_settings_page = $is_ppcp_settings_page; + $this->settings_status = $settings_status; } /** @@ -82,8 +97,25 @@ class GatewayWithoutPayPalAdminNotice { * @return Message|null */ public function message(): ?Message { - if ( ! $this->should_display() ) { - return null; + $notice_type = $this->check(); + + switch ( $notice_type ) { + case self::NOTICE_DISABLED_GATEWAY: + /* translators: %1$s the gateway name, %2$s URL. */ + $text = __( + '%1$s cannot be used without the PayPal gateway. Enable the PayPal gateway.', + 'woocommerce-paypal-payments' + ); + break; + case self::NOTICE_DISABLED_LOCATION: + /* translators: %1$s the gateway name, %2$s URL. */ + $text = __( + '%1$s cannot be used without enabling the Checkout location for the PayPal gateway. Enable the Checkout location.', + 'woocommerce-paypal-payments' + ); + break; + default: + return null; } $gateway = $this->get_gateway(); @@ -94,11 +126,7 @@ class GatewayWithoutPayPalAdminNotice { $name = $gateway->get_method_title(); $message = sprintf( - /* translators: %1$s the gateway name, %2$s URL. */ - __( - '%1$s cannot be used without the PayPal gateway. Enable the PayPal gateway.', - 'woocommerce-paypal-payments' - ), + $text, $name, admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) ); @@ -106,22 +134,33 @@ class GatewayWithoutPayPalAdminNotice { } /** - * Whether the message should be displayed. + * Checks whether one of the messages should be displayed. * - * @return bool + * @return string One of the NOTICE_* constants. */ - protected function should_display(): bool { + protected function check(): string { if ( State::STATE_ONBOARDED !== $this->state->current_state() || ( ! $this->is_payments_page && ! $this->is_ppcp_settings_page ) ) { - return false; - } - if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) { - return false; + return self::NOTICE_OK; } - $gateway = $this->get_gateway(); + $gateway = $this->get_gateway(); + $gateway_enabled = $gateway && wc_string_to_bool( $gateway->get_option( 'enabled' ) ); - return $gateway && wc_string_to_bool( $gateway->get_option( 'enabled' ) ); + if ( ! $gateway_enabled ) { + return self::NOTICE_OK; + } + + $paypal_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); + if ( ! $paypal_enabled ) { + return self::NOTICE_DISABLED_GATEWAY; + } + + if ( $this->settings_status && ! $this->settings_status->is_smart_button_enabled_for_location( 'checkout' ) ) { + return self::NOTICE_DISABLED_LOCATION; + } + + return self::NOTICE_OK; } /** diff --git a/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php new file mode 100644 index 000000000..27ef14f79 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Notice/UnsupportedCurrencyAdminNotice.php @@ -0,0 +1,97 @@ +state = $state; + $this->shop_currency = $shop_currency; + $this->supported_currencies = $supported_currencies; + } + + /** + * Returns the message. + * + * @return Message|null + */ + public function unsupported_currency_message() { + if ( ! $this->should_display() ) { + return null; + } + + $message = sprintf( + /* translators: %1$s the shop currency, 2$s the gateway name. */ + __( + 'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the PayPal currency support page for more information on supported currencies.', + 'woocommerce-paypal-payments' + ), + $this->shop_currency, + 'https://developer.paypal.com/api/rest/reference/currency-codes/' + ); + return new Message( $message, 'warning' ); + } + + /** + * Whether the message should display. + * + * @return bool + */ + protected function should_display(): bool { + return $this->state->current_state() === State::STATE_ONBOARDED && ! $this->currency_supported(); + } + + /** + * Whether the currency is supported by PayPal. + * + * @return bool + */ + private function currency_supported(): bool { + $currency = $this->shop_currency; + $supported_currencies = $this->supported_currencies; + return in_array( $currency, $supported_currencies, true ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php index fab61ca83..f87a7285e 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php @@ -25,7 +25,7 @@ return function ( ContainerInterface $container, array $fields ): array { $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - $vault_enabled = $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ); + $vault_enabled = false;//$settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ); $pay_later_messaging_enabled_label = $vault_enabled ? __( "You have PayPal vaulting enabled, that's why Pay Later options are unavailable now. You cannot use both features at the same time.", 'woocommerce-paypal-payments' ) @@ -33,11 +33,12 @@ return function ( ContainerInterface $container, array $fields ): array { $selected_country = $container->get( 'api.shop.country' ); $default_messaging_flex_color = $selected_country === 'US' ? 'white-no-border' : 'white'; - - $render_preview_element = function ( string $id, string $type ): string { + $button_message = __( 'Pay Later Button Preview', 'woocommerce-paypal-payments' ); + $messaging_message = __( 'Pay Later Messaging Preview', 'woocommerce-paypal-payments' ); + $render_preview_element = function ( string $id, string $type, string $message ): string { return '