diff --git a/changelog.txt b/changelog.txt index cb7599d85b..3d02735e68 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ *** Changelog *** = 9.7.0 - xxxx-xx-xx = +* Update - Removes BNPL payment methods (Klarna and Affirm) when other official plugins are active * Fix - Moves the existing order lock functionality earlier in the order processing flow to prevent duplicate processing requests * Add - Adds two new safety filters to the subscriptions detached debug tool: `wc_stripe_detached_subscriptions_maximum_time` and `wc_stripe_detached_subscriptions_maximum_count` * Add - Show a notice when editing an active subscription that has no payment method attached diff --git a/client/blocks/upe/index.js b/client/blocks/upe/index.js index 8ba2852327..12f8a438f6 100644 --- a/client/blocks/upe/index.js +++ b/client/blocks/upe/index.js @@ -3,9 +3,11 @@ import { registerExpressPaymentMethod, } from '@woocommerce/blocks-registry'; import { + PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_AMAZON_PAY, PAYMENT_METHOD_CARD, PAYMENT_METHOD_GIROPAY, + PAYMENT_METHOD_KLARNA, PAYMENT_METHOD_LINK, } from '../../stripe-utils/constants'; import { updateTokenLabelsWhenLoaded } from './token-label-updater.js'; @@ -38,18 +40,26 @@ const api = new WCStripeAPI( const paymentMethodsConfig = getBlocksConfiguration()?.paymentMethodsConfig ?? {}; -const methodsToFilter = [ - PAYMENT_METHOD_AMAZON_PAY, - PAYMENT_METHOD_LINK, - PAYMENT_METHOD_GIROPAY, // Skip giropay as it was deprecated by Jun, 30th 2024. -]; - // Register UPE Elements. if ( getBlocksConfiguration()?.isOCEnabled ) { registerPaymentMethod( upeElement( PAYMENT_METHOD_CARD, api, paymentMethodsConfig.card ) ); } else { + const methodsToFilter = [ + PAYMENT_METHOD_AMAZON_PAY, + PAYMENT_METHOD_LINK, + PAYMENT_METHOD_GIROPAY, // Skip giropay as it was deprecated by Jun, 30th 2024. + ]; + + // Filter out some BNPLs when other official extensions are present. + if ( getBlocksConfiguration()?.hasAffirmGatewayPlugin ) { + methodsToFilter.push( PAYMENT_METHOD_AFFIRM ); + } + if ( getBlocksConfiguration()?.hasKlarnaGatewayPlugin ) { + methodsToFilter.push( PAYMENT_METHOD_KLARNA ); + } + Object.entries( paymentMethodsConfig ) .filter( ( [ method ] ) => ! methodsToFilter.includes( method ) ) .forEach( ( [ method, config ] ) => { diff --git a/client/components/payment-method-unavailable-due-conflict-pill/__tests__/index.test.js b/client/components/payment-method-unavailable-due-conflict-pill/__tests__/index.test.js new file mode 100644 index 0000000000..c59e69fad1 --- /dev/null +++ b/client/components/payment-method-unavailable-due-conflict-pill/__tests__/index.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import PaymentMethodUnavailableDueConflictPill from '..'; +import { PAYMENT_METHOD_AFFIRM } from 'wcstripe/stripe-utils/constants'; + +describe( 'PaymentMethodUnavailableDueConflictPill', () => { + beforeEach( () => { + global.wc_stripe_settings_params = { has_affirm_gateway_plugin: false }; + } ); + + it( 'should render the "Has plugin conflict" text', () => { + global.wc_stripe_settings_params = { has_affirm_gateway_plugin: true }; + + render( + + ); + + expect( + screen.queryByText( 'Has plugin conflict' ) + ).toBeInTheDocument(); + } ); + + it( 'should not render when other extensions are not active', () => { + const { container } = render( + + ); + + expect( container.firstChild ).toBeNull(); + } ); +} ); diff --git a/client/components/payment-method-unavailable-due-conflict-pill/index.js b/client/components/payment-method-unavailable-due-conflict-pill/index.js new file mode 100644 index 0000000000..7610b54346 --- /dev/null +++ b/client/components/payment-method-unavailable-due-conflict-pill/index.js @@ -0,0 +1,96 @@ +/* global wc_stripe_settings_params */ +import { __, sprintf } from '@wordpress/i18n'; +import React from 'react'; +import styled from '@emotion/styled'; +import interpolateComponents from 'interpolate-components'; +import { Icon, info } from '@wordpress/icons'; +import Popover from 'wcstripe/components/popover'; +import { + PAYMENT_METHOD_AFFIRM, + PAYMENT_METHOD_KLARNA, +} from 'wcstripe/stripe-utils/constants'; + +const StyledPill = styled.span` + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border: 1px solid #fcf9e8; + border-radius: 2px; + background-color: #fcf9e8; + color: #674600; + font-size: 12px; + font-weight: 400; + line-height: 16px; + width: fit-content; +`; + +const StyledLink = styled.a` + &:focus, + &:visited { + box-shadow: none; + } +`; + +const IconWrapper = styled.span` + height: 16px; + cursor: pointer; +`; + +const AlertIcon = styled( Icon )` + fill: #674600; +`; + +const IconComponent = ( { children, ...props } ) => ( + + + { children } + +); + +const PaymentMethodUnavailableDueConflictPill = ( { id, label } ) => { + if ( + ( id === PAYMENT_METHOD_AFFIRM && + // eslint-disable-next-line camelcase + wc_stripe_settings_params.has_affirm_gateway_plugin ) || + ( id === PAYMENT_METHOD_KLARNA && + // eslint-disable-next-line camelcase + wc_stripe_settings_params.has_klarna_gateway_plugin ) + ) { + return ( + + { __( 'Has plugin conflict', 'woocommerce-gateway-stripe' ) } + { + // Stop propagation is necessary so it doesn't trigger the tooltip click event. + ev.stopPropagation(); + } } + /> + ), + }, + } ) } + /> + + ); + } + + return null; +}; + +export default PaymentMethodUnavailableDueConflictPill; diff --git a/client/settings/general-settings-section/payment-method-description.js b/client/settings/general-settings-section/payment-method-description.js index 2c89f81ee9..42582b0132 100644 --- a/client/settings/general-settings-section/payment-method-description.js +++ b/client/settings/general-settings-section/payment-method-description.js @@ -4,6 +4,7 @@ import PaymentMethodMissingCurrencyPill from '../../components/payment-method-mi import RecurringPaymentIcon from '../../components/recurring-payment-icon'; import PaymentMethodCapabilityStatusPill from 'wcstripe/components/payment-method-capability-status-pill'; import PaymentMethodDeprecationPill from 'wcstripe/components/payment-method-deprecation-pill'; +import PaymentMethodUnavailableDueConflictPill from 'wcstripe/components/payment-method-unavailable-due-conflict-pill'; const Wrapper = styled.div` display: flex; @@ -69,6 +70,10 @@ const PaymentMethodDescription = ( { id={ id } label={ label } /> + ) } diff --git a/client/settings/general-settings-section/payment-method.js b/client/settings/general-settings-section/payment-method.js index dda0e555e9..eccb367ac2 100644 --- a/client/settings/general-settings-section/payment-method.js +++ b/client/settings/general-settings-section/payment-method.js @@ -12,6 +12,7 @@ import { PAYMENT_METHOD_AFFIRM, PAYMENT_METHOD_AFTERPAY_CLEARPAY, PAYMENT_METHOD_CARD, + PAYMENT_METHOD_KLARNA, } from 'wcstripe/stripe-utils/constants'; import PaymentMethodFeesPill from 'wcstripe/components/payment-method-fees-pill'; import { usePaymentMethodCurrencies } from 'utils/use-payment-method-currencies'; @@ -127,8 +128,14 @@ const PaymentMethod = ( { method, data } ) => { const storeCurrency = window?.wcSettings?.currency?.code; const isDisabled = - paymentMethodCurrencies.length && - ! paymentMethodCurrencies.includes( storeCurrency ); + ( paymentMethodCurrencies.length && + ! paymentMethodCurrencies.includes( storeCurrency ) ) || + ( PAYMENT_METHOD_AFFIRM === method && + // eslint-disable-next-line camelcase + wc_stripe_settings_params.has_affirm_gateway_plugin ) || + ( PAYMENT_METHOD_KLARNA === method && + // eslint-disable-next-line camelcase + wc_stripe_settings_params.has_klarna_gateway_plugin ); return (
diff --git a/includes/admin/class-wc-stripe-settings-controller.php b/includes/admin/class-wc-stripe-settings-controller.php index 9a103dd751..937883d2e7 100644 --- a/includes/admin/class-wc-stripe-settings-controller.php +++ b/includes/admin/class-wc-stripe-settings-controller.php @@ -257,15 +257,6 @@ public function admin_scripts( $hook_suffix ) { // Show the BNPL promotional banner only if no BNPL payment methods are enabled. && ! array_intersect( WC_Stripe_Payment_Methods::BNPL_PAYMENT_METHODS, $enabled_payment_methods ); - $has_other_bnpl_plugins_active = false; - $available_payment_gateways = WC()->payment_gateways->payment_gateways; - foreach ( $available_payment_gateways as $gateway ) { - if ( ( 'affirm' === $gateway->id || 'klarna_payments' === $gateway->id ) && 'yes' === $gateway->enabled ) { - $has_other_bnpl_plugins_active = true; - break; - } - } - $params = [ 'time' => time(), 'i18n_out_of_sync' => $message, @@ -287,7 +278,9 @@ public function admin_scripts( $hook_suffix ) { 'is_oc_available' => WC_Stripe_Feature_Flags::is_oc_available(), 'oauth_nonce' => wp_create_nonce( 'wc_stripe_get_oauth_urls' ), 'is_sepa_tokens_enabled' => 'yes' === $this->gateway->get_option( 'sepa_tokens_for_other_methods', 'no' ), - 'has_other_bnpl_plugins' => $has_other_bnpl_plugins_active, + 'has_affirm_gateway_plugin' => WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_AFFIRM ), + 'has_klarna_gateway_plugin' => WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_KLARNA ), + 'has_other_bnpl_plugins' => WC_Stripe_Helper::has_other_bnpl_plugins_active(), 'is_payments_onboarding_task_completed' => $this->is_payments_onboarding_task_completed(), ]; wp_localize_script( diff --git a/includes/class-wc-stripe-helper.php b/includes/class-wc-stripe-helper.php index 98d17456b0..d1620b843d 100644 --- a/includes/class-wc-stripe-helper.php +++ b/includes/class-wc-stripe-helper.php @@ -20,6 +20,20 @@ class WC_Stripe_Helper { const META_NAME_STRIPE_CURRENCY = '_stripe_currency'; const PAYMENT_AWAITING_ACTION_META = '_stripe_payment_awaiting_action'; + /** + * The identifier for the official Affirm gateway plugin. + * + * @var string + */ + const OFFICIAL_PLUGIN_ID_AFFIRM = 'affirm'; + + /** + * The identifier for the official Klarna gateway plugin. + * + * @var string + */ + const OFFICIAL_PLUGIN_ID_KLARNA = 'klarna_payments'; + /** * List of legacy Stripe gateways. * @@ -1851,4 +1865,35 @@ public static function get_refund_reason_description( $refund_reason_key ) { return __( 'Unknown reason', 'woocommerce-gateway-stripe' ); } } + + /** + * Checks if there are other Buy Now Pay Later plugins active. + * + * @return bool + */ + public static function has_other_bnpl_plugins_active() { + $other_bnpl_gateway_ids = [ self::OFFICIAL_PLUGIN_ID_AFFIRM, self::OFFICIAL_PLUGIN_ID_KLARNA ]; + foreach ( $other_bnpl_gateway_ids as $bnpl_gateway_id ) { + if ( self::has_gateway_plugin_active( $bnpl_gateway_id ) ) { + return true; + } + } + return false; + } + + /** + * Checks if a given payment gateway plugin is active. + * + * @param string $plugin_id + * @return bool + */ + public static function has_gateway_plugin_active( $plugin_id ) { + $available_payment_gateways = WC()->payment_gateways->payment_gateways ?? []; + foreach ( $available_payment_gateways as $available_payment_gateway ) { + if ( $plugin_id === $available_payment_gateway->id && 'yes' === $available_payment_gateway->enabled ) { + return true; + } + } + return false; + } } diff --git a/includes/notes/class-wc-stripe-bnpl-promotion-note.php b/includes/notes/class-wc-stripe-bnpl-promotion-note.php index 13b2cc3178..5833972db0 100644 --- a/includes/notes/class-wc-stripe-bnpl-promotion-note.php +++ b/includes/notes/class-wc-stripe-bnpl-promotion-note.php @@ -93,16 +93,7 @@ public static function init( WC_Stripe_Payment_Gateway $gateway ) { } } - $has_other_bnpl_plugins_active = false; - $available_payment_gateways = WC()->payment_gateways->payment_gateways; - $other_bnpl_gateway_ids = [ 'affirm', 'klarna_payments' ]; - foreach ( $available_payment_gateways as $available_payment_gateway ) { - if ( in_array( $available_payment_gateway->id, $other_bnpl_gateway_ids, true ) && 'yes' === $available_payment_gateway->enabled ) { - $has_other_bnpl_plugins_active = true; - break; - } - } - if ( $has_other_bnpl_plugins_active ) { + if ( WC_Stripe_Helper::has_other_bnpl_plugins_active() ) { return; } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 44eb735f60..de26369b1d 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -557,6 +557,10 @@ public function javascript_params() { // Single Payment Element payment method parent configuration ID $stripe_params['paymentMethodConfigurationParentId'] = WC_Stripe_Payment_Method_Configurations::get_parent_configuration_id(); + // Checking for other BNPL extensions. + $stripe_params['hasAffirmGatewayPlugin'] = WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_AFFIRM ); + $stripe_params['hasKlarnaGatewayPlugin'] = WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_KLARNA ); + $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); $currency = get_woocommerce_currency(); diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-affirm.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-affirm.php index 5553eeb8cb..617530251d 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-affirm.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-affirm.php @@ -61,4 +61,18 @@ public function is_available_for_account_country() { public function requires_automatic_capture() { return false; } + + /** + * Returns true if the UPE method is available. + * + * @inheritDoc + */ + public function is_available() { + // Affirm is only available if the official Affirm plugin is not active. + if ( WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_AFFIRM ) ) { + return false; + } + + return parent::is_available(); + } } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-klarna.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-klarna.php index 460cbac7fb..dd8daa0ac2 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-klarna.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-klarna.php @@ -156,4 +156,18 @@ public function is_available_for_account_country() { public function requires_automatic_capture() { return false; } + + /** + * Returns true if the UPE method is available. + * + * @inheritDoc + */ + public function is_available() { + // Klarna is only available if the official Klarna plugin is not active. + if ( WC_Stripe_Helper::has_gateway_plugin_active( WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_KLARNA ) ) { + return false; + } + + return parent::is_available(); + } } diff --git a/readme.txt b/readme.txt index 1a08d7180d..58b392c075 100644 --- a/readme.txt +++ b/readme.txt @@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o == Changelog == = 9.7.0 - xxxx-xx-xx = +* Update - Removes BNPL payment methods (Klarna and Affirm) when other official plugins are active * Fix - Moves the existing order lock functionality earlier in the order processing flow to prevent duplicate processing requests * Add - Adds two new safety filters to the subscriptions detached debug tool: `wc_stripe_detached_subscriptions_maximum_time` and `wc_stripe_detached_subscriptions_maximum_count` * Add - Show a notice when editing an active subscription that has no payment method attached diff --git a/tests/phpunit/WC_Stripe_Helper_Test.php b/tests/phpunit/WC_Stripe_Helper_Test.php index 415e614edb..52dc076739 100644 --- a/tests/phpunit/WC_Stripe_Helper_Test.php +++ b/tests/phpunit/WC_Stripe_Helper_Test.php @@ -595,4 +595,107 @@ public function provide_test_get_refund_reason_description() { ], ]; } + + /** + * Tests for `has_other_bnpl_plugins_active`. + * + * @param array $payment_gateways The available payment gateways. + * @param bool $expected The expected result. + * @dataProvider provide_test_has_other_bnpl_plugins_active + * @return void + */ + public function test_has_other_bnpl_plugins_active( $payment_gateways, $expected ) { + $original_payment_gateways = WC()->payment_gateways->payment_gateways; + + // Mock the available payment gateways. + WC()->payment_gateways->payment_gateways = $payment_gateways; + + $actual = WC_Stripe_Helper::has_other_bnpl_plugins_active(); + + // Clean up. + WC()->payment_gateways->payment_gateways = $original_payment_gateways; + + $this->assertSame( $expected, $actual ); + } + + /** + * Provider for `test_has_other_bnpl_plugins_active`. + * + * @return array + */ + public function provide_test_has_other_bnpl_plugins_active() { + return [ + 'has other plugins' => [ + 'payment gateways' => [ + 'klarna' => (object) [ + 'id' => 'klarna_payments', + 'enabled' => 'yes', + ], + 'affirm' => (object) [ + 'id' => 'affirm', + 'enabled' => 'yes', + ], + ], + 'expected' => true, + ], + 'does not have other plugins' => [ + 'payment gateways' => [], + 'expected' => false, + ], + ]; + } + + /** + * Tests for `has_gateway_plugin_active`. + * + * @param string $plugin_id The plugin ID to evaluate. + * @param array $payment_gateways The available payment gateways. + * @param bool $expected The expected result. + * @return void + * + * @dataProvider provide_has_gateway_plugin_active + */ + public function test_has_gateway_plugin_active( $plugin_id, $payment_gateways, $expected ) { + $original_payment_gateways = WC()->payment_gateways->payment_gateways; + + // Mock the available payment gateways. + WC()->payment_gateways->payment_gateways = $payment_gateways; + + $actual = WC_Stripe_Helper::has_gateway_plugin_active( $plugin_id ); + + // Clean up. + WC()->payment_gateways->payment_gateways = $original_payment_gateways; + + $this->assertSame( $expected, $actual ); + } + + /** + * Provider for `test_has_gateway_plugin_active`. + * + * @return array + */ + public function provide_has_gateway_plugin_active() { + return [ + 'has Klarna official plugin active' => [ + 'plugin id' => WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_KLARNA, + 'payment gateways' => [ + 'klarna' => (object) [ + 'id' => 'klarna_payments', + 'enabled' => 'yes', + ], + ], + 'expected' => true, + ], + 'does not have Klarna official plugin active' => [ + 'plugin id' => WC_Stripe_Helper::OFFICIAL_PLUGIN_ID_KLARNA, + 'payment gateways' => [ + 'affirm' => (object) [ + 'id' => 'affirm', + 'enabled' => 'yes', + ], + ], + 'expected' => false, + ], + ]; + } }