mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +08:00
Merge branch 'trunk'
# Conflicts: # modules/ppcp-button/src/Helper/ThreeDSecure.php
This commit is contained in:
commit
ebcf91461f
54 changed files with 1197 additions and 448 deletions
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,7 +59,7 @@ class Cache {
|
|||
*
|
||||
* @param string $key The key.
|
||||
*/
|
||||
public function delete( string $key ): void {
|
||||
public function delete( string $key ) : void {
|
||||
delete_transient( $this->prefix . $key );
|
||||
}
|
||||
|
||||
|
@ -71,7 +72,34 @@ class Cache {
|
|||
*
|
||||
* @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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
@ -29,6 +40,7 @@ class PaymentMethodSettingsMapHelper {
|
|||
return array(
|
||||
'dcc_enabled' => CreditCardGateway::ID,
|
||||
'axo_enabled' => AxoGateway::ID,
|
||||
'3d_secure_contingency' => 'three_d_secure',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -36,10 +48,21 @@ class PaymentMethodSettingsMapHelper {
|
|||
* Retrieves the value of a mapped key from the new 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
default:
|
||||
$payment_method = $this->map()[ $old_key ] ?? false;
|
||||
|
||||
if ( ! $payment_method ) {
|
||||
|
@ -48,6 +71,7 @@ class PaymentMethodSettingsMapHelper {
|
|||
|
||||
return $this->is_gateway_enabled( $payment_method );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the payment gateway with the given name is enabled.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
document.addEventListener( 'DOMContentLoaded', () => {
|
||||
|
||||
const disableFields = ( productId ) => {
|
||||
const variations = document.querySelector( '.woocommerce_variations' );
|
||||
if ( variations ) {
|
||||
|
@ -70,80 +68,145 @@ document.addEventListener( 'DOMContentLoaded', () => {
|
|||
soldIndividually.setAttribute( 'disabled', 'disabled' );
|
||||
};
|
||||
|
||||
const checkSubscriptionPeriodsInterval = (period, period_interval, price, linkBtn) => {
|
||||
const checkSubscriptionPeriodsInterval = (
|
||||
period,
|
||||
period_interval,
|
||||
price,
|
||||
linkBtn
|
||||
) => {
|
||||
if ( ! linkBtn ) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 )
|
||||
! 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' ) );
|
||||
if ( ! price || parseInt( price ) <= 0 ) {
|
||||
linkBtn.setAttribute(
|
||||
'title',
|
||||
PayPalCommerceGatewayPayPalSubscriptionProducts.i18n
|
||||
.prices_must_be_above_zero
|
||||
);
|
||||
} else {
|
||||
linkBtn.setAttribute('title', __( 'Not allowed period interval combination for PayPal Subscriptions!', 'woocommerce-paypal-subscriptions' ) );
|
||||
linkBtn.setAttribute(
|
||||
'title',
|
||||
PayPalCommerceGatewayPayPalSubscriptionProducts.i18n
|
||||
.not_allowed_period_interval
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
linkBtn.disabled = false;
|
||||
linkBtn.removeAttribute('title');
|
||||
}
|
||||
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;
|
||||
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;
|
||||
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"]');
|
||||
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;
|
||||
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;
|
||||
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' );
|
||||
|
||||
let variationProductIds = [ PayPalCommerceGatewayPayPalSubscriptionProducts.product_id ];
|
||||
const variationsInput = document.querySelectorAll( '.variable_post_id' );
|
||||
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 ) => {
|
||||
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 ) => {
|
||||
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 ( event.target.checked === true ) {
|
||||
if ( unlinkBtnP ) {
|
||||
unlinkBtnP.style.display = 'none';
|
||||
}
|
||||
|
@ -158,7 +221,8 @@ document.addEventListener( 'DOMContentLoaded', () => {
|
|||
titleP.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
} );
|
||||
}
|
||||
|
||||
const unlinkBtn = document.getElementById(
|
||||
`ppcp-unlink-sub-plan-${ productId }`
|
||||
|
@ -171,18 +235,23 @@ document.addEventListener( 'DOMContentLoaded', () => {
|
|||
);
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
fetch( PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.endpoint, {
|
||||
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();
|
||||
} )
|
||||
|
@ -197,20 +266,26 @@ document.addEventListener( 'DOMContentLoaded', () => {
|
|||
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 );
|
||||
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
|
||||
'ppcp_enable_subscription_product-' +
|
||||
data.data.product_id
|
||||
);
|
||||
enable_subscription_product.disabled = true;
|
||||
|
||||
const planUnlinked =
|
||||
document.getElementById( 'pcpp-plan-unlinked-' + data.data.product_id );
|
||||
const planUnlinked = document.getElementById(
|
||||
'pcpp-plan-unlinked-' + data.data.product_id
|
||||
);
|
||||
planUnlinked.style.display = 'block';
|
||||
|
||||
setTimeout( () => {
|
||||
|
@ -218,8 +293,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
|
|||
}, 1000 );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
setupProducts();
|
||||
|
|
|
@ -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' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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%);
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
|
||||
&__dismiss {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: 2px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: transparent;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
<div className={ wrapperClasses } { ...( id && { id } ) }>
|
||||
<div
|
||||
className={ classNames( 'ppcp-r-accordion', className, {
|
||||
'ppcp--is-open': isOpen,
|
||||
} ) }
|
||||
id={ id || undefined }
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="ppcp--toggler"
|
||||
onClick={ toggleOpen }
|
||||
aria-expanded={ isOpen }
|
||||
aria-controls={ contentId }
|
||||
>
|
||||
<Header>
|
||||
<TitleWrapper>
|
||||
<Title noCaps={ noCaps }>{ title }</Title>
|
||||
<Action>
|
||||
<Icon icon={ icon } />
|
||||
<Icon icon={ isOpen ? chevronUp : chevronDown } />
|
||||
</Action>
|
||||
</TitleWrapper>
|
||||
{ description && (
|
||||
<Description>{ description }</Description>
|
||||
) }
|
||||
</Header>
|
||||
</button>
|
||||
<div className={ contentClass }>
|
||||
<div
|
||||
className={ classNames( 'ppcp--accordion-content', {
|
||||
'ppcp--is-open': isOpen,
|
||||
} ) }
|
||||
id={ contentId }
|
||||
aria-hidden={ ! isOpen }
|
||||
inert={ isOpen ? undefined : '' }
|
||||
>
|
||||
<Content asCard={ false }>{ children }</Content>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<span className="ppcp--item-notes">
|
||||
{ notes.map( ( note, index ) => (
|
||||
<span key={ index }>{ note }</span>
|
||||
) ) }
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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 <Button { ...buttonProps }>{ text }</Button>;
|
||||
};
|
||||
|
||||
const renderDescription = () => {
|
||||
return (
|
||||
<span
|
||||
className="ppcp-r-feature-item__description ppcp-r-settings-block__feature__description"
|
||||
dangerouslySetInnerHTML={ { __html: description } }
|
||||
/>
|
||||
);
|
||||
return url;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -60,38 +42,52 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
<Header>
|
||||
<Title>
|
||||
{ title }
|
||||
{ props.actionProps?.enabled && (
|
||||
<TitleBadge { ...props.actionProps?.badge } />
|
||||
{ actionProps?.enabled && (
|
||||
<TitleBadge { ...actionProps?.badge } />
|
||||
) }
|
||||
</Title>
|
||||
<Description className="ppcp-r-settings-block__feature__description">
|
||||
{ renderDescription() }
|
||||
{ printNotes() }
|
||||
<span
|
||||
className="ppcp-r-feature-item__description"
|
||||
dangerouslySetInnerHTML={ { __html: description } }
|
||||
/>
|
||||
|
||||
{ actionProps?.notes?.length > 0 && (
|
||||
<span className="ppcp--item-notes">
|
||||
{ actionProps.notes.map( ( note, index ) => (
|
||||
<span key={ index }>{ note }</span>
|
||||
) ) }
|
||||
</span>
|
||||
) }
|
||||
</Description>
|
||||
</Header>
|
||||
|
||||
<Action>
|
||||
<div className="ppcp--action-buttons">
|
||||
{ props.actionProps?.buttons.map(
|
||||
( {
|
||||
{ actionProps?.buttons.map( ( buttonData ) => {
|
||||
const {
|
||||
class: className,
|
||||
type,
|
||||
text,
|
||||
url,
|
||||
urls,
|
||||
onClick,
|
||||
} ) => (
|
||||
<FeatureButton
|
||||
} = buttonData;
|
||||
|
||||
const buttonUrl = getButtonUrl( buttonData );
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={ text }
|
||||
className={ className }
|
||||
variant={ type }
|
||||
text={ text }
|
||||
isBusy={ props.actionProps.isBusy }
|
||||
url={ url }
|
||||
urls={ urls }
|
||||
onClick={ onClick }
|
||||
/>
|
||||
)
|
||||
) }
|
||||
isBusy={ actionProps.isBusy }
|
||||
href={ buttonUrl }
|
||||
target={ buttonUrl ? '_blank' : undefined }
|
||||
onClick={ ! buttonUrl ? onClick : undefined }
|
||||
>
|
||||
{ text }
|
||||
</Button>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
</Action>
|
||||
</SettingsBlock>
|
||||
|
|
|
@ -31,10 +31,15 @@ const PaymentMethodItemBlock = ( {
|
|||
id={ paymentMethod.id }
|
||||
className={ methodItemClasses }
|
||||
separatorAndGap={ false }
|
||||
aria-disabled={ isDisabled ? 'true' : 'false' }
|
||||
>
|
||||
{ isDisabled && (
|
||||
<div className="ppcp--method-disabled-overlay">
|
||||
<p className="ppcp--method-disabled-message">
|
||||
<div
|
||||
className="ppcp--method-disabled-overlay"
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
>
|
||||
<p className="ppcp--method-disabled-message" tabIndex="0">
|
||||
{ disabledMessage }
|
||||
</p>
|
||||
</div>
|
||||
|
@ -60,6 +65,8 @@ const PaymentMethodItemBlock = ( {
|
|||
__nextHasNoMarginBottom
|
||||
checked={ isSelected }
|
||||
onChange={ onSelect }
|
||||
disabled={ isDisabled }
|
||||
aria-label={ `Enable ${ paymentMethod.itemTitle }` }
|
||||
/>
|
||||
{ hasWarning && ! isDisabled && isSelected && (
|
||||
<WarningMessages
|
||||
|
@ -70,7 +77,9 @@ const PaymentMethodItemBlock = ( {
|
|||
{ paymentMethod?.fields && onTriggerModal && (
|
||||
<Button
|
||||
className="ppcp--method-settings"
|
||||
disabled={ isDisabled }
|
||||
onClick={ onTriggerModal }
|
||||
aria-label={ `Configure ${ paymentMethod.itemTitle } settings` }
|
||||
>
|
||||
<Icon icon={ cog } />
|
||||
</Button>
|
||||
|
|
|
@ -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 (
|
||||
<div { ...cardProps }>
|
||||
<div { ...cardProps } role="region" aria-labelledby={ titleId }>
|
||||
<div className="ppcp-r-settings-card__header">
|
||||
<div className="ppcp-r-settings-card__content-inner">
|
||||
<span className="ppcp-r-settings-card__title">
|
||||
<h2 id={ titleId } className="ppcp-r-settings-card__title">
|
||||
{ title }
|
||||
</span>
|
||||
<div className="ppcp-r-settings-card__description">
|
||||
</h2>
|
||||
<div
|
||||
id={ descriptionId }
|
||||
className="ppcp-r-settings-card__description"
|
||||
>
|
||||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<div className={ className }>
|
||||
{ message && (
|
||||
<span className="ppcp--spinner-message">{ message }</span>
|
||||
) }
|
||||
<div className={ className } role="status" aria-label={ ariaLabel }>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -30,8 +30,16 @@ const TabBar = ( { tabs, activePanel, setActivePanel } ) => {
|
|||
initialTabName={ activePanel }
|
||||
onSelect={ updateActivePanel }
|
||||
tabs={ tabs }
|
||||
orientation="horizontal"
|
||||
selectOnMove={ false }
|
||||
>
|
||||
{ () => '' }
|
||||
{ ( tab ) => (
|
||||
<div
|
||||
className={ `ppcp-r-tabpanel-content ppcp-r-tabpanel-${ tab.name }` }
|
||||
>
|
||||
{ tab.render ? tab.render() : '' }
|
||||
</div>
|
||||
) }
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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: <OptionalMethodDescription />,
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<div className="ppcp-r-page-optional-payment-methods">
|
||||
<OnboardingHeader title={ <PaymentStepTitle /> } />
|
||||
<OnboardingHeader
|
||||
title={ <PaymentStepTitle isBrandedOnly={ ownBrandOnly } /> }
|
||||
/>
|
||||
<div className="ppcp-r-inner-container">
|
||||
<OptionSelector
|
||||
multiSelect={ false }
|
||||
|
@ -58,7 +64,13 @@ const StepPaymentMethods = () => {
|
|||
|
||||
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' );
|
||||
};
|
||||
|
||||
|
|
|
@ -22,13 +22,14 @@ const StepWelcome = ( { setStep, currentStep } ) => {
|
|||
ownBrandOnly
|
||||
);
|
||||
|
||||
const onboardingHeaderDescription = canUseCardPayments
|
||||
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, all major credit/debit cards, and more.',
|
||||
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, and more.',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<TopNavigation
|
||||
|
@ -38,10 +48,19 @@ const SettingsNavigation = ( {
|
|||
>
|
||||
{ canSave && (
|
||||
<>
|
||||
<Button variant="primary" onClick={ persistAll }>
|
||||
{ __( 'Save', 'woocommerce-paypal-payments' ) }
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ handleSave }
|
||||
aria-busy={ isSaving }
|
||||
>
|
||||
{ isSaving
|
||||
? __( 'Saving…', 'woocommerce-paypal-payments' )
|
||||
: __( 'Save', 'woocommerce-paypal-payments' ) }
|
||||
</Button>
|
||||
<SaveStateMessage />
|
||||
<SaveStateMessage
|
||||
setIsSaving={ setIsSaving }
|
||||
isSaving={ isSaving }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</TopNavigation>
|
||||
|
@ -50,14 +69,14 @@ 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 ) => {
|
||||
const handleActivityStart = useCallback(
|
||||
( started ) => {
|
||||
if ( started.startsWith( 'persist' ) ) {
|
||||
setIsSaving( true );
|
||||
setIsVisible( false );
|
||||
|
@ -67,7 +86,9 @@ const SaveStateMessage = () => {
|
|||
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 (
|
||||
<span className={ className }>
|
||||
<span className={ className } role="status" aria-live="polite">
|
||||
<span className="ppcp--inner-text">
|
||||
{ __( 'Completed', 'woocommerce-paypal-payments' ) }
|
||||
</span>
|
||||
|
|
|
@ -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 }
|
||||
>
|
||||
<ContentWrapper>
|
||||
{ features.map( ( { id, enabled, ...feature } ) => (
|
||||
|
|
|
@ -33,7 +33,10 @@ const Todos = () => {
|
|||
'Dismissed items restored successfully.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
{ icon: NOTIFICATION_SUCCESS }
|
||||
{
|
||||
icon: NOTIFICATION_SUCCESS,
|
||||
speak: true,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
setIsResetting( false );
|
||||
|
|
|
@ -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 <SpinnerOverlay asModal={ true } />;
|
||||
return (
|
||||
<SpinnerOverlay
|
||||
asModal={ true }
|
||||
ariaLabel={ __(
|
||||
'Loading PayPal settings',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-tab-overview">
|
||||
<div
|
||||
className="ppcp-r-tab-overview"
|
||||
role="region"
|
||||
aria-label={ __(
|
||||
'PayPal Overview',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
>
|
||||
<Todos />
|
||||
<Features />
|
||||
<Help />
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -34,9 +34,10 @@ export const flags = ( state ) => {
|
|||
* that should be returned.
|
||||
*
|
||||
* @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 ) {
|
||||
/**
|
||||
|
|
|
@ -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( '' );
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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' ),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,17 +113,42 @@ class ConnectionListener {
|
|||
*/
|
||||
public function process( int $user_id, array $request ) : void {
|
||||
$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'] ?? '' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
73
modules/ppcp-settings/src/Service/LoadingScreenService.php
Normal file
73
modules/ppcp-settings/src/Service/LoadingScreenService.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/**
|
||||
* Provides loading screen handling logic for PayPal settings page.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Service
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Service;
|
||||
|
||||
/**
|
||||
* LoadingScreenService class. Handles the display of loading screen for the PayPal settings page.
|
||||
*/
|
||||
class LoadingScreenService {
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'admin_head',
|
||||
array( $this, 'add_settings_loading_screen' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add CSS to permanently hide specific WooCommerce elements on the PayPal settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_settings_loading_screen(): void {
|
||||
// Only run on the specific WooCommerce PayPal settings page.
|
||||
if ( ! $this->is_ppcp_settings_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<style>
|
||||
/* Permanently hide these WooCommerce elements. */
|
||||
.woocommerce form#mainform > *:not(#ppcp-settings-container),
|
||||
#woocommerce-embedded-root {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#wpcontent #wpbody {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're on the PayPal checkout settings page.
|
||||
*
|
||||
* @return bool True if we're on the PayPal settings page
|
||||
*/
|
||||
private function is_ppcp_settings_page(): bool {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
$page = wc_clean( wp_unslash( $_GET['page'] ?? '' ) );
|
||||
$tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) );
|
||||
$section = wc_clean( wp_unslash( $_GET['section'] ?? '' ) );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
return $page === 'wc-settings' && $tab === 'checkout' && $section === 'ppcp-gateway';
|
||||
}
|
||||
}
|
|
@ -234,6 +234,9 @@ class SettingsDataManager {
|
|||
$methods_apm = $this->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 ),
|
||||
);
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +34,23 @@ 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.
|
||||
|
@ -46,6 +61,15 @@ class WooCommerceLogger implements LoggerInterface {
|
|||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
26
readme.txt
26
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
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
* @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.' );
|
||||
|
@ -20,13 +22,13 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
|||
$root_dir = __DIR__;
|
||||
$main_plugin_file = "{$root_dir}/woocommerce-paypal-payments.php";
|
||||
|
||||
if ( !file_exists( $main_plugin_file ) ) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue