diff --git a/changelog.txt b/changelog.txt index f89b8dbf0..408a70333 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,27 @@ *** Changelog *** += 3.0.4 - xxxx-xx-xx = +* Fix - Onboarding screen blank when WooPayments plugin is active #3312 + += 3.0.3 - 2025-04-08 = +* Fix - BN code was set before the installation path was initialized #3309 +* Fix - Things to do next referenced Apple Pay while in branded-only mode #3308 +* Fix - Disabled payment methods were not hidden in reactified WooCommerce Payments settings tab #3290 + += 3.0.2 - 2025-04-03 = +* Enhancement - Check the branded-only flag when settings-UI is loaded the first time #3278 +* Enhancement - Implement a Cache-Flush API #3276 +* Enhancement - Disable the mini-cart location by default #3284 +* Enhancement - Remove branded-only flag when uninstalling PayPal Payments #3295 +* Fix - Welcome screen lists "all major credit/debit cards, Apple Pay, Google Pay," in branded-only mode #3281 +* Fix - Correct heading in onboarding step 4 in branded-only mode #3282 +* Fix - Hide the payment methods screen for personal user in branded-only mode #3286 +* Fix - Enabling Save PayPal does not disable Pay Later messaging #3288 +* Fix - Settings UI: Fix Feature button links #3285 +* Fix - Create mapping for the 3d_secure_contingency setting #3262 +* Fix - Enable Fastlane Watermark by default in new settings UI #3296 +* Fix - Payment method screen is referencing credit cards, digital wallets in branded-only mode #3297 + = 3.0.1 - 2025-03-26 = * Enhancement - Include Fastlane meta on homepage #3151 * Enhancement - Include Branded-only plugin configuration for certain installation paths diff --git a/modules/ppcp-api-client/src/ApiModule.php b/modules/ppcp-api-client/src/ApiModule.php index 12cdd12a6..a1559f12c 100644 --- a/modules/ppcp-api-client/src/ApiModule.php +++ b/modules/ppcp-api-client/src/ApiModule.php @@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; +use Psr\Log\LoggerInterface; /** * Class ApiModule @@ -113,23 +114,20 @@ class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule { 'woocommerce_paypal_payments_flush_api_cache', static function () use ( $c ) { $caches = array( - 'api.paypal-bearer-cache' => array( - PayPalBearer::CACHE_KEY, - ), - 'api.client-credentials-cache' => array( - SdkClientToken::CACHE_KEY, - ), + 'api.paypal-bearer-cache', + 'api.client-credentials-cache', + 'settings.service.signup-link-cache', ); - foreach ( $caches as $cache_id => $keys ) { + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + $logger->info( 'Flushing API caches...' ); + + foreach ( $caches as $cache_id ) { $cache = $c->get( $cache_id ); assert( $cache instanceof Cache ); - foreach ( $keys as $key ) { - if ( $cache->has( $key ) ) { - $cache->delete( $key ); - } - } + $cache->flush(); } } ); diff --git a/modules/ppcp-api-client/src/Helper/Cache.php b/modules/ppcp-api-client/src/Helper/Cache.php index 3b831ea00..e77f78825 100644 --- a/modules/ppcp-api-client/src/Helper/Cache.php +++ b/modules/ppcp-api-client/src/Helper/Cache.php @@ -5,7 +5,7 @@ * @package WooCommerce\PayPalCommerce\ApiClient\Helper */ -declare( strict_types=1 ); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\ApiClient\Helper; @@ -48,8 +48,9 @@ class Cache { * * @return bool */ - public function has( string $key ): bool { + public function has( string $key ) : bool { $value = $this->get( $key ); + return false !== $value; } @@ -58,20 +59,47 @@ class Cache { * * @param string $key The key. */ - public function delete( string $key ): void { + public function delete( string $key ) : void { delete_transient( $this->prefix . $key ); } /** * Caches a value. * - * @param string $key The key under which the value should be cached. - * @param mixed $value The value to cache. + * @param string $key The key under which the value should be cached. + * @param mixed $value The value to cache. * @param int $expiration Time until expiration in seconds. * * @return bool */ - public function set( string $key, $value, int $expiration = 0 ): bool { + public function set( string $key, $value, int $expiration = 0 ) : bool { return (bool) set_transient( $this->prefix . $key, $value, $expiration ); } + + /** + * Flushes all items of the current "cache group", i.e., items that use the defined prefix. + * + * @return void + */ + public function flush() : void { + global $wpdb; + + // Get a list of all transients with the relevant "group prefix" from the DB. + $transients = $wpdb->get_col( + $wpdb->prepare( + "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", + $wpdb->esc_like( '_transient_' . $this->prefix ) . '%' + ) + ); + + /** + * Delete each cache item individually to ensure WP can fire all relevant + * actions, perform checks and other cleanup tasks and ensures eventually + * object cache systems, like Redis, are kept in-sync with the DB. + */ + foreach ( $transients as $transient ) { + $key = str_replace( '_transient_' . $this->prefix, '', $transient ); + $this->delete( $key ); + } + } } diff --git a/modules/ppcp-axo-block/resources/css/gateway.scss b/modules/ppcp-axo-block/resources/css/gateway.scss index 4611bfa35..605dbf4ca 100644 --- a/modules/ppcp-axo-block/resources/css/gateway.scss +++ b/modules/ppcp-axo-block/resources/css/gateway.scss @@ -101,7 +101,7 @@ $fast-transition-duration: 0.5s; } // 3. Express Payment Block -.wp-block-woocommerce-checkout-express-payment-block { +.wc-block-components-express-payment--checkout, .wp-block-woocommerce-checkout-express-payment-block { transition: opacity $transition-duration ease-in, scale $transition-duration ease-in, display $transition-duration ease-in; diff --git a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js index 7b25cec31..713b548ba 100644 --- a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js +++ b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js @@ -10,7 +10,7 @@ import { STORE_NAME } from '../stores/axoStore'; */ export const setupAuthenticationClassToggle = () => { const targetSelector = - '.wp-block-woocommerce-checkout-express-payment-block'; + '.wc-block-components-express-payment--checkout, .wp-block-woocommerce-checkout-express-payment-block'; const authClass = 'wc-block-axo-is-authenticated'; const updateAuthenticationClass = () => { diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index f097882b9..1b9b6f59a 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -181,8 +181,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { add_action( 'wp_loaded', function () use ( $c ) { - $module = $this; - $this->session_handler = $c->get( 'session.handler' ); $settings = $c->get( 'wcgateway.settings' ); @@ -208,12 +206,12 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { // Enqueue frontend scripts. add_action( 'wp_enqueue_scripts', - static function () use ( $c, $manager, $module ) { + function () use ( $c, $manager ) { $smart_button = $c->get( 'button.smart-button' ); assert( $smart_button instanceof SmartButtonInterface ); - if ( $module->should_render_fastlane( $c ) && $smart_button->should_load_ppcp_script() ) { + if ( $this->should_render_fastlane( $c ) && $smart_button->should_load_ppcp_script() ) { $manager->enqueue(); } } @@ -222,8 +220,8 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { // Render submit button. add_action( $manager->checkout_button_renderer_hook(), - static function () use ( $c, $manager, $module ) { - if ( $module->should_render_fastlane( $c ) ) { + function () use ( $c, $manager ) { + if ( $this->should_render_fastlane( $c ) ) { $manager->render_checkout_button(); } } @@ -278,14 +276,14 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { add_filter( 'woocommerce_paypal_payments_localized_script_data', - function( array $localized_script_data ) use ( $c, $module ) { + function( array $localized_script_data ) use ( $c ) { $api = $c->get( 'api.sdk-client-token' ); assert( $api instanceof SdkClientToken ); $logger = $c->get( 'woocommerce.logger.woocommerce' ); assert( $logger instanceof LoggerInterface ); - return $module->add_sdk_client_token_to_script_data( $api, $logger, $localized_script_data ); + return $this->add_sdk_client_token_to_script_data( $api, $logger, $localized_script_data ); } ); @@ -349,6 +347,26 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); + // Remove Fastlane on the Pay for Order page. + add_filter( + 'woocommerce_available_payment_gateways', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + static function ( $methods ) { + if ( ! is_array( $methods ) || ! is_wc_endpoint_url( 'order-pay' ) ) { + return $methods; + } + + // Remove Fastlane if present. + unset( $methods[ AxoGateway::ID ] ); + + return $methods; + } + ); + return true; } @@ -403,6 +421,7 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { * @return bool */ private function should_render_fastlane( ContainerInterface $c ): bool { + $dcc_configuration = $c->get( 'wcgateway.configuration.card-configuration' ); assert( $dcc_configuration instanceof CardPaymentsConfiguration ); diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 5cd26fcbe..6ba97bed1 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -466,6 +466,10 @@ class SmartButton implements SmartButtonInterface { * @return bool */ private function render_message_wrapper_registrar(): bool { + if ( ! apply_filters( 'woocommerce_paypal_payments_should_render_pay_later_messaging', true ) ) { + return false; + } + if ( ! $this->settings_status->is_pay_later_messaging_enabled() || ! $this->settings_status->has_pay_later_messaging_locations() ) { return false; } diff --git a/modules/ppcp-button/src/Helper/ThreeDSecure.php b/modules/ppcp-button/src/Helper/ThreeDSecure.php index dee633fe2..1c694dc06 100644 --- a/modules/ppcp-button/src/Helper/ThreeDSecure.php +++ b/modules/ppcp-button/src/Helper/ThreeDSecure.php @@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory class ThreeDSecure { public const NO_DECISION = 0; - public const PROCEED = 1; + public const PROCEED = 1; public const REJECT = 2; public const RETRY = 3; diff --git a/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php index 1faf46955..d1efea41f 100644 --- a/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php @@ -10,6 +10,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat\Settings; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; /** @@ -20,6 +22,15 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; */ class PaymentMethodSettingsMapHelper { + /** + * A map of new to old 3d secure values. + */ + protected const THREE_D_SECURE_VALUES_MAP = array( + 'no-3d-secure' => 'NO_3D_SECURE', + 'only-required-3d-secure' => 'SCA_WHEN_REQUIRED', + 'always-3d-secure' => 'SCA_ALWAYS', + ); + /** * Maps old setting keys to new payment method settings names. * @@ -27,26 +38,39 @@ class PaymentMethodSettingsMapHelper { */ public function map(): array { return array( - 'dcc_enabled' => CreditCardGateway::ID, - 'axo_enabled' => AxoGateway::ID, + 'dcc_enabled' => CreditCardGateway::ID, + 'axo_enabled' => AxoGateway::ID, + '3d_secure_contingency' => 'three_d_secure', ); } /** * Retrieves the value of a mapped key from the new settings. * - * @param string $old_key The key from the legacy settings. + * @param string $old_key The key from the legacy settings. + * @param AbstractDataModel|null $payment_settings The payment settings model. * @return mixed The value of the mapped setting, (null if not found). */ - public function mapped_value( string $old_key ): ?bool { + public function mapped_value( string $old_key, ?AbstractDataModel $payment_settings ) { + switch ( $old_key ) { + case '3d_secure_contingency': + if ( is_null( $payment_settings ) ) { + return null; + } - $payment_method = $this->map()[ $old_key ] ?? false; + assert( $payment_settings instanceof PaymentSettings ); + $selected_three_d_secure = $payment_settings->get_three_d_secure(); + return self::THREE_D_SECURE_VALUES_MAP[ $selected_three_d_secure ] ?? null; - if ( ! $payment_method ) { - return null; + default: + $payment_method = $this->map()[ $old_key ] ?? false; + + if ( ! $payment_method ) { + return null; + } + + return $this->is_gateway_enabled( $payment_method ); } - - return $this->is_gateway_enabled( $payment_method ); } /** diff --git a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php index 39a285041..263ac45cb 100644 --- a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php @@ -214,7 +214,7 @@ class SettingsMapHelper { : $this->settings_tab_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); case $model instanceof PaymentSettings: - return $this->payment_method_settings_map_helper->mapped_value( $old_key ); + return $this->payment_method_settings_map_helper->mapped_value( $old_key, $this->get_payment_settings_model() ); default: return $this->model_cache[ $model_id ][ $new_key ] ?? null; diff --git a/modules/ppcp-paypal-subscriptions/resources/js/paypal-subscription.js b/modules/ppcp-paypal-subscriptions/resources/js/paypal-subscription.js index f2015194e..876cbf796 100644 --- a/modules/ppcp-paypal-subscriptions/resources/js/paypal-subscription.js +++ b/modules/ppcp-paypal-subscriptions/resources/js/paypal-subscription.js @@ -1,8 +1,6 @@ -import { __ } from '@wordpress/i18n'; document.addEventListener( 'DOMContentLoaded', () => { - const disableFields = ( productId ) => { - const variations = document.querySelector( '.woocommerce_variations' ); + const variations = document.querySelector( '.woocommerce_variations' ); if ( variations ) { const children = variations.children; for ( let i = 0; i < children.length; i++ ) { @@ -70,156 +68,232 @@ document.addEventListener( 'DOMContentLoaded', () => { soldIndividually.setAttribute( 'disabled', 'disabled' ); }; - const checkSubscriptionPeriodsInterval = (period, period_interval, price, linkBtn) => { - if ( - ( period === 'year' && parseInt( period_interval ) > 1 ) || - ( period === 'month' && parseInt( period_interval ) > 12 ) || - ( period === 'week' && parseInt( period_interval ) > 52 ) || - ( period === 'day' && parseInt( period_interval ) > 356 ) || - ( ! price || parseInt( price ) <= 0 ) - ) { - linkBtn.disabled = true; - linkBtn.checked = false; - if (! price || parseInt( price ) <= 0 ) { - linkBtn.setAttribute('title', __( 'Prices must be above zero for PayPal Subscriptions!', 'woocommerce-paypal-subscriptions' ) ); - } else { - linkBtn.setAttribute('title', __( 'Not allowed period interval combination for PayPal Subscriptions!', 'woocommerce-paypal-subscriptions' ) ); - } + const checkSubscriptionPeriodsInterval = ( + period, + period_interval, + price, + linkBtn + ) => { + if ( ! linkBtn ) { + return; + } - } else { - linkBtn.disabled = false; - linkBtn.removeAttribute('title'); - } - } + if ( + ( period === 'year' && parseInt( period_interval ) > 1 ) || + ( period === 'month' && parseInt( period_interval ) > 12 ) || + ( period === 'week' && parseInt( period_interval ) > 52 ) || + ( period === 'day' && parseInt( period_interval ) > 356 ) || + ! price || + parseInt( price ) <= 0 + ) { + linkBtn.disabled = true; + linkBtn.checked = false; + if ( ! price || parseInt( price ) <= 0 ) { + linkBtn.setAttribute( + 'title', + PayPalCommerceGatewayPayPalSubscriptionProducts.i18n + .prices_must_be_above_zero + ); + } else { + linkBtn.setAttribute( + 'title', + PayPalCommerceGatewayPayPalSubscriptionProducts.i18n + .not_allowed_period_interval + ); + } + } else { + linkBtn.disabled = false; + linkBtn.removeAttribute( 'title' ); + } + }; const setupProducts = () => { - jQuery( '.wc_input_subscription_period' ).on( 'change', (e) => { - const linkBtn = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('input[name="_ppcp_enable_subscription_product"]'); - const period_interval = e.target.parentElement.querySelector('select.wc_input_subscription_period_interval')?.value; - const period = e.target.value; - const price = e.target.parentElement.querySelector('input.wc_input_subscription_price')?.value; + jQuery( '.wc_input_subscription_period' ).on( 'change', ( e ) => { + const linkBtn = + e.target.parentElement.parentElement.parentElement.parentElement.querySelector( + 'input[name="_ppcp_enable_subscription_product"]' + ); + if ( linkBtn ) { + const period_interval = e.target.parentElement.querySelector( + 'select.wc_input_subscription_period_interval' + )?.value; + const period = e.target.value; + const price = e.target.parentElement.querySelector( + 'input.wc_input_subscription_price' + )?.value; - checkSubscriptionPeriodsInterval(period, period_interval, price, linkBtn); - }); + checkSubscriptionPeriodsInterval( + period, + period_interval, + price, + linkBtn + ); + } + } ); - jQuery( '.wc_input_subscription_period_interval' ).on( 'change', (e) => { - const linkBtn = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('input[name="_ppcp_enable_subscription_product"]'); - const period_interval = e.target.value; - const period = e.target.parentElement.querySelector('select.wc_input_subscription_period')?.value; - const price = e.target.parentElement.querySelector('input.wc_input_subscription_price')?.value; + jQuery( '.wc_input_subscription_period_interval' ).on( + 'change', + ( e ) => { + const linkBtn = + e.target.parentElement.parentElement.parentElement.parentElement.querySelector( + 'input[name="_ppcp_enable_subscription_product"]' + ); + if ( linkBtn ) { + const period_interval = e.target.value; + const period = e.target.parentElement.querySelector( + 'select.wc_input_subscription_period' + )?.value; + const price = e.target.parentElement.querySelector( + 'input.wc_input_subscription_price' + )?.value; - checkSubscriptionPeriodsInterval(period, period_interval, price, linkBtn); - }); + checkSubscriptionPeriodsInterval( + period, + period_interval, + price, + linkBtn + ); + } + } + ); - jQuery( '.wc_input_subscription_price' ).on( 'change', (e) => { - const linkBtn = e.target.parentElement.parentElement.parentElement.parentElement.querySelector('input[name="_ppcp_enable_subscription_product"]'); - const period_interval = e.target.parentElement.querySelector('select.wc_input_subscription_period_interval')?.value; - const period = e.target.parentElement.querySelector('select.wc_input_subscription_period')?.value; - const price = e.target.value; + jQuery( '.wc_input_subscription_price' ).on( 'change', ( e ) => { + const linkBtn = + e.target.parentElement.parentElement.parentElement.parentElement.querySelector( + 'input[name="_ppcp_enable_subscription_product"]' + ); + if ( linkBtn ) { + const period_interval = e.target.parentElement.querySelector( + 'select.wc_input_subscription_period_interval' + )?.value; + const period = e.target.parentElement.querySelector( + 'select.wc_input_subscription_period' + )?.value; + const price = e.target.value; - checkSubscriptionPeriodsInterval(period, period_interval, price, linkBtn); - }); + checkSubscriptionPeriodsInterval( + period, + period_interval, + price, + linkBtn + ); + } + } ); - jQuery( '.wc_input_subscription_price' ).trigger( 'change' ); + jQuery( '.wc_input_subscription_price' ).trigger( 'change' ); - let variationProductIds = [ PayPalCommerceGatewayPayPalSubscriptionProducts.product_id ]; - const variationsInput = document.querySelectorAll( '.variable_post_id' ); - for ( let i = 0; i < variationsInput.length; i++ ) { - variationProductIds.push( variationsInput[ i ].value ); - } + const variationProductIds = [ + PayPalCommerceGatewayPayPalSubscriptionProducts.product_id, + ]; + const variationsInput = + document.querySelectorAll( '.variable_post_id' ); + for ( let i = 0; i < variationsInput.length; i++ ) { + variationProductIds.push( variationsInput[ i ].value ); + } - variationProductIds?.forEach( - ( productId ) => { - const linkBtn = document.getElementById( - `ppcp_enable_subscription_product-${ productId }` - ); + variationProductIds?.forEach( ( productId ) => { + const linkBtn = document.getElementById( + `ppcp_enable_subscription_product-${ productId }` + ); + if ( linkBtn ) { if ( linkBtn.checked && linkBtn.value === 'yes' ) { disableFields( productId ); } - linkBtn?.addEventListener( 'click', ( event ) => { - const unlinkBtnP = document.getElementById( - `ppcp-enable-subscription-${ productId }` - ); - const titleP = document.getElementById( - `ppcp_subscription_plan_name_p-${ productId }` - ); - if (event.target.checked === true) { - if ( unlinkBtnP ) { - unlinkBtnP.style.display = 'none'; - } - if ( titleP ) { - titleP.style.display = 'block'; - } - } else { - if ( unlinkBtnP ) { - unlinkBtnP.style.display = 'block'; - } - if ( titleP ) { - titleP.style.display = 'none'; - } - } - }); - - const unlinkBtn = document.getElementById( - `ppcp-unlink-sub-plan-${ productId }` - ); - unlinkBtn?.addEventListener( 'click', ( event ) => { - event.preventDefault(); - unlinkBtn.disabled = true; - const spinner = document.getElementById( - `spinner-unlink-plan-${ productId }` + linkBtn.addEventListener( 'click', ( event ) => { + const unlinkBtnP = document.getElementById( + `ppcp-enable-subscription-${ productId }` ); - spinner.style.display = 'inline-block'; + const titleP = document.getElementById( + `ppcp_subscription_plan_name_p-${ productId }` + ); + if ( event.target.checked === true ) { + if ( unlinkBtnP ) { + unlinkBtnP.style.display = 'none'; + } + if ( titleP ) { + titleP.style.display = 'block'; + } + } else { + if ( unlinkBtnP ) { + unlinkBtnP.style.display = 'block'; + } + if ( titleP ) { + titleP.style.display = 'none'; + } + } + } ); + } - fetch( PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.endpoint, { + const unlinkBtn = document.getElementById( + `ppcp-unlink-sub-plan-${ productId }` + ); + unlinkBtn?.addEventListener( 'click', ( event ) => { + event.preventDefault(); + unlinkBtn.disabled = true; + const spinner = document.getElementById( + `spinner-unlink-plan-${ productId }` + ); + spinner.style.display = 'inline-block'; + + fetch( + PayPalCommerceGatewayPayPalSubscriptionProducts.ajax + .deactivate_plan.endpoint, + { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'same-origin', body: JSON.stringify( { - nonce: PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.nonce, + nonce: PayPalCommerceGatewayPayPalSubscriptionProducts + .ajax.deactivate_plan.nonce, plan_id: linkBtn.dataset.subsPlan, product_id: productId, } ), + } + ) + .then( function ( res ) { + return res.json(); } ) - .then( function ( res ) { - return res.json(); - } ) - .then( function ( data ) { - if ( ! data.success ) { - unlinkBtn.disabled = false; - spinner.style.display = 'none'; - console.error( data ); - throw Error( data.data.message ); - } + .then( function ( data ) { + if ( ! data.success ) { + unlinkBtn.disabled = false; + spinner.style.display = 'none'; + console.error( data ); + throw Error( data.data.message ); + } - const enableSubscription = document.getElementById( - 'ppcp-enable-subscription-' + data.data.product_id + const enableSubscription = document.getElementById( + 'ppcp-enable-subscription-' + data.data.product_id + ); + const product = document.getElementById( + 'pcpp-product-' + data.data.product_id + ); + const plan = document.getElementById( + 'pcpp-plan-' + data.data.product_id + ); + enableSubscription.style.display = 'none'; + product.style.display = 'none'; + plan.style.display = 'none'; + + const enable_subscription_product = + document.getElementById( + 'ppcp_enable_subscription_product-' + + data.data.product_id ); - const product = document.getElementById( 'pcpp-product-' + data.data.product_id ); - const plan = document.getElementById( 'pcpp-plan-' + data.data.product_id ); - enableSubscription.style.display = 'none'; - product.style.display = 'none'; - plan.style.display = 'none'; + enable_subscription_product.disabled = true; - const enable_subscription_product = - document.getElementById( - 'ppcp_enable_subscription_product-' + data.data.product_id - ); - enable_subscription_product.disabled = true; + const planUnlinked = document.getElementById( + 'pcpp-plan-unlinked-' + data.data.product_id + ); + planUnlinked.style.display = 'block'; - const planUnlinked = - document.getElementById( 'pcpp-plan-unlinked-' + data.data.product_id ); - planUnlinked.style.display = 'block'; - - setTimeout( () => { - location.reload(); - }, 1000 ); - } ); - } ); - } - ); + setTimeout( () => { + location.reload(); + }, 1000 ); + } ); + } ); + } ); }; setupProducts(); diff --git a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php index d35905ab7..b2c2d264f 100644 --- a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php +++ b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php @@ -581,6 +581,10 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu ), ), 'product_id' => $product->get_id(), + 'i18n' => array( + 'prices_must_be_above_zero' => __( 'Prices must be above zero for PayPal Subscriptions!', 'woocommerce-paypal-payments' ), + 'not_allowed_period_interval' => __( 'Not allowed period interval combination for PayPal Subscriptions!', 'woocommerce-paypal-payments' ), + ), ) ); } diff --git a/modules/ppcp-settings/resources/css/components/_app.scss b/modules/ppcp-settings/resources/css/components/_app.scss index 7e69cbada..6f5d8dc9a 100644 --- a/modules/ppcp-settings/resources/css/components/_app.scss +++ b/modules/ppcp-settings/resources/css/components/_app.scss @@ -3,13 +3,6 @@ */ .ppcp-r-app.loading { - height: 400px; - width: 400px; - position: absolute; - left: 50%; - transform: translate(-50%, 0); - text-align: center; - .ppcp-r-spinner-overlay { display: flex; flex-direction: column; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 8116068a9..884493ada 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -70,6 +70,15 @@ button.components-button, a.components-button { --button-disabled-color: #{$color-gray-100}; --button-disabled-background: #{$color-gray-500}; + + &:hover:not(:disabled) { + background: #{$color-blue}; + } + + + &:not(.components-tab-panel__tabs-item):focus-visible:not(:disabled) { + outline: 2px solid #{$color-gray-500}; + } } &.is-secondary { @@ -86,7 +95,7 @@ button.components-button, a.components-button { --button-color: #{$color-blueberry}; --button-hover-color: #{$color-gradient-dark}; - &:focus:not(:disabled) { + &:focus-visible:not(:disabled) { border: none; box-shadow: none; } @@ -95,6 +104,16 @@ button.components-button, a.components-button { &.small-button { @include small-button; } + + &:focus:not(:disabled) { + outline: none; + } + + &:focus-visible:not(:disabled), + &:not(.components-tab-panel__tabs-item):focus-visible:not(:disabled) + &[data-focus-visible="true"] { + outline: 2px solid #{$color-blueberry}; + } } .ppcp--is-loading { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss index 195367dfb..7dba93d17 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss @@ -111,12 +111,7 @@ margin: 0; } } - // Custom styles. - .components-form-toggle.is-checked > .components-form-toggle__track { - background-color: $color-blueberry; - } - .ppcp-r-vertical-text-control { .components-base-control__field { display: flex; @@ -126,3 +121,74 @@ } } } + +.ppcp-r-app, .ppcp-r-modal__container { + // Form toggle styling. + .components-form-toggle { + &.is-checked { + > .components-form-toggle__track { + background-color: $color-blueberry; + } + .components-form-toggle__track { + border-color: $color-blueberry; + } + } + .components-form-toggle__input { + &:focus { + + .components-form-toggle__track { + box-shadow: none; + } + } + &:focus-visible + .components-form-toggle__track { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, + 0 0 0 calc(var(--wp-admin-border-width-focus)*2) $color-blueberry; + } + } + } + + // Form inputs. + .components-text-control__input { + &:focus, + &[type="color"]:focus, + &[type="date"]:focus, + &[type="datetime-local"]:focus, + &[type="datetime"]:focus, + &[type="email"]:focus, + &[type="month"]:focus, + &[type="number"]:focus, + &[type="password"]:focus, + &[type="tel"]:focus, + &[type="text"]:focus, + &[type="time"]:focus, + &[type="url"]:focus, + &[type="week"]:focus { + border-color: $color-blueberry; + } + } + + // Radio inputs. + .components-radio-control__input[type="radio"] { + &:checked { + background-color: $color-blueberry; + border-color: $color-blueberry; + } + &:focus { + box-shadow: none; + } + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, + 0 0 0 calc(var(--wp-admin-border-width-focus)*2) $color-blueberry; + } + } + + // Checkbox inputs. + .components-checkbox-control__input[type="checkbox"] { + &:focus { + box-shadow: none; + } + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, + 0 0 0 calc(var(--wp-admin-border-width-focus)*2) $color-blueberry; + } + } + } \ No newline at end of file diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss index b3188b461..50c798438 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss @@ -103,6 +103,11 @@ $margin_bottom: 48px; .components-tab-panel__tabs-item { height: var(--subnavigation-height); + + &:focus-visible:not(:disabled), + &[data-focus-visible="true"]:focus:not(:disabled) { + outline: none; + } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss index 59d897132..f4b02442a 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss @@ -60,7 +60,7 @@ $width_gap: 24px; .ppcp-r-settings-card__title { @include font(13, 24, 600); color: var(--color-text-main); - margin: 0 0 4px 0; + margin: 0 0 12px 0; display: block; } @@ -68,6 +68,16 @@ $width_gap: 24px; @include font(13, 20, 400); color: var(--color-text-teriary); margin: 0; + + + p { + margin: 0 0 12px 0; + } + + button { + padding: 0; + margin: 0; + } } + .ppcp-r-settings-card { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss index 8a34ff17c..79d2f2c05 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss @@ -1,5 +1,4 @@ .ppcp-r-spinner-overlay { - background: var(--spinner-overlay-color); position: absolute; width: 100%; height: 100%; @@ -13,8 +12,6 @@ left: 50%; transform: translate(-50%, -50%); margin: 0; - width: var(--spinner-size); - height: var(--spinner-size); } .ppcp--spinner-message { @@ -29,7 +26,6 @@ position: fixed; width: var(--spinner-overlay-width); height: var(--spinner-overlay-height); - box-shadow: var(--spinner-overlay-box-shadow); left: 50%; top: 50%; transform: translate(-50%, -50%); diff --git a/modules/ppcp-settings/resources/css/components/screens/_modals.scss b/modules/ppcp-settings/resources/css/components/screens/_modals.scss index 5dbdf7652..0b628556e 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_modals.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_modals.scss @@ -17,3 +17,18 @@ } } } + +.ppcp-r-modal { + button.components-button, + a.components-button { + &:focus:not(:disabled) { + outline: none; + } + + &:focus-visible:not(:disabled), + &:not(.components-tab-panel__tabs-item):focus-visible:not(:disabled) + &[data-focus-visible="true"] { + outline: 2px solid #{$color-blueberry}; + } + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 63952ed1f..232dbabd8 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -103,7 +103,7 @@ &__dismiss { position: absolute; - right: 0; + right: 2px; top: 50%; transform: translateY(-50%); background-color: transparent; diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss index e37fce07a..999820bdf 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss @@ -1,19 +1,31 @@ .ppcp-r-paylater-configurator { display: flex; - border: 1px solid var(--color-separators); border-radius: var(--container-border-radius); overflow: hidden; font-family: "PayPalPro", sans-serif; -webkit-font-smoothing: antialiased; + width: 1200px; + + // Reset box-sizing for the preview container. + .etu8a6w3 * { + box-sizing: unset; + } .css-1snxoyf.eolpigi0 { margin: 0; } + .css-1f9aeda { + width: 100%; + } + + .css-1adsww8 { + padding: 0; + } + #configurator-eligibleContainer.css-4nclxm.e1vy3g880 { width: 100%; max-width: 100%; - padding: 16px 0px 16px 16px; #configurator-controlPanelContainer.css-5urmrq.e1vy3g880 { width: 374px; @@ -43,7 +55,7 @@ } .css-8vwtr6-state { - height: 1.4rem; + height: 1.5rem; width: 3rem; } } @@ -54,17 +66,27 @@ } &__subheader, #configurator-controlPanelSubHeader { - color: var(--color-text-description); + color: var(--color-text-teriary); margin: 0 0 18px 0; + @include font(13, 20, 400); } &__header, #configurator-controlPanelHeader, #configurator-previewSectionSubHeaderText.css-14ujlqd-text_body, .css-16jt5za-text_body { - @include font(16, 20, 600); + @include font(13, 20, 600); color: var(--color-text-title); - margin-bottom: 6px; font-family: "PayPalPro", sans-serif; -webkit-font-smoothing: antialiased; } + &__header, + #configurator-controlPanelHeader { + margin-bottom: 12px; + } + + + #configurator-previewSectionSubHeaderText.css-14ujlqd-text_body, + .css-16jt5za-text_body { + color: var(--color-text-teriary); + } .css-1yo2lxy-text_body_strong { color: var(--color-text-description); @@ -73,8 +95,9 @@ } .css-rok10q, .css-dfgbdq-text_body_strong { - margin-top: 0; - margin-bottom: 0; + margin: 0 0 12px 0; + padding: 0; + width: 100%; } &__publish-button { @@ -109,9 +132,9 @@ display: none; } - .css-4nclxm.e1vy3g880, { + .css-4nclxm.e1vy3g880 { width: 100%; - padding: 48px 8px; + padding: 0 0 48px 0; .css-11hsg2u.e1vy3g880 { width: 100%; @@ -119,7 +142,7 @@ } .css-n4cwz8 { - margin-top: 20px; + margin-top: 48px; } .css-1ce6bcu-container { diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index ca73ab531..228dafc5f 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,7 +1,6 @@ import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; - import { useToggleState } from '../../hooks/useToggleState'; import { Content, @@ -22,33 +21,44 @@ const Accordion = ( { className = '', } ) => { const { isOpen, toggleOpen } = useToggleState( id, initiallyOpen ); - const wrapperClasses = classNames( 'ppcp-r-accordion', className, { - 'ppcp--is-open': isOpen, - } ); - const contentClass = classNames( 'ppcp--accordion-content', { - 'ppcp--is-open': isOpen, - } ); - - const icon = isOpen ? chevronUp : chevronDown; + const contentId = id + ? `${ id }-content` + : `accordion-${ title.replace( /\s+/g, '-' ).toLowerCase() }-content`; return ( -
+
-
+
{ children }
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js index c4ccc1d02..9eeaec0a6 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js @@ -1,58 +1,40 @@ import { Button } from '@wordpress/components'; - import { Header, Title, Action, Description } from '../Elements'; import SettingsBlock from '../SettingsBlock'; import TitleBadge from '../TitleBadge'; +import { CommonHooks } from '../../../data'; +/** + * Renders a feature settings block with title, description, and action buttons. + * + * @param {Object} props Component properties + * @param {string} props.title The feature title + * @param {string} props.description HTML description of the feature + * @return {JSX.Element} The rendered component + */ const FeatureSettingsBlock = ( { title, description, ...props } ) => { - const printNotes = () => { - const notes = props.actionProps?.notes; - if ( ! notes || ( Array.isArray( notes ) && notes.length === 0 ) ) { - return null; + const { actionProps } = props; + const { isSandbox } = CommonHooks.useMerchant(); + + /** + * Gets the appropriate URL for a button based on environment + * Always prioritizes urls object over url when it exists + * + * @param {Object} buttonData The button configuration object + * @param {string} [buttonData.url] Single URL for the button + * @param {Object} [buttonData.urls] Environment-specific URLs + * @param {string} [buttonData.urls.sandbox] URL for sandbox environment + * @param {string} [buttonData.urls.live] URL for live environment + * @return {string|undefined} The appropriate URL to use for the button + */ + const getButtonUrl = ( buttonData ) => { + const { url, urls } = buttonData; + + if ( urls ) { + return isSandbox ? urls.sandbox : urls.live; } - return ( - - { notes.map( ( note, index ) => ( - { note } - ) ) } - - ); - }; - - const FeatureButton = ( { - className, - variant, - text, - isBusy, - url, - urls, - onClick, - } ) => { - const buttonProps = { - className, - isBusy, - variant, - }; - - if ( url || urls ) { - buttonProps.href = urls ? urls.live : url; - buttonProps.target = '_blank'; - } - if ( ! buttonProps.href ) { - buttonProps.onClick = onClick; - } - - return ; - }; - - const renderDescription = () => { - return ( - - ); + return url; }; return ( @@ -60,38 +42,52 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
{ title } - { props.actionProps?.enabled && ( - <TitleBadge { ...props.actionProps?.badge } /> + { actionProps?.enabled && ( + <TitleBadge { ...actionProps?.badge } /> ) } - { renderDescription() } - { printNotes() } + + + { actionProps?.notes?.length > 0 && ( + + { actionProps.notes.map( ( note, index ) => ( + { note } + ) ) } + + ) }
+
- { props.actionProps?.buttons.map( - ( { + { actionProps?.buttons.map( ( buttonData ) => { + const { class: className, type, text, - url, - urls, onClick, - } ) => ( - - ) - ) } + isBusy={ actionProps.isBusy } + href={ buttonUrl } + target={ buttonUrl ? '_blank' : undefined } + onClick={ ! buttonUrl ? onClick : undefined } + > + { text } + + ); + } ) }
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js index 2a2b52b41..a415ae7f1 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js @@ -31,10 +31,15 @@ const PaymentMethodItemBlock = ( { id={ paymentMethod.id } className={ methodItemClasses } separatorAndGap={ false } + aria-disabled={ isDisabled ? 'true' : 'false' } > { isDisabled && ( -
-

+

+

{ disabledMessage }

@@ -60,6 +65,8 @@ const PaymentMethodItemBlock = ( { __nextHasNoMarginBottom checked={ isSelected } onChange={ onSelect } + disabled={ isDisabled } + aria-label={ `Enable ${ paymentMethod.itemTitle }` } /> { hasWarning && ! isDisabled && isSelected && ( diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js index 97db00c2f..9cb809635 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js @@ -2,6 +2,18 @@ import classNames from 'classnames'; import { Content } from './Elements'; +/** + * Renders a settings card. + * + * @param {Object} props Component properties + * @param {string} [props.id] Unique identifier for the card + * @param {string} [props.className] Additional CSS classes + * @param {string} props.title Card title + * @param {*} props.description Card description content + * @param {*} props.children Card content + * @param {boolean} [props.contentContainer=true] Whether to wrap content in a container + * @return {JSX.Element} The settings card component + */ const SettingsCard = ( { id, className, @@ -16,14 +28,20 @@ const SettingsCard = ( { id, }; + const titleId = id ? `${ id }-title` : undefined; + const descriptionId = id ? `${ id }-description` : undefined; + return ( -
+
- +

{ title } - -
+

+
{ description }
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js index a3b16c07c..44810871d 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js @@ -2,20 +2,24 @@ import { __ } from '@wordpress/i18n'; import { Spinner } from '@wordpress/components'; import classnames from 'classnames'; -const SpinnerOverlay = ( { asModal = false, message = null } ) => { +/** + * Renders a loading spinner. + * + * @param {Object} props Component properties. + * @param {boolean} [props.asModal=false] Whether to display the spinner as a modal overlay. + * @param {string} [props.ariaLabel] Accessible label for screen readers. + * @return {JSX.Element} The spinner overlay component. + */ +const SpinnerOverlay = ( { + asModal = false, + ariaLabel = __( 'Loading…', 'woocommerce-paypal-payments' ), +} ) => { const className = classnames( 'ppcp-r-spinner-overlay', { 'ppcp--is-modal': asModal, } ); - if ( null === message ) { - message = __( 'Loading…', 'woocommerce-paypal-payments' ); - } - return ( -
- { message && ( - { message } - ) } +
); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js index 0d902e12c..f5387b617 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js @@ -30,8 +30,16 @@ const TabBar = ( { tabs, activePanel, setActivePanel } ) => { initialTabName={ activePanel } onSelect={ updateActivePanel } tabs={ tabs } + orientation="horizontal" + selectOnMove={ false } > - { () => '' } + { ( tab ) => ( +
+ { tab.render ? tab.render() : '' } +
+ ) } ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js index 4b819e1da..2925aacb4 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepPaymentMethods.js @@ -10,6 +10,7 @@ import PaymentFlow from '../Components/PaymentFlow'; const StepPaymentMethods = () => { const { optionalMethods, setOptionalMethods } = OnboardingHooks.useOptionalPaymentMethods(); + const { ownBrandOnly } = CommonHooks.useWooSettings(); const { isCasualSeller } = OnboardingHooks.useBusiness(); const optionalMethodTitle = useMemo( () => { @@ -31,7 +32,10 @@ const StepPaymentMethods = () => { description: , }, { - title: __( + title: ownBrandOnly ? __( + 'No thanks, I prefer to use a different provider for local payment methods', + 'woocommerce-paypal-payments' + ) : __( 'No thanks, I prefer to use a different provider for processing credit cards, digital wallets, and local payment methods', 'woocommerce-paypal-payments' ), @@ -41,7 +45,9 @@ const StepPaymentMethods = () => { return (
- } /> + } + />
{ export default StepPaymentMethods; -const PaymentStepTitle = () => { +const PaymentStepTitle = ( ownBrandOnly ) => { + if ( ownBrandOnly.isBrandedOnly ) { + return __( + 'Add Expanded Checkout for more ways to pay', + 'woocommerce-paypal-payments' + ); + } return __( 'Add Credit and Debit Cards', 'woocommerce-paypal-payments' ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js index bfce0b5f8..0ccaab8f2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js @@ -22,15 +22,16 @@ const StepWelcome = ( { setStep, currentStep } ) => { ownBrandOnly ); - const onboardingHeaderDescription = canUseCardPayments - ? __( - 'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.', - 'woocommerce-paypal-payments' - ) - : __( - 'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, and more.', - 'woocommerce-paypal-payments' - ); + const onboardingHeaderDescription = + canUseCardPayments && ! ownBrandOnly + ? __( + 'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.', + 'woocommerce-paypal-payments' + ) + : __( + 'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, and more.', + 'woocommerce-paypal-payments' + ); return (
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js index cf680db70..5d0790cf6 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/index.js @@ -1,5 +1,6 @@ import { __ } from '@wordpress/i18n'; +import { CommonHooks, OnboardingHooks } from '../../../../data'; import StepWelcome from './StepWelcome'; import StepBusiness from './StepBusiness'; import StepProducts from './StepProducts'; @@ -56,11 +57,17 @@ const filterSteps = ( steps, conditions ) => { }; export const getSteps = ( flags ) => { + const { ownBrandOnly } = CommonHooks.useWooSettings(); + const { isCasualSeller } = OnboardingHooks.useBusiness(); + const steps = filterSteps( ALL_STEPS, [ // Casual selling: Unlock the "Personal Account" choice. ( step ) => flags.canUseCasualSelling || step.id !== 'business', // Skip payment methods screen. - ( step ) => ! flags.shouldSkipPaymentMethods || step.id !== 'methods', + ( step ) => + step.id !== 'methods' || + ( ! flags.shouldSkipPaymentMethods && + ! ( ownBrandOnly && isCasualSeller ) ), ] ); const totalStepsCount = steps.length; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js index 1c8c8fe9e..875eef258 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/index.js @@ -10,6 +10,15 @@ const OnboardingScreen = () => { const Steps = getSteps( flags ); const currentStep = getCurrentStep( step, Steps ); + if ( ! currentStep?.StepComponent ) { + console.error( 'Invalid Onboarding State', { + step, + flags, + Steps, + currentStep, + } ); + } + const handleNext = () => setStep( currentStep.nextStep ); const handlePrev = () => setStep( currentStep.prevStep ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js index ad72cbfe1..a1f18e7e7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js @@ -1,5 +1,6 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import TopNavigation from '../../../ReusableComponents/TopNavigation'; @@ -21,8 +22,17 @@ const SettingsNavigation = ( { setActivePanel = () => {}, } ) => { const { persistAll } = useStoreManager(); - const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); + const [ isSaving, setIsSaving ] = useState( false ); + + const handleSave = () => { + setIsSaving( true ); + speak( + __( 'Saving settings…', 'woocommerce-paypal-payments' ), + 'assertive' + ); + persistAll(); + }; return ( { canSave && ( <> - - + ) } @@ -50,24 +69,26 @@ const SettingsNavigation = ( { export default SettingsNavigation; -const SaveStateMessage = () => { - const [ isSaving, setIsSaving ] = useState( false ); +const SaveStateMessage = ( { setIsSaving, isSaving } ) => { const [ isVisible, setIsVisible ] = useState( false ); const [ isAnimating, setIsAnimating ] = useState( false ); const { onStarted, onFinished } = CommonHooks.useActivityObserver(); const timerRef = useRef( null ); - const handleActivityStart = useCallback( ( started ) => { - if ( started.startsWith( 'persist' ) ) { - setIsSaving( true ); - setIsVisible( false ); - setIsAnimating( false ); + const handleActivityStart = useCallback( + ( started ) => { + if ( started.startsWith( 'persist' ) ) { + setIsSaving( true ); + setIsVisible( false ); + setIsAnimating( false ); - if ( timerRef.current ) { - clearTimeout( timerRef.current ); + if ( timerRef.current ) { + clearTimeout( timerRef.current ); + } } - } - }, [] ); + }, + [ setIsSaving ] + ); const handleActivityDone = useCallback( ( done, remaining ) => { @@ -76,6 +97,14 @@ const SaveStateMessage = () => { setIsVisible( true ); setTimeout( () => setIsAnimating( true ), 50 ); + speak( + __( + 'Settings saved successfully.', + 'woocommerce-paypal-payments' + ), + 'assertive' + ); + timerRef.current = setTimeout( () => { setIsAnimating( false ); setTimeout( @@ -85,7 +114,7 @@ const SaveStateMessage = () => { }, SAVE_CONFIRMATION_DURATION ); } }, - [ isSaving ] + [ isSaving, setIsSaving ] ); useEffect( () => { @@ -102,7 +131,7 @@ const SaveStateMessage = () => { } ); return ( - + { __( 'Completed', 'woocommerce-paypal-payments' ) } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js index c8a20186d..cd5215b0f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js @@ -43,7 +43,10 @@ const Features = () => { 'Features refreshed successfully.', 'woocommerce-paypal-payments' ), - { icon: NOTIFICATION_SUCCESS } + { + icon: NOTIFICATION_SUCCESS, + speak: true, + } ); } else { throw new Error( @@ -58,7 +61,10 @@ const Features = () => { error.message || __( 'Unknown error', 'woocommerce-paypal-payments' ) ), - { icon: NOTIFICATION_ERROR } + { + icon: NOTIFICATION_ERROR, + speak: true, + } ); } finally { setIsRefreshing( false ); @@ -76,6 +82,8 @@ const Features = () => { /> } contentContainer={ false } + aria-live="polite" + aria-busy={ isRefreshing } > { features.map( ( { id, enabled, ...feature } ) => ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js index 6be06ab6b..cc7ce7c7d 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js @@ -33,7 +33,10 @@ const Todos = () => { 'Dismissed items restored successfully.', 'woocommerce-paypal-payments' ), - { icon: NOTIFICATION_SUCCESS } + { + icon: NOTIFICATION_SUCCESS, + speak: true, + } ); } finally { setIsResetting( false ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js index 0d16c707c..ee864ed34 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js @@ -1,3 +1,4 @@ +import { __ } from '@wordpress/i18n'; import Todos from '../Components/Overview/Todos/Todos'; import Features from '../Components/Overview/Features/Features'; import Help from '../Components/Overview/Help/Help'; @@ -14,11 +15,26 @@ const TabOverview = () => { usePaymentGatewaySync(); if ( ! areTodosReady || ! merchantIsReady || ! featuresIsReady ) { - return ; + return ( + + ); } return ( -
+
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index db8dfbbbc..c7efe1d6b 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -162,10 +162,15 @@ export const useNavigationState = () => { }; }; -export const useDetermineProducts = () => { - return useSelect( ( select ) => { - return select( STORE_NAME ).determineProductsAndCaps(); - }, [] ); +export const useDetermineProducts = ( ownBrandOnly ) => { + return useSelect( + ( select ) => { + return select( STORE_NAME ).determineProductsAndCaps( + ownBrandOnly + ); + }, + [ ownBrandOnly ] + ); }; export const useFlags = () => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index 8a137dff2..0ea630072 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -33,10 +33,11 @@ export const flags = ( state ) => { * This selector does not return state-values, but uses the state to derive the products-array * that should be returned. * - * @param {{}} state + * @param {{}} state + * @param {boolean} ownBrandOnly * @return {{products:string[], options:{}}} The ISU products, based on choices made in the onboarding wizard. */ -export const determineProductsAndCaps = ( state ) => { +export const determineProductsAndCaps = ( state, ownBrandOnly ) => { /** * An array of product-names that are used to build an onboarding URL via the * PartnerReferrals API. To avoid confusion with the "products" property from the @@ -58,8 +59,12 @@ export const determineProductsAndCaps = ( state ) => { const { isCasualSeller, areOptionalPaymentMethodsEnabled, products } = persistentData( state ); const { canUseVaulting, canUseCardPayments } = flags( state ); + const isBrandedCasualSeller = isCasualSeller && ownBrandOnly; + const cardPaymentsEligibleAndSelected = - canUseCardPayments && areOptionalPaymentMethodsEnabled; + canUseCardPayments && + areOptionalPaymentMethodsEnabled && + ! isBrandedCasualSeller; if ( ! cardPaymentsEligibleAndSelected ) { /** diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index aa00c3416..d174c26a7 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -31,7 +31,9 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const { onboardingUrl } = isSandbox ? CommonHooks.useSandbox() : CommonHooks.useProduction(); - const { products, options } = OnboardingHooks.useDetermineProducts(); + const { ownBrandOnly } = CommonHooks.useWooSettings(); + const { products, options } = + OnboardingHooks.useDetermineProducts( ownBrandOnly ); const { startActivity } = CommonHooks.useBusyState(); const { authenticateWithOAuth } = CommonHooks.useAuthentication(); const [ onboardingUrlState, setOnboardingUrl ] = useState( '' ); diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 1310c7ee7..57fb110bb 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -51,6 +51,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\PathRepository use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService; use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService; +use WooCommerce\PayPalCommerce\Settings\Service\LoadingScreenService; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService; use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService; @@ -452,15 +453,26 @@ return array( ); }, 'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array { + /** + * Use the REST API filter to collect eligibility flags. + * + * TODO: We should switch to using the new `*.eligibility.check` services, which return a callback instead of a boolean. + * Problem with booleans is, that they are evaluated during DI service creation (plugin_loaded), and some relevant filters are not registered at that point. + * Overthink the capability system, it's difficult to reuse across the plugin. + */ $features = apply_filters( 'woocommerce_paypal_payments_rest_common_merchant_features', array() ); + // TODO: This condition included in the `*.eligibility.check` services; it can be removed when we switch to those services. + $general_settings = $container->get( 'settings.data.general' ); + assert( $general_settings instanceof GeneralSettings ); + return array( - 'apple_pay' => $features['apple_pay']['enabled'] ?? false, - 'google_pay' => $features['google_pay']['enabled'] ?? false, - 'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false, + 'apple_pay' => ( $features['apple_pay']['enabled'] ?? false ) && ! $general_settings->own_brand_only(), + 'google_pay' => ( $features['google_pay']['enabled'] ?? false ) && ! $general_settings->own_brand_only(), + 'acdc' => ( $features['advanced_credit_and_debit_cards']['enabled'] ?? false ) && ! $general_settings->own_brand_only(), 'save_paypal' => $features['save_paypal_and_venmo']['enabled'] ?? false, 'apm' => $features['alternative_payment_methods']['enabled'] ?? false, 'paylater' => $features['pay_later_messaging']['enabled'] ?? false, @@ -474,6 +486,8 @@ return array( $button_locations = $container->get( 'settings.service.button_locations' ); $gateways = $container->get( 'settings.service.gateways_status' ); + + // TODO: This "merchant_capabilities" service is only used here. Could it be merged to make the code cleaner and less segmented? $capabilities = $container->get( 'settings.service.merchant_capabilities' ); /** @@ -514,7 +528,7 @@ return array( ! $button_locations['cart_enabled'], // Add PayPal buttons to cart. ! $button_locations['block_checkout_enabled'], // Add PayPal buttons to block checkout. ! $button_locations['product_enabled'], // Add PayPal buttons to product. - $capabilities['apple_pay'], // Register Domain for Apple Pay. + $container->get( 'applepay.eligible' ) && $capabilities['apple_pay'], // Register Domain for Apple Pay. $capabilities['acdc'] && ! ( $capabilities['apple_pay'] && $capabilities['google_pay'] ), // Add digital wallets to your account. $container->get( 'applepay.eligible' ) && $capabilities['acdc'] && ! $capabilities['apple_pay'], // Add Apple Pay to your account. $container->get( 'googlepay.eligible' ) && $capabilities['acdc'] && ! $capabilities['google_pay'], // Add Google Pay to your account. @@ -591,6 +605,9 @@ return array( 'settings.service.gateway-redirect' => static function (): GatewayRedirectService { return new GatewayRedirectService(); }, + 'settings.services.loading-screen-service' => static function ( ContainerInterface $container ) : LoadingScreenService { + return new LoadingScreenService(); + }, /** * Returns a list of all payment gateway IDs created by this plugin. * diff --git a/modules/ppcp-settings/src/Data/GeneralSettings.php b/modules/ppcp-settings/src/Data/GeneralSettings.php index 7f3031ca0..8a9ec78ab 100644 --- a/modules/ppcp-settings/src/Data/GeneralSettings.php +++ b/modules/ppcp-settings/src/Data/GeneralSettings.php @@ -40,6 +40,13 @@ class GeneralSettings extends AbstractDataModel { */ protected array $woo_settings = array(); + /** + * Contexts in which the installation path can be reset. + */ + private const ALLOWED_RESET_REASONS = array( + 'plugin_uninstall', + ); + /** * Constructor. * @@ -82,7 +89,7 @@ class GeneralSettings extends AbstractDataModel { 'seller_type' => 'unknown', // Branded experience installation path. - 'installation_path' => '', + 'wc_installation_path' => '', ); } @@ -279,7 +286,7 @@ class GeneralSettings extends AbstractDataModel { */ public function set_installation_path( string $installation_path ) : void { // The installation path can be set only once. - if ( InstallationPathEnum::is_valid( $this->data['installation_path'] ?? '' ) ) { + if ( InstallationPathEnum::is_valid( $this->data['wc_installation_path'] ?? '' ) ) { return; } @@ -288,7 +295,7 @@ class GeneralSettings extends AbstractDataModel { return; } - $this->data['installation_path'] = $installation_path; + $this->data['wc_installation_path'] = $installation_path; } /** @@ -297,7 +304,23 @@ class GeneralSettings extends AbstractDataModel { * @return string */ public function get_installation_path() : string { - return $this->data['installation_path'] ?? InstallationPathEnum::DIRECT; + return $this->data['wc_installation_path'] ?? InstallationPathEnum::DIRECT; + } + + /** + * Resets the installation path to empty string. This method should only be called + * during specific circumstances like plugin uninstallation. + * + * @param string $reason The reason for resetting the path, must be an allowed value. + * @return bool Whether the reset was successful. + */ + public function reset_installation_path( string $reason ) : bool { + if ( ! in_array( $reason, self::ALLOWED_RESET_REASONS, true ) ) { + return false; + } + + $this->data['wc_installation_path'] = ''; + return true; } /** diff --git a/modules/ppcp-settings/src/Data/StylingSettings.php b/modules/ppcp-settings/src/Data/StylingSettings.php index 32fc3de2f..85f362f53 100644 --- a/modules/ppcp-settings/src/Data/StylingSettings.php +++ b/modules/ppcp-settings/src/Data/StylingSettings.php @@ -56,7 +56,7 @@ class StylingSettings extends AbstractDataModel { 'cart' => new LocationStylingDTO( 'cart' ), 'classic_checkout' => new LocationStylingDTO( 'classic_checkout' ), 'express_checkout' => new LocationStylingDTO( 'express_checkout' ), - 'mini_cart' => new LocationStylingDTO( 'mini_cart' ), + 'mini_cart' => new LocationStylingDTO( 'mini_cart', false ), 'product' => new LocationStylingDTO( 'product' ), ); } diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php index 8a462e5f1..a1ed6dd29 100644 --- a/modules/ppcp-settings/src/Handler/ConnectionListener.php +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -67,9 +67,18 @@ class ConnectionListener { /** * ID of the current user, set by the process() method. * + * Default value is 0 (guest), until the real ID is provided to process(). + * * @var int */ - private int $user_id; + private int $user_id = 0; + + /** + * The request details (usually the GET data) which were provided. + * + * @var array + */ + private array $request_data = array(); /** * Prepare the instance. @@ -92,9 +101,6 @@ class ConnectionListener { $this->authentication_manager = $authentication_manager; $this->redirector = $redirector; $this->logger = $logger ?: new NullLogger(); - - // Initialize as "guest", the real ID is provided via process(). - $this->user_id = 0; } /** @@ -106,18 +112,43 @@ class ConnectionListener { * @throws RuntimeException If the merchant ID does not match the ID previously set via OAuth. */ public function process( int $user_id, array $request ) : void { - $this->user_id = $user_id; + $this->user_id = $user_id; + $this->request_data = $request; - if ( ! $this->is_valid_request( $request ) ) { + if ( ! $this->is_valid_request() ) { + return; + } + + $token = $this->get_token_from_request(); + + $this->process_oauth_token( $token ); + + $this->redirect_after_authentication(); + } + + /** + * Processes the OAuth token from the request. + * + * @param string $token The OAuth token extracted from the request. + * @return void + */ + private function process_oauth_token( string $token ) : void { + // The request contains OAuth details: To avoid abuse we'll slow down the processing. + sleep( 2 ); + + if ( ! $token ) { + return; + } + + if ( $this->was_token_processed( $token ) ) { return; } - $token = $this->get_token_from_request( $request ); if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) { return; } - $data = $this->extract_data( $request ); + $data = $this->extract_data(); if ( ! $data ) { return; } @@ -126,22 +157,19 @@ class ConnectionListener { try { $this->authentication_manager->finish_oauth_authentication( $data ); + $this->mark_token_as_processed( $token ); } catch ( \Exception $e ) { $this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() ); } - - $this->redirect_after_authentication(); } /** * Determine, if the request details contain connection data that should be * extracted and stored. * - * @param array $request Request details to verify. - * * @return bool True, if the request contains valid connection details. */ - private function is_valid_request( array $request ) : bool { + private function is_valid_request() : bool { if ( $this->user_id < 1 || ! $this->settings_page_id ) { return false; } @@ -157,7 +185,7 @@ class ConnectionListener { ); foreach ( $required_params as $param ) { - if ( empty( $request[ $param ] ) ) { + if ( empty( $this->request_data[ $param ] ) ) { return false; } } @@ -166,19 +194,43 @@ class ConnectionListener { } /** - * Extract the merchant details (ID & email) from the request details. + * Checks if the provided authentication token is new or has been used before. * - * @param array $request The full request details. + * This check catches an issue where we receive the same authentication token twice, + * which does not impact the login flow but creates noise in the logs. + * + * @param string $token The authentication token to check. + * @return bool True if the token was already processed. + */ + private function was_token_processed( string $token ) : bool { + $prev_token = get_transient( 'ppcp_previous_auth_token' ); + + return $prev_token && $prev_token === $token; + } + + /** + * Stores the processed authentication token so we can prevent double-processing + * of already verified token. + * + * @param string $token The processed authentication token. + * @return void + */ + private function mark_token_as_processed( string $token ) : void { + set_transient( 'ppcp_previous_auth_token', $token, 60 ); + } + + /** + * Extract the merchant details (ID & email) from the request details. * * @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys, * or an empty array on failure. */ - private function extract_data( array $request ) : array { + private function extract_data() : array { $this->logger->info( 'Extracting connection data from request...' ); - $merchant_id = $this->get_merchant_id_from_request( $request ); - $merchant_email = $this->get_merchant_email_from_request( $request ); - $seller_type = $this->get_seller_type_from_request( $request ); + $merchant_id = $this->get_merchant_id_from_request( $this->request_data ); + $merchant_email = $this->get_merchant_email_from_request( $this->request_data ); + $seller_type = $this->get_seller_type_from_request( $this->request_data ); if ( ! $merchant_id || ! $merchant_email ) { return array(); @@ -200,17 +252,16 @@ class ConnectionListener { $redirect_url = $this->get_onboarding_redirect_url(); $this->redirector->redirect( $redirect_url ); + exit; } /** * Returns the sanitized connection token from the incoming request. * - * @param array $request Full request details. - * * @return string The sanitized token, or an empty string. */ - private function get_token_from_request( array $request ) : string { - return $this->sanitize_string( $request['ppcpToken'] ?? '' ); + private function get_token_from_request() : string { + return $this->sanitize_string( $this->request_data['ppcpToken'] ?? '' ); } /** diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 67baaf9b3..3f6cd4909 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -198,8 +198,6 @@ class AuthenticationManager { * @throws RuntimeException When failed to retrieve payee. */ public function authenticate_via_direct_api( bool $use_sandbox, string $client_id, string $client_secret ) : void { - $this->disconnect(); - $this->logger->info( 'Attempting manual connection to PayPal...', array( @@ -261,8 +259,6 @@ class AuthenticationManager { * @throws RuntimeException When failed to retrieve payee. */ public function authenticate_via_oauth( bool $use_sandbox, string $shared_id, string $auth_code ) : void { - $this->disconnect(); - $this->logger->info( 'Attempting OAuth login to PayPal...', array( diff --git a/modules/ppcp-settings/src/Service/GatewayRedirectService.php b/modules/ppcp-settings/src/Service/GatewayRedirectService.php index 7b043d04f..bae38be7c 100644 --- a/modules/ppcp-settings/src/Service/GatewayRedirectService.php +++ b/modules/ppcp-settings/src/Service/GatewayRedirectService.php @@ -85,14 +85,9 @@ class GatewayRedirectService { // Get current URL parameters. // phpcs:disable WordPress.Security.NonceVerification.Recommended - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash - // The sanitize_get_param method handles unslashing and sanitization internally. - $page = isset( $_GET['page'] ) ? $this->sanitize_get_param( $_GET['page'] ) : ''; - $tab = isset( $_GET['tab'] ) ? $this->sanitize_get_param( $_GET['tab'] ) : ''; - $section = isset( $_GET['section'] ) ? $this->sanitize_get_param( $_GET['section'] ) : ''; - // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash - // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $page = isset( $_GET['page'] ) ? wc_clean( wp_unslash( $_GET['page'] ) ) : ''; + $tab = isset( $_GET['tab'] ) ? wc_clean( wp_unslash( $_GET['tab'] ) ) : ''; + $section = isset( $_GET['section'] ) ? wc_clean( wp_unslash( $_GET['section'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended // Check if we're on a WooCommerce settings page and checkout tab. @@ -113,17 +108,4 @@ class GatewayRedirectService { exit; } } - - /** - * Sanitizes a GET parameter that could be string or array. - * - * @param mixed $param The parameter to sanitize. - * @return string The sanitized parameter. - */ - private function sanitize_get_param( $param ): string { - if ( is_array( $param ) ) { - return ''; - } - return sanitize_text_field( wp_unslash( $param ) ); - } } diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php index baa57a80b..61c7a2e02 100644 --- a/modules/ppcp-settings/src/Service/InternalRestService.php +++ b/modules/ppcp-settings/src/Service/InternalRestService.php @@ -56,7 +56,7 @@ class InternalRestService { $rest_nonce = wp_create_nonce( 'wp_rest' ); $auth_cookies = $this->build_authentication_cookie(); - $this->logger->info( "Calling internal REST endpoint: $rest_url" ); + $this->logger->info( "Calling internal REST endpoint [$rest_url]" ); $response = wp_remote_request( $rest_url, @@ -69,6 +69,7 @@ class InternalRestService { 'cookies' => $auth_cookies, ) ); + $this->logger->debug( "Finished internal REST call [$rest_url]" ); if ( is_wp_error( $response ) ) { // Error: The wp_remote_request() call failed (timeout or similar). diff --git a/modules/ppcp-settings/src/Service/LoadingScreenService.php b/modules/ppcp-settings/src/Service/LoadingScreenService.php new file mode 100644 index 000000000..dbda474e8 --- /dev/null +++ b/modules/ppcp-settings/src/Service/LoadingScreenService.php @@ -0,0 +1,73 @@ +is_ppcp_settings_page() ) { + return; + } + + ?> + + methods_definition->group_apms(); $all_methods = array_merge( $methods_paypal, $methods_cards, $methods_apm ); + // Enable the Fastlane watermark by default. + $this->payment_methods->set_fastlane_display_watermark( true ); + foreach ( $all_methods as $method ) { $this->payment_methods->toggle_method_state( $method['id'], false ); } @@ -330,7 +333,7 @@ class SettingsDataManager { 'cart' => new LocationStylingDTO( 'cart', true, $methods_full ), 'classic_checkout' => new LocationStylingDTO( 'classic_checkout', true, $methods_full ), 'express_checkout' => new LocationStylingDTO( 'express_checkout', true, $methods_full ), - 'mini_cart' => new LocationStylingDTO( 'mini_cart', true, $methods_full ), + 'mini_cart' => new LocationStylingDTO( 'mini_cart', false, $methods_full ), 'product' => new LocationStylingDTO( 'product', true, $methods_own ), ); diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index e0170cedc..3aec6725a 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -10,6 +10,7 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; use WC_Payment_Gateway; +use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution; use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; @@ -25,11 +26,13 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Data\TodosModel; use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\PathRepository; use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService; +use WooCommerce\PayPalCommerce\Settings\Service\LoadingScreenService; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; @@ -123,7 +126,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { ) ); - wp_enqueue_script( 'ppcp-switch-settings-ui', '', array( 'wp-i18n' ), $script_asset_file['version'] ); + wp_enqueue_script( 'ppcp-switch-settings-ui', '', array( 'wp-i18n' ), $script_asset_file['version'], false ); wp_set_script_translations( 'ppcp-switch-settings-ui', 'woocommerce-paypal-payments', @@ -174,6 +177,11 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); + // Suppress WooCommerce Settings UI elements via CSS to improve the loading experience. + $loading_screen_service = $container->get( 'settings.services.loading-screen-service' ); + assert( $loading_screen_service instanceof LoadingScreenService ); + $loading_screen_service->register(); + $this->apply_branded_only_limitations( $container ); add_action( @@ -205,7 +213,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { true ); - wp_enqueue_script( 'ppcp-admin-settings', '', array( 'wp-i18n' ), $script_asset_file['version'] ); + wp_enqueue_script( 'ppcp-admin-settings', '', array( 'wp-i18n' ), $script_asset_file['version'], false ); wp_set_script_translations( 'ppcp-admin-settings', 'woocommerce-paypal-payments', @@ -281,10 +289,12 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_action( 'woocommerce_paypal_payments_gateway_admin_options_wrapper', - function () : void { + function () use ( $container ) : void { global $hide_save_button; $hide_save_button = true; + $this->initialize_branded_only( $container ); + $this->render_header(); $this->render_content(); } @@ -329,6 +339,10 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_action( 'woocommerce_paypal_payments_merchant_disconnected', static function () use ( $container ) : void { + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + $logger->info( 'Merchant disconnected, reset onboarding' ); + // Reset onboarding profile. $onboarding_profile = $container->get( 'settings.data.onboarding' ); assert( $onboarding_profile instanceof OnboardingProfile ); @@ -350,6 +364,10 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_action( 'woocommerce_paypal_payments_authenticated_merchant', static function () use ( $container ) : void { + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + $logger->info( 'Merchant connected, complete onboarding and set defaults.' ); + $onboarding_profile = $container->get( 'settings.data.onboarding' ); assert( $onboarding_profile instanceof OnboardingProfile ); @@ -403,18 +421,18 @@ class SettingsModule implements ServiceModule, ExecutableModule { unset( $payment_methods[ CardButtonGateway::ID ] ); } else { // For non-ACDC regions unset ACDC, local APMs and set BCDC. - unset( $payment_methods[ CreditCardGateway::ID ] ); - unset( $payment_methods['pay-later'] ); - unset( $payment_methods[ BancontactGateway::ID ] ); - unset( $payment_methods[ BlikGateway::ID ] ); - unset( $payment_methods[ EPSGateway::ID ] ); - unset( $payment_methods[ IDealGateway::ID ] ); - unset( $payment_methods[ MyBankGateway::ID ] ); - unset( $payment_methods[ P24Gateway::ID ] ); - unset( $payment_methods[ TrustlyGateway::ID ] ); - unset( $payment_methods[ MultibancoGateway::ID ] ); - unset( $payment_methods[ PayUponInvoiceGateway::ID ] ); - unset( $payment_methods[ OXXO::ID ] ); + unset( $payment_methods[ CreditCardGateway::ID ] ); + unset( $payment_methods['pay-later'] ); + unset( $payment_methods[ BancontactGateway::ID ] ); + unset( $payment_methods[ BlikGateway::ID ] ); + unset( $payment_methods[ EPSGateway::ID ] ); + unset( $payment_methods[ IDealGateway::ID ] ); + unset( $payment_methods[ MyBankGateway::ID ] ); + unset( $payment_methods[ P24Gateway::ID ] ); + unset( $payment_methods[ TrustlyGateway::ID ] ); + unset( $payment_methods[ MultibancoGateway::ID ] ); + unset( $payment_methods[ PayUponInvoiceGateway::ID ] ); + unset( $payment_methods[ OXXO::ID ] ); } // Unset Venmo when store location is not United States. @@ -481,34 +499,42 @@ class SettingsModule implements ServiceModule, ExecutableModule { $methods[] = $applepay_gateway; $methods[] = $axo_gateway; - $is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' ); - $all_gateway_ids = $container->get( 'settings.config.all-gateway-ids' ); - - if ( $is_payments_page ) { - $methods = array_filter( - $methods, - function ( $method ) use ( $all_gateway_ids ): bool { - if ( ! is_object( $method ) - || $method->id === PayPalGateway::ID - || ! in_array( $method->id, $all_gateway_ids, true ) - ) { - return true; - } - - if ( ! $this->is_gateway_enabled( $method->id ) ) { - return false; - } - - return true; - } - ); - } - return $methods; }, 99 ); + /** + * Filters the available payment gateways in the WooCommerce admin settings. + * + * Ensures that only enabled PayPal payment gateways are displayed. + * + * @hook woocommerce_admin_field_payment_gateways + * @priority 5 Allows modifying the registered gateways before they are displayed. + */ + add_action( + 'woocommerce_admin_field_payment_gateways', + function () use ( $container ) : void { + $all_gateway_ids = $container->get( 'settings.config.all-gateway-ids' ); + $payment_gateways = WC()->payment_gateways->payment_gateways; + + foreach ( $payment_gateways as $index => $payment_gateway ) { + $payment_gateway_id = $payment_gateway->id; + + if ( + ! in_array( $payment_gateway_id, $all_gateway_ids, true ) + || $payment_gateway_id === PayPalGateway::ID + || $this->is_gateway_enabled( $payment_gateway_id ) + ) { + continue; + } + + unset( WC()->payment_gateways->payment_gateways[ $index ] ); + } + }, + 5 + ); + // Remove the Fastlane gateway if the customer is logged in, ensuring that we don't interfere with the Fastlane gateway status in the settings UI. add_filter( 'woocommerce_available_payment_gateways', @@ -611,7 +637,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { // Enable Fastlane after onboarding if the store is compatible. add_action( 'woocommerce_paypal_payments_toggle_payment_gateways', - function( PaymentSettings $payment_methods, ConfigurationFlagsDTO $flags ) use ( $container ) { + function ( PaymentSettings $payment_methods, ConfigurationFlagsDTO $flags ) use ( $container ) { if ( $flags->is_business_seller && $flags->use_card_payments ) { $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' ); assert( $compatibility_checker instanceof CompatibilityChecker ); @@ -628,7 +654,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { // Toggle payment gateways after onboarding based on flags. add_action( 'woocommerce_paypal_payments_sync_gateways', - static function() use ( $container ) { + static function () use ( $container ) { $settings_data_manager = $container->get( 'settings.service.data-manager' ); assert( $settings_data_manager instanceof SettingsDataManager ); $settings_data_manager->sync_gateway_settings(); @@ -640,6 +666,17 @@ class SettingsModule implements ServiceModule, ExecutableModule { assert( $gateway_redirect_service instanceof GatewayRedirectService ); $gateway_redirect_service->register(); + // Do not render Pay Later messaging if the "Save PayPal and Venmo" setting is enabled. + add_filter( + 'woocommerce_paypal_payments_should_render_pay_later_messaging', + static function() use ( $container ): bool { + $settings_model = $container->get( 'settings.data.settings' ); + assert( $settings_model instanceof SettingsModel ); + + return ! $settings_model->get_save_paypal_and_venmo(); + } + ); + return true; } @@ -667,6 +704,36 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_filter( 'woocommerce_paypal_payments_is_eligible_for_card_fields', '__return_false' ); } + /** + * Initializes the branded-only flags if they are not set. + * + * This method can be called multiple times: + * The flags are only initialized once but does not change afterward. + * + * Also, this check has no impact on performance for two reasons: + * 1. The GeneralSettings class is already initialized and will short-circuit + * the check if the settings are already initialized. + * 2. The settings UI is a React app, this method only runs when the React app + * is injected to the DOM, and not while the UI is used. + * + * @param ContainerInterface $container The DI container provider. + * @return void + */ + protected function initialize_branded_only( ContainerInterface $container ) : void { + $path_repository = $container->get( 'settings.service.branded-experience.path-repository' ); + assert( $path_repository instanceof PathRepository ); + + $partner_attribution = $container->get( 'api.helper.partner-attribution' ); + assert( $partner_attribution instanceof PartnerAttribution ); + + $general_settings = $container->get( 'settings.data.general' ); + assert( $general_settings instanceof GeneralSettings ); + + $path_repository->persist(); + + $partner_attribution->initialize_bn_code( $general_settings->get_installation_path() ); + } + /** * Outputs the settings page header (title and back-link). * @@ -693,7 +760,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { * @param string $gateway_name The gateway name. * @return bool True if the payment gateway with the given name is enabled, otherwise false. */ - protected function is_gateway_enabled( string $gateway_name ): bool { + protected function is_gateway_enabled( string $gateway_name ) : bool { $gateway_settings = get_option( "woocommerce_{$gateway_name}_settings", array() ); $gateway_enabled = $gateway_settings['enabled'] ?? false; diff --git a/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php b/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php index 7ec997d55..80f26c597 100644 --- a/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php +++ b/modules/ppcp-wc-subscriptions/src/Helper/SubscriptionHelper.php @@ -49,7 +49,11 @@ class SubscriptionHelper { return false; } $cart = WC()->cart; - if ( ! $cart || $cart->is_empty() ) { + /** + * Don't use `$cart->is_empty()` for checking for an empty cart. + * This is maybe called so early that it can corrupt it because it loads it than from session + */ + if ( ! $cart || empty( $cart->cart_contents ) ) { return false; } diff --git a/modules/woocommerce-logging/src/Logger/WooCommerceLogger.php b/modules/woocommerce-logging/src/Logger/WooCommerceLogger.php index bee7e7774..11619c46b 100644 --- a/modules/woocommerce-logging/src/Logger/WooCommerceLogger.php +++ b/modules/woocommerce-logging/src/Logger/WooCommerceLogger.php @@ -8,7 +8,7 @@ * @package WooCommerce\WooCommerce\Logging\Logger */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\WooCommerce\Logging\Logger; @@ -20,7 +20,6 @@ use Psr\Log\LoggerTrait; */ class WooCommerceLogger implements LoggerInterface { - use LoggerTrait; /** @@ -35,23 +34,48 @@ class WooCommerceLogger implements LoggerInterface { * * @var string The source. */ - private $source; + private string $source; + + /** + * Details that are output before the first real log message, to help + * identify the request. + * + * @var string + */ + private string $request_info; + + /** + * A random prefix which is visible in every log message, to better + * understand which messages belong to the same request. + * + * @var string + */ + private string $prefix; /** * WooCommerceLogger constructor. * * @param \WC_Logger_Interface $wc_logger The WooCommerce logger. - * @param string $source The source. + * @param string $source The source. */ public function __construct( \WC_Logger_Interface $wc_logger, string $source ) { $this->wc_logger = $wc_logger; $this->source = $source; + $this->prefix = sprintf( '#%s - ', wp_rand( 1000, 9999 ) ); + + // phpcs:disable -- Intentionally not sanitized, for logging purposes. + $method = wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'CLI' ); + $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '-' ); + // phpcs:enable + + $request_path = wp_parse_url( $request_uri, PHP_URL_PATH ); + $this->request_info = "$method $request_path"; } /** * Logs a message. * - * @param mixed $level The logging level. + * @param mixed $level The logging level. * @param string $message The message. * @param array $context The context. */ @@ -59,6 +83,16 @@ class WooCommerceLogger implements LoggerInterface { if ( ! isset( $context['source'] ) ) { $context['source'] = $this->source; } - $this->wc_logger->log( $level, $message, $context ); + + if ( $this->request_info ) { + $this->wc_logger->log( + 'debug', + "{$this->prefix}[New Request] $this->request_info", + array( 'source' => $context['source'] ) + ); + $this->request_info = ''; + } + + $this->wc_logger->log( $level, "{$this->prefix}$message", $context ); } } diff --git a/package.json b/package.json index b48936765..a672f7980 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "3.0.1", + "version": "3.0.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 d93d9eebb..f78b532eb 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: paypal, woocommerce, automattic, syde Tags: woocommerce, paypal, payments, ecommerce, credit card Requires at least: 6.5 -Tested up to: 6.7 +Tested up to: 6.8 Requires PHP: 7.4 -Stable tag: 3.0.1 +Stable tag: 3.0.4 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -156,6 +156,28 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == += 3.0.4 - xxxx-xx-xx = +* Fix - Onboarding screen blank when WooPayments plugin is active #3312 + += 3.0.3 - 2025-04-08 = +* Fix - BN code was set before the installation path was initialized #3309 +* Fix - Things to do next referenced Apple Pay while in branded-only mode #3308 +* Fix - Disabled payment methods were not hidden in reactified WooCommerce Payments settings tab #3290 + += 3.0.2 - 2025-04-03 = +* Enhancement - Check the branded-only flag when settings-UI is loaded the first time #3278 +* Enhancement - Implement a Cache-Flush API #3276 +* Enhancement - Disable the mini-cart location by default #3284 +* Enhancement - Remove branded-only flag when uninstalling PayPal Payments #3295 +* Fix - Welcome screen lists "all major credit/debit cards, Apple Pay, Google Pay," in branded-only mode #3281 +* Fix - Correct heading in onboarding step 4 in branded-only mode #3282 +* Fix - Hide the payment methods screen for personal user in branded-only mode #3286 +* Fix - Enabling Save PayPal does not disable Pay Later messaging #3288 +* Fix - Settings UI: Fix Feature button links #3285 +* Fix - Create mapping for the 3d_secure_contingency setting #3262 +* Fix - Enable Fastlane Watermark by default in new settings UI #3296 +* Fix - Payment method screen is referencing credit cards, digital wallets in branded-only mode #3297 + = 3.0.1 - 2025-03-26 = * Enhancement - Include Fastlane meta on homepage #3151 * Enhancement - Include Branded-only plugin configuration for certain installation paths diff --git a/uninstall.php b/uninstall.php index 7ae2763b4..1edb13027 100644 --- a/uninstall.php +++ b/uninstall.php @@ -5,28 +5,30 @@ * @package WooCommerce\PayPalCommerce */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce; use WooCommerce\PayPalCommerce\Uninstall\ClearDatabaseInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Vendor\Psr\Container\NotFoundExceptionInterface; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; +use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { die( 'Direct access not allowed.' ); } -$root_dir = __DIR__; +$root_dir = __DIR__; $main_plugin_file = "{$root_dir}/woocommerce-paypal-payments.php"; -if ( !file_exists( $main_plugin_file ) ) { - return; +if ( ! file_exists( $main_plugin_file ) ) { + return; } require $main_plugin_file; -( static function (string $root_dir): void { +( static function ( string $root_dir ) : void { $autoload_filepath = "{$root_dir}/vendor/autoload.php"; if ( file_exists( $autoload_filepath ) && ! class_exists( '\WooCommerce\PayPalCommerce\PluginModule' ) ) { @@ -39,9 +41,12 @@ require $main_plugin_file; $app_container = $bootstrap( $root_dir ); assert( $app_container instanceof ContainerInterface ); + clear_plugin_branding( $app_container ); + $settings = $app_container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); + // TODO: This is a flag only present in the #legacy-ui. Should we change this to a filter, or remove the DB reset code? $should_clear_db = $settings->has( 'uninstall_clear_db_on_uninstall' ) && $settings->get( 'uninstall_clear_db_on_uninstall' ); if ( ! $should_clear_db ) { return; @@ -74,4 +79,33 @@ require $main_plugin_file; } ); } -} )($root_dir); +} )( $root_dir ); + +/** + * Clears plugin branding by resetting the installation path flag. + * + * @param ContainerInterface $container The plugin's DI container. + * @return void + */ +function clear_plugin_branding( ContainerInterface $container ) : void { + /* + * This flag is set by WooCommerce when the plugin is installed via their + * Settings page. We remove it here, as uninstalling the plugin should + * open up the possibility of installing it from a different source in + * "white label" mode. + */ + delete_option( 'woocommerce_paypal_branded' ); + + try { + $general_settings = $container->get( 'settings.data.general' ); + assert( $general_settings instanceof GeneralSettings ); + + if ( $general_settings->reset_installation_path( 'plugin_uninstall' ) ) { + $general_settings->save(); + } + } catch ( NotFoundExceptionInterface $e ) { + // The container does not exist or did not return a GeneralSettings instance. + // In any case: A failure can be ignored, as it means we cannot reset anything. + return; + } +} diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index c2d3aa61b..c01b48bf9 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,7 +3,7 @@ * 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: 3.0.1 + * Version: 3.0.4 * Author: PayPal * Author URI: https://paypal.com/ * License: GPL-2.0 @@ -27,7 +27,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); define( 'PAYPAL_URL', 'https://www.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2025-03-25' ); +define( 'PAYPAL_INTEGRATION_DATE', '2025-04-23' ); define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );