From a19f2b0f38abdbce2946103fc9cfabe2d42684da Mon Sep 17 00:00:00 2001 From: Hsing-yu Flowers Date: Mon, 13 Nov 2023 12:03:17 -0500 Subject: [PATCH 01/61] Do not show the WooPay button on the product page when WC Bookings require confirmation (#7682) --- ...-button-when-bookings-require-confirmation | 4 ++++ ...lass-wc-payments-woopay-button-handler.php | 24 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 changelog/do-not-show-woopay-button-when-bookings-require-confirmation diff --git a/changelog/do-not-show-woopay-button-when-bookings-require-confirmation b/changelog/do-not-show-woopay-button-when-bookings-require-confirmation new file mode 100644 index 00000000000..2ed714ec01b --- /dev/null +++ b/changelog/do-not-show-woopay-button-when-bookings-require-confirmation @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Do not show the WooPay button on the product page when WC Bookings require confirmation diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index b60e325a126..a7fa287761c 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -634,6 +634,11 @@ private function is_product_supported() { $is_supported = false; } + // WC Bookings require confirmation products are not supported. + if ( is_a( $product, 'WC_Product_Booking' ) && $product->get_requires_confirmation() ) { + $is_supported = false; + } + return apply_filters( 'wcpay_woopay_button_is_product_supported', $is_supported, $product ); } @@ -647,14 +652,19 @@ private function is_product_supported() { private function has_allowed_items_in_cart() { $is_supported = true; + /** + * Psalm throws an error here even though we check the class existence. + * + * @psalm-suppress UndefinedClass + */ // We don't support pre-order products to be paid upon release. - if ( - class_exists( 'WC_Pre_Orders_Cart' ) && - WC_Pre_Orders_Cart::cart_contains_pre_order() && - class_exists( 'WC_Pre_Orders_Product' ) && - WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) - ) { - $is_supported = false; + if ( class_exists( 'WC_Pre_Orders_Cart' ) && class_exists( 'WC_Pre_Orders_Product' ) ) { + if ( + WC_Pre_Orders_Cart::cart_contains_pre_order() && + WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) + ) { + $is_supported = false; + } } return apply_filters( 'wcpay_platform_checkout_button_are_cart_items_supported', $is_supported ); From 00917e54e1299c1d5493c09e2b577b8caa86d9b3 Mon Sep 17 00:00:00 2001 From: Taha Paksu <3295+tpaksu@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:53:35 +0300 Subject: [PATCH 02/61] Fix saved card payment error on blocks checkout (#7701) --- ...nts-saved-card-payments-on-blocks-checkout | 4 +++ client/checkout/blocks/fields.js | 4 +-- .../blocks/generate-payment-method.js | 2 +- client/checkout/blocks/saved-token-handler.js | 28 ++++++++++++++++++- client/checkout/blocks/upe-fields.js | 4 +-- client/checkout/blocks/upe-split-fields.js | 4 +-- 6 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 changelog/fix-7700-card-testing-prevention-prevents-saved-card-payments-on-blocks-checkout diff --git a/changelog/fix-7700-card-testing-prevention-prevents-saved-card-payments-on-blocks-checkout b/changelog/fix-7700-card-testing-prevention-prevents-saved-card-payments-on-blocks-checkout new file mode 100644 index 00000000000..59e04c1f49c --- /dev/null +++ b/changelog/fix-7700-card-testing-prevention-prevents-saved-card-payments-on-blocks-checkout @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix saved card payments not working on block checkout while card testing prevention is active diff --git a/client/checkout/blocks/fields.js b/client/checkout/blocks/fields.js index 67cd5ad2a3d..c89c34888bc 100644 --- a/client/checkout/blocks/fields.js +++ b/client/checkout/blocks/fields.js @@ -23,7 +23,7 @@ const WCPayFields = ( { stripe, elements, billing: { billingData }, - eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, shouldSavePayment, } ) => { @@ -49,7 +49,7 @@ const WCPayFields = ( { // When it's time to process the payment, generate a Stripe payment method object. useEffect( () => - onPaymentProcessing( () => { + onPaymentSetup( () => { if ( PAYMENT_METHOD_NAME_CARD !== activePaymentMethod ) { return; } diff --git a/client/checkout/blocks/generate-payment-method.js b/client/checkout/blocks/generate-payment-method.js index 01e55a4f267..61e9e74c42c 100644 --- a/client/checkout/blocks/generate-payment-method.js +++ b/client/checkout/blocks/generate-payment-method.js @@ -11,7 +11,7 @@ import { PAYMENT_METHOD_NAME_CARD } from '../constants.js'; * @param {Object} billingData The billing data, which was collected from the checkout block. * @param {string} fingerprint User fingerprint. * - * @return {Object} The `onPaymentProcessing` response object, including a type and meta data/error message. + * @return {Object} The `onPaymentSetup` response object, including a type and meta data/error message. */ const generatePaymentMethod = async ( api, diff --git a/client/checkout/blocks/saved-token-handler.js b/client/checkout/blocks/saved-token-handler.js index 2ec311c8d5e..0f8d6e42d46 100644 --- a/client/checkout/blocks/saved-token-handler.js +++ b/client/checkout/blocks/saved-token-handler.js @@ -1,15 +1,41 @@ /** * Internal dependencies */ +import { useEffect } from 'react'; import { usePaymentCompleteHandler } from './hooks'; +import { useSelect } from '@wordpress/data'; export const SavedTokenHandler = ( { api, stripe, elements, - eventRegistration: { onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, } ) => { + const paymentMethodData = useSelect( ( select ) => { + const store = select( 'wc/store/payment' ); + return store.getPaymentMethodData(); + } ); + + useEffect( () => { + return onPaymentSetup( () => { + const fraudPreventionToken = document + .querySelector( '#wcpay-fraud-prevention-token' ) + ?.getAttribute( 'value' ); + + return { + type: 'success', + meta: { + paymentMethodData: { + ...paymentMethodData, + 'wcpay-fraud-prevention-token': + fraudPreventionToken ?? '', + }, + }, + }; + } ); + }, [ onPaymentSetup, paymentMethodData ] ); + // Once the server has completed payment processing, confirm the intent of necessary. usePaymentCompleteHandler( api, diff --git a/client/checkout/blocks/upe-fields.js b/client/checkout/blocks/upe-fields.js index c248d7b949b..4198367146f 100644 --- a/client/checkout/blocks/upe-fields.js +++ b/client/checkout/blocks/upe-fields.js @@ -41,7 +41,7 @@ const WCPayUPEFields = ( { activePaymentMethod, billing: { billingData }, shippingData, - eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, paymentIntentId, paymentIntentSecret, @@ -135,7 +135,7 @@ const WCPayUPEFields = ( { // When it's time to process the payment, generate a Stripe payment method object. useEffect( () => - onPaymentProcessing( () => { + onPaymentSetup( () => { if ( PAYMENT_METHOD_NAME_CARD !== activePaymentMethod ) { return; } diff --git a/client/checkout/blocks/upe-split-fields.js b/client/checkout/blocks/upe-split-fields.js index 5961ae7607e..dc17dc8273f 100644 --- a/client/checkout/blocks/upe-split-fields.js +++ b/client/checkout/blocks/upe-split-fields.js @@ -45,7 +45,7 @@ const WCPayUPEFields = ( { testingInstructions, billing: { billingData }, shippingData, - eventRegistration: { onPaymentProcessing, onCheckoutSuccess }, + eventRegistration: { onPaymentSetup, onCheckoutSuccess }, emitResponse, paymentMethodId, upeMethods, @@ -133,7 +133,7 @@ const WCPayUPEFields = ( { // When it's time to process the payment, generate a Stripe payment method object. useEffect( () => - onPaymentProcessing( () => { + onPaymentSetup( () => { if ( upeMethods[ paymentMethodId ] !== activePaymentMethod ) { return; } From 97dd26c9dc30303b1b5975445fe4f949782a80a3 Mon Sep 17 00:00:00 2001 From: Anurag Bhandari Date: Tue, 14 Nov 2023 15:18:05 +0530 Subject: [PATCH 03/61] E2E tests for checking out with Giropay (#7650) Co-authored-by: Francesco --- changelog/add-6761-giropay-e2e | 4 +++ ...ferred-intent-creation-upe-enabled.spec.js | 25 ++++++++++++---- tests/e2e/utils/payments.js | 29 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 changelog/add-6761-giropay-e2e diff --git a/changelog/add-6761-giropay-e2e b/changelog/add-6761-giropay-e2e new file mode 100644 index 00000000000..cd1099fb43e --- /dev/null +++ b/changelog/add-6761-giropay-e2e @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add E2E tests for checking out with Giropay diff --git a/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js b/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js index cb05d0220ca..21c1c471815 100644 --- a/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js +++ b/tests/e2e/specs/upe-split/shopper/shopper-deferred-intent-creation-upe-enabled.spec.js @@ -11,16 +11,18 @@ import { confirmCardAuthentication, fillCardDetails, setupProductCheckout, + selectGiropayOnCheckout, + completeGiropayPayment, } from '../../../utils/payments'; import { uiUnblocked } from '@woocommerce/e2e-utils/build/page-utils'; const { shopper, merchant } = require( '@woocommerce/e2e-utils' ); const UPE_METHOD_CHECKBOXES = [ - '#inspector-checkbox-control-3', // bancontact - '#inspector-checkbox-control-4', // eps - '#inspector-checkbox-control-5', // giropay - '#inspector-checkbox-control-6', // ideal - '#inspector-checkbox-control-7', // sofort + '#inspector-checkbox-control-5', // bancontact + '#inspector-checkbox-control-6', // eps + '#inspector-checkbox-control-7', // giropay + '#inspector-checkbox-control-8', // ideal + '#inspector-checkbox-control-9', // sofort ]; const card = config.get( 'cards.basic' ); const MIN_WAIT_TIME_BETWEEN_PAYMENT_METHODS = 20000; @@ -45,6 +47,19 @@ describe( 'Enabled UPE with deferred intent creation', () => { } ); describe( 'Enabled UPE with deferred intent creation', () => { + it( 'should successfully place order with Giropay', async () => { + await setupProductCheckout( + config.get( 'addresses.customer.billing' ) + ); + await selectGiropayOnCheckout( page ); + await shopper.placeOrder(); + await completeGiropayPayment( page, 'success' ); + await page.waitForNavigation( { + waitUntil: 'networkidle0', + } ); + await expect( page ).toMatch( 'Order received' ); + } ); + it( 'should successfully place order with the default card', async () => { await setupProductCheckout( config.get( 'addresses.customer.billing' ) diff --git a/tests/e2e/utils/payments.js b/tests/e2e/utils/payments.js index b625eed5607..3c8084c0811 100644 --- a/tests/e2e/utils/payments.js +++ b/tests/e2e/utils/payments.js @@ -296,3 +296,32 @@ export async function setupCheckout( billingDetails ) { '.wc_payment_method.payment_method_woocommerce_payments' ); } + +/** + * Selects the Giropay payment method on the checkout page. + * + * @param {*} page The page reference object. + */ +export async function selectGiropayOnCheckout( page ) { + await page.$( '#payment .payment_method_woocommerce_payments_giropay' ); + const giropayRadioLabel = await page.waitForSelector( + '#payment .payment_method_woocommerce_payments_giropay label' + ); + giropayRadioLabel.click(); + await page.waitFor( 1000 ); +} + +/** + * Authorizes or fails a Giropay payment. + * + * @param {*} page The page reference object. + * @param {string} action Either of 'success' or 'failure'. + */ +export async function completeGiropayPayment( page, action ) { + await page.$( '.actions .common-ButtonGroup' ); + const actionButton = await page.waitForSelector( + `.actions .common-ButtonGroup a[name=${ action }]` + ); + actionButton.click(); + await page.waitFor( 1000 ); +} From c4448072a793b73bab9634522fc2ed081f926911 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Tue, 14 Nov 2023 11:11:11 +0000 Subject: [PATCH 04/61] Update to not hardcode country/address on builder flow account creation. (#7706) --- .../dev-7678-dev-mode-country-cant-be-changed | 4 ++++ includes/class-wc-payments-account.php | 19 ------------------- 2 files changed, 4 insertions(+), 19 deletions(-) create mode 100644 changelog/dev-7678-dev-mode-country-cant-be-changed diff --git a/changelog/dev-7678-dev-mode-country-cant-be-changed b/changelog/dev-7678-dev-mode-country-cant-be-changed new file mode 100644 index 00000000000..63916dff8c7 --- /dev/null +++ b/changelog/dev-7678-dev-mode-country-cant-be-changed @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update to the new onboarding builder flow to not prefill country/address to US. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 5d8a63ed520..30a409752cd 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -1329,26 +1329,7 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = } $account_data = [ 'setup_mode' => 'test', - 'country' => 'US', 'business_type' => 'individual', - 'individual' => [ - 'first_name' => 'John', - 'last_name' => 'Woolliams', - 'address' => [ - 'country' => 'US', - 'state' => 'California', - 'city' => 'South San Francisco', - 'line1' => '1040 Grand Ave', - 'postal_code' => '94080', - ], - 'ssn_last_4' => '0000', - 'phone' => '+10000000000', - 'dob' => [ - 'day' => '1', - 'month' => '1', - 'year' => '1980', - ], - ], 'mcc' => '5734', 'url' => $url, 'business_name' => get_bloginfo( 'name' ), From 1f9e6f347fe598138ec1842e7ec07fed1c91ee6c Mon Sep 17 00:00:00 2001 From: Dat Hoang Date: Tue, 14 Nov 2023 21:20:04 +0700 Subject: [PATCH 05/61] Handle minimum amount error in InitialState (project RPP). (#7608) --- .../rpp-7415-verification-minimum-amount | 4 + .../PaymentsServiceProvider.php | 10 +- src/Internal/Payment/State/InitialState.php | 37 +++++-- src/Internal/Service/MinimumAmountService.php | 90 ++++++++++++++++ .../Payment/State/InitialStateTest.php | 79 +++++++++++++- .../Service/MinimumAmountServiceTest.php | 100 ++++++++++++++++++ .../Internal/Service/SessionServiceTest.php | 2 - 7 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 changelog/rpp-7415-verification-minimum-amount create mode 100644 src/Internal/Service/MinimumAmountService.php create mode 100644 tests/unit/src/Internal/Service/MinimumAmountServiceTest.php diff --git a/changelog/rpp-7415-verification-minimum-amount b/changelog/rpp-7415-verification-minimum-amount new file mode 100644 index 00000000000..671ad7d5386 --- /dev/null +++ b/changelog/rpp-7415-verification-minimum-amount @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Handle mimium amount in InitialState (project RPP). diff --git a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php index e4467539f14..85d0bb1acbd 100644 --- a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php +++ b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php @@ -25,6 +25,7 @@ use WCPay\Internal\Payment\State\SystemErrorState; use WCPay\Internal\Proxy\HooksProxy; use WCPay\Internal\Proxy\LegacyProxy; +use WCPay\Internal\Service\MinimumAmountService; use WCPay\Internal\Service\PaymentContextLoggerService; use WCPay\Internal\Service\DuplicatePaymentPreventionService; use WCPay\Internal\Service\PaymentProcessingService; @@ -47,6 +48,7 @@ class PaymentsServiceProvider extends AbstractServiceProvider { protected $provides = [ PaymentProcessingService::class, Router::class, + StateFactory::class, InitialState::class, DuplicateOrderDetectedState::class, @@ -55,10 +57,12 @@ class PaymentsServiceProvider extends AbstractServiceProvider { CompletedState::class, SystemErrorState::class, PaymentErrorState::class, + ExampleService::class, ExampleServiceWithDependencies::class, PaymentRequestService::class, DuplicatePaymentPreventionService::class, + MinimumAmountService::class, ]; /** @@ -84,13 +88,17 @@ public function register(): void { ->addArgument( HooksProxy::class ) ->addArgument( LegacyProxy::class ); + $container->addShared( MinimumAmountService::class ) + ->addArgument( LegacyProxy::class ); + $container->add( InitialState::class ) ->addArgument( StateFactory::class ) ->addArgument( OrderService::class ) ->addArgument( WC_Payments_Customer_Service::class ) ->addArgument( Level3Service::class ) ->addArgument( PaymentRequestService::class ) - ->addArgument( DuplicatePaymentPreventionService::class ); + ->addArgument( DuplicatePaymentPreventionService::class ) + ->addArgument( MinimumAmountService::class ); $container->add( ProcessedState::class ) ->addArgument( StateFactory::class ) diff --git a/src/Internal/Payment/State/InitialState.php b/src/Internal/Payment/State/InitialState.php index 3041c93f990..283a52e6af5 100644 --- a/src/Internal/Payment/State/InitialState.php +++ b/src/Internal/Payment/State/InitialState.php @@ -12,6 +12,8 @@ use WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception; use WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; +use WCPay\Exceptions\Amount_Too_Small_Exception; +use WCPay\Internal\Service\MinimumAmountService; use WCPay\Internal\Service\PaymentRequestService; use WCPay\Internal\Service\DuplicatePaymentPreventionService; use WCPay\Vendor\League\Container\Exception\ContainerException; @@ -61,6 +63,13 @@ class InitialState extends AbstractPaymentState { */ private $dpps; + /** + * Service for handling minimum amount. + * + * @var MinimumAmountService + */ + private $minimum_amount_service; + /** * Class constructor, only meant for storing dependencies. * @@ -70,6 +79,7 @@ class InitialState extends AbstractPaymentState { * @param Level3Service $level3_service Service for Level3 Data. * @param PaymentRequestService $payment_request_service Connection with the server. * @param DuplicatePaymentPreventionService $dpps Service for preventing duplicate payments. + * @param MinimumAmountService $minimum_amount_service Service for handling minimum amount. */ public function __construct( StateFactory $state_factory, @@ -77,7 +87,8 @@ public function __construct( WC_Payments_Customer_Service $customer_service, Level3Service $level3_service, PaymentRequestService $payment_request_service, - DuplicatePaymentPreventionService $dpps + DuplicatePaymentPreventionService $dpps, + MinimumAmountService $minimum_amount_service ) { parent::__construct( $state_factory ); @@ -86,6 +97,7 @@ public function __construct( $this->level3_service = $level3_service; $this->payment_request_service = $payment_request_service; $this->dpps = $dpps; + $this->minimum_amount_service = $minimum_amount_service; } /** @@ -93,11 +105,12 @@ public function __construct( * * @param PaymentRequest $request The incoming payment processing request. * - * @return AbstractPaymentState The next state. - * @throws StateTransitionException In case the completed state could not be initialized. - * @throws ContainerException When the dependency container cannot instantiate the state. - * @throws Order_Not_Found_Exception Order could not be found. - * @throws PaymentRequestException When data is not available or invalid. + * @return AbstractPaymentState The next state. + * @throws StateTransitionException In case the completed state could not be initialized. + * @throws ContainerException When the dependency container cannot instantiate the state. + * @throws Order_Not_Found_Exception Order could not be found. + * @throws PaymentRequestException When data is not available or invalid. + * @throws Amount_Too_Small_Exception When the order amount is too small. */ public function start_processing( PaymentRequest $request ) { // Populate basic details from the request. @@ -118,13 +131,21 @@ public function start_processing( PaymentRequest $request ) { if ( null !== $duplicate_payment_result ) { return $duplicate_payment_result; } + + $context = $this->get_context(); + $this->minimum_amount_service->verify_amount( + $context->get_currency(), + $context->get_amount() + ); // End multiple verification checks. // Payments are currently based on intents, request one from the API. try { - $context = $this->get_context(); - $intent = $this->payment_request_service->create_intent( $context ); + $intent = $this->payment_request_service->create_intent( $context ); $context->set_intent( $intent ); + } catch ( Amount_Too_Small_Exception $e ) { + $this->minimum_amount_service->store_amount_from_exception( $e ); + throw $e; } catch ( Invalid_Request_Parameter_Exception | Extend_Request_Exception | Immutable_Parameter_Exception $e ) { return $this->create_state( SystemErrorState::class ); } diff --git a/src/Internal/Service/MinimumAmountService.php b/src/Internal/Service/MinimumAmountService.php new file mode 100644 index 00000000000..c30a6094c4c --- /dev/null +++ b/src/Internal/Service/MinimumAmountService.php @@ -0,0 +1,90 @@ +legacy_proxy = $legacy_proxy; + } + + /** + * Extracts and stores the amount, provided by the API through an exception. + * + * @param Amount_Too_Small_Exception $exception The exception that was thrown. + */ + public function store_amount_from_exception( Amount_Too_Small_Exception $exception ): void { + $this->set_cached_amount( + $exception->get_currency(), + $exception->get_minimum_amount() + ); + } + + /** + * Checks if there is a minimum amount required for transactions in a given currency. + * + * @param string $currency Currency to check. + * @param int $amount Amount in cents to check. + * + * @throws Amount_Too_Small_Exception + */ + public function verify_amount( string $currency, int $amount ): void { + $minimum_amount = $this->get_cached_amount( $currency ); + + if ( $minimum_amount > $amount ) { + throw new Amount_Too_Small_Exception( __( 'Order amount too small', 'woocommerce-payments' ), $minimum_amount, $currency, 400 ); + } + } + + /** + * Saves the minimum amount required for transactions in a given currency. + * + * @param string $currency The currency. + * @param int $amount The minimum amount in cents. + */ + private function set_cached_amount( string $currency, int $amount ): void { + $key = self::TRANSIENT_KEY . strtolower( $currency ); + $this->legacy_proxy->call_function( 'set_transient', $key, $amount, DAY_IN_SECONDS ); + } + + /** + * Checks if there is a minimum amount required for transactions in a given currency. + * + * @param string $currency The currency to check for. + * + * @return int The minimum amount in cents. 0 if the cache has not been set, or it is an invalid value. + */ + private function get_cached_amount( string $currency ): int { + $key = self::TRANSIENT_KEY . strtolower( $currency ); + $cached = $this->legacy_proxy->call_function( 'get_transient', $key ); + + return (int) $cached; + } +} diff --git a/tests/unit/src/Internal/Payment/State/InitialStateTest.php b/tests/unit/src/Internal/Payment/State/InitialStateTest.php index 1244fe69a67..e479e6535ad 100644 --- a/tests/unit/src/Internal/Payment/State/InitialStateTest.php +++ b/tests/unit/src/Internal/Payment/State/InitialStateTest.php @@ -9,12 +9,13 @@ use WC_Helper_Intention; use WCPay\Constants\Intent_Status; +use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Internal\Payment\Exception\StateTransitionException; use WCPay\Internal\Payment\State\AuthenticationRequiredState; use WCPay\Internal\Payment\State\ProcessedState; -use Exception; use WCPay\Internal\Payment\State\DuplicateOrderDetectedState; use WCPay\Internal\Service\DuplicatePaymentPreventionService; +use WCPay\Internal\Service\MinimumAmountService; use WCPAY_UnitTestCase; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit_Utils; @@ -86,6 +87,11 @@ class InitialStateTest extends WCPAY_UnitTestCase { */ private $mock_context; + /** + * @var MinimumAmountService|MockObject + */ + private $mock_minimum_amount_service; + /** * Set up the test. */ @@ -99,6 +105,7 @@ protected function setUp(): void { $this->mock_level3_service = $this->createMock( Level3Service::class ); $this->mock_payment_request_service = $this->createMock( PaymentRequestService::class ); $this->mock_dpps = $this->createMock( DuplicatePaymentPreventionService::class ); + $this->mock_minimum_amount_service = $this->createMock( MinimumAmountService::class ); $this->sut = new InitialState( $this->mock_state_factory, @@ -106,7 +113,8 @@ protected function setUp(): void { $this->mock_customer_service, $this->mock_level3_service, $this->mock_payment_request_service, - $this->mock_dpps + $this->mock_dpps, + $this->mock_minimum_amount_service ); $this->sut->set_context( $this->mock_context ); @@ -134,6 +142,7 @@ protected function setUp(): void { $this->mock_level3_service, $this->mock_payment_request_service, $this->mock_dpps, + $this->mock_minimum_amount_service, ] ) ->getMock(); @@ -152,6 +161,11 @@ public function test_start_processing() { // Verify that the context is populated. $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 100 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'usd' ); + $this->mocked_sut->expects( $this->once() )->method( 'process_duplicate_order' )->willReturn( null ); $this->mocked_sut->expects( $this->once() )->method( 'process_duplicate_payment' )->willReturn( null ); @@ -175,6 +189,31 @@ public function test_start_processing() { $this->assertSame( $mock_completed_state, $result ); } + public function test_start_processing_will_throw_exception_when_minimum_amount_occurs() { + $mock_request = $this->createMock( PaymentRequest::class ); + $small_amount_exception = new Amount_Too_Small_Exception( 'Amount too small', 50, 'EUR', 400 ); + + $this->mock_payment_request_service + ->expects( $this->once() ) + ->method( 'create_intent' ) + ->with( $this->mock_context ) + ->willThrowException( $small_amount_exception ); + + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 1 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'eur' ); + + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + $this->mock_minimum_amount_service->expects( $this->once() )->method( 'store_amount_from_exception' ) + ->with( $small_amount_exception ); + + $this->expectExceptionObject( $small_amount_exception ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); + } + public function test_start_processing_will_transition_to_error_state_when_api_exception_occurs() { $mock_request = $this->createMock( PaymentRequest::class ); $mock_error_state = $this->createMock( SystemErrorState::class ); @@ -189,6 +228,10 @@ public function test_start_processing_will_transition_to_error_state_when_api_ex $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 100 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'usd' ); + $this->mock_state_factory->expects( $this->once() ) ->method( 'create_state' ) ->with( SystemErrorState::class, $this->mock_context ) @@ -214,6 +257,10 @@ public function test_processing_will_transition_to_auth_required_state() { $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + // Make sure that the minimum amount verification is properly called. + $this->mock_context->expects( $this->once() )->method( 'get_amount' )->willReturn( 100 ); + $this->mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( 'usd' ); + // Before the transition, the order service should update the order. $this->mock_context->expects( $this->once() ) ->method( 'get_order_id' ) @@ -245,7 +292,6 @@ public function test_start_processing_throw_exceptions_due_to_invalid_phone() { // Act. $this->mocked_sut->start_processing( $mock_request ); - } public function provider_start_processing_then_detect_duplicates() { @@ -278,6 +324,33 @@ public function test_start_processing_then_detect_duplicates( bool $is_duplicate $this->assertInstanceOf( $return_state_class, $result ); } + public function test_start_processing_throws_exception_due_to_minimum_amount() { + $mock_request = $this->createMock( PaymentRequest::class ); + $small_amount_exception = new Amount_Too_Small_Exception( 'Amount too small', 50, 'EUR', 400 ); + + // Arrange mocks. + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); + $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_currency' ) + ->willReturn( 'EUR' ); + + $this->mock_context->expects( $this->once() ) + ->method( 'get_amount' ) + ->willReturn( 50 ); + + $this->mock_minimum_amount_service->expects( $this->once() ) + ->method( 'verify_amount' ) + ->with( 'EUR', 50 ) + ->willThrowException( $small_amount_exception ); + + $this->expectExceptionObject( $small_amount_exception ); + + // Act. + $this->mocked_sut->start_processing( $mock_request ); + } + public function test_populate_context_from_request() { $payment_method = new NewPaymentMethod( 'pm_123' ); $fingerprint = 'fingerprint'; diff --git a/tests/unit/src/Internal/Service/MinimumAmountServiceTest.php b/tests/unit/src/Internal/Service/MinimumAmountServiceTest.php new file mode 100644 index 00000000000..9d834026e30 --- /dev/null +++ b/tests/unit/src/Internal/Service/MinimumAmountServiceTest.php @@ -0,0 +1,100 @@ +mock_legacy_proxy = $this->createMock( LegacyProxy::class ); + $this->sut = new MinimumAmountService( $this->mock_legacy_proxy ); + } + + public function test_store_amount_from_exception() { + $exception = new Amount_Too_Small_Exception( 'Amount too small', 100, 'EUR', 400 ); + + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'set_transient', 'wcpay_minimum_amount_eur', 100, DAY_IN_SECONDS ); + + $this->sut->store_amount_from_exception( $exception ); + } + + public function test_verify_amount_returns_void() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_transient', 'wcpay_minimum_amount_eur' ) + ->willReturn( 100 ); + + $this->sut->verify_amount( 'EUR', 150 ); + } + + public function test_verify_amount_throw_exception() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_transient', 'wcpay_minimum_amount_eur' ) + ->willReturn( 100 ); + + $this->expectException( Amount_Too_Small_Exception::class ); + $this->expectExceptionMessage( 'Order amount too small' ); + + $this->sut->verify_amount( 'EUR', 50 ); + } + + public function test_set_cached_amount() { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'set_transient', 'wcpay_minimum_amount_usd', 100, DAY_IN_SECONDS ); + + \PHPUnit_Utils::call_method( $this->sut, 'set_cached_amount', [ 'USD', 100 ] ); + } + + public function provider_get_cached_amount() { + return [ + 'Transient not set' => [ false, 0 ], + 'Transient invalid value' => [ null, 0 ], + 'Transient valid value ' => [ 123, 123 ], + ]; + } + + /** + * @dataProvider provider_get_cached_amount + */ + public function test_get_cached_amount( $transient_value, $expected ) { + $this->mock_legacy_proxy->expects( $this->once() ) + ->method( 'call_function' ) + ->with( 'get_transient', 'wcpay_minimum_amount_eur' ) + ->willReturn( $transient_value ); + + \PHPUnit_Utils::call_method( $this->sut, 'get_cached_amount', [ 'EUR' ] ); + } +} diff --git a/tests/unit/src/Internal/Service/SessionServiceTest.php b/tests/unit/src/Internal/Service/SessionServiceTest.php index b2218754ca0..41be39e2f27 100644 --- a/tests/unit/src/Internal/Service/SessionServiceTest.php +++ b/tests/unit/src/Internal/Service/SessionServiceTest.php @@ -24,13 +24,11 @@ class SessionServiceTest extends WCPAY_UnitTestCase { */ private $sut; - /** * @var LegacyProxy|MockObject */ private $mock_legacy_proxy; - /** * Set up the test. */ From 2c4a9b6756dea2f11e687633e6e8f077392ad479 Mon Sep 17 00:00:00 2001 From: Alefe Souza Date: Tue, 14 Nov 2023 16:51:58 -0300 Subject: [PATCH 06/61] Add AutomateWoo - Refer A Friend Add-On support on WooPay (#7300) --- ...-woopay-automatewoo-refer-a-friend-support | 4 ++ .../class-woopay-adapted-extensions.php | 39 +++++++++++++++++++ includes/woopay/class-woopay-session.php | 16 ++++++++ 3 files changed, 59 insertions(+) create mode 100644 changelog/fix-add-woopay-automatewoo-refer-a-friend-support diff --git a/changelog/fix-add-woopay-automatewoo-refer-a-friend-support b/changelog/fix-add-woopay-automatewoo-refer-a-friend-support new file mode 100644 index 00000000000..4a7f164310f --- /dev/null +++ b/changelog/fix-add-woopay-automatewoo-refer-a-friend-support @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add AutomateWoo - Refer A Friend Add-On support on WooPay. diff --git a/includes/woopay/class-woopay-adapted-extensions.php b/includes/woopay/class-woopay-adapted-extensions.php index e5a602dca9f..cd6ed56f2d0 100644 --- a/includes/woopay/class-woopay-adapted-extensions.php +++ b/includes/woopay/class-woopay-adapted-extensions.php @@ -180,6 +180,14 @@ public function get_extension_data() { ]; } + if ( $this->is_automate_woo_referrals_enabled() ) { + $advocate_id = $this->get_automate_woo_advocate_id_from_cookie(); + + $extension_data[ 'automatewoo-referrals' ] = [ + 'advocate_id' => $advocate_id, + ]; + } + return $extension_data; } @@ -236,4 +244,35 @@ class_exists( 'AFWC_API' ) && method_exists( 'AFWC_API', 'get_instance' ) && method_exists( 'AFWC_API', 'track_conversion' ); } + + /** + * Check if Automate Woo Referrals is enabled and + * its functions used on WCPay are available. + * + * @psalm-suppress UndefinedClass + * @psalm-suppress UndefinedFunction + * + * @return boolean + */ + private function is_automate_woo_referrals_enabled() { + return function_exists( 'AW_Referrals' ) && + method_exists( AW_Referrals(), 'options' ) && + AW_Referrals()->options()->type === 'link' && + class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) && + method_exists( \AutomateWoo\Referrals\Referral_Manager::class, 'get_advocate_key_from_cookie' ) && class_exists( 'AFWC_API' ) && + method_exists( 'AFWC_API', 'get_instance' ) && + method_exists( 'AFWC_API', 'track_conversion' ); + } + + /** + * Get AutomateWoo advocate id from cookie. + * + * @psalm-suppress UndefinedClass + * + * @return string|null + */ + private function get_automate_woo_advocate_id_from_cookie() { + $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie(); + return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null; + } } diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php index 29952ce5f9f..5fb2af81ef5 100644 --- a/includes/woopay/class-woopay-session.php +++ b/includes/woopay/class-woopay-session.php @@ -61,6 +61,8 @@ public static function init() { add_action( 'woopay_restore_order_customer_id', [ __CLASS__, 'restore_order_customer_id_from_requests_with_verified_email' ] ); register_deactivation_hook( WCPAY_PLUGIN_FILE, [ __CLASS__, 'run_and_remove_woopay_restore_order_customer_id_schedules' ] ); + + add_filter( 'automatewoo/referrals/referred_order_advocate', [ __CLASS__, 'automatewoo_refer_a_friend_referral_from_parameter' ] ); } /** @@ -256,6 +258,20 @@ public static function run_and_remove_woopay_restore_order_customer_id_schedules wp_clear_scheduled_hook( 'woopay_restore_order_customer_id' ); } + /** + * Fix for AutomateWoo - Refer A Friend Add-on + * plugin when using link referrals. + */ + public static function automatewoo_refer_a_friend_referral_from_parameter() { + if ( empty( $_GET['automatewoo_referral_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + return false; + } + + $automatewoo_referral = (int) wc_clean( wp_unslash( $_GET['automatewoo_referral_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + + return $automatewoo_referral; + } + /** * Returns the payload from a cart token. * From 7b92882f22769af088f07e47647b73dff6bc80fc Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:25:26 -0600 Subject: [PATCH 07/61] Add client's user agent to Tracks props (#7644) --- changelog/add-ua-field-to-tracks | 4 ++++ includes/class-woopay-tracker.php | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 changelog/add-ua-field-to-tracks diff --git a/changelog/add-ua-field-to-tracks b/changelog/add-ua-field-to-tracks new file mode 100644 index 00000000000..14f18a44396 --- /dev/null +++ b/changelog/add-ua-field-to-tracks @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add client user-agent value to Tracks event props diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index 564fd0c5033..a1c638bb36d 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -288,6 +288,11 @@ private function tracks_build_event_obj( $user, $event_name, $properties = [] ) $properties['test_mode'] = WC_Payments::mode()->is_test() ? 1 : 0; $properties['wcpay_version'] = WCPAY_VERSION_NUMBER; + // Add client's user agent to the event properties. + if ( !empty( $_SERVER['HTTP_USER_AGENT'] ) ) { + $properties['_via_ua'] = $_SERVER['HTTP_USER_AGENT']; + } + $blog_details = [ 'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ), ]; From f6306651cf442fcbd2ddec61ace230fd5d1765a5 Mon Sep 17 00:00:00 2001 From: Hsing-yu Flowers Date: Tue, 14 Nov 2023 18:32:22 -0500 Subject: [PATCH 08/61] Add invalid prodcut id error check (#7666) --- changelog/fix-product-not-found-error | 4 ++++ ...ayments-payment-request-button-handler.php | 18 +++++++++++++++-- ...lass-wc-payments-woopay-button-handler.php | 20 ++++++++++++++++--- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 changelog/fix-product-not-found-error diff --git a/changelog/fix-product-not-found-error b/changelog/fix-product-not-found-error new file mode 100644 index 00000000000..0e699fd1860 --- /dev/null +++ b/changelog/fix-product-not-found-error @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add invalid prodcut id error check diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 414ce0fca37..af7f9c1abe3 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -1144,9 +1144,23 @@ public function ajax_add_to_cart() { WC()->shipping->reset_shipping(); - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + wp_send_json( + [ + 'error' => [ + 'code' => 'invalid_product_id', + 'message' => __( 'Invalid product id', 'woocommerce-payments' ), + ], + ], + 404 + ); + return; + } + $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] ); - $product = wc_get_product( $product_id ); $product_type = $product->get_type(); // First empty the cart to prevent wrong calculation. diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index a7fa287761c..18367161543 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -226,9 +226,23 @@ public function ajax_add_to_cart() { WC()->shipping->reset_shipping(); - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; - $quantity = ! isset( $_POST['quantity'] ) ? 1 : absint( $_POST['quantity'] ); - $product = wc_get_product( $product_id ); + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $quantity = ! isset( $_POST['quantity'] ) ? 1 : absint( $_POST['quantity'] ); + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + wp_send_json( + [ + 'error' => [ + 'code' => 'invalid_product_id', + 'message' => __( 'Invalid product id', 'woocommerce-payments' ), + ], + ], + 404 + ); + return; + } + $product_type = $product->get_type(); // First empty the cart to prevent wrong calculation. From 4233d9cd0f373d886c7457cb65d508aa4cbf1dfa Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:28:23 +1100 Subject: [PATCH 09/61] Introduce `wcpay_payment_request_is_cart_supported` filter. (#7439) Co-authored-by: Francesco --- changelog/dev-7438-payment-cart-supported-filter | 4 ++++ ...ss-wc-payments-payment-request-button-handler.php | 12 ++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 changelog/dev-7438-payment-cart-supported-filter diff --git a/changelog/dev-7438-payment-cart-supported-filter b/changelog/dev-7438-payment-cart-supported-filter new file mode 100644 index 00000000000..36514fd3d18 --- /dev/null +++ b/changelog/dev-7438-payment-cart-supported-filter @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Introduce filter `wcpay_payment_request_is_cart_supported`. Allow plugins to conditionally disable payment request buttons on cart and checkout pages containing products that do not support them. diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index af7f9c1abe3..0ae12077830 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -596,6 +596,18 @@ public function has_allowed_items_in_cart() { return false; } + /** + * Filter whether product supports Payment Request Button on cart page. + * + * @since 6.9.0 + * + * @param boolean $is_supported Whether product supports Payment Request Button on cart page. + * @param object $_product Product object. + */ + if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) { + return false; + } + // Trial subscriptions with shipping are not supported. if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) { return false; From 61b72e7424d8a2321582221651e7ddd572fdb69c Mon Sep 17 00:00:00 2001 From: Vlad Olaru Date: Wed, 15 Nov 2023 13:02:22 +0200 Subject: [PATCH 10/61] Silence QIT semgrep false positives (#7719) --- changelog/dev-silence-qit-false-positives | 5 +++++ includes/admin/class-wc-rest-user-exists-controller.php | 4 ++++ ...s-wc-payments-express-checkout-button-display-handler.php | 2 ++ includes/multi-currency/CurrencySwitcherBlock.php | 3 +++ 4 files changed, 14 insertions(+) create mode 100644 changelog/dev-silence-qit-false-positives diff --git a/changelog/dev-silence-qit-false-positives b/changelog/dev-silence-qit-false-positives new file mode 100644 index 00000000000..fe09582a561 --- /dev/null +++ b/changelog/dev-silence-qit-false-positives @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Just silencing QIT false positives. + + diff --git a/includes/admin/class-wc-rest-user-exists-controller.php b/includes/admin/class-wc-rest-user-exists-controller.php index da673cc0e08..bd9d1eeb6aa 100644 --- a/includes/admin/class-wc-rest-user-exists-controller.php +++ b/includes/admin/class-wc-rest-user-exists-controller.php @@ -40,6 +40,10 @@ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, + // Silence the nosemgrep audit rule because this controller (and its routes) is not being used. + // This file is only left to avoid plugin upgrade errors. + // See this issue for a permanent fix: https://github.com/Automattic/woocommerce-payments/issues/6304 + // nosemgrep: audit.php.wp.security.rest-route.permission-callback.return-true -- reason: this controller is not being used. [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'user_exists' ], diff --git a/includes/class-wc-payments-express-checkout-button-display-handler.php b/includes/class-wc-payments-express-checkout-button-display-handler.php index 5ee2e42c03f..cdc04b3fd69 100644 --- a/includes/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/class-wc-payments-express-checkout-button-display-handler.php @@ -149,6 +149,8 @@ function( $js_config ) use ( $order ) { $session_email = is_array( $customer ) && isset( $customer['email'] ) ? $customer['email'] : ''; } + // Silence the filter_input warning because we are sanitizing the input with sanitize_email(). + // nosemgrep: audit.php.lang.misc.filter-input-no-filter $user_email = sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) ?? $session_email; $js_config['order_id'] = $order->get_id(); diff --git a/includes/multi-currency/CurrencySwitcherBlock.php b/includes/multi-currency/CurrencySwitcherBlock.php index 2dd7029386f..840530e32a2 100644 --- a/includes/multi-currency/CurrencySwitcherBlock.php +++ b/includes/multi-currency/CurrencySwitcherBlock.php @@ -149,6 +149,9 @@ public function render_block_widget( $block_attributes ): string { } $widget_content .= ''; + + // Silence XSS warning because we are manually constructing the content and escaping everything above. + // nosemgrep: audit.php.wp.security.xss.block-attr -- reason: we are manually constructing the content and escaping everything above. return $widget_content; } From ed5db4a5a5fa43852fa599cfcd49195a2c89ea36 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Wed, 15 Nov 2023 11:06:51 +0000 Subject: [PATCH 11/61] Adds a new option which reflects the modal being dismissed. (#7693) --- ...-prompt-to-complete-onboarding-shows-twice | 4 +++ client/globals.d.ts | 1 + .../index.tsx | 21 +++++++++++++++- .../test/index.test.tsx | 24 ++++++++++++++++++ includes/class-wc-payments-account.php | 25 +++++++++++++------ .../class-wc-payments-onboarding-service.php | 24 ++++++++++++++++-- 6 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 changelog/7683-prompt-to-complete-onboarding-shows-twice diff --git a/changelog/7683-prompt-to-complete-onboarding-shows-twice b/changelog/7683-prompt-to-complete-onboarding-shows-twice new file mode 100644 index 00000000000..b60419ea0a2 --- /dev/null +++ b/changelog/7683-prompt-to-complete-onboarding-shows-twice @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adds new option to track dismissal of PO eligibility modal. diff --git a/client/globals.d.ts b/client/globals.d.ts index 7132f49949f..eaffe24b8b6 100644 --- a/client/globals.d.ts +++ b/client/globals.d.ts @@ -84,6 +84,7 @@ declare global { isNewFlowEnabled: boolean; isEnabled: boolean; isComplete: boolean; + isEligibilityModalDismissed: boolean; }; enabledPaymentMethods: string[]; accountDefaultCurrency: string; diff --git a/client/overview/modal/progressive-onboarding-eligibility/index.tsx b/client/overview/modal/progressive-onboarding-eligibility/index.tsx index 085114876cb..6e16ac57a1f 100644 --- a/client/overview/modal/progressive-onboarding-eligibility/index.tsx +++ b/client/overview/modal/progressive-onboarding-eligibility/index.tsx @@ -6,6 +6,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { Button, Modal } from '@wordpress/components'; import { Icon, store, widget, tool } from '@wordpress/icons'; +import { useDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -16,9 +17,25 @@ import './style.scss'; const ProgressiveOnboardingEligibilityModal: React.FC = () => { const [ modalVisible, setModalVisible ] = useState( true ); + const [ modalDismissed, setModalDismissed ] = useState( + wcpaySettings.progressiveOnboarding?.isEligibilityModalDismissed + ); + + const { updateOptions } = useDispatch( 'wc/admin/options' ); + + const markAsDismissed = async () => { + setModalDismissed( true ); + + // Update the option to mark the modal as dismissed. + await updateOptions( { + wcpay_onboarding_eligibility_modal_dismissed: true, + } ); + }; const handleSetup = () => { trackEligibilityModalClosed( 'setup_deposits' ); + + // Note: we don't need to update the option here because it will be handled upon redirect to the connect URL. window.location.href = addQueryArgs( wcpaySettings.connectUrl, { collect_payout_requirements: true, } ); @@ -26,11 +43,13 @@ const ProgressiveOnboardingEligibilityModal: React.FC = () => { const handlePaymentsOnly = () => { trackEligibilityModalClosed( 'enable_payments_only' ); + markAsDismissed(); setModalVisible( false ); }; const handleDismiss = () => { trackEligibilityModalClosed( 'dismiss' ); + markAsDismissed(); setModalVisible( false ); }; @@ -43,7 +62,7 @@ const ProgressiveOnboardingEligibilityModal: React.FC = () => { ?.remove(); }, [] ); - if ( ! modalVisible ) return null; + if ( ! modalVisible || modalDismissed ) return null; return ( ( { + useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), +} ) ); + declare const global: { wcpaySettings: { connectUrl: string; + progressiveOnboarding?: { + isEligibilityModalDismissed: boolean; + }; }; }; describe( 'Progressive Onboarding Eligibility Modal', () => { + global.wcpaySettings = { + connectUrl: 'https://wcpay.test/connect', + progressiveOnboarding: { + isEligibilityModalDismissed: false, + }, + }; + it( 'modal is open by default', () => { render( ); @@ -30,6 +44,13 @@ describe( 'Progressive Onboarding Eligibility Modal', () => { } ); it( 'closes modal when enable button is clicked', () => { + global.wcpaySettings = { + connectUrl: 'https://wcpay.test/connect', + progressiveOnboarding: { + isEligibilityModalDismissed: false, + }, + }; + render( ); user.click( @@ -49,6 +70,9 @@ describe( 'Progressive Onboarding Eligibility Modal', () => { it( 'calls `handleSetup` when setup button is clicked', () => { global.wcpaySettings = { connectUrl: 'https://wcpay.test/connect', + progressiveOnboarding: { + isEligibilityModalDismissed: false, + }, }; Object.defineProperty( window, 'location', { diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 30a409752cd..0b982e9b4cf 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -517,9 +517,10 @@ public function get_fees() { public function get_progressive_onboarding_details(): array { $account = $this->get_cached_account_data(); return [ - 'isEnabled' => $account['progressive_onboarding']['is_enabled'] ?? false, - 'isComplete' => $account['progressive_onboarding']['is_complete'] ?? false, - 'isNewFlowEnabled' => WC_Payments_Utils::should_use_progressive_onboarding_flow(), + 'isEnabled' => $account['progressive_onboarding']['is_enabled'] ?? false, + 'isComplete' => $account['progressive_onboarding']['is_complete'] ?? false, + 'isNewFlowEnabled' => WC_Payments_Utils::should_use_progressive_onboarding_flow(), + 'isEligibilityModalDismissed' => get_option( WC_Payments_Onboarding_Service::ONBOARDING_ELIGIBILITY_MODAL_OPTION, false ), ]; } @@ -1275,8 +1276,13 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = // Clear account transient when generating Stripe's oauth data. $this->clear_cache(); + // Flags to enable progressive onboarding and collect payout requirements. + $progressive = ! empty( $_GET['progressive'] ) && 'true' === $_GET['progressive']; + $collect_payout_requirements = ! empty( $_GET['collect_payout_requirements'] ) && 'true' === $_GET['collect_payout_requirements']; + // Enable dev mode if the test_mode query param is set. $test_mode = isset( $_GET['test_mode'] ) ? boolval( wc_clean( wp_unslash( $_GET['test_mode'] ) ) ) : false; + if ( $test_mode ) { WC_Payments_Onboarding_Service::set_test_mode( true ); } @@ -1284,15 +1290,20 @@ private function init_stripe_onboarding( $wcpay_connect_from, $additional_args = // Clear persisted onboarding flow state. WC_Payments_Onboarding_Service::clear_onboarding_flow_state(); + if ( ! $collect_payout_requirements ) { + // Clear onboarding related account options if this is an initial onboarding attempt. + WC_Payments_Onboarding_Service::clear_account_options(); + } else { + // Since we assume user has already either gotten here from the eligibility modal, + // or has already dismissed it, we should set the modal as dismissed so it doesn't display again. + WC_Payments_Onboarding_Service::set_onboarding_eligibility_modal_dismissed(); + } + $return_url = $this->get_onboarding_return_url( $wcpay_connect_from ); if ( ! empty( $additional_args ) ) { $return_url = add_query_arg( $additional_args, $return_url ); } - // Flags to enable progressive onboarding and collect payout requirements. - $progressive = ! empty( $_GET['progressive'] ) && 'true' === $_GET['progressive']; - $collect_payout_requirements = ! empty( $_GET['collect_payout_requirements'] ) && 'true' === $_GET['collect_payout_requirements']; - // Onboarding self-assessment data. $self_assessment_data = isset( $_GET['self_assessment'] ) ? wc_clean( wp_unslash( $_GET['self_assessment'] ) ) : []; if ( $self_assessment_data ) { diff --git a/includes/class-wc-payments-onboarding-service.php b/includes/class-wc-payments-onboarding-service.php index 904462c37ef..42fad9d1af3 100644 --- a/includes/class-wc-payments-onboarding-service.php +++ b/includes/class-wc-payments-onboarding-service.php @@ -17,8 +17,9 @@ */ class WC_Payments_Onboarding_Service { - const TEST_MODE_OPTION = 'wcpay_onboarding_test_mode'; - const ONBOARDING_FLOW_STATE_OPTION = 'wcpay_onboarding_flow_state'; + const TEST_MODE_OPTION = 'wcpay_onboarding_test_mode'; + const ONBOARDING_FLOW_STATE_OPTION = 'wcpay_onboarding_flow_state'; + const ONBOARDING_ELIGIBILITY_MODAL_OPTION = 'wcpay_onboarding_eligibility_modal_dismissed'; /** * Client for making requests to the WooCommerce Payments API @@ -227,6 +228,25 @@ public static function clear_onboarding_flow_state(): bool { return delete_option( self::ONBOARDING_FLOW_STATE_OPTION ); } + /** + * Clear any account options we may want to reset when a new onboarding flow is initialised. + * Currently, just deletes the option which stores whether the eligibility modal has been dismissed. + * + * @return boolean Whether the option was deleted successfully. + */ + public static function clear_account_options(): bool { + return delete_option( self::ONBOARDING_ELIGIBILITY_MODAL_OPTION ); + } + + /** + * Set the onboarding eligibility modal dismissed option to true. + * + * @return void + */ + public static function set_onboarding_eligibility_modal_dismissed(): void { + update_option( self::ONBOARDING_ELIGIBILITY_MODAL_OPTION, true ); + } + /** * Set onboarding test mode. * Will also switch WC_Payments mode immediately. From 22656a50b3703b270fedaacd63324c4ebabde191 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 15 Nov 2023 03:32:30 -0800 Subject: [PATCH 12/61] chore: change PRB default height (#7712) --- changelog/chore-change-prb-default-height | 4 ++ ...general-payment-request-button-settings.js | 4 +- .../payment-request-button-preview.js | 6 +- .../express-checkout-settings/test/index.js | 2 +- .../test/payment-request-settings.test.js | 6 +- .../test/woopay-settings.test.js | 2 +- includes/class-wc-payment-gateway-wcpay.php | 8 +-- ...ayments-payment-request-button-handler.php | 2 +- includes/class-wc-payments.php | 2 + ...ed-payment-request-button-sizes-update.php | 69 +++++++++++++++++++ ...s-wc-rest-payments-settings-controller.php | 6 +- .../test-class-wc-payment-gateway-wcpay.php | 2 +- ...ayments-payment-request-button-handler.php | 2 +- 13 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 changelog/chore-change-prb-default-height create mode 100644 includes/migrations/class-allowed-payment-request-button-sizes-update.php diff --git a/changelog/chore-change-prb-default-height b/changelog/chore-change-prb-default-height new file mode 100644 index 00000000000..20e1ce8563c --- /dev/null +++ b/changelog/chore-change-prb-default-height @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +chore: change PRB default height for new installations diff --git a/client/settings/express-checkout-settings/general-payment-request-button-settings.js b/client/settings/express-checkout-settings/general-payment-request-button-settings.js index 773562d7dd2..c5a3f231b69 100644 --- a/client/settings/express-checkout-settings/general-payment-request-button-settings.js +++ b/client/settings/express-checkout-settings/general-payment-request-button-settings.js @@ -40,11 +40,11 @@ const buttonSizeOptions = [ { label: makeButtonSizeText( __( - 'Default {{helpText}}(40 px){{/helpText}}', + 'Small {{helpText}}(40 px){{/helpText}}', 'woocommerce-payments' ) ), - value: 'default', + value: 'small', }, { label: makeButtonSizeText( diff --git a/client/settings/express-checkout-settings/payment-request-button-preview.js b/client/settings/express-checkout-settings/payment-request-button-preview.js index ed5a865fee0..da09e6c7cbd 100644 --- a/client/settings/express-checkout-settings/payment-request-button-preview.js +++ b/client/settings/express-checkout-settings/payment-request-button-preview.js @@ -56,7 +56,7 @@ const BrowserHelpText = () => { }; const buttonSizeToPxMap = { - default: 40, + small: 40, medium: 48, large: 56, }; @@ -120,7 +120,7 @@ const PaymentRequestButtonPreview = () => { theme: theme, height: `${ buttonSizeToPxMap[ size ] || - buttonSizeToPxMap.default + buttonSizeToPxMap.medium }px`, size, } } @@ -142,7 +142,7 @@ const PaymentRequestButtonPreview = () => { theme: theme, height: `${ buttonSizeToPxMap[ size ] || - buttonSizeToPxMap.default + buttonSizeToPxMap.medium }px`, }, }, diff --git a/client/settings/express-checkout-settings/test/index.js b/client/settings/express-checkout-settings/test/index.js index 73d6af2e49d..b9b55537fb0 100644 --- a/client/settings/express-checkout-settings/test/index.js +++ b/client/settings/express-checkout-settings/test/index.js @@ -24,7 +24,7 @@ jest.mock( '../../../data', () => ( { useWooPayCustomMessage: jest.fn().mockReturnValue( [ 'test', jest.fn() ] ), useWooPayStoreLogo: jest.fn().mockReturnValue( [ 'test', jest.fn() ] ), usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'buy' ] ), - usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), + usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayLocations: jest .fn() diff --git a/client/settings/express-checkout-settings/test/payment-request-settings.test.js b/client/settings/express-checkout-settings/test/payment-request-settings.test.js index d0d4360b6b1..3b21c6bf3a6 100644 --- a/client/settings/express-checkout-settings/test/payment-request-settings.test.js +++ b/client/settings/express-checkout-settings/test/payment-request-settings.test.js @@ -24,7 +24,7 @@ jest.mock( '../../../data', () => ( { usePaymentRequestEnabledSettings: jest.fn(), usePaymentRequestLocations: jest.fn(), usePaymentRequestButtonType: jest.fn().mockReturnValue( [ 'buy' ] ), - usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'default' ] ), + usePaymentRequestButtonSize: jest.fn().mockReturnValue( [ 'small' ] ), usePaymentRequestButtonTheme: jest.fn().mockReturnValue( [ 'dark' ] ), useWooPayEnabledSettings: jest.fn(), useWooPayShowIncompatibilityNotice: jest.fn().mockReturnValue( false ), @@ -147,7 +147,7 @@ describe( 'PaymentRequestSettings', () => { // confirm default values expect( screen.getByLabelText( 'Buy with' ) ).toBeChecked(); - expect( screen.getByLabelText( 'Default (40 px)' ) ).toBeChecked(); + expect( screen.getByLabelText( 'Small (40 px)' ) ).toBeChecked(); expect( screen.getByLabelText( /Dark/ ) ).toBeChecked(); } ); @@ -191,7 +191,7 @@ describe( 'PaymentRequestSettings', () => { setButtonTypeMock, ] ); usePaymentRequestButtonSize.mockReturnValue( [ - 'default', + 'small', setButtonSizeMock, ] ); usePaymentRequestButtonTheme.mockReturnValue( [ diff --git a/client/settings/express-checkout-settings/test/woopay-settings.test.js b/client/settings/express-checkout-settings/test/woopay-settings.test.js index fef25431a56..5846e397ad6 100644 --- a/client/settings/express-checkout-settings/test/woopay-settings.test.js +++ b/client/settings/express-checkout-settings/test/woopay-settings.test.js @@ -94,7 +94,7 @@ describe( 'WooPaySettings', () => { ); usePaymentRequestButtonSize.mockReturnValue( - getMockPaymentRequestButtonSize( [ 'default' ], jest.fn() ) + getMockPaymentRequestButtonSize( [ 'small' ], jest.fn() ) ); usePaymentRequestButtonTheme.mockReturnValue( diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 541b1ba53c4..97f4377cfa4 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -374,12 +374,12 @@ public function __construct( 'title' => __( 'Size of the button displayed for Express Checkouts', 'woocommerce-payments' ), 'type' => 'select', 'description' => __( 'Select the size of the button.', 'woocommerce-payments' ), - 'default' => 'default', + 'default' => 'medium', 'desc_tip' => true, 'options' => [ - 'default' => __( 'Default', 'woocommerce-payments' ), - 'medium' => __( 'Medium', 'woocommerce-payments' ), - 'large' => __( 'Large', 'woocommerce-payments' ), + 'small' => __( 'Small', 'woocommerce-payments' ), + 'medium' => __( 'Medium', 'woocommerce-payments' ), + 'large' => __( 'Large', 'woocommerce-payments' ), ], ], 'platform_checkout_button_locations' => [ diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 0ae12077830..c7e291af92a 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -197,7 +197,7 @@ public function get_button_height() { return '56'; } - // for the "default" and "catch-all" scenarios. + // for the "default"/"small" and "catch-all" scenarios. return '40'; } diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index a167138b7b2..23a19991182 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -601,10 +601,12 @@ public static function init() { add_action( 'woocommerce_admin_field_payment_gateways', [ __CLASS__, 'hide_gateways_on_settings_page' ], 5 ); require_once __DIR__ . '/migrations/class-allowed-payment-request-button-types-update.php'; + require_once __DIR__ . '/migrations/class-allowed-payment-request-button-sizes-update.php'; require_once __DIR__ . '/migrations/class-update-service-data-from-server.php'; require_once __DIR__ . '/migrations/class-track-upe-status.php'; require_once __DIR__ . '/migrations/class-delete-active-woopay-webhook.php'; add_action( 'woocommerce_woocommerce_payments_updated', [ new Allowed_Payment_Request_Button_Types_Update( self::get_gateway() ), 'maybe_migrate' ] ); + add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Allowed_Payment_Request_Button_Sizes_Update( self::get_gateway() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ new \WCPay\Migrations\Update_Service_Data_From_Server( self::get_account_service() ), 'maybe_migrate' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ '\WCPay\Migrations\Track_Upe_Status', 'maybe_track' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ '\WCPay\Migrations\Delete_Active_WooPay_Webhook', 'maybe_delete' ] ); diff --git a/includes/migrations/class-allowed-payment-request-button-sizes-update.php b/includes/migrations/class-allowed-payment-request-button-sizes-update.php new file mode 100644 index 00000000000..4be05fa2bf2 --- /dev/null +++ b/includes/migrations/class-allowed-payment-request-button-sizes-update.php @@ -0,0 +1,69 @@ +gateway = $gateway; + } + + /** + * Only execute the migration if not applied yet. + */ + public function maybe_migrate() { + $previous_version = get_option( 'woocommerce_woocommerce_payments_version' ); + if ( version_compare( self::VERSION_SINCE, $previous_version, '>' ) ) { + $this->migrate(); + } + } + + /** + * Does the actual migration as described in the class docblock. + */ + private function migrate() { + $button_size = $this->gateway->get_option( 'payment_request_button_size' ); + if ( 'default' === $button_size ) { + $this->gateway->update_option( + 'payment_request_button_size', + 'small' + ); + } + + } +} diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php index 9bb51089ac7..77d6450563f 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php @@ -624,14 +624,14 @@ public function test_update_settings_saves_payment_request_button_theme() { } public function test_update_settings_saves_payment_request_button_size() { - $this->assertEquals( 'default', $this->gateway->get_option( 'payment_request_button_size' ) ); + $this->assertEquals( 'medium', $this->gateway->get_option( 'payment_request_button_size' ) ); $request = new WP_REST_Request(); - $request->set_param( 'payment_request_button_size', 'medium' ); + $request->set_param( 'payment_request_button_size', 'default' ); $this->controller->update_settings( $request ); - $this->assertEquals( 'medium', $this->gateway->get_option( 'payment_request_button_size' ) ); + $this->assertEquals( 'default', $this->gateway->get_option( 'payment_request_button_size' ) ); } public function test_update_settings_saves_payment_request_button_type() { diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index fe235a0c7fa..c4aeaa6b275 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -1333,7 +1333,7 @@ public function test_payment_request_form_field_defaults() { $this->wcpay_gateway->get_option( 'payment_request_button_locations' ) ); $this->assertEquals( - 'default', + 'medium', $this->wcpay_gateway->get_option( 'payment_request_button_size' ) ); diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php index 4f32c569d4e..b0e5dd3fd90 100644 --- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php +++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php @@ -252,7 +252,7 @@ public function test_get_button_settings() { [ 'type' => 'buy', 'theme' => 'dark', - 'height' => '40', + 'height' => '48', 'locale' => 'en', 'branded_type' => 'long', ], From 72e4fb33ee7a567961b81f24a9c2f370b9ddf5b8 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 15 Nov 2023 04:20:43 -0800 Subject: [PATCH 13/61] Revert "feat: add UPE appearance filter (#7578)" (#7722) --- changelog/feat-add-upe-appearance-filter | 4 ---- client/checkout/api/index.js | 3 ++- .../upe-deferred-intent-creation/payment-elements.js | 7 ++----- client/checkout/blocks/upe-fields.js | 7 ++----- client/checkout/blocks/upe-split-fields.js | 7 ++----- .../payment-processing.js | 12 ++++++------ .../test/payment-processing.test.js | 7 ------- client/checkout/classic/upe-split.js | 3 ++- client/checkout/classic/upe.js | 3 ++- .../payment-methods/class-upe-payment-gateway.php | 10 +--------- 10 files changed, 19 insertions(+), 44 deletions(-) delete mode 100644 changelog/feat-add-upe-appearance-filter diff --git a/changelog/feat-add-upe-appearance-filter b/changelog/feat-add-upe-appearance-filter deleted file mode 100644 index 550253ef8de..00000000000 --- a/changelog/feat-add-upe-appearance-filter +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Add UPE appearance filter. diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index bab84c6790e..b7deafca8af 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -553,7 +553,8 @@ export default class WCPayAPI { _ajax_nonce: getConfig( 'saveUPEAppearanceNonce' ), } ) .then( ( response ) => { - return response.data; + // There is not any action to take or harm caused by a failed update, so just returning success status. + return response.success; } ) .catch( ( error ) => { if ( error.message ) { diff --git a/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js b/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js index 7e43346486a..36abca3728f 100644 --- a/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js +++ b/client/checkout/blocks/upe-deferred-intent-creation/payment-elements.js @@ -24,11 +24,8 @@ const PaymentElements = ( { api, ...props } ) => { useEffect( () => { async function generateUPEAppearance() { // Generate UPE input styles. - let upeAppearance = getAppearance( true ); - upeAppearance = await api.saveUPEAppearance( - upeAppearance, - 'true' - ); + const upeAppearance = getAppearance( true ); + await api.saveUPEAppearance( upeAppearance, 'true' ); setAppearance( upeAppearance ); } diff --git a/client/checkout/blocks/upe-fields.js b/client/checkout/blocks/upe-fields.js index 4198367146f..2f8c06a7ccf 100644 --- a/client/checkout/blocks/upe-fields.js +++ b/client/checkout/blocks/upe-fields.js @@ -295,11 +295,8 @@ const ConsumableWCPayFields = ( { api, ...props } ) => { useEffect( () => { async function generateUPEAppearance() { // Generate UPE input styles. - let upeAppearance = getAppearance( true ); - upeAppearance = await api.saveUPEAppearance( - upeAppearance, - 'true' - ); + const upeAppearance = getAppearance( true ); + await api.saveUPEAppearance( upeAppearance, 'true' ); // Update appearance state setAppearance( upeAppearance ); diff --git a/client/checkout/blocks/upe-split-fields.js b/client/checkout/blocks/upe-split-fields.js index dc17dc8273f..ff75fd8ea81 100644 --- a/client/checkout/blocks/upe-split-fields.js +++ b/client/checkout/blocks/upe-split-fields.js @@ -301,11 +301,8 @@ const ConsumableWCPayFields = ( { api, ...props } ) => { useEffect( () => { async function generateUPEAppearance() { // Generate UPE input styles. - let upeAppearance = getAppearance( true ); - upeAppearance = await api.saveUPEAppearance( - upeAppearance, - 'true' - ); + const upeAppearance = getAppearance( true ); + await api.saveUPEAppearance( upeAppearance, 'true' ); // Update appearance state setAppearance( upeAppearance ); diff --git a/client/checkout/classic/upe-deferred-intent-creation/payment-processing.js b/client/checkout/classic/upe-deferred-intent-creation/payment-processing.js index 82937a1e194..513a0b87a5e 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/payment-processing.js +++ b/client/checkout/classic/upe-deferred-intent-creation/payment-processing.js @@ -41,13 +41,13 @@ for ( const paymentMethodType in getUPEConfig( 'paymentMethodsConfig' ) ) { * @param {Object} api The API object used to save the UPE configuration. * @return {Object} The appearance object for the UPE. */ -async function initializeAppearance( api ) { - const appearance = getUPEConfig( 'upeAppearance' ); - if ( appearance ) { - return appearance; +function initializeAppearance( api ) { + let appearance = getUPEConfig( 'upeAppearance' ); + if ( ! appearance ) { + appearance = getAppearance(); + api.saveUPEAppearance( appearance ); } - - return await api.saveUPEAppearance( getAppearance() ); + return appearance; } /** diff --git a/client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js b/client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js index 44bb0efba7e..05970174879 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js +++ b/client/checkout/classic/upe-deferred-intent-creation/test/payment-processing.test.js @@ -266,13 +266,6 @@ describe( 'Stripe Payment Element mounting', () => { } ); getUPEConfig.mockImplementation( ( argument ) => { - if ( - argument === 'wcBlocksUPEAppearance' || - argument === 'upeAppearance' - ) { - return {}; - } - if ( argument === 'currency' ) { return 'eur'; } diff --git a/client/checkout/classic/upe-split.js b/client/checkout/classic/upe-split.js index 09018bd73f9..05ad9f1de49 100644 --- a/client/checkout/classic/upe-split.js +++ b/client/checkout/classic/upe-split.js @@ -210,7 +210,8 @@ jQuery( function ( $ ) { let appearance = getUPEConfig( 'upeAppearance' ); if ( ! appearance ) { - appearance = await api.saveUPEAppearance( getAppearance() ); + appearance = getAppearance(); + api.saveUPEAppearance( appearance ); } const elements = api.getStripe().elements( { diff --git a/client/checkout/classic/upe.js b/client/checkout/classic/upe.js index eb684a9e10d..519a6f39b80 100644 --- a/client/checkout/classic/upe.js +++ b/client/checkout/classic/upe.js @@ -244,7 +244,8 @@ jQuery( function ( $ ) { let appearance = getConfig( 'upeAppearance' ); if ( ! appearance ) { - appearance = await api.saveUPEAppearance( getAppearance() ); + appearance = getAppearance(); + api.saveUPEAppearance( appearance ); } elements = api.getStripe().elements( { diff --git a/includes/payment-methods/class-upe-payment-gateway.php b/includes/payment-methods/class-upe-payment-gateway.php index 59c1beccfaa..7023ba0ca2d 100644 --- a/includes/payment-methods/class-upe-payment-gateway.php +++ b/includes/payment-methods/class-upe-payment-gateway.php @@ -1104,14 +1104,6 @@ public function save_upe_appearance_ajax() { $is_blocks_checkout = isset( $_POST['is_blocks_checkout'] ) ? rest_sanitize_boolean( wc_clean( wp_unslash( $_POST['is_blocks_checkout'] ) ) ) : false; $appearance = isset( $_POST['appearance'] ) ? json_decode( wc_clean( wp_unslash( $_POST['appearance'] ) ) ) : null; - /** - * This filter is only called on "save" of the appearance, to avoid calling it on every page load. - * If you apply changes through this filter, you'll need to clear the transient data to see them at checkout. - * - * @since 6.8.0 - */ - $appearance = apply_filters( 'wcpay_upe_appearance', $appearance, $is_blocks_checkout ); - $appearance_transient = $is_blocks_checkout ? self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT : self::UPE_APPEARANCE_TRANSIENT; if ( null !== $appearance ) { @@ -1120,7 +1112,7 @@ public function save_upe_appearance_ajax() { wp_send_json_success( $appearance, 200 ); } catch ( Exception $e ) { - // Send back error, so it can be displayed to the customer. + // Send back error so it can be displayed to the customer. wp_send_json_error( [ 'error' => [ From acc531f527dd679aa2d7d66c125b213467981f21 Mon Sep 17 00:00:00 2001 From: jessy <32092402+jessy-p@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:49:35 +0530 Subject: [PATCH 14/61] Changes to Payment Process to store the working mode of the gateway (#7651) Co-authored-by: Jessy P --- changelog/add-7421-store-working-mode | 4 +++ .../PaymentsServiceProvider.php | 3 +- src/Internal/Payment/PaymentContext.php | 18 ++++++++++ src/Internal/Service/OrderService.php | 28 ++++++++++++++++ .../Service/PaymentProcessingService.php | 18 +++++++++- .../Internal/Payment/PaymentContextTest.php | 7 ++++ .../src/Internal/Service/OrderServiceTest.php | 24 +++++++++++++- .../Service/PaymentProcessingServiceTest.php | 33 ++++++++++++++++--- 8 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 changelog/add-7421-store-working-mode diff --git a/changelog/add-7421-store-working-mode b/changelog/add-7421-store-working-mode new file mode 100644 index 00000000000..7f74cff000b --- /dev/null +++ b/changelog/add-7421-store-working-mode @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Store the working mode of the gateway (RPP) diff --git a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php index 85d0bb1acbd..3443239f879 100644 --- a/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php +++ b/src/Internal/DependencyManagement/ServiceProvider/PaymentsServiceProvider.php @@ -77,7 +77,8 @@ public function register(): void { $container->addShared( PaymentProcessingService::class ) ->addArgument( StateFactory::class ) ->addArgument( LegacyProxy::class ) - ->addArgument( PaymentContextLoggerService::class ); + ->addArgument( PaymentContextLoggerService::class ) + ->addArgument( Mode::class ); $container->addShared( PaymentRequestService::class ); diff --git a/src/Internal/Payment/PaymentContext.php b/src/Internal/Payment/PaymentContext.php index 27c12991953..63fcc90d115 100644 --- a/src/Internal/Payment/PaymentContext.php +++ b/src/Internal/Payment/PaymentContext.php @@ -300,6 +300,24 @@ public function get_transitions(): array { return $this->transitions; } + /** + * Sets the mode (test or prod). + * + * @param string $mode mode. + */ + public function set_mode( string $mode ) { + $this->set( 'mode', $mode ); + } + + /** + * Returns the mode (test or prod). + * + * @return string|null mode. + */ + public function get_mode(): ?string { + return $this->get( 'mode' ); + } + /** * Updates previous transition with the next state and creates new transition. * diff --git a/src/Internal/Service/OrderService.php b/src/Internal/Service/OrderService.php index c10ce3b63bd..24dcde9cc13 100644 --- a/src/Internal/Service/OrderService.php +++ b/src/Internal/Service/OrderService.php @@ -207,12 +207,39 @@ public function update_order_from_successful_intent( $this->legacy_service->attach_transaction_fee_to_order( $order, $charge ); $this->legacy_service->update_order_status_from_intent( $order, $intent ); + $this->set_mode( $order_id, $context->get_mode() ); if ( ! is_null( $charge ) ) { $this->attach_exchange_info_to_order( $order_id, $charge ); } } + /** + * Sets the '_wcpay_mode' meta data on an order. + * + * @param string $order_id The order id. + * @param string $mode Mode from the context. + * @throws Order_Not_Found_Exception + */ + public function set_mode( string $order_id, string $mode ) : void { + $order = $this->get_order( $order_id ); + $order->update_meta_data( '_wcpay_mode', $mode ); + $order->save_meta_data(); + } + + /** + * Gets the '_wcpay_mode' meta data on an order. + * + * @param string $order_id The order id. + * + * @return string The mode. + * @throws Order_Not_Found_Exception + */ + public function get_mode( string $order_id ) : string { + $order = $this->get_order( $order_id ); + return $order->get_meta( '_wcpay_mode', true ); + } + /** * Updates the order with the necessary details whenever an intent requires action. * @@ -421,4 +448,5 @@ protected function get_order( int $order_id ): WC_Order { } return $order; } + } diff --git a/src/Internal/Service/PaymentProcessingService.php b/src/Internal/Service/PaymentProcessingService.php index e2a93aeb23d..4fd974ffce6 100644 --- a/src/Internal/Service/PaymentProcessingService.php +++ b/src/Internal/Service/PaymentProcessingService.php @@ -7,10 +7,12 @@ namespace WCPay\Internal\Service; +use Exception; use WC_Payments_API_Abstract_Intention; use WC_Payments_API_Setup_Intention; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Vendor\League\Container\Exception\ContainerException; +use WCPay\Core\Mode; use WCPay\Internal\Payment\PaymentContext; use WCPay\Internal\Payment\State\InitialState; use WCPay\Internal\Payment\State\StateFactory; @@ -45,6 +47,12 @@ class PaymentProcessingService { */ private $context_logger_service; + /** + * Mode + * + * @var Mode + */ + private $mode; /** * Service constructor. @@ -52,15 +60,18 @@ class PaymentProcessingService { * @param StateFactory $state_factory Factory for payment states. * @param LegacyProxy $legacy_proxy Legacy proxy. * @param PaymentContextLoggerService $context_logger_service Context Logging Service. + * @param Mode $mode Mode. */ public function __construct( StateFactory $state_factory, LegacyProxy $legacy_proxy, - PaymentContextLoggerService $context_logger_service + PaymentContextLoggerService $context_logger_service, + Mode $mode ) { $this->state_factory = $state_factory; $this->legacy_proxy = $legacy_proxy; $this->context_logger_service = $context_logger_service; + $this->mode = $mode; } /** @@ -133,6 +144,11 @@ public function get_authentication_redirect_url( $intent, int $order_id ) { */ protected function create_payment_context( int $order_id, bool $automatic_capture = false ): PaymentContext { $context = new PaymentContext( $order_id ); + try { + $context->set_mode( $this->mode->is_test() ? 'test' : 'prod' ); + } catch ( Exception $e ) { + $context->set_mode( 'unknown' ); + } $context->toggle_automatic_capture( $automatic_capture ); return $context; diff --git a/tests/unit/src/Internal/Payment/PaymentContextTest.php b/tests/unit/src/Internal/Payment/PaymentContextTest.php index c34171136d2..3f70d0ae47e 100644 --- a/tests/unit/src/Internal/Payment/PaymentContextTest.php +++ b/tests/unit/src/Internal/Payment/PaymentContextTest.php @@ -140,6 +140,13 @@ public function test_intent() { $this->assertSame( $intent, $this->sut->get_intent() ); } + public function test_mode() { + $mode = 'prod'; + + $this->sut->set_mode( $mode ); + $this->assertSame( $mode, $this->sut->get_mode() ); + } + public function test_log_state_transition() { $this->sut->log_state_transition( 'First_State' ); // first transition has 'from_state' null and 'to_state' as 'First_State'. diff --git a/tests/unit/src/Internal/Service/OrderServiceTest.php b/tests/unit/src/Internal/Service/OrderServiceTest.php index 1aba6fa24d2..1132d8e3ce5 100644 --- a/tests/unit/src/Internal/Service/OrderServiceTest.php +++ b/tests/unit/src/Internal/Service/OrderServiceTest.php @@ -319,7 +319,7 @@ public function test_update_order_from_successful_intent( $intent ) { // Create a mock order that will be used. $mock_order = $this->createMock( WC_Order::class ); - $this->sut->expects( $this->once() ) + $this->sut->expects( $this->exactly( 2 ) ) ->method( 'get_order' ) ->with( $this->order_id ) ->willReturn( $mock_order ); @@ -355,6 +355,9 @@ public function test_update_order_from_successful_intent( $intent ) { $mock_context->expects( $this->once() ) ->method( 'get_currency' ) ->willReturn( $currency ); + $mock_context->expects( $this->once() ) + ->method( 'get_mode' ) + ->willReturn( 'prod' ); $this->mock_legacy_service->expects( $this->once() ) ->method( 'attach_intent_info_to_order' ) @@ -637,6 +640,24 @@ public function test_delete_order() { $this->assertSame( $expected, $result ); } + public function test_set_mode() { + $this->mock_get_order() + ->expects( $this->once() ) + ->method( 'update_meta_data' ) + ->with( '_wcpay_mode', 'prod' ); + $this->sut->set_mode( $this->order_id, 'prod' ); + } + + public function test_get_mode() { + $this->mock_get_order() + ->expects( $this->once() ) + ->method( 'get_meta' ) + ->with( '_wcpay_mode', true ) + ->willReturn( 'test' ); + $result = $this->sut->get_mode( $this->order_id, true ); + $this->assertSame( 'test', $result ); + } + /** * Mocks order retrieval. * @@ -654,4 +675,5 @@ private function mock_get_order( int $order_id = null ) { return $mock_order; } + } diff --git a/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php b/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php index feb7b538a16..b2512762a3e 100644 --- a/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php +++ b/tests/unit/src/Internal/Service/PaymentProcessingServiceTest.php @@ -7,6 +7,9 @@ namespace WCPay\Tests\Internal\Service; +use Exception; +use WCPAY_UnitTestCase; +use PHPUnit_Utils; use PHPUnit\Framework\MockObject\MockObject; use WC_Helper_Intention; use WC_Payment_Gateway_WCPay; @@ -17,7 +20,7 @@ use WCPay\Internal\Payment\PaymentRequest; use WCPay\Internal\Payment\State\CompletedState; use WCPay\Internal\Payment\State\InitialState; -use WCPAY_UnitTestCase; +use WCPay\Core\Mode; use WCPay\Internal\Proxy\LegacyProxy; use WCPay\Internal\Payment\State\StateFactory; use WCPay\Internal\Service\PaymentProcessingService; @@ -27,6 +30,7 @@ * Payment processing service unit tests. */ class PaymentProcessingServiceTest extends WCPAY_UnitTestCase { + /** * Service under test. * @@ -49,6 +53,11 @@ class PaymentProcessingServiceTest extends WCPAY_UnitTestCase { */ private $mock_context_logger; + /** + * @var Mode|MockObject + */ + private $mock_mode; + /** * Set up the test. */ @@ -58,6 +67,7 @@ protected function setUp(): void { $this->mock_state_factory = $this->createMock( StateFactory::class ); $this->mock_legacy_proxy = $this->createMock( LegacyProxy::class ); $this->mock_context_logger = $this->createMock( PaymentContextLoggerService::class ); + $this->mock_mode = $this->createMock( Mode::class ); $this->sut = $this->getMockBuilder( PaymentProcessingService::class ) ->setConstructorArgs( @@ -65,6 +75,7 @@ protected function setUp(): void { $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, + $this->mock_mode, ] ) ->onlyMethods( [ 'create_payment_context' ] ) @@ -79,7 +90,7 @@ public function test_process_payment_happy_path() { $mock_initial_state = $this->createMock( InitialState::class ); $mock_completed_state = $this->createMock( CompletedState::class ); - $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); $this->mock_state_factory->expects( $this->once() ) ->method( 'create_state' ) @@ -103,7 +114,7 @@ public function test_process_payment_happy_path() { * Test the basic happy path of processing a payment. */ public function test_process_payment_happy_path_without_mock_builder() { - $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); $mock_initial_state = $this->createMock( InitialState::class ); $mock_completed_state = $this->createMock( CompletedState::class ); @@ -126,8 +137,21 @@ public function test_process_payment_happy_path_without_mock_builder() { $this->assertSame( $mock_completed_state, $result ); } + /** + * Test the process payment when mode not initialized. + */ + public function test_process_payment_mode_throws_exception() { + $this->mock_mode + ->expects( $this->once() ) + ->method( 'is_test' ) + ->willThrowException( new Exception( 'Could not initialize' ) ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); + $context = PHPUnit_Utils::call_method( $sut, 'create_payment_context', [ 123 ] ); + $this->assertEquals( 'unknown', $context->get_mode() ); + } + public function test_get_authentication_redirect_url_will_return_url_from_payment_intent() { - $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger ); + $sut = new PaymentProcessingService( $this->mock_state_factory, $this->mock_legacy_proxy, $this->mock_context_logger, $this->mock_mode ); $url = 'localhost'; $intent_data = [ @@ -149,7 +173,6 @@ public function test_get_authentication_redirect_url_will_return_url_from_paymen $result = $sut->get_authentication_redirect_url( $intent, 1 ); $this->assertSame( $url, $result ); - } /** From 2a1fef5c1de9e9b2a9a58b98cbc77288788f5678 Mon Sep 17 00:00:00 2001 From: Zvonimir Maglica Date: Wed, 15 Nov 2023 17:46:58 +0100 Subject: [PATCH 15/61] Add customer details flow to inital state (#7640) Co-authored-by: Radoslav Georgiev --- changelog/rpp-7417-manage-customer-details | 4 + .../class-wc-payments-customer-service.php | 47 ++++++------ src/Internal/Payment/PaymentContext.php | 4 +- src/Internal/Payment/State/InitialState.php | 30 +++++--- src/Internal/Service/OrderService.php | 9 +-- .../Service/PaymentProcessingService.php | 2 + .../Payment/State/InitialStateTest.php | 74 ++++++++++++++----- .../src/Internal/Service/OrderServiceTest.php | 13 +--- 8 files changed, 111 insertions(+), 72 deletions(-) create mode 100644 changelog/rpp-7417-manage-customer-details diff --git a/changelog/rpp-7417-manage-customer-details b/changelog/rpp-7417-manage-customer-details new file mode 100644 index 00000000000..2c2924614b7 --- /dev/null +++ b/changelog/rpp-7417-manage-customer-details @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Added customer details management within the re-engineered payment process. diff --git a/includes/class-wc-payments-customer-service.php b/includes/class-wc-payments-customer-service.php index 0b5bf35c5f2..ecfabd4077e 100644 --- a/includes/class-wc-payments-customer-service.php +++ b/includes/class-wc-payments-customer-service.php @@ -107,7 +107,7 @@ public function __construct( /** * Get WCPay customer ID for the given WordPress user ID * - * @param int $user_id The user ID to look for a customer ID with. + * @param int|null $user_id The user ID to look for a customer ID with. * * @return string|null WCPay customer ID or null if not found. */ @@ -134,21 +134,21 @@ public function get_customer_id_by_user_id( $user_id ) { /** * Create a customer and associate it with a WordPress user. * - * @param WP_User $user User to create a customer for. - * @param array $customer_data Customer data. + * @param WP_User|null $user User to create a customer for. + * @param array $customer_data Customer data. * * @return string The created customer's ID * * @throws API_Exception Error creating customer. */ - public function create_customer_for_user( WP_User $user, array $customer_data ): string { + public function create_customer_for_user( ?WP_User $user, array $customer_data = [] ): string { // Include the session ID for the user. $customer_data['session_id'] = $this->session_service->get_sift_session_id() ?? null; // Create a customer on the WCPay server. $customer_id = $this->payments_api_client->create_customer( $customer_data ); - if ( $user->ID > 0 ) { + if ( $user instanceof WP_User && $user->ID > 0 ) { $this->update_user_customer_id( $user->ID, $customer_id ); } @@ -163,38 +163,37 @@ public function create_customer_for_user( WP_User $user, array $customer_data ): /** * Manages customer details held on WCPay server for WordPress user associated with an order. * - * @param int $user_id ID of the WP user to associate with the customer. + * @param int|null $user_id ID of the WP user to associate with the customer. * @param WC_Order $order Woo Order. + * * @return string WooPayments customer ID. - */ - public function get_or_create_customer_id_from_order( int $user_id, WC_Order $order ): string { + * @throws API_Exception Throws when server API request fails. +*/ + public function get_or_create_customer_id_from_order( ?int $user_id, WC_Order $order ): string { // Determine the customer making the payment, create one if we don't have one already. - $customer_id = $this->get_customer_id_by_user_id( $user_id ); + $customer_id = $this->get_customer_id_by_user_id( $user_id ); + $customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ?? 0 ) ); + $user = null === $user_id ? null : get_user_by( 'id', $user_id ); if ( null !== $customer_id ) { - // @todo: We need to update the customer here. + $this->update_customer_for_user( $customer_id, $user, $customer_data ); return $customer_id; } - - $customer_data = self::map_customer_data( $order, new WC_Customer( $user_id ) ); - $user = get_user_by( 'id', $user_id ); - $customer_id = $this->create_customer_for_user( $user, $customer_data ); - - return $customer_id; + return $this->create_customer_for_user( $user, $customer_data ); } /** * Update the customer details held on the WCPay server associated with the given WordPress user. * - * @param string $customer_id WCPay customer ID. - * @param WP_User $user WordPress user. - * @param array $customer_data Customer data. + * @param string $customer_id WCPay customer ID. + * @param WP_User|null $user WordPress user. + * @param array $customer_data Customer data. * * @return string The updated customer's ID. Can be different to the ID parameter if the customer was re-created. * * @throws API_Exception Error updating the customer. */ - public function update_customer_for_user( string $customer_id, WP_User $user, array $customer_data ): string { + public function update_customer_for_user( string $customer_id, ?WP_User $user, array $customer_data ): string { try { // Update the customer on the WCPay server. $this->payments_api_client->update_customer( @@ -385,15 +384,15 @@ public function delete_cached_payment_methods() { /** * Recreates the customer for this user. * - * @param WP_User $user User to recreate a customer for. - * @param array $customer_data Customer data. + * @param WP_User|null $user User to recreate a customer for. + * @param array $customer_data Customer data. * * @return string The newly created customer's ID * * @throws API_Exception Error creating customer. */ - private function recreate_customer( WP_User $user, array $customer_data ): string { - if ( $user->ID > 0 ) { + private function recreate_customer( ?WP_User $user, array $customer_data ): string { + if ( $user instanceof WP_User && $user->ID > 0 ) { $result = delete_user_option( $user->ID, $this->get_customer_id_option() ); if ( ! $result ) { // Log the error, but continue since we'll be trying to update this option in create_customer. diff --git a/src/Internal/Payment/PaymentContext.php b/src/Internal/Payment/PaymentContext.php index 63fcc90d115..68266427aca 100644 --- a/src/Internal/Payment/PaymentContext.php +++ b/src/Internal/Payment/PaymentContext.php @@ -203,9 +203,9 @@ public function get_payment_method(): ?PaymentMethodInterface { /** * Stores the WP user ID, associated with the payment. * - * @param int $user_id ID of the user. + * @param int|null $user_id ID of the user. */ - public function set_user_id( int $user_id ) { + public function set_user_id( ?int $user_id ) { $this->set( 'user_id', $user_id ); } diff --git a/src/Internal/Payment/State/InitialState.php b/src/Internal/Payment/State/InitialState.php index 283a52e6af5..e71435e1c24 100644 --- a/src/Internal/Payment/State/InitialState.php +++ b/src/Internal/Payment/State/InitialState.php @@ -13,6 +13,7 @@ use WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Exceptions\Amount_Too_Small_Exception; +use WCPay\Exceptions\API_Exception; use WCPay\Internal\Service\MinimumAmountService; use WCPay\Internal\Service\PaymentRequestService; use WCPay\Internal\Service\DuplicatePaymentPreventionService; @@ -110,6 +111,7 @@ public function __construct( * @throws ContainerException When the dependency container cannot instantiate the state. * @throws Order_Not_Found_Exception Order could not be found. * @throws PaymentRequestException When data is not available or invalid. + * @throws API_Exception When server request fails. * @throws Amount_Too_Small_Exception When the order amount is too small. */ public function start_processing( PaymentRequest $request ) { @@ -139,8 +141,24 @@ public function start_processing( PaymentRequest $request ) { ); // End multiple verification checks. - // Payments are currently based on intents, request one from the API. + /** + * Payments are based on intents, and intents use customer objects for billing details. + * + * The customer is created/updated right before requesting the creation of + * a payment intent, and the two actions must be adjacent to each-other. + */ try { + $context = $this->get_context(); + $order_id = $context->get_order_id(); + + // Create or update customer and customer details. + $customer_id = $this->customer_service->get_or_create_customer_id_from_order( + $context->get_user_id(), + $this->order_service->_deprecated_get_order( $order_id ) + ); + $context->set_customer_id( $customer_id ); + + // After customer is updated or created, make sure that intent is created. $intent = $this->payment_request_service->create_intent( $context ); $context->set_intent( $intent ); } catch ( Amount_Too_Small_Exception $e ) { @@ -152,7 +170,7 @@ public function start_processing( PaymentRequest $request ) { // Intent requires authorization (3DS check). if ( Intent_Status::REQUIRES_ACTION === $intent->get_status() ) { - $this->order_service->update_order_from_intent_that_requires_action( $context->get_order_id(), $intent, $context ); + $this->order_service->update_order_from_intent_that_requires_action( $order_id, $intent, $context ); return $this->create_state( AuthenticationRequiredState::class ); } @@ -191,7 +209,6 @@ protected function populate_context_from_request( PaymentRequest $request ) { /** * Populates the context with details, available in the order. - * This includes the update/creation of a customer. * * @throws Order_Not_Found_Exception In case the order could not be found. */ @@ -208,13 +225,6 @@ protected function populate_context_from_order() { ) ); $context->set_level3_data( $this->level3_service->get_data_from_order( $order_id ) ); - - // Customer management involves a remote call. - $customer_id = $this->customer_service->get_or_create_customer_id_from_order( - $context->get_user_id(), - $this->order_service->_deprecated_get_order( $order_id ) - ); - $context->set_customer_id( $customer_id ); } /** diff --git a/src/Internal/Service/OrderService.php b/src/Internal/Service/OrderService.php index 24dcde9cc13..e2f094944f7 100644 --- a/src/Internal/Service/OrderService.php +++ b/src/Internal/Service/OrderService.php @@ -162,15 +162,12 @@ public function import_order_data_to_payment_context( int $order_id, PaymentCont $currency = strtolower( $order->get_currency() ); $amount = WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ); - - $user = $order->get_user(); - if ( false === $user ) { // Default to the current user. - $user = $this->legacy_proxy->call_function( 'wp_get_current_user' ); - } + $user = $order->get_user(); $context->set_currency( $currency ); $context->set_amount( $amount ); - $context->set_user_id( $user->ID ); + // In case we don't have user, we are setting user id to be 0 which could cause more harm since we don't have a real user. + $context->set_user_id( $user->ID ?? null ); } /** diff --git a/src/Internal/Service/PaymentProcessingService.php b/src/Internal/Service/PaymentProcessingService.php index 4fd974ffce6..ca3647fcfc7 100644 --- a/src/Internal/Service/PaymentProcessingService.php +++ b/src/Internal/Service/PaymentProcessingService.php @@ -10,6 +10,7 @@ use Exception; use WC_Payments_API_Abstract_Intention; use WC_Payments_API_Setup_Intention; +use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Order_Not_Found_Exception; use WCPay\Vendor\League\Container\Exception\ContainerException; use WCPay\Core\Mode; @@ -84,6 +85,7 @@ public function __construct( * @throws PaymentRequestException When the request is malformed. This should be converted to a failure state. * @throws Order_Not_Found_Exception When order is not found. * @throws ContainerException When the dependency container cannot instantiate the state. + * @throws API_Exception When server requests fails. */ public function process_payment( int $order_id, bool $automatic_capture = false ) { // Start with a basis context. diff --git a/tests/unit/src/Internal/Payment/State/InitialStateTest.php b/tests/unit/src/Internal/Payment/State/InitialStateTest.php index e479e6535ad..c8c01f8cb27 100644 --- a/tests/unit/src/Internal/Payment/State/InitialStateTest.php +++ b/tests/unit/src/Internal/Payment/State/InitialStateTest.php @@ -150,9 +150,13 @@ protected function setUp(): void { } public function test_start_processing() { + $order_id = 123; + $user_id = 456; + $customer_id = 'cus_123'; $mock_request = $this->createMock( PaymentRequest::class ); $mock_processed_state = $this->createMock( ProcessedState::class ); $mock_completed_state = $this->createMock( CompletedState::class ); + $mock_order = $this->createMock( WC_Order::class ); $mock_processed_state->expects( $this->once() ) ->method( 'complete_processing' ) @@ -169,6 +173,8 @@ public function test_start_processing() { $this->mocked_sut->expects( $this->once() )->method( 'process_duplicate_order' )->willReturn( null ); $this->mocked_sut->expects( $this->once() )->method( 'process_duplicate_payment' )->willReturn( null ); + $this->mock_customer_data( $user_id, $order_id, $mock_order, $customer_id ); + $intent = WC_Helper_Intention::create_intention(); $this->mock_payment_request_service @@ -191,6 +197,7 @@ public function test_start_processing() { public function test_start_processing_will_throw_exception_when_minimum_amount_occurs() { $mock_request = $this->createMock( PaymentRequest::class ); + $mock_order = $this->createMock( WC_Order::class ); $small_amount_exception = new Amount_Too_Small_Exception( 'Amount too small', 50, 'EUR', 400 ); $this->mock_payment_request_service @@ -205,6 +212,8 @@ public function test_start_processing_will_throw_exception_when_minimum_amount_o $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_request' )->with( $mock_request ); $this->mocked_sut->expects( $this->once() )->method( 'populate_context_from_order' ); + // Mock get customer. + $this->mock_customer_data( 1, 1, $mock_order, 'cus_mock' ); $this->mock_minimum_amount_service->expects( $this->once() )->method( 'store_amount_from_exception' ) ->with( $small_amount_exception ); @@ -215,8 +224,15 @@ public function test_start_processing_will_throw_exception_when_minimum_amount_o } public function test_start_processing_will_transition_to_error_state_when_api_exception_occurs() { + + $order_id = 123; + $user_id = 456; + $customer_id = 'cus_123'; $mock_request = $this->createMock( PaymentRequest::class ); $mock_error_state = $this->createMock( SystemErrorState::class ); + $mock_order = $this->createMock( WC_Order::class ); + + $this->mock_customer_data( $user_id, $order_id, $mock_order, $customer_id ); $this->mock_payment_request_service ->expects( $this->once() ) @@ -242,9 +258,14 @@ public function test_start_processing_will_transition_to_error_state_when_api_ex public function test_processing_will_transition_to_auth_required_state() { $order_id = 123; + $user_id = 456; + $customer_id = 'cus_123'; + $mock_order = $this->createMock( WC_Order::class ); $mock_request = $this->createMock( PaymentRequest::class ); $mock_auth_state = $this->createMock( AuthenticationRequiredState::class ); + $this->mock_customer_data( $user_id, $order_id, $mock_order, $customer_id ); + // Create an intent, and make sure it will be returned by the service. $mock_intent = $this->createMock( WC_Payments_API_Payment_Intention::class ); $mock_intent->expects( $this->once() )->method( 'get_status' )->willReturn( Intent_Status::REQUIRES_ACTION ); @@ -371,12 +392,10 @@ public function test_populate_context_from_request() { } public function test_populate_context_from_order() { - $order_id = 123; - $user_id = 456; - $customer_id = 'cus_123'; + $order_id = 123; + $metadata = [ 'sample' => 'true' ]; $level3_data = [ 'items' => [] ]; - $mock_order = $this->createMock( WC_Order::class ); // Prepare the order ID. $this->mock_context->expects( $this->once() ) @@ -406,22 +425,6 @@ public function test_populate_context_from_order() { ->method( 'set_level3_data' ) ->with( $level3_data ); - // Arrange customer management. - $this->mock_context->expects( $this->once() ) - ->method( 'get_user_id' ) - ->willReturn( $user_id ); - $this->mock_order_service->expects( $this->once() ) - ->method( '_deprecated_get_order' ) - ->with( $order_id ) - ->willReturn( $mock_order ); - $this->mock_customer_service->expects( $this->once() ) - ->method( 'get_or_create_customer_id_from_order' ) - ->with( $user_id, $mock_order ) - ->willReturn( $customer_id ); - $this->mock_context->expects( $this->once() ) - ->method( 'set_customer_id' ) - ->with( $customer_id ); - PHPUnit_Utils::call_method( $this->sut, 'populate_context_from_order', [] ); } @@ -593,4 +596,35 @@ public function test_process_duplicate_payment_returns_completed_state() { $result = PHPUnit_Utils::call_method( $this->sut, 'process_duplicate_payment', [] ); $this->assertInstanceOf( CompletedState::class, $result ); } + + /** + * Mock customer data. + * @param int $user_id User id. + * @param int $order_id Order id. + * @param MockObject|WC_Order $mock_order Mock order. + * @param string $customer_id Customer id. + * + * @return void + */ + private function mock_customer_data( int $user_id, int $order_id, $mock_order, string $customer_id ) { + + // Arrange customer management. + $this->mock_context->expects( $this->once() ) + ->method( 'get_user_id' ) + ->willReturn( $user_id ); + $this->mock_context->expects( $this->once() ) + ->method( 'get_order_id' ) + ->willReturn( $order_id ); + $this->mock_order_service->expects( $this->once() ) + ->method( '_deprecated_get_order' ) + ->with( $order_id ) + ->willReturn( $mock_order ); + $this->mock_customer_service->expects( $this->once() ) + ->method( 'get_or_create_customer_id_from_order' ) + ->with( $user_id, $mock_order ) + ->willReturn( $customer_id ); + $this->mock_context->expects( $this->once() ) + ->method( 'set_customer_id' ) + ->with( $customer_id ); + } } diff --git a/tests/unit/src/Internal/Service/OrderServiceTest.php b/tests/unit/src/Internal/Service/OrderServiceTest.php index 1132d8e3ce5..6ef4f3ca3f0 100644 --- a/tests/unit/src/Internal/Service/OrderServiceTest.php +++ b/tests/unit/src/Internal/Service/OrderServiceTest.php @@ -277,18 +277,11 @@ public function test_import_order_data_to_payment_context( $user ) { $mock_order->expects( $this->once() ) ->method( 'get_user' ) ->willReturn( $user ?? false ); - if ( ! $user ) { - $user = $this->createMock( WP_User::class ); - $user->ID = 10; - - $this->mock_legacy_proxy->expects( $this->once() ) - ->method( 'call_function' ) - ->with( 'wp_get_current_user' ) - ->willReturn( $user ); - } + + // Mock set user id. $mock_context->expects( $this->once() ) ->method( 'set_user_id' ) - ->with( 10 ); + ->with( $user->ID ?? null ); // Act. $this->sut->import_order_data_to_payment_context( $this->order_id, $mock_context ); From ec6ffa59bbce7404e4d7d956952900803f586142 Mon Sep 17 00:00:00 2001 From: Adam Heckler <5512652+aheckler@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:14:46 -0500 Subject: [PATCH 16/61] Fix old subs links (#7469) Co-authored-by: Chris McNeill <82999806+csmcneill@users.noreply.github.com> Co-authored-by: Francesco --- changelog/fix-update-subscriptions-related-docs-links | 4 ++++ .../stripe-billing-notices/migrate-automatically-notice.tsx | 2 +- .../stripe-billing-notices/migrate-option-notice.tsx | 2 +- client/settings/advanced-settings/stripe-billing-toggle.tsx | 2 +- .../settings/advanced-settings/wcpay-subscriptions-toggle.js | 2 +- .../templates/html-subscriptions-plugin-notice.php | 2 +- .../subscriptions/templates/html-wcpay-deactivate-warning.php | 4 ++-- .../templates/html-woo-payments-deactivate-warning.php | 4 ++-- 8 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 changelog/fix-update-subscriptions-related-docs-links diff --git a/changelog/fix-update-subscriptions-related-docs-links b/changelog/fix-update-subscriptions-related-docs-links new file mode 100644 index 00000000000..735765cdc65 --- /dev/null +++ b/changelog/fix-update-subscriptions-related-docs-links @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Correct some links that now lead to better documentation diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx index b55a2af7cac..b56f06a3d58 100644 --- a/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-automatically-notice.tsx @@ -85,7 +85,7 @@ const MigrateAutomaticallyNotice: React.FC< Props > = ( { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx index 504b6a1a689..89a0005e954 100644 --- a/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx +++ b/client/settings/advanced-settings/stripe-billing-notices/migrate-option-notice.tsx @@ -134,7 +134,7 @@ const MigrateOptionNotice: React.FC< Props > = ( { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/stripe-billing-toggle.tsx b/client/settings/advanced-settings/stripe-billing-toggle.tsx index f41ea396a69..a340fabbc88 100644 --- a/client/settings/advanced-settings/stripe-billing-toggle.tsx +++ b/client/settings/advanced-settings/stripe-billing-toggle.tsx @@ -56,7 +56,7 @@ const StripeBillingToggle: React.FC< Props > = ( { onChange } ) => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js index e08216cbec1..187feff74e1 100644 --- a/client/settings/advanced-settings/wcpay-subscriptions-toggle.js +++ b/client/settings/advanced-settings/wcpay-subscriptions-toggle.js @@ -54,7 +54,7 @@ const WCPaySubscriptionsToggle = () => { components: { learnMoreLink: ( // eslint-disable-next-line max-len - + ), }, } ) } diff --git a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php index a38038a247a..983c5f86ac0 100644 --- a/includes/subscriptions/templates/html-subscriptions-plugin-notice.php +++ b/includes/subscriptions/templates/html-subscriptions-plugin-notice.php @@ -22,7 +22,7 @@ printf( // Translators: %1-%4 placeholders are opening and closing a or strong HTML tags. %5$s: WooPayments, %6$s: Woo Subscriptions. esc_html__( 'Your store has subscriptions using %5$s Stripe Billing functionality for payment processing. Due to the %1$soff-site billing engine%2$s these subscriptions use,%3$s they will continue to renew even after you deactivate %6$s%4$s.', 'woocommerce-payments' ), - '', + '', '', '', '', diff --git a/includes/subscriptions/templates/html-wcpay-deactivate-warning.php b/includes/subscriptions/templates/html-wcpay-deactivate-warning.php index 25c443258ed..e111793e290 100644 --- a/includes/subscriptions/templates/html-wcpay-deactivate-warning.php +++ b/includes/subscriptions/templates/html-wcpay-deactivate-warning.php @@ -22,8 +22,8 @@ printf( // translators: $1 $2 $3 placeholders are opening and closing HTML link tags, linking to documentation. $4 $5 placeholders are opening and closing strong HTML tags. $6 is WooPayments. esc_html__( 'Your store has active subscriptions using the built-in %6$s functionality. Due to the %1$soff-site billing engine%3$s these subscriptions use, %4$sthey will continue to renew even after you deactivate %6$s%5$s. %2$sLearn more%3$s.', 'woocommerce-payments' ), - '', - '', + '', + '', '', '', '', diff --git a/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php b/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php index 05b54e6f36a..bfbd8f32b3a 100644 --- a/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php +++ b/includes/subscriptions/templates/html-woo-payments-deactivate-warning.php @@ -22,8 +22,8 @@ printf( // Translators: placeholders are opening and closing strong HTML tags. %6$s: WooPayments, %7$s: Woo Subscriptions. esc_html__( 'Your store has subscriptions using %6$s Stripe Billing functionality for payment processing. Due to the %1$soff-site billing engine%3$s these subscriptions use,%4$s they will continue to renew even after you deactivate %6$s%5$s.', 'woocommerce-payments' ), - '', - '', + '', + '', '', '', '', From 8a48349ac220a93544b12bdb14e0db977927d7c4 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:13:24 +1000 Subject: [PATCH 17/61] Remove "Estimated" option from the deposit list advanced filters (#7632) --- ...remove-estimated-deposit-status-filter-option | 4 ++++ .../account-balances/test/index.test.tsx | 14 +------------- client/components/deposit-status-chip/index.tsx | 1 - .../deposit-status-chip/test/index.test.tsx | 13 +++---------- .../test/__snapshots__/index.tsx.snap | 4 ++-- .../components/deposits-overview/test/index.tsx | 16 ++++++++-------- client/components/deposits-overview/utils.ts | 6 +++--- client/data/transactions/hooks.ts | 9 ++------- .../filters/test/__snapshots__/index.js.snap | 5 ----- client/deposits/filters/test/index.js | 13 ------------- client/deposits/strings.ts | 9 +++++++-- client/types/deposits.d.ts | 3 +-- 12 files changed, 31 insertions(+), 66 deletions(-) create mode 100644 changelog/fix-7630-remove-estimated-deposit-status-filter-option diff --git a/changelog/fix-7630-remove-estimated-deposit-status-filter-option b/changelog/fix-7630-remove-estimated-deposit-status-filter-option new file mode 100644 index 00000000000..6bdd2b939d9 --- /dev/null +++ b/changelog/fix-7630-remove-estimated-deposit-status-filter-option @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Remove estimated status option from the advanced filters on the deposits list screen diff --git a/client/components/account-balances/test/index.test.tsx b/client/components/account-balances/test/index.test.tsx index 6d1cf7d6cdf..3a09b9b2004 100644 --- a/client/components/account-balances/test/index.test.tsx +++ b/client/components/account-balances/test/index.test.tsx @@ -138,19 +138,7 @@ const createMockOverview = ( fee_percentage: 0, status: 'paid', }, - nextScheduled: { - id: '456', - type: 'deposit', - amount: 0, - automatic: true, - currency: null, - bankAccount: null, - created: Date.now(), - date: Date.now(), - fee: 0, - fee_percentage: 0, - status: 'estimated', - }, + nextScheduled: undefined, instant: { currency: currencyCode, amount: instantAmount, diff --git a/client/components/deposit-status-chip/index.tsx b/client/components/deposit-status-chip/index.tsx index e0e7ca10050..f6b456d2f24 100644 --- a/client/components/deposit-status-chip/index.tsx +++ b/client/components/deposit-status-chip/index.tsx @@ -14,7 +14,6 @@ import type { DepositStatus } from 'wcpay/types/deposits'; * Maps a DepositStatus to a ChipType. */ const mappings: Record< DepositStatus, ChipType > = { - estimated: 'light', pending: 'warning', in_transit: 'success', paid: 'success', diff --git a/client/components/deposit-status-chip/test/index.test.tsx b/client/components/deposit-status-chip/test/index.test.tsx index ee1575e1d85..54abc52546c 100644 --- a/client/components/deposit-status-chip/test/index.test.tsx +++ b/client/components/deposit-status-chip/test/index.test.tsx @@ -10,24 +10,17 @@ import { render } from '@testing-library/react'; import DepositStatusChip from '..'; describe( 'Deposits status chip renders', () => { - test( 'Renders In Transit status chip.', () => { - const { getByText } = render( - - ); - expect( getByText( 'Estimated' ) ).toBeTruthy(); - } ); - - test( 'Renders In Transit status chip.', () => { + test( 'Renders "Pending" status chip.', () => { const { getByText } = render( ); expect( getByText( 'Pending' ) ).toBeTruthy(); } ); - test( 'Renders In Transit status chip.', () => { + test( 'Renders "Paid" status chip.', () => { const { getByText } = render( ); expect( getByText( 'Paid' ) ).toBeTruthy(); } ); - test( 'Renders In Transit status chip.', () => { + test( 'Renders "In transit" status chip.', () => { const { getByText } = render( ); diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index 93b88c89b33..7f4bf264503 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -141,9 +141,9 @@ exports[`Deposits Overview information Component Renders 1`] = ` data-wp-component="FlexItem" > - Estimated + Pending
{ } ); test( 'Component Renders', () => { - mockOverviews( [ createMockOverview( 'usd', 100, 0, 'estimated' ) ] ); + mockOverviews( [ createMockOverview( 'usd', 100, 0, 'pending' ) ] ); mockUseDeposits.mockReturnValue( { depositsCount: 0, deposits: mockDeposits, @@ -282,7 +282,7 @@ describe( 'Deposits Overview information', () => { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, } ); - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + const overview = createMockOverview( 'usd', 100, 0, 'pending' ); const { getByText } = render( ); @@ -298,7 +298,7 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const overview = createMockOverview( 'EUR', 647049, 0, 'estimated' ); + const overview = createMockOverview( 'EUR', 647049, 0, 'pending' ); const { getByText } = render( ); @@ -308,7 +308,7 @@ describe( 'Deposits Overview information', () => { test( 'Confirm next deposit dates', () => { const date = Date.parse( '2021-10-01' ); - const overview = createMockOverview( 'usd', 100, date, 'estimated' ); + const overview = createMockOverview( 'usd', 100, date, 'pending' ); mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); mockUseSelectedCurrency.mockReturnValue( { @@ -336,7 +336,7 @@ describe( 'Deposits Overview information', () => { } ); test( 'Renders capital loan notice if deposit includes financing payout', () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + const overview = createMockOverview( 'usd', 100, 0, 'pending' ); mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: true, isLoading: false, @@ -369,7 +369,7 @@ describe( 'Deposits Overview information', () => { } ); test( `Doesn't render capital loan notice if deposit does not include financing payout`, () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + const overview = createMockOverview( 'usd', 100, 0, 'pending' ); mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: false, isLoading: false, @@ -527,7 +527,7 @@ describe( 'Suspended Deposit Notice Renders', () => { describe( 'Paused Deposit notice Renders', () => { test( 'When available balance is negative', () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + const overview = createMockOverview( 'usd', 100, 0, 'pending' ); mockUseDeposits.mockReturnValue( { depositsCount: 0, deposits: mockDeposits, @@ -550,7 +550,7 @@ describe( 'Paused Deposit notice Renders', () => { ); } ); test( 'When available balance is positive', () => { - const overview = createMockOverview( 'usd', 100, 0, 'estimated' ); + const overview = createMockOverview( 'usd', 100, 0, 'pending' ); mockUseDeposits.mockReturnValue( { depositsCount: 0, deposits: mockDeposits, diff --git a/client/components/deposits-overview/utils.ts b/client/components/deposits-overview/utils.ts index e3c93323fd0..b6ae462576c 100644 --- a/client/components/deposits-overview/utils.ts +++ b/client/components/deposits-overview/utils.ts @@ -19,7 +19,7 @@ type NextDepositTableData = { * @return {NextDepositTableData} An object containing the formatted next deposit data, with the following properties: * - id: An optional string representing the ID of the next scheduled deposit. * - date: A Unix timestamp representing the date of the next scheduled deposit. - * - status: A string representing the status of the next scheduled deposit. If no status is provided, defaults to 'estimated. + * - status: A string representing the status of the next scheduled deposit. If no status is provided, defaults to 'pending'. * - amount: A formatted string representing the amount of the next scheduled deposit in the currency specified in the overview object. */ export const getNextDeposit = ( @@ -29,7 +29,7 @@ export const getNextDeposit = ( return { id: undefined, date: 0, - status: 'estimated', + status: 'pending', amount: formatCurrency( 0, overview?.currency ), }; } @@ -39,7 +39,7 @@ export const getNextDeposit = ( return { id: nextScheduled.id, date: nextScheduled.date ?? 0, - status: nextScheduled.status ?? 'estimated', + status: nextScheduled.status ?? '-', amount: formatCurrency( nextScheduled.amount ?? 0, currency ), }; }; diff --git a/client/data/transactions/hooks.ts b/client/data/transactions/hooks.ts index be8386916fc..a93f43c8ff4 100644 --- a/client/data/transactions/hooks.ts +++ b/client/data/transactions/hooks.ts @@ -10,6 +10,7 @@ import type { Query } from '@woocommerce/navigation'; * Internal dependencies */ import { STORE_NAME } from '../constants'; +import type { DepositStatus } from 'wcpay/types/deposits'; // TODO: refine this type with more detailed information. export interface Transaction { @@ -25,13 +26,7 @@ export interface Transaction { customer_country: string; customer_currency: string; deposit_id?: string; - deposit_status?: - | 'paid' - | 'pending' - | 'in_transit' - | 'canceled' - | 'failed' - | 'estimated'; + deposit_status?: DepositStatus; available_on: string; currency: string; transaction_id: string; diff --git a/client/deposits/filters/test/__snapshots__/index.js.snap b/client/deposits/filters/test/__snapshots__/index.js.snap index 505550cbfb6..5f02b248a6c 100644 --- a/client/deposits/filters/test/__snapshots__/index.js.snap +++ b/client/deposits/filters/test/__snapshots__/index.js.snap @@ -27,10 +27,5 @@ HTMLOptionsCollection [ > Failed , - , ] `; diff --git a/client/deposits/filters/test/index.js b/client/deposits/filters/test/index.js index b89278b01dc..ba69a1ab7fe 100644 --- a/client/deposits/filters/test/index.js +++ b/client/deposits/filters/test/index.js @@ -172,19 +172,6 @@ describe( 'Deposits filters', () => { expect( getQuery().status_is ).toEqual( 'failed' ); } ); - - test( 'should filter by estimated', () => { - user.selectOptions( ruleSelector, 'is' ); - - // need to include $ in name, otherwise "Select a deposit status filter" is also matched. - user.selectOptions( - screen.getByRole( 'combobox', { name: /deposit status$/i } ), - 'estimated' - ); - user.click( screen.getByRole( 'link', { name: /Filter/ } ) ); - - expect( getQuery().status_is ).toEqual( 'estimated' ); - } ); } ); function addAdvancedFilter( filter ) { diff --git a/client/deposits/strings.ts b/client/deposits/strings.ts index a2ea34690aa..f4cbfbfb3a6 100644 --- a/client/deposits/strings.ts +++ b/client/deposits/strings.ts @@ -5,16 +5,21 @@ */ import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ + +import type { DepositStatus } from 'wcpay/types/deposits'; + export const displayType = { deposit: __( 'Deposit', 'woocommerce-payments' ), withdrawal: __( 'Withdrawal', 'woocommerce-payments' ), }; -export const displayStatus = { +export const displayStatus: Record< DepositStatus, string > = { paid: __( 'Paid', 'woocommerce-payments' ), pending: __( 'Pending', 'woocommerce-payments' ), in_transit: __( 'In transit', 'woocommerce-payments' ), canceled: __( 'Canceled', 'woocommerce-payments' ), failed: __( 'Failed', 'woocommerce-payments' ), - estimated: __( 'Estimated', 'woocommerce-payments' ), }; diff --git a/client/types/deposits.d.ts b/client/types/deposits.d.ts index 9dab619c2c7..ce749a58c30 100644 --- a/client/types/deposits.d.ts +++ b/client/types/deposits.d.ts @@ -43,5 +43,4 @@ export type DepositStatus = | 'pending' | 'in_transit' | 'canceled' - | 'failed' - | 'estimated'; + | 'failed'; From 82285137fe845e301fddfe2530760888a0bad667 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:13:42 +1000 Subject: [PATCH 18/61] Exclude estimated deposits from useDeposits data hook by default (#7688) --- ...estimated-deposits-on-deposits-detail-page | 4 ++++ client/data/deposits/hooks.ts | 22 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 changelog/7604-no-estimated-deposits-on-deposits-detail-page diff --git a/changelog/7604-no-estimated-deposits-on-deposits-detail-page b/changelog/7604-no-estimated-deposits-on-deposits-detail-page new file mode 100644 index 00000000000..aa2c5fd29c4 --- /dev/null +++ b/changelog/7604-no-estimated-deposits-on-deposits-detail-page @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Exclude estimated deposits from the deposits list screen diff --git a/client/data/deposits/hooks.ts b/client/data/deposits/hooks.ts index 7e0b53bac21..577cc7ef04d 100644 --- a/client/data/deposits/hooks.ts +++ b/client/data/deposits/hooks.ts @@ -124,8 +124,14 @@ export const useDeposits = ( { date_between: dateBetween, status_is: statusIs, status_is_not: statusIsNot, -}: Query ): CachedDeposits => - useSelect( +}: Query ): CachedDeposits => { + // Temporarily default to excluding estimated deposits. + // Client components can (temporarily) opt-in by passing `status_is=estimated`. + // When we remove estimated deposits from server / APIs we can remove this default. + if ( ! statusIsNot && statusIs !== 'estimated' ) { + statusIsNot = 'estimated'; + } + return useSelect( ( select ) => { const { getDeposits, @@ -176,6 +182,7 @@ export const useDeposits = ( { statusIsNot, ] ); +}; export const useDepositsSummary = ( { match, @@ -185,8 +192,14 @@ export const useDepositsSummary = ( { date_between: dateBetween, status_is: statusIs, status_is_not: statusIsNot, -}: Query ): DepositsSummaryCache => - useSelect( +}: Query ): DepositsSummaryCache => { + // Temporarily default to excluding estimated deposits. + // Client components can (temporarily) opt-in by passing `status_is=estimated`. + // When we remove estimated deposits from server / APIs we can remove this default. + if ( ! statusIsNot && statusIs !== 'estimated' ) { + statusIsNot = 'estimated'; + } + return useSelect( ( select ) => { const { getDepositsSummary, isResolving } = select( STORE_NAME ); @@ -215,6 +228,7 @@ export const useDepositsSummary = ( { statusIsNot, ] ); +}; export const useInstantDeposit = ( transactionIds: string[] From 1374d1e7f95ef0512b95a2b6b406c257bc5148e2 Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Thu, 16 Nov 2023 12:13:58 +1300 Subject: [PATCH 19/61] Remove estimated deposits from transaction list and details page (#7670) Co-authored-by: Rua Haszard --- changelog/fix-7657_7658-estimated_deposits | 5 +++++ client/payment-details/timeline/map-events.js | 5 ++--- client/transactions/list/deposit.tsx | 17 ++++++----------- .../list/test/__snapshots__/deposit.tsx.snap | 15 +-------------- .../list/test/__snapshots__/index.tsx.snap | 10 ---------- 5 files changed, 14 insertions(+), 38 deletions(-) create mode 100644 changelog/fix-7657_7658-estimated_deposits diff --git a/changelog/fix-7657_7658-estimated_deposits b/changelog/fix-7657_7658-estimated_deposits new file mode 100644 index 00000000000..bd042c3df34 --- /dev/null +++ b/changelog/fix-7657_7658-estimated_deposits @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Smaller part of a larger change that has a clear changelog entry + + diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index 91fb76fb187..49b74106f6d 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -70,7 +70,7 @@ const getDepositTimelineItem = ( body = [] ) => { let headline = ''; - if ( event.deposit ) { + if ( event.deposit && ! event.deposit.id.includes( 'wcpay_estimated_' ) ) { headline = sprintf( isPositive ? // translators: %1$s - formatted amount, %2$s - deposit arrival date, - link to the deposit @@ -89,7 +89,6 @@ const getDepositTimelineItem = ( moment( event.deposit.arrival_date * 1000 ).toISOString() ) ); - const depositUrl = getAdminUrl( { page: 'wc-admin', path: '/payments/deposits/details', @@ -136,7 +135,7 @@ const getDepositTimelineItem = ( */ const getFinancingPaydownTimelineItem = ( event, formattedAmount, body ) => { let headline = ''; - if ( event.deposit ) { + if ( event.deposit && ! event.deposit.id.includes( 'wcpay_estimated_' ) ) { headline = sprintf( // translators: %1$s - formatted amount, %2$s - deposit arrival date, - link to the deposit __( diff --git a/client/transactions/list/deposit.tsx b/client/transactions/list/deposit.tsx index c32a9ed8f88..c0604f0b097 100644 --- a/client/transactions/list/deposit.tsx +++ b/client/transactions/list/deposit.tsx @@ -6,7 +6,6 @@ import { dateI18n } from '@wordpress/date'; import moment from 'moment'; import { Link } from '@woocommerce/components'; -import { __ } from '@wordpress/i18n'; import React from 'react'; import { getAdminUrl } from 'wcpay/utils'; @@ -16,7 +15,11 @@ interface DepositProps { } const Deposit = ( { depositId, dateAvailable }: DepositProps ): JSX.Element => { - if ( depositId && dateAvailable ) { + if ( + depositId && + dateAvailable && + ! depositId.includes( 'wcpay_estimated_' ) + ) { const depositUrl = getAdminUrl( { page: 'wc-admin', path: '/payments/deposits/details', @@ -29,15 +32,7 @@ const Deposit = ( { depositId, dateAvailable }: DepositProps ): JSX.Element => { true // TODO Change call to gmdateI18n and remove this deprecated param once WP 5.4 support ends. ); - const estimated = depositId.includes( 'wcpay_estimated_' ) - ? __( 'Estimated', 'woocommerce-payments' ) - : ''; - - return ( - - { estimated } { formattedDateAvailable } - - ); + return { formattedDateAvailable }; } return <>; diff --git a/client/transactions/list/test/__snapshots__/deposit.tsx.snap b/client/transactions/list/test/__snapshots__/deposit.tsx.snap index 97999125648..ff5e2f4fb9d 100644 --- a/client/transactions/list/test/__snapshots__/deposit.tsx.snap +++ b/client/transactions/list/test/__snapshots__/deposit.tsx.snap @@ -6,8 +6,6 @@ exports[`Deposit renders with date and deposit available 1`] = ` data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020
@@ -17,17 +15,6 @@ exports[`Deposit renders with date available but no deposit 1`] = `
`; exports[`Deposit renders with deposit but no date available 1`] = `
`; -exports[`Deposit renders with estimated date and deposit available 1`] = ` - -`; +exports[`Deposit renders with estimated date and deposit available 1`] = `
`; exports[`Deposit renders with no date or deposit available 1`] = `
`; diff --git a/client/transactions/list/test/__snapshots__/index.tsx.snap b/client/transactions/list/test/__snapshots__/index.tsx.snap index 8f9669c6be4..2f460c0f997 100644 --- a/client/transactions/list/test/__snapshots__/index.tsx.snap +++ b/client/transactions/list/test/__snapshots__/index.tsx.snap @@ -767,8 +767,6 @@ exports[`Transactions list renders correctly when can filter by several currenci data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -1700,8 +1698,6 @@ exports[`Transactions list renders correctly when filtered by currency 1`] = ` data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -3336,8 +3332,6 @@ exports[`Transactions list subscription column renders correctly 1`] = ` data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -4314,8 +4308,6 @@ exports[`Transactions list when not filtered by deposit renders correctly 1`] = data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 @@ -5289,8 +5281,6 @@ exports[`Transactions list when not filtered by deposit renders table summary on data-link-type="wc-admin" href="admin.php?page=wc-admin&path=%2Fpayments%2Fdeposits%2Fdetails&id=po_mock" > - - Jan 7, 2020 From 8e3699810721f1378a3dedacb9c806f26aa6b0d9 Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:29:29 +1000 Subject: [PATCH 20/61] Update Payments Overview deposits UI to simplify how we communicate upcoming deposits (#7656) Co-authored-by: Bruce Aldridge --- .../update-7646-payments-overview-deposits-ui | 4 + .../deposits-overview/deposit-notices.tsx | 152 ++++++++++++ .../deposits-overview/deposit-schedule.tsx | 61 +++-- .../components/deposits-overview/footer.tsx | 67 ----- client/components/deposits-overview/index.tsx | 195 +++++++++++---- .../deposits-overview/next-deposit.tsx | 234 ------------------ .../recent-deposits-list.tsx | 20 -- .../deposits-overview/section-heading.tsx | 64 ----- .../components/deposits-overview/strings.ts | 21 -- .../components/deposits-overview/style.scss | 57 +++-- .../suspended-deposit-notice.tsx | 48 ---- .../test/__snapshots__/index.tsx.snap | 221 +---------------- .../deposits-overview/test/index.tsx | 229 ++++++++--------- client/components/deposits-overview/utils.ts | 45 ---- 14 files changed, 486 insertions(+), 932 deletions(-) create mode 100644 changelog/update-7646-payments-overview-deposits-ui create mode 100644 client/components/deposits-overview/deposit-notices.tsx delete mode 100644 client/components/deposits-overview/footer.tsx delete mode 100644 client/components/deposits-overview/next-deposit.tsx delete mode 100644 client/components/deposits-overview/section-heading.tsx delete mode 100644 client/components/deposits-overview/strings.ts delete mode 100644 client/components/deposits-overview/suspended-deposit-notice.tsx delete mode 100644 client/components/deposits-overview/utils.ts diff --git a/changelog/update-7646-payments-overview-deposits-ui b/changelog/update-7646-payments-overview-deposits-ui new file mode 100644 index 00000000000..8b7c430bbd3 --- /dev/null +++ b/changelog/update-7646-payments-overview-deposits-ui @@ -0,0 +1,4 @@ +Significance: major +Type: update + +Update Payments Overview deposits UI to simplify how we communicate upcoming deposits. diff --git a/client/components/deposits-overview/deposit-notices.tsx b/client/components/deposits-overview/deposit-notices.tsx new file mode 100644 index 00000000000..7a38783a33b --- /dev/null +++ b/client/components/deposits-overview/deposit-notices.tsx @@ -0,0 +1,152 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; +import { tip } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import InlineNotice from 'components/inline-notice'; + +/** + * Renders a notice informing the user that their deposits are suspended. + */ +export const SuspendedDepositNotice: React.FC = () => { + return ( + + { interpolateComponents( { + /** translators: {{strong}}: placeholders are opening and closing strong tags. {{suspendLink}}: is a link element */ + mixedString: __( + 'Your deposits are {{strong}}temporarily suspended{{/strong}}. {{suspendLink}}Learn more{{/suspendLink}}', + 'woocommerce-payments' + ), + components: { + strong: , + suspendLink: ( + + ), + }, + } ) } + + ); +}; + +/** + * Renders a notice informing the user that the next deposit will include funds from a loan disbursement. + */ +export const DepositIncludesLoanPayoutNotice: React.FC = () => ( + + { interpolateComponents( { + mixedString: __( + 'This deposit will include funds from your WooCommerce Capital loan. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + +); + +/** + * Renders a notice informing the user of the new account deposit waiting period. + */ +export const NewAccountWaitingPeriodNotice: React.FC = () => ( + + { interpolateComponents( { + mixedString: __( + 'Your first deposit is held for seven business days. {{whyLink}}Why?{{/whyLink}}', + 'woocommerce-payments' + ), + components: { + whyLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + +); + +/** + * Renders a notice informing the user of the number of days it may take for deposits to appear in their bank account. + */ +export const DepositTransitDaysNotice: React.FC = () => ( + + { __( + 'It may take 1-3 business days for deposits to reach your bank account.', + 'woocommerce-payments' + ) } + +); + +/** + * Renders a notice informing the user that their deposits may be paused due to a negative balance. + */ +export const NegativeBalanceDepositsPausedNotice: React.FC = () => ( + + { interpolateComponents( { + mixedString: sprintf( + /* translators: %s: WooPayments */ + __( + 'Deposits may be interrupted while your %s balance remains negative. {{whyLink}}Why?{{/whyLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + whyLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + +); diff --git a/client/components/deposits-overview/deposit-schedule.tsx b/client/components/deposits-overview/deposit-schedule.tsx index 86c9493bfd0..c7eae7bca78 100644 --- a/client/components/deposits-overview/deposit-schedule.tsx +++ b/client/components/deposits-overview/deposit-schedule.tsx @@ -9,33 +9,35 @@ import moment from 'moment'; /** * Internal dependencies */ -import { getDepositMonthlyAnchorLabel } from 'wcpay/deposits/utils'; +import { + getDepositMonthlyAnchorLabel, + getNextDepositDate, +} from 'wcpay/deposits/utils'; import type * as AccountOverview from 'wcpay/types/account-overview'; -/** - * The type of the props for the DepositScheduleDescription component. - * Mimics the AccountOverview.Account['deposits_schedule'] declaration. - */ -type DepositsScheduleProps = AccountOverview.Account[ 'deposits_schedule' ]; - +interface DepositScheduleProps { + depositsSchedule: AccountOverview.Account[ 'deposits_schedule' ]; +} /** * Renders the Deposit Schedule details component. * * eg "Your deposits are dispatched automatically every day" - * - * @param {DepositsScheduleProps} depositsSchedule The account's deposit schedule. - * @return {JSX.Element} Rendered element with Deposit Schedule details. */ -const DepositSchedule: React.FC< DepositsScheduleProps > = ( - depositsSchedule: DepositsScheduleProps -): JSX.Element => { +const DepositSchedule: React.FC< DepositScheduleProps > = ( { + depositsSchedule, +} ) => { + const nextDepositDate = getNextDepositDate( depositsSchedule ); + switch ( depositsSchedule.interval ) { case 'daily': return interpolateComponents( { - /** translators: {{strong}}: placeholders are opening and closing strong tags. */ - mixedString: __( - 'Your deposits are dispatched {{strong}}automatically every day{{/strong}}', - 'woocommerce-payments' + mixedString: sprintf( + /** translators: {{strong}}: placeholders are opening and closing strong tags. %s: is the date of the next deposit, e.g. "January 1st, 2023". */ + __( + 'Available funds are automatically dispatched {{strong}}every day{{/strong}} – your next deposit is scheduled for {{strong}}%s{{/strong}}.', + 'woocommerce-payments' + ), + nextDepositDate ), components: { strong: , @@ -50,12 +52,13 @@ const DepositSchedule: React.FC< DepositsScheduleProps > = ( return interpolateComponents( { mixedString: sprintf( - /** translators: %s: is the day of the week. eg "Friday". {{strong}}: placeholders are opening and closing strong tags.*/ + /** translators: %1$s: is the day of the week. eg "Friday". %2$s: is the date of the next deposit, e.g. "January 1st, 2023". {{strong}}: placeholders are opening and closing strong tags. */ __( - 'Your deposits are dispatched {{strong}}automatically every %s{{/strong}}', + 'Available funds are automatically dispatched {{strong}}every %1$s{{/strong}} – your next deposit is scheduled for {{strong}}%2$s{{/strong}}.', 'woocommerce-payments' ), - dayOfWeek + dayOfWeek, + nextDepositDate ), components: { strong: , @@ -67,10 +70,13 @@ const DepositSchedule: React.FC< DepositsScheduleProps > = ( // If the monthly anchor is 31, it means the deposit is scheduled for the last day of the month and has special handling. if ( monthlyAnchor === 31 ) { return interpolateComponents( { - /** translators: {{strong}}: placeholders are opening and closing strong tags. */ - mixedString: __( - 'Your deposits are dispatched {{strong}}automatically on the last day of every month{{/strong}}', - 'woocommerce-payments' + mixedString: sprintf( + /** translators: {{strong}}: placeholders are opening and closing strong tags. %s: is the date of the next deposit, e.g. "January 1st, 2023". */ + __( + 'Available funds are automatically dispatched {{strong}}on the last day of every month{{/strong}} – your next deposit is scheduled for {{strong}}%s{{/strong}}.', + 'woocommerce-payments' + ), + nextDepositDate ), components: { strong: , @@ -80,15 +86,16 @@ const DepositSchedule: React.FC< DepositsScheduleProps > = ( return interpolateComponents( { mixedString: sprintf( - /** translators: %s: is the day of the month. eg "15th". {{strong}}: placeholders are opening and closing strong tags.*/ + /** translators: {{strong}}: placeholders are opening and closing strong tags. %1$s: is the day of the month. eg "31st". %2$s: is the date of the next deposit, e.g. "January 1st, 2023". */ __( - 'Your deposits are dispatched {{strong}}automatically on the %s of every month{{/strong}}', + 'Available funds are automatically dispatched {{strong}}on the %1$s of every month{{/strong}} – your next deposit is scheduled for {{strong}}%2$s{{/strong}}.', 'woocommerce-payments' ), getDepositMonthlyAnchorLabel( { monthlyAnchor: monthlyAnchor, capitalize: false, - } ) + } ), + nextDepositDate ), components: { strong: , diff --git a/client/components/deposits-overview/footer.tsx b/client/components/deposits-overview/footer.tsx deleted file mode 100644 index 497412288b4..00000000000 --- a/client/components/deposits-overview/footer.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { CardFooter, Button, Flex } from '@wordpress/components'; -import { Link } from '@woocommerce/components'; - -/** - * Internal dependencies. - */ -import { getAdminUrl } from 'wcpay/utils'; -import strings from './strings'; -import wcpayTracks from 'tracks'; - -/** - * Renders the footer of the deposits overview card. - * - * @return {JSX.Element} Rendered footer of the deposits overview card. - */ -const DepositsOverviewFooter: React.FC = () => { - // The URL to the deposits list table. - const depositListTableUrl = getAdminUrl( { - page: 'wc-admin', - path: '/payments/deposits', - } ); - - // The URL to the deposit schedule settings page. - const depositScheduleUrl = - getAdminUrl( { - page: 'wc-settings', - tab: 'checkout', - section: 'woocommerce_payments', - } ) + '#deposit-schedule'; - - return ( - - - - - wcpayTracks.recordEvent( - wcpayTracks.events - .OVERVIEW_DEPOSITS_CHANGE_SCHEDULE_CLICK - ) - } - > - { strings.changeDepositSchedule } - - - - ); -}; - -export default DepositsOverviewFooter; diff --git a/client/components/deposits-overview/index.tsx b/client/components/deposits-overview/index.tsx index 59c6b762e2d..27c754c0640 100644 --- a/client/components/deposits-overview/index.tsx +++ b/client/components/deposits-overview/index.tsx @@ -2,87 +2,184 @@ * External dependencies */ import * as React from 'react'; -import { Card, CardHeader } from '@wordpress/components'; +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies. */ +import { getAdminUrl } from 'wcpay/utils'; +import wcpayTracks from 'tracks'; +import Loadable from 'components/loadable'; import { useSelectedCurrencyOverview } from 'wcpay/overview/hooks'; -import strings from './strings'; -import NextDepositDetails from './next-deposit'; import RecentDepositsList from './recent-deposits-list'; import DepositSchedule from './deposit-schedule'; -import SuspendedDepositNotice from './suspended-deposit-notice'; -import DepositsOverviewFooter from './footer'; -import DepositOverviewSectionHeading from './section-heading'; +import { + DepositTransitDaysNotice, + NegativeBalanceDepositsPausedNotice, + NewAccountWaitingPeriodNotice, + SuspendedDepositNotice, +} from './deposit-notices'; import useRecentDeposits from './hooks'; import './style.scss'; -const DepositsOverview = (): JSX.Element => { +const DepositsOverview: React.FC = () => { const { account, overview, isLoading: isLoadingOverview, } = useSelectedCurrencyOverview(); + const selectedCurrency = + overview?.currency || wcpaySettings.accountDefaultCurrency; + const { isLoading: isLoadingDeposits, deposits } = useRecentDeposits( + selectedCurrency + ); - let currency = wcpaySettings.accountDefaultCurrency; + const isLoading = isLoadingOverview || isLoadingDeposits; - if ( overview?.currency ) { - currency = overview.currency; - } + const availableFunds = overview?.available?.amount ?? 0; - const { isLoading: isLoadingDeposits, deposits } = useRecentDeposits( - currency - ); + // If the account has deposits blocked, there is no available balance or it is negative, there is no future deposit expected. + const isNextDepositExpected = + ! account?.deposits_blocked && availableFunds > 0; + // If the available balance is negative, deposits may be paused. + const isNegativeBalanceDepositsPaused = availableFunds < 0; + const hasCompletedWaitingPeriod = + wcpaySettings.accountStatus.deposits?.completed_waiting_period; + // Only show the deposit history section if the page is finished loading and there are deposits. */ } + const showRecentDeposits = + ! isLoading && + deposits?.length > 0 && + !! account && + ! account?.deposits_blocked; - const hasNextDeposit = !! overview?.nextScheduled; + // Show a loading state if the page is still loading. + if ( isLoading ) { + return ( + + + { __( 'Deposits', 'woocommerce-payments' ) } + - const isLoading = isLoadingOverview || isLoadingDeposits; + + + } + /> + + + ); + } // This card isn't shown if there are no deposits, so we can bail early. - if ( ! hasNextDeposit && ! isLoading && deposits.length === 0 ) { - return <>; + if ( ! isLoading && availableFunds === 0 && deposits.length === 0 ) { + return null; } return ( - { strings.heading } - { /* Only show the next deposit section if the page is loading or if deposits are not blocked. */ } - { ( isLoading || ! account?.deposits_blocked ) && ( - <> - - + { __( 'Deposits', 'woocommerce-payments' ) } + + + { /* Deposit schedule message */ } + { isNextDepositExpected && !! account && ( + + - + ) } - { /* Only show the deposit history section if the page is finished loading and there are deposits. */ } - { ! isLoading && !! account && !! deposits && deposits.length > 0 && ( + + { /* Notices */ } + + { account?.deposits_blocked ? ( + + ) : ( + <> + { isNextDepositExpected && ( + + ) } + { ! hasCompletedWaitingPeriod && ( + + ) } + { isNegativeBalanceDepositsPaused && ( + + ) } + + ) } + + + { showRecentDeposits && ( <> - { account.deposits_blocked ? ( - } - /> - ) : ( - - } - /> - ) } + + + { __( 'Deposit history', 'woocommerce-payments' ) } + + ) } - + + + + + { ! account?.deposits_blocked && ( + + ) } + ); }; diff --git a/client/components/deposits-overview/next-deposit.tsx b/client/components/deposits-overview/next-deposit.tsx deleted file mode 100644 index bcb588e60cc..00000000000 --- a/client/components/deposits-overview/next-deposit.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/** - * External dependencies - */ -import * as React from 'react'; -import { - CardBody, - CardDivider, - Flex, - FlexItem, - Icon, -} from '@wordpress/components'; -import { calendar } from '@wordpress/icons'; -import interpolateComponents from '@automattic/interpolate-components'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies. - */ -import Loadable from 'components/loadable'; -import { getNextDeposit } from './utils'; -import DepositStatusChip from 'components/deposit-status-chip'; -import { getDepositDate } from 'deposits/utils'; -import { useAllDepositsOverviews, useDepositIncludesLoan } from 'wcpay/data'; -import InlineNotice from 'components/inline-notice'; -import { useSelectedCurrency } from 'wcpay/overview/hooks'; -import type * as AccountOverview from 'wcpay/types/account-overview'; - -type NextDepositProps = { - isLoading: boolean; - overview?: AccountOverview.Overview; -}; - -const DepositIncludesLoanPayoutNotice = () => ( - - { interpolateComponents( { - mixedString: __( - 'This deposit will include funds from your WooCommerce Capital loan. {{learnMoreLink}}Learn more{{/learnMoreLink}}', - 'woocommerce-payments' - ), - components: { - learnMoreLink: ( - // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - }, - } ) } - -); - -const NewAccountWaitingPeriodNotice = () => ( - - { interpolateComponents( { - mixedString: __( - 'Your first deposit is held for seven business days. {{whyLink}}Why?{{/whyLink}}', - 'woocommerce-payments' - ), - components: { - whyLink: ( - // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - }, - } ) } - -); - -const NegativeBalanceDepositsPausedNotice = () => ( - - { interpolateComponents( { - mixedString: sprintf( - /* translators: %s: WooPayments */ - __( - 'Deposits may be interrupted while your %s balance remains negative. {{whyLink}}Why?{{/whyLink}}', - 'woocommerce-payments' - ), - 'WooPayments' - ), - components: { - whyLink: ( - // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. - // eslint-disable-next-line jsx-a11y/anchor-has-content - - ), - }, - } ) } - -); - -/** - * Renders the Next Deposit details component. - * - * This component included the next deposit heading, table and notice. - * - * @param {NextDepositProps} props Next Deposit details props. - * @return {JSX.Element} Rendered element with Next Deposit details. - */ -const NextDepositDetails: React.FC< NextDepositProps > = ( { - isLoading, - overview, -} ): JSX.Element => { - const tableClass = 'wcpay-deposits-overview__table'; - const nextDeposit = getNextDeposit( overview ); - const nextDepositDate = getDepositDate( - nextDeposit.date > 0 ? nextDeposit : null - ); - - const { includesFinancingPayout } = useDepositIncludesLoan( - nextDeposit.id - ); - const completedWaitingPeriod = - wcpaySettings.accountStatus.deposits?.completed_waiting_period; - - const { - overviews, - } = useAllDepositsOverviews() as AccountOverview.OverviewsResponse; - const { selectedCurrency } = useSelectedCurrency(); - const displayedCurrency = - selectedCurrency ?? wcpaySettings.accountDefaultCurrency; - - const availableBalance = overviews?.currencies.find( - ( currencyOverview ) => displayedCurrency === currencyOverview.currency - )?.available; - - const negativeBalanceDepositsPaused = - availableBalance && availableBalance.amount < 0; - - return ( - <> - { /* Next Deposit Table */ } - - - - - - - - - - - - - - - - - - { ! isLoading && ( - - ) } - - - - - } - /> - - - - - - - { /* Notices */ } - { ! isLoading && ( - - { includesFinancingPayout && ( - - ) } - { ! completedWaitingPeriod && ( - - ) } - { negativeBalanceDepositsPaused && ( - - ) } - - ) } - - ); -}; - -export default NextDepositDetails; diff --git a/client/components/deposits-overview/recent-deposits-list.tsx b/client/components/deposits-overview/recent-deposits-list.tsx index a0cbfea5fc2..119e7adc92f 100644 --- a/client/components/deposits-overview/recent-deposits-list.tsx +++ b/client/components/deposits-overview/recent-deposits-list.tsx @@ -23,7 +23,6 @@ import { getDepositDate } from 'deposits/utils'; import { CachedDeposit } from 'wcpay/types/deposits'; import { formatCurrency } from 'wcpay/utils/currency'; import { getDetailsURL } from 'wcpay/components/details-link'; -import InlineNotice from '../inline-notice'; interface DepositRowProps { deposit: CachedDeposit; @@ -76,28 +75,9 @@ const RecentDepositsList: React.FC< RecentDepositsProps > = ( { return <>; } - // Add a notice indicating the potential business day delay for pending and in_transit deposits. - // The notice is added after the oldest pending or in_transit deposit. - const oldestPendingDepositId = [ ...deposits ] - .reverse() - .find( - ( deposit ) => - 'pending' === deposit.status || 'in_transit' === deposit.status - )?.id; const depositRows = deposits.map( ( deposit ) => ( - { deposit.id === oldestPendingDepositId && ( - - ) } ) ); diff --git a/client/components/deposits-overview/section-heading.tsx b/client/components/deposits-overview/section-heading.tsx deleted file mode 100644 index a7573b50ae4..00000000000 --- a/client/components/deposits-overview/section-heading.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { CardBody } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import Loadable from '../loadable'; - -/** - * SectionHeadingProps - * - * @typedef {Object} SectionHeadingProps - * - * @property {string} title Section heading title. - * @property {string} description Section heading description. - * @property {boolean} [isLoading] Optional. Whether the section heading is loading. - */ -type SectionHeadingProps = { - title: string; - text?: string | React.ReactNode; - children?: React.ReactNode; - isLoading?: boolean; -}; - -/** - * Renders the section heading component. - * - * @param {SectionHeadingProps} props Section heading props. - * @param {string} props.title Section heading title. - * @param {string} props.description Section heading description. - * @param {boolean} [props.isLoading] Optional. Whether the section heading should is loading. - * - * @return {JSX.Element} Rendered element with section heading. - */ -const DepositOverviewSectionHeading: React.FC< SectionHeadingProps > = ( { - title, - text = '', - children = null, - isLoading = false, -} ): JSX.Element => { - return ( - - - - -
- - { text !== '' ? ( - - { text } - - ) : ( - <>{ children } - ) } - -
-
- ); -}; - -export default DepositOverviewSectionHeading; diff --git a/client/components/deposits-overview/strings.ts b/client/components/deposits-overview/strings.ts deleted file mode 100644 index 2e6ab00b6da..00000000000 --- a/client/components/deposits-overview/strings.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -export default { - heading: __( 'Deposits', 'woocommerce-payments' ), - nextDeposit: { - title: __( 'Next deposit', 'woocommerce-payments' ), - description: __( - 'The amount may change while payments are still accumulating', - 'woocommerce-payments' - ), - }, - viewAllDeposits: __( 'View full deposits history', 'woocommerce-payments' ), - changeDepositSchedule: __( - 'Change deposit schedule', - 'woocommerce-payments' - ), - depositHistoryHeading: __( 'Deposit history', 'woocommerce-payments' ), -}; diff --git a/client/components/deposits-overview/style.scss b/client/components/deposits-overview/style.scss index 9b2c500af31..bfecab9f98c 100644 --- a/client/components/deposits-overview/style.scss +++ b/client/components/deposits-overview/style.scss @@ -11,31 +11,18 @@ line-height: 24px; color: $gray-900; } - - &__description { - & > &__text { - line-height: 16px; - color: $gray-700; - } - } } .wcpay-inline-notice.components-notice { - margin: 0; + margin: 16px 0; } - // Apply a margin bottom to all except the last notice - // in the notices container and to the business delay - // notice if it's the last child of the Deposit history table. - &__notices__container - > .wcpay-inline-notice.components-notice:not( :last-child ), - .wcpay-deposits-overview__business-day-delay-notice:last-child { - margin-bottom: 16px; - } + .components-card__body.wcpay-deposits-overview__schedule__container { + padding-top: 24px; + padding-bottom: 0; - // If the notices container is the last element before the footer (no deposit history), apply a margin to the footer, to float the notices on. - .wcpay-deposits-overview__notices__container:not( :empty ) - + .wcpay-deposits-overview__footer { - margin-top: 16px; + .is-loadable-placeholder { + margin-bottom: 24px; + } } // Override extraneous CardBody vertical padding - @@ -52,6 +39,15 @@ display: flex; } + // Override notice colors for transit days notice + .wcpay-inline-notice.components-notice.wcpay-deposit-transit-days-notice { + background-color: $gray-0; + + .wcpay-inline-notice__icon svg { + fill: $gray-900; + } + } + &__table { &__row { &__header { @@ -83,12 +79,21 @@ } } } - &__footer { - :not( :first-child ) { - margin-left: 12px; - } - a:not( .components-button ) { - text-decoration: none; + + &__footer.components-card__footer { + padding: 24px; + flex-wrap: wrap; + justify-content: flex-start; + align-items: center; + + @media screen and ( max-width: 550px ) { + flex-direction: column; + justify-content: center; + padding: 16px 24px; + + > * { + margin: 0; + } } } } diff --git a/client/components/deposits-overview/suspended-deposit-notice.tsx b/client/components/deposits-overview/suspended-deposit-notice.tsx deleted file mode 100644 index 58f6b23e8a4..00000000000 --- a/client/components/deposits-overview/suspended-deposit-notice.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { __ } from '@wordpress/i18n'; -import interpolateComponents from '@automattic/interpolate-components'; -import { Link } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import InlineNotice from 'components/inline-notice'; - -/** - * Renders a notice informing the user that their deposits are suspended. - * - * @return {JSX.Element} Rendered notice. - */ -function SuspendedDepositNotice(): JSX.Element { - return ( - - { interpolateComponents( { - /** translators: {{strong}}: placeholders are opening and closing strong tags. {{suspendLink}}: is a
link element */ - mixedString: __( - 'Your deposits are {{strong}}temporarily suspended{{/strong}}. {{suspendLink}}Learn more{{/suspendLink}}', - 'woocommerce-payments' - ), - components: { - strong: , - suspendLink: ( - - ), - }, - } ) } - - ); -} - -export default SuspendedDepositNotice; diff --git a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap index 7f4bf264503..9dcdb85548d 100644 --- a/client/components/deposits-overview/test/__snapshots__/index.tsx.snap +++ b/client/components/deposits-overview/test/__snapshots__/index.tsx.snap @@ -1,34 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Deposits Overview footer renders Component Renders 1`] = ` - -`; - exports[`Deposits Overview information Component Renders 1`] = `
Deposits
-
- - Next deposit - -
- - The amount may change while payments are still accumulating - -
-
-
-
-
- Estimated dispatch date -
-
- Status -
-
- Amount -
-
-
- -
-
-
- - — -
-
- - Pending - -
-
- $1.00 -
-
-
Deposit history -
- - Your deposits are dispatched - - automatically every Monday - - -
-
-
-
-
- - - - - -
-
- Deposits pending or in-transit may take 1-2 business days to appear in your bank account once dispatched -
-
-
-
-
{ includesFinancingPayout: false, isLoading: false, } ); + mockAccount.deposits_blocked = false; } ); afterEach( () => { jest.clearAllMocks(); @@ -261,65 +260,71 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const { container } = render( ); + const { container, getByText } = render( ); + // Check that the button and link is rendered. + getByText( 'View full deposits history' ); + getByText( 'Change deposit schedule' ); expect( container ).toMatchSnapshot(); } ); - test( 'Component renders without errors for new account', () => { + test( `Component doesn't render for new account`, () => { mockOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: [], + isLoading: false, + } ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, } ); - const { getByText } = render( ); - getByText( '€0.00' ); + const { container } = render( ); + expect( container ).toBeEmptyDOMElement(); } ); - test( 'Confirm next deposit in EUR amount', () => { - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + test( `Component doesn't render for new accounts with pending funds but no available funds`, () => { + mockOverviews( [ createMockNewAccountOverview( 'eur', 5000, 0 ) ] ); + mockDepositOverviews( [ + createMockNewAccountOverview( 'eur', 5000, 0 ), + ] ); + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: [], + isLoading: false, + } ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, } ); - const overview = createMockOverview( 'usd', 100, 0, 'pending' ); - const { getByText } = render( - - ); - - expect( getByText( '$1.00' ) ).toBeTruthy(); + const { container } = render( ); + expect( container ).toBeEmptyDOMElement(); } ); - test( 'Confirm next deposit in EUR amount', () => { - global.wcpaySettings.connect.country = 'EU'; - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + test( 'Confirm notice renders if deposits blocked', () => { + mockAccount.deposits_blocked = true; + mockOverviews( [ + createMockOverview( 'usd', 30000, 50000, 'pending' ), + ] ); + mockUseDeposits.mockReturnValue( { + depositsCount: 0, + deposits: mockDeposits, + isLoading: false, + } ); + mockDepositOverviews( [ createMockNewAccountOverview( 'usd' ) ] ); mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'eur', + selectedCurrency: 'usd', setSelectedCurrency: mockSetSelectedCurrency, } ); - const overview = createMockOverview( 'EUR', 647049, 0, 'pending' ); - const { getByText } = render( - - ); - - expect( getByText( '€6.470,49' ) ).toBeTruthy(); - } ); + const { getByText, queryByText } = render( ); - test( 'Confirm next deposit dates', () => { - const date = Date.parse( '2021-10-01' ); - const overview = createMockOverview( 'usd', 100, date, 'pending' ); + getByText( /Your deposits are temporarily suspended/ ); - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); - mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'eur', - setSelectedCurrency: mockSetSelectedCurrency, - } ); - - const { getByText } = render( - - ); - expect( getByText( 'October 1, 2021' ) ).toBeTruthy(); + // Check that the buttons are rendered as expected. + getByText( 'View full deposits history' ); + // This one is not rendered when deposits are blocked. + expect( queryByText( 'Change deposit schedule' ) ).toBeFalsy(); } ); test( 'Confirm recent deposits renders ', () => { @@ -335,8 +340,8 @@ describe( 'Deposits Overview information', () => { expect( container ).toBeEmptyDOMElement(); } ); - test( 'Renders capital loan notice if deposit includes financing payout', () => { - const overview = createMockOverview( 'usd', 100, 0, 'pending' ); + // Capital loans notice temporarily disabled, tests skipped until resolved. See #7689. + test.skip( 'Renders capital loan notice if deposit includes financing payout', () => { mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: true, isLoading: false, @@ -347,9 +352,7 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const { getByRole, getByText } = render( - - ); + const { getByRole, getByText } = render( ); getByText( 'deposit will include funds from your WooCommerce Capital loan', @@ -368,8 +371,8 @@ describe( 'Deposits Overview information', () => { ); } ); - test( `Doesn't render capital loan notice if deposit does not include financing payout`, () => { - const overview = createMockOverview( 'usd', 100, 0, 'pending' ); + // Capital loans notice temporarily disabled, tests skipped until resolved. See #7689. + test.skip( `Doesn't render capital loan notice if deposit does not include financing payout`, () => { mockUseDepositIncludesLoan.mockReturnValue( { includesFinancingPayout: false, isLoading: false, @@ -380,9 +383,7 @@ describe( 'Deposits Overview information', () => { setSelectedCurrency: mockSetSelectedCurrency, } ); - const { queryByRole, queryByText } = render( - - ); + const { queryByRole, queryByText } = render( ); expect( queryByText( @@ -402,7 +403,13 @@ describe( 'Deposits Overview information', () => { test( 'Confirm new account waiting period notice does not show', () => { global.wcpaySettings.accountStatus.deposits.completed_waiting_period = true; - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + const accountOverview = createMockNewAccountOverview( + 'eur', + 12300, + 45600 + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, @@ -416,7 +423,13 @@ describe( 'Deposits Overview information', () => { test( 'Confirm new account waiting period notice shows', () => { global.wcpaySettings.accountStatus.deposits.completed_waiting_period = false; - mockDepositOverviews( [ createMockNewAccountOverview( 'eur' ) ] ); + const accountOverview = createMockNewAccountOverview( + 'eur', + 12300, + 45600 + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'eur', setSelectedCurrency: mockSetSelectedCurrency, @@ -433,84 +446,75 @@ describe( 'Deposits Overview information', () => { } ); } ); -describe( 'Deposits Overview footer renders', () => { - test( 'Component Renders', () => { - const { container, getByText } = render( ); - expect( container ).toMatchSnapshot(); - - // Check that the button and link is rendered. - getByText( 'View full deposits history' ); - getByText( 'Change deposit schedule' ); - } ); -} ); - describe( 'Deposit Schedule renders', () => { test( 'with a weekly schedule', () => { const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically every Monday' - ); + expect( descriptionText ).toContain( 'every Monday' ); } ); test( 'with a monthly schedule on the 14th', () => { mockAccount.deposits_schedule.interval = 'monthly'; mockAccount.deposits_schedule.monthly_anchor = 14; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically on the 14th of every month' - ); + expect( descriptionText ).toContain( 'on the 14th of every month' ); } ); test( 'with a monthly schedule on the last day', () => { mockAccount.deposits_schedule.interval = 'monthly'; mockAccount.deposits_schedule.monthly_anchor = 31; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically on the last day of every month' - ); + expect( descriptionText ).toContain( 'on the last day of every month' ); } ); test( 'with a monthly schedule on the 2nd', () => { mockAccount.deposits_schedule.interval = 'monthly'; mockAccount.deposits_schedule.monthly_anchor = 2; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically on the 2nd of every month' - ); + expect( descriptionText ).toContain( 'on the 2nd of every month' ); } ); test( 'with a daily schedule', () => { mockAccount.deposits_schedule.interval = 'daily'; const { container } = render( - + ); const descriptionText = container.textContent; - expect( descriptionText ).toContain( - 'Your deposits are dispatched automatically every day' - ); + expect( descriptionText ).toContain( 'every day' ); } ); test( 'with a daily schedule', () => { mockAccount.deposits_schedule.interval = 'manual'; const { container } = render( - + ); // Check that a manual schedule is not rendered. @@ -527,51 +531,34 @@ describe( 'Suspended Deposit Notice Renders', () => { describe( 'Paused Deposit notice Renders', () => { test( 'When available balance is negative', () => { - const overview = createMockOverview( 'usd', 100, 0, 'pending' ); - mockUseDeposits.mockReturnValue( { - depositsCount: 0, - deposits: mockDeposits, - isLoading: false, - } ); - mockDepositOverviews( [ - // Negative 100 available balance - createMockNewAccountOverview( 'usd', 100, -100 ), - ] ); + const accountOverview = createMockNewAccountOverview( + 'usd', + 100, + -100 // Negative 100 available balance + ); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + mockUseSelectedCurrency.mockReturnValue( { selectedCurrency: 'usd', setSelectedCurrency: mockSetSelectedCurrency, } ); - const { getByText } = render( - - ); - getByText( - 'Deposits may be interrupted while your WooPayments balance remains negative. Why?' - ); + const { getByText } = render( ); + getByText( /Deposits may be interrupted/, { + ignore: '.a11y-speak-region', + } ); } ); test( 'When available balance is positive', () => { - const overview = createMockOverview( 'usd', 100, 0, 'pending' ); - mockUseDeposits.mockReturnValue( { - depositsCount: 0, - deposits: mockDeposits, - isLoading: false, - } ); - mockDepositOverviews( [ - // Positive 100 available balance - createMockNewAccountOverview( 'usd', 100, 100 ), - ] ); - mockUseSelectedCurrency.mockReturnValue( { - selectedCurrency: 'usd', - setSelectedCurrency: mockSetSelectedCurrency, - } ); - - const { queryByText } = render( - + const accountOverview = createMockNewAccountOverview( + 'usd', + 100, + 100 // Positive 100 available balance ); - expect( - queryByText( - 'Deposits may be interrupted while your WooPayments balance remains negative. Why?' - ) - ).toBeFalsy(); + mockOverviews( [ accountOverview ] ); + mockDepositOverviews( [ accountOverview ] ); + + const { queryByText } = render( ); + expect( queryByText( /Deposits may be interrupted/ ) ).toBeFalsy(); } ); } ); diff --git a/client/components/deposits-overview/utils.ts b/client/components/deposits-overview/utils.ts deleted file mode 100644 index b6ae462576c..00000000000 --- a/client/components/deposits-overview/utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Internal dependencies - */ -import { formatCurrency } from 'utils/currency'; -import type { DepositStatus } from 'wcpay/types/deposits'; -import type * as AccountOverview from 'wcpay/types/account-overview'; - -type NextDepositTableData = { - id?: string; - date: number; - status: DepositStatus; - amount: string; -}; - -/** - * Formats the next deposit data from the overview object into an object that can be used in the Next Deposits table. - * - * @param {AccountOverview.Overview} [overview] - The overview object containing information about the next scheduled deposit. - * @return {NextDepositTableData} An object containing the formatted next deposit data, with the following properties: - * - id: An optional string representing the ID of the next scheduled deposit. - * - date: A Unix timestamp representing the date of the next scheduled deposit. - * - status: A string representing the status of the next scheduled deposit. If no status is provided, defaults to 'pending'. - * - amount: A formatted string representing the amount of the next scheduled deposit in the currency specified in the overview object. - */ -export const getNextDeposit = ( - overview?: AccountOverview.Overview -): NextDepositTableData => { - if ( ! overview?.nextScheduled ) { - return { - id: undefined, - date: 0, - status: 'pending', - amount: formatCurrency( 0, overview?.currency ), - }; - } - - const { currency, nextScheduled } = overview; - - return { - id: nextScheduled.id, - date: nextScheduled.date ?? 0, - status: nextScheduled.status ?? '-', - amount: formatCurrency( nextScheduled.amount ?? 0, currency ), - }; -}; From be1b142013b7a11cb0d147ca13134885ab845d3d Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 16 Nov 2023 10:11:22 +0100 Subject: [PATCH 21/61] Remove unsupported EUR currency from Afterpay payment method (#7723) --- ...eur-from-the-list-of-supported-currencies-for-afterpay | 4 ++++ .../payment-methods/class-afterpay-payment-method.php | 8 +------- 2 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 changelog/update-7713-remove-eur-from-the-list-of-supported-currencies-for-afterpay diff --git a/changelog/update-7713-remove-eur-from-the-list-of-supported-currencies-for-afterpay b/changelog/update-7713-remove-eur-from-the-list-of-supported-currencies-for-afterpay new file mode 100644 index 00000000000..5de0634dbb7 --- /dev/null +++ b/changelog/update-7713-remove-eur-from-the-list-of-supported-currencies-for-afterpay @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Remove unsupported EUR currency from Afterpay payment method. diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php index fd7bbcabb7b..5cc180b55c7 100644 --- a/includes/payment-methods/class-afterpay-payment-method.php +++ b/includes/payment-methods/class-afterpay-payment-method.php @@ -28,7 +28,7 @@ public function __construct( $token_service ) { $this->title = __( 'Afterpay', 'woocommerce-payments' ); $this->is_reusable = false; $this->icon_url = plugins_url( 'assets/images/payment-methods/afterpay.svg', WCPAY_PLUGIN_FILE ); - $this->currencies = [ 'USD', 'CAD', 'AUD', 'NZD', 'GBP', 'EUR' ]; + $this->currencies = [ 'USD', 'CAD', 'AUD', 'NZD', 'GBP' ]; $this->accept_only_domestic_payment = true; $this->limits_per_currency = [ 'AUD' => [ @@ -61,12 +61,6 @@ public function __construct( $token_service ) { 'max' => 400000, ], // Represents USD 1 - 4,000 USD. ], - 'EUR' => [ - 'default' => [ - 'min' => 100, - 'max' => 100000, - ], // Represents EUR 1 - 1,000 EUR. - ], ]; } From 9e27d8b214357e1f90fea4ec12b4275f9a195e98 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:14:27 +0200 Subject: [PATCH 22/61] Show Payments menu sub-items only for merchants that completed KYC (#7721) --- ...ts-menu-subitems-only-if-details-submitted | 4 +++ client/components/test-mode-notice/index.js | 5 +++- .../test/__snapshots__/index.js.snap | 21 ++++++++++++++ .../components/test-mode-notice/test/index.js | 18 ++++++++++++ includes/admin/class-wc-payments-admin.php | 11 ++++---- includes/class-wc-payments-account.php | 15 ++++++++++ .../admin/test-class-wc-payments-admin.php | 28 ++++++++++++------- 7 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 changelog/fix-7705-show-payments-menu-subitems-only-if-details-submitted diff --git a/changelog/fix-7705-show-payments-menu-subitems-only-if-details-submitted b/changelog/fix-7705-show-payments-menu-subitems-only-if-details-submitted new file mode 100644 index 00000000000..da810180ed7 --- /dev/null +++ b/changelog/fix-7705-show-payments-menu-subitems-only-if-details-submitted @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Show Payments menu sub-items only for merchants that completed KYC diff --git a/client/components/test-mode-notice/index.js b/client/components/test-mode-notice/index.js index 5d3d8738d38..bdc028c741f 100644 --- a/client/components/test-mode-notice/index.js +++ b/client/components/test-mode-notice/index.js @@ -145,7 +145,10 @@ export const getTopicDetails = ( topic ) => { * @return {string} The correct notice message. */ export const getNoticeMessage = ( topic ) => { - const urlComponent = getPaymentsSettingsUrlComponent(); + const { detailsSubmitted } = wcpaySettings.accountStatus; + const urlComponent = detailsSubmitted + ? getPaymentsSettingsUrlComponent() + : ''; if ( detailsTopics.includes( topic ) ) { return ( diff --git a/client/components/test-mode-notice/test/__snapshots__/index.js.snap b/client/components/test-mode-notice/test/__snapshots__/index.js.snap index e4a830ae214..a2539f2bee4 100644 --- a/client/components/test-mode-notice/test/__snapshots__/index.js.snap +++ b/client/components/test-mode-notice/test/__snapshots__/index.js.snap @@ -231,3 +231,24 @@ exports[`Test mode notification Component is rendered correctly 14`] = `
` exports[`Test mode notification Component is rendered correctly 15`] = `
`; exports[`Test mode notification Component is rendered correctly 16`] = `
`; + +exports[`Test mode notification Returns right notice message without URL component 1`] = ` +
+
+
+ + WooPayments is in test mode. + + + +
+
+
+
+`; diff --git a/client/components/test-mode-notice/test/index.js b/client/components/test-mode-notice/test/index.js index 8a39450739e..da49b19a57b 100644 --- a/client/components/test-mode-notice/test/index.js +++ b/client/components/test-mode-notice/test/index.js @@ -21,6 +21,13 @@ jest.mock( 'utils', () => ( { } ) ); describe( 'Test mode notification', () => { + beforeEach( () => { + global.wcpaySettings = { + accountStatus: { + detailsSubmitted: true, + }, + }; + } ); // Set up easy to use lists containing test inputs. const listTopics = [ topics.transactions, @@ -93,6 +100,17 @@ describe( 'Test mode notification', () => { } ); + test( 'Returns right notice message without URL component', () => { + global.wcpaySettings.accountStatus.detailsSubmitted = false; + const topic = topics.overview; + isInTestMode.mockReturnValue( true ); + const { container: testModeNotice } = render( + + ); + + expect( testModeNotice ).toMatchSnapshot(); + } ); + test.each( topicsWithTestMode )( 'Component is rendered correctly', ( topic, isTestMode ) => { diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 1398f37da59..12e727d69a5 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -344,13 +344,14 @@ public function add_payments_menu() { global $submenu; try { - $should_render_full_menu = $this->account->is_stripe_account_valid(); + // Render full payments menu with sub-items only if the merchant completed the KYC (details_submitted = true). + $should_render_full_menu = $this->account->is_account_fully_onboarded(); } catch ( Exception $e ) { - // There is an issue with connection but render full menu anyways to provide access to settings. - $should_render_full_menu = true; + // There is an issue with connection, don't render full menu, user will get redirected to the connect page. + $should_render_full_menu = false; } - $top_level_link = $should_render_full_menu ? '/payments/overview' : '/payments/connect'; + $top_level_link = $this->account->is_stripe_connected() ? '/payments/overview' : '/payments/connect'; $menu_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNjciCiAgIHNvZGlwb2RpOmRvY25hbWU9IndjcGF5X21lbnVfaWNvbi5zdmciCiAgIHdpZHRoPSI4NTIiCiAgIGhlaWdodD0iNjg0IgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEgKGM0ZThmOWUsIDIwMjEtMDUtMjQpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM3MSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzY5IgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOnpvb209IjI1NiIKICAgICBpbmtzY2FwZTpjeD0iLTg0Ljg1NzQyMiIKICAgICBpbmtzY2FwZTpjeT0iLTgzLjI5NDkyMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA4MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTE2IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyMDIiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc2NyIgLz4KICA8cGF0aAogICAgIHRyYW5zZm9ybT0ic2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtODUwLCAwKSIKICAgICBkPSJNIDc2OCw4NiBWIDU5OCBIIDg0IFYgODYgWiBtIDAsNTk4IGMgNDgsMCA4NCwtMzggODQsLTg2IFYgODYgQyA4NTIsMzggODE2LDAgNzY4LDAgSCA4NCBDIDM2LDAgMCwzOCAwLDg2IHYgNTEyIGMgMCw0OCAzNiw4NiA4NCw4NiB6IE0gMzg0LDEyOCB2IDQ0IGggLTg2IHYgODQgaCAxNzAgdiA0NCBIIDM0MCBjIC0yNCwwIC00MiwxOCAtNDIsNDIgdiAxMjggYyAwLDI0IDE4LDQyIDQyLDQyIGggNDQgdiA0NCBoIDg0IHYgLTQ0IGggODYgViA0MjggSCAzODQgdiAtNDQgaCAxMjggYyAyNCwwIDQyLC0xOCA0MiwtNDIgViAyMTQgYyAwLC0yNCAtMTgsLTQyIC00MiwtNDIgaCAtNDQgdiAtNDQgeiIKICAgICBmaWxsPSIjYTJhYWIyIgogICAgIGlkPSJwYXRoNjUiIC8+Cjwvc3ZnPgo='; @@ -377,7 +378,7 @@ public function add_payments_menu() { return; } - if ( ! $should_render_full_menu ) { + if ( ! $this->account->is_stripe_connected() ) { if ( WC_Payments_Utils::should_use_progressive_onboarding_flow() ) { wc_admin_register_page( [ diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 0b982e9b4cf..8491400b098 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -238,6 +238,21 @@ public function is_account_partially_onboarded(): bool { return false === $account['details_submitted']; } + /** + * Checks if the account has completed onboarding/KYC. + * Returns true if the onboarding/KYC is completed. + * + * @return bool True if the account is connected and details are submitted, false otherwise. + */ + public function is_account_fully_onboarded(): bool { + if ( ! $this->is_stripe_connected() ) { + return false; + } + + $account = $this->get_cached_account_data(); + return true === $account['details_submitted']; + } + /** * Gets the account status data for rendering on the settings page. * diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 0f902436028..6c2734f248e 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -149,7 +149,8 @@ public function test_it_does_not_render_settings_badge( $is_upe_settings_preview update_option( '_wcpay_feature_upe', $is_upe_enabled ? '1' : '0' ); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu['wc-admin&path=/payments/overview'], 0, 2 ); @@ -163,7 +164,8 @@ public function test_it_does_not_render_payments_badge_if_stripe_is_connected() $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $menu, 0, 2 ); @@ -175,8 +177,9 @@ public function test_it_renders_payments_badge_if_activation_date_is_older_than_ global $menu; $this->mock_current_user_is_admin(); - // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( false ); + // Make sure we render the menu without submenu items. + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( false ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( false ); update_option( 'wcpay_activation_timestamp', time() - ( 3 * DAY_IN_SECONDS ) ); $this->payments_admin->add_payments_menu(); @@ -189,8 +192,9 @@ public function test_it_does_not_render_payments_badge_if_activation_date_is_les global $menu; $this->mock_current_user_is_admin(); - // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( false ); + // Make sure we render the menu without submenu items. + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( false ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( false ); update_option( 'wcpay_menu_badge_hidden', 'no' ); update_option( 'wcpay_activation_timestamp', time() - ( DAY_IN_SECONDS * 2 ) ); $this->payments_admin->add_payments_menu(); @@ -493,7 +497,8 @@ public function test_disputes_notification_badge_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); @@ -534,7 +539,8 @@ public function test_disputes_notification_badge_no_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); @@ -577,7 +583,8 @@ public function test_transactions_notification_badge_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); @@ -622,7 +629,8 @@ public function test_transactions_notification_badge_no_display() { $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. - $this->mock_account->method( 'is_stripe_account_valid' )->willReturn( true ); + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); $this->payments_admin->add_payments_menu(); $item_names_by_urls = wp_list_pluck( $submenu[ WC_Payments_Admin::PAYMENTS_SUBMENU_SLUG ], 0, 2 ); From 505ebf6c55aa3bb6748f81f3ed6a6fa409004dfc Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Fri, 17 Nov 2023 18:12:24 +0100 Subject: [PATCH 23/61] Enable deferred intent creation when initialization process encounters cache unavailability (#7686) Co-authored-by: Timur Karimov --- changelog/fix-more-use-cases-for-enabled-dupe | 4 + includes/class-wc-payments-features.php | 5 +- ...s-wc-rest-payments-settings-controller.php | 28 +-- tests/unit/bootstrap.php | 3 + .../helpers/class-wc-helper-site-currency.php | 2 +- .../test-class-track-upe-status.php | 15 -- ...ments-notes-additional-payment-methods.php | 72 ------- ...ss-wc-payments-notes-set-up-stripelink.php | 8 - .../test-class-upe-payment-gateway.php | 195 ++++++------------ .../test-class-upe-split-payment-gateway.php | 3 - ...-payment-gateway-wcpay-process-payment.php | 24 +-- .../test-class-wc-payment-gateway-wcpay.php | 15 +- .../unit/test-class-wc-payments-features.php | 50 +---- .../test-class-wc-payments-token-service.php | 2 +- tests/unit/test-class-woopay-tracker.php | 1 + .../woopay/services/test-checkout-service.php | 2 +- 16 files changed, 111 insertions(+), 318 deletions(-) create mode 100644 changelog/fix-more-use-cases-for-enabled-dupe diff --git a/changelog/fix-more-use-cases-for-enabled-dupe b/changelog/fix-more-use-cases-for-enabled-dupe new file mode 100644 index 00000000000..c4ae4e88f26 --- /dev/null +++ b/changelog/fix-more-use-cases-for-enabled-dupe @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Enable deferred intent creation when initialization process encounters cache unavailability diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 84c45998636..9302f1face2 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -78,7 +78,10 @@ public static function is_upe_split_enabled() { */ public static function is_upe_deferred_intent_enabled() { $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); - return is_array( $account ) && ( $account[ self::DEFERRED_UPE_SERVER_FLAG_NAME ] ?? false ); + if ( null === $account ) { + return true; + } + return is_array( $account ) && ( $account[ self::DEFERRED_UPE_SERVER_FLAG_NAME ] ?? true ); } /** diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php index 77d6450563f..54f42f8de84 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php @@ -69,16 +69,16 @@ class WC_REST_Payments_Settings_Controller_Test extends WCPAY_UnitTestCase { /** * An array of mocked split UPE payment gateways mapped to payment method ID. * - * @var array + * @var UPE_Payment_Gateway */ private $mock_upe_payment_gateway; /** * An array of mocked split UPE payment gateways mapped to payment method ID. * - * @var array + * @var UPE_Split_Payment_Gateway */ - private $mock_split_upe_payment_gateways; + private $mock_split_upe_payment_gateway; /** * UPE system under test. @@ -201,7 +201,7 @@ public function set_up() { $this->upe_controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->mock_upe_payment_gateway, $this->mock_wcpay_account ); - $this->mock_upe_split_payment_gateway = new UPE_Split_Payment_Gateway( + $this->mock_split_upe_payment_gateway = new UPE_Split_Payment_Gateway( $this->mock_api_client, $this->mock_wcpay_account, $customer_service, @@ -216,7 +216,7 @@ public function set_up() { $this->mock_fraud_service ); - $this->upe_split_controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->mock_upe_split_payment_gateway, $this->mock_wcpay_account ); + $this->upe_split_controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->mock_split_upe_payment_gateway, $this->mock_wcpay_account ); $this->mock_api_client ->method( 'is_server_connected' ) @@ -451,22 +451,14 @@ public function test_upe_update_settings_saves_enabled_payment_methods() { } public function test_upe_split_update_settings_saves_enabled_payment_methods() { - $this->mock_upe_split_payment_gateway->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD ] ); + $this->mock_split_upe_payment_gateway->update_option( 'upe_enabled_payment_method_ids', [ Payment_Method::CARD ] ); - $request = new WP_REST_Request(); - $request->set_param( 'enabled_payment_method_ids', [ Payment_Method::CARD, Payment_Method::GIROPAY ] ); - - $this->upe_split_controller->update_settings( $request ); - - $this->assertEquals( [ Payment_Method::CARD, Payment_Method::GIROPAY ], $this->mock_upe_split_payment_gateway->get_option( 'upe_enabled_payment_method_ids' ) ); - } + $request = new WP_REST_Request(); + $request->set_param( 'enabled_payment_method_ids', [ Payment_Method::CARD, Payment_Method::GIROPAY ] ); - public function test_update_settings_validation_fails_if_invalid_gateway_id_supplied() { - $request = new WP_REST_Request( 'POST', self::$settings_route ); - $request->set_param( 'enabled_payment_method_ids', [ 'foo', 'baz' ] ); + $this->upe_split_controller->update_settings( $request ); - $response = rest_do_request( $request ); - $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( [ Payment_Method::CARD, Payment_Method::GIROPAY ], $this->mock_split_upe_payment_gateway->get_option( 'upe_enabled_payment_method_ids' ) ); } public function test_update_settings_fails_if_user_cannot_manage_woocommerce() { diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index b986ab01ef4..295fd43e82d 100755 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -98,6 +98,9 @@ function() { require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-payment-intents-controller.php'; require_once $_plugin_dir . 'includes/class-woopay-tracker.php'; require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-customer-controller.php'; + + // Load currency helper class early to ensure its implementation is used over the one resolved during further test initialization. + require_once __DIR__ . '/helpers/class-wc-helper-site-currency.php'; } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); diff --git a/tests/unit/helpers/class-wc-helper-site-currency.php b/tests/unit/helpers/class-wc-helper-site-currency.php index 4869f2a3b3e..49cfbed41b1 100644 --- a/tests/unit/helpers/class-wc-helper-site-currency.php +++ b/tests/unit/helpers/class-wc-helper-site-currency.php @@ -8,7 +8,7 @@ namespace WCPay\Payment_Methods; /** - * Overriding global function within namespace for testing + * If mock value is set, return mock value. Otherwise, return the global function value. */ function get_woocommerce_currency() { return WC_Helper_Site_Currency::$mock_site_currency ? WC_Helper_Site_Currency::$mock_site_currency : \get_woocommerce_currency(); diff --git a/tests/unit/migrations/test-class-track-upe-status.php b/tests/unit/migrations/test-class-track-upe-status.php index 5acd90e9f87..6fbf4817162 100644 --- a/tests/unit/migrations/test-class-track-upe-status.php +++ b/tests/unit/migrations/test-class-track-upe-status.php @@ -59,21 +59,6 @@ public function test_track_enabled_on_upgrade() { $this->assertSame( '1', get_option( Track_Upe_Status::IS_TRACKED_OPTION ) ); } - public function test_track_disabled_on_upgrade() { - update_option( WC_Payments_Features::UPE_FLAG_NAME, 'disabled' ); - - Track_Upe_Status::maybe_track(); - - $this->assertEquals( - [ - 'wcpay_upe_disabled' => [], - ], - Tracker::get_admin_events() - ); - - $this->assertSame( '1', get_option( Track_Upe_Status::IS_TRACKED_OPTION ) ); - } - public function test_do_nothing_default_on_upgrade() { Track_Upe_Status::maybe_track(); diff --git a/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php b/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php index 095cc756132..53553340b2b 100644 --- a/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php +++ b/tests/unit/notes/test-class-wc-payments-notes-additional-payment-methods.php @@ -24,78 +24,6 @@ public function tear_down() { delete_option( '_wcpay_feature_upe' ); } - public function test_get_note() { - $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); - - $this->assertSame( 'Boost your sales by accepting new payment methods', $note->get_title() ); - $this->assertSame( 'Get early access to additional payment methods and an improved checkout experience, coming soon to WooPayments. Learn more', $note->get_content() ); - $this->assertSame( 'info', $note->get_type() ); - $this->assertSame( 'wc-payments-notes-additional-payment-methods', $note->get_name() ); - $this->assertSame( 'woocommerce-payments', $note->get_source() ); - - list( $enable_upe_action ) = $note->get_actions(); - $this->assertSame( 'wc-payments-notes-additional-payment-methods', $enable_upe_action->name ); - $this->assertSame( 'Enable on your store', $enable_upe_action->label ); - $this->assertStringStartsWith( 'http://example.org/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=woocommerce_payments&action=enable-upe', $enable_upe_action->query ); - - /** - * The $primary property was deprecated from WooCommerce core. Keeping this to maintain the compatibility with old WooCommerce versions. - * @see https://github.com/woocommerce/woocommerce/blob/ff2d7d704a8f72aeb4990811b6972097aa167bea/plugins/woocommerce/src/Admin/Notes/Note.php#L623-L623. - * @see https://github.com/woocommerce/woocommerce-admin/pull/8474 - */ - if ( isset( $enable_upe_action->primary ) ) { - $this->assertSame( true, $enable_upe_action->primary ); - } - } - - public function test_get_note_does_not_return_note_when_account_is_not_connected() { - $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected' ] )->getMock(); - $account_mock->expects( $this->atLeastOnce() )->method( 'is_stripe_connected' )->will( $this->returnValue( false ) ); - WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); - - $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); - - $this->assertNull( $note ); - } - - public function test_get_note_returns_note_when_account_is_connected() { - $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'is_account_partially_onboarded', 'is_progressive_onboarding_in_progress' ] )->getMock(); - $account_mock->expects( $this->once() )->method( 'is_stripe_connected' )->willReturn( true ); - $account_mock->expects( $this->once() )->method( 'is_account_partially_onboarded' )->willReturn( false ); - $account_mock->expects( $this->once() )->method( 'is_progressive_onboarding_in_progress' )->willReturn( false ); - - WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); - - $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); - - $this->assertSame( 'Boost your sales by accepting new payment methods', $note->get_title() ); - } - - public function test_get_note_returns_note_when_account_is_partially_onboarded() { - $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'is_account_partially_onboarded', 'is_progressive_onboarding_in_progress' ] )->getMock(); - $account_mock->expects( $this->once() )->method( 'is_stripe_connected' )->willReturn( true ); - $account_mock->expects( $this->once() )->method( 'is_account_partially_onboarded' )->willReturn( true ); - - WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); - - $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); - - $this->assertNull( $note ); - } - - public function test_get_note_returns_note_when_account_is_progressive_in_progress() { - $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'is_account_partially_onboarded', 'is_progressive_onboarding_in_progress' ] )->getMock(); - $account_mock->expects( $this->once() )->method( 'is_stripe_connected' )->willReturn( true ); - $account_mock->expects( $this->once() )->method( 'is_account_partially_onboarded' )->willReturn( false ); - $account_mock->expects( $this->once() )->method( 'is_progressive_onboarding_in_progress' )->willReturn( true ); - - WC_Payments_Notes_Additional_Payment_Methods::set_account( $account_mock ); - - $note = WC_Payments_Notes_Additional_Payment_Methods::get_note(); - - $this->assertNull( $note ); - } - public function test_maybe_enable_feature_flag_redirects_to_onboarding_when_account_not_connected() { $account_mock = $this->getMockBuilder( \WC_Payments_Account::class )->disableOriginalConstructor()->setMethods( [ 'is_stripe_connected', 'redirect_to_onboarding_welcome_page' ] )->getMock(); $account_mock->expects( $this->atLeastOnce() )->method( 'is_stripe_connected' )->will( $this->returnValue( false ) ); diff --git a/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php b/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php index 84294afade2..883bd5cd96e 100644 --- a/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php +++ b/tests/unit/notes/test-class-wc-payments-notes-set-up-stripelink.php @@ -55,14 +55,6 @@ public function test_stripelink_setup_get_note() { $this->assertStringStartsWith( 'https://woo.com/document/woopayments/payment-methods/link-by-stripe/', $set_up_action->query ); } - public function test_stripelink_setup_note_null_when_upe_disabled() { - $this->mock_gateway_data( '0', [ 'card', 'link' ], [ 'card' ] ); - - $note = \WC_Payments_Notes_Set_Up_StripeLink::get_note(); - - $this->assertNull( $note ); - } - public function test_stripelink_setup_note_null_when_link_not_available() { $this->mock_gateway_data( '1', [ 'card' ], [ 'card' ] ); diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php index 3f0aed8474a..43069a5fb24 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -41,8 +41,6 @@ use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; -require_once dirname( __FILE__ ) . '/../helpers/class-wc-helper-site-currency.php'; - /** * UPE_Payment_Gateway unit tests */ @@ -1888,19 +1886,6 @@ function( $argument ) { } } - /** - * @dataProvider maybe_filter_gateway_title_data_provider - */ - public function test_maybe_filter_gateway_title_with_no_additional_feature_flags_enabled( $data ) { - $data = $data[0]; - $default_option = $this->mock_upe_gateway->get_option( 'upe_enabled_payment_method_ids' ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $data['methods'] ); - WC_Helper_Site_Currency::$mock_site_currency = $data['currency']; - $this->set_get_upe_enabled_payment_method_statuses_return_value( $data['statuses'] ); - $this->assertSame( $data['expected'], $this->mock_upe_gateway->maybe_filter_gateway_title( $data['title'], $data['id'] ) ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $default_option ); - } - public function test_maybe_filter_gateway_title_skips_update_due_to_enabled_split_upe() { $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); @@ -2080,140 +2065,80 @@ public function test_link_payment_method_if_card_enabled() { $upe_checkout->get_payment_fields_js_config()['paymentMethodsConfig'], [ 'card' => [ - 'isReusable' => true, - 'title' => 'Credit card / debit card', - 'icon' => $this->icon_url, - 'showSaveOption' => true, - 'countries' => [], + 'isReusable' => true, + 'title' => 'Credit card / debit card', + 'icon' => $this->icon_url, + 'showSaveOption' => true, + 'countries' => [], + 'upePaymentIntentData' => null, + 'upeSetupIntentData' => null, + 'testingInstructions' => 'Test mode: use the test VISA card 4242424242424242 with any expiry date and CVC. Other payment methods may redirect to a Stripe test page to authorize payment. More test card numbers are listed here.', + 'forceNetworkSavedCards' => false, ], 'link' => [ - 'isReusable' => true, - 'title' => 'Link', - 'icon' => $this->icon_url, - 'showSaveOption' => true, - 'countries' => [], + 'isReusable' => true, + 'title' => 'Link', + 'icon' => $this->icon_url, + 'showSaveOption' => true, + 'countries' => [], + 'upePaymentIntentData' => null, + 'upeSetupIntentData' => null, + 'testingInstructions' => '', + 'forceNetworkSavedCards' => false, ], ] ); } - public function maybe_filter_gateway_title_data_provider() { - $method_title = 'WooPayments'; - $checkout_title = 'Popular payment methods'; - $card_title = 'Credit card / debit card'; + /** + * @dataProvider available_payment_methods_provider + */ + public function test_get_upe_available_payment_methods( $payment_methods, $expected_result ) { + $mock_wcpay_account = $this->createMock( WC_Payments_Account::class ); + $mock_wcpay_account + ->expects( $this->any() ) + ->method( 'get_fees' ) + ->willReturn( $payment_methods ); - $data_set[] = [ // Allows for $checkout_title due to UPE method and EUR. - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $checkout_title, - ]; - $data_set[] = [ // No UPE method, only card, so $card_title is expected. - 'methods' => [ - 'card', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $card_title, - ]; - $data_set[] = [ // Only UPE method, so UPE method title is expected. - 'methods' => [ - 'bancontact', - ], - 'statuses' => [ - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'Bancontact', - ]; - $data_set[] = [ // Card and UPE enabled, but USD, $card_title expected. - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], + $gateway = new UPE_Payment_Gateway( + $this->mock_api_client, + $mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + $this->mock_payment_methods, + $this->mock_rate_limiter, + $this->mock_order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + + $this->assertEquals( $expected_result, $gateway->get_upe_available_payment_methods() ); + } + + public function available_payment_methods_provider() { + return [ + 'card only' => [ + [ 'card' => [ 'base' => 0.1 ] ], + [ 'card' ], ], - 'currency' => 'USD', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $card_title, - ]; - $data_set[] = [ // Card and UPE enabled, but not our title, other title expected. - 'methods' => [ - 'card', - 'bancontact', + 'no match with fees' => [ + [ 'some_other_payment_method' => [ 'base' => 0.1 ] ], + [], ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', + 'multiple matches with fees' => [ + [ + 'card' => [ 'base' => 0.1 ], + 'bancontact' => [ 'base' => 0.2 ], ], + [ 'card', 'bancontact' ], ], - 'currency' => 'EUR', - 'title' => 'Some other title', - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'Some other title', - ]; - $data_set[] = [ // Card and UPE enabled, but not our id, $method_title expected. - 'methods' => [ - 'card', - 'bancontact', + 'no fees no methods' => [ + [], + [], ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'USD', - 'title' => $method_title, - 'id' => 'some_other_id', - 'expected' => $method_title, - ]; - $data_set[] = [ // No methods at all, so defaults to card, so $card_title is expected. - 'methods' => [], - 'statuses' => [], - 'currency' => 'EUR', - 'title' => $method_title, - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => $card_title, ]; - foreach ( $data_set as $data ) { - $return_data[] = [ [ $data ] ]; - } - return $return_data; } /** diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index 0d6e8fc4b97..1559a659dd9 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -41,9 +41,6 @@ use WCPay\Database_Cache; use WCPay\Internal\Service\Level3Service; use WCPay\Internal\Service\OrderService; - -require_once dirname( __FILE__ ) . '/../helpers/class-wc-helper-site-currency.php'; - /** * UPE_Split_Payment_Gateway unit tests */ diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php index a125c94a57e..da7ed2798db 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php @@ -321,7 +321,7 @@ public function test_intent_status_success() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process a successful payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -385,7 +385,7 @@ public function test_intent_status_success_logged_out_user() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process a successful payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -490,7 +490,7 @@ public function test_intent_status_requires_capture() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::MANUAL() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::MANUAL(), 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -510,7 +510,7 @@ public function test_exception_thrown() { ->expects( $this->once() ) ->method( 'get_customer_id_by_user_id' ) ->willReturn( 'cus_mock' ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::AUTOMATIC() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, WCPay\Constants\Payment_Type::SINGLE(), WCPay\Constants\Payment_Initiated_By::CUSTOMER(), WCPay\Constants\Payment_Capture_Type::AUTOMATIC(), 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Arrange: Throw an exception in create_and_confirm_intention. $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); @@ -937,7 +937,7 @@ public function test_intent_status_requires_action() { ->willReturn( [ 'balance_transaction' => [ 'exchange_rate' => 0.86 ] ] ); // Act: process payment. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); // Assert: Returning correct array. @@ -1052,7 +1052,7 @@ public function test_setup_intent_status_requires_action() { ->method( 'empty_cart' ); // Act: prepare payment information. - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information->must_save_payment_method_to_store(); // Act: process payment. @@ -1205,7 +1205,7 @@ public function test_updates_customer_with_order_data() { // Arrange: Create a mock cart. $mock_cart = $this->createMock( 'WC_Cart' ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1417,7 +1417,7 @@ public function test_process_payment_using_platform_payment_method_adds_platform ->method( 'format_response' ) ->willReturn( [ 'id' => 'ch_mock' ] ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1476,7 +1476,7 @@ public function test_process_payment_for_subscription_in_woopay_adds_subscriptio $request->expects( $this->once() ) ->method( 'format_response' ) ->willReturn( $intent ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1548,7 +1548,7 @@ public function test_process_payment_from_woopay_adds_meta_to_oder() { $charge_request->expects( $this->once() ) ->method( 'format_response' ) ->willReturn( [ 'id' => 'ch_mock' ] ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing // Act: process a successful payment. $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); @@ -1591,7 +1591,7 @@ public function test_process_payment_for_subscription_from_woopay_does_not_save_ ->method( 'format_response' ) ->willReturn( $intent ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information->must_save_payment_method_to_store(); // Assert: The payment method is not added to the user. @@ -1637,7 +1637,7 @@ public function test_process_payment_for_subscription_from_woopay_save_token_if_ ->method( 'format_response' ) ->willReturn( $intent ); - $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $order, null, null, null, 'card' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $payment_information->must_save_payment_method_to_store(); // Assert: The payment method is added to the user. diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index c4aeaa6b275..c23e4159896 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -201,6 +201,7 @@ public function set_up() { $this->mock_localization_service, $this->mock_fraud_service ); + WC_Payments::set_gateway( $this->wcpay_gateway ); $this->woopay_utilities = new WooPay_Utilities(); @@ -364,12 +365,6 @@ public function test_attach_exchange_info_to_order_with_different_order_currency $this->assertEquals( 0.853, $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate' ) ); } - public function test_payment_fields_outputs_fields() { - $this->wcpay_gateway->payment_fields(); - - $this->expectOutputRegex( '/
<\/div>/' ); - } - public function test_save_card_checkbox_not_displayed_when_saved_cards_disabled() { $this->wcpay_gateway->update_option( 'saved_cards', 'no' ); @@ -1404,7 +1399,7 @@ public function test_process_payment_for_order_not_from_request() { $order->add_payment_token( $token ); $order->save(); - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); $request->expects( $this->once() ) @@ -1425,7 +1420,7 @@ public function test_process_payment_for_order_rejects_with_cached_minimum_amoun $order->set_total( 0.45 ); $order->save(); - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $this->expectException( Exception::class ); $this->expectExceptionMessage( 'The selected payment method requires a total amount of at least $0.50.' ); @@ -1442,7 +1437,7 @@ public function test_process_payment_for_order_cc_payment_method() { $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; $_POST['payment_method'] = $payment_method; - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); $request->expects( $this->once() ) @@ -1465,7 +1460,7 @@ public function test_process_payment_for_order_upe_payment_method() { $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; $_POST['payment_method'] = $payment_method; - $pi = new Payment_Information( 'pm_test', $order ); + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); $request->expects( $this->once() ) diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index 0c7967dd8d6..3a0756d5bdc 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -19,13 +19,14 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase { protected $mock_cache; const FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING = [ - '_wcpay_feature_upe' => 'upe', - '_wcpay_feature_upe_split' => 'upeSplit', - '_wcpay_feature_upe_settings_preview' => 'upeSettingsPreview', - '_wcpay_feature_customer_multi_currency' => 'multiCurrency', - '_wcpay_feature_documents' => 'documents', - '_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled', - '_wcpay_feature_progressive_onboarding' => 'progressiveOnboarding', + '_wcpay_feature_upe' => 'upe', + '_wcpay_feature_upe_split' => 'upeSplit', + '_wcpay_feature_upe_settings_preview' => 'upeSettingsPreview', + '_wcpay_feature_customer_multi_currency' => 'multiCurrency', + '_wcpay_feature_documents' => 'documents', + '_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled', + '_wcpay_feature_progressive_onboarding' => 'progressiveOnboarding', + 'is_deferred_intent_creation_upe_enabled' => 'upeDeferred', ]; public function set_up() { @@ -63,7 +64,7 @@ public function test_it_returns_expected_to_array_result( array $enabled_flags ) public function enabled_flags_provider() { return [ - 'no flags' => [ [] ], + 'no flags' => [ [ '_wcpay_feature_upe', 'is_deferred_intent_creation_upe_enabled' ] ], 'all flags' => [ array_keys( self::FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING ) ], ]; } @@ -272,39 +273,6 @@ public function test_is_progressive_onboarding_enabled_returns_false_when_flag_i $this->assertFalse( WC_Payments_Features::is_progressive_onboarding_enabled() ); } - public function test_split_upe_disabled_with_ineligible_merchant() { - $this->mock_cache->method( 'get' )->willReturn( [ 'capabilities' => [ 'sepa_debit_payments' => 'active' ] ] ); - add_filter( - 'pre_option_' . WC_Payments_Features::UPE_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - add_filter( - 'pre_option_' . WC_Payments_Features::UPE_SPLIT_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - add_filter( - 'pre_option_' . WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - - $this->assertFalse( WC_Payments_Features::is_upe_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_legacy_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_split_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_deferred_intent_enabled() ); - } - public function test_deferred_upe_enabled_with_sepa() { $this->mock_cache->method( 'get' )->willReturn( [ diff --git a/tests/unit/test-class-wc-payments-token-service.php b/tests/unit/test-class-wc-payments-token-service.php index 0aa3a4b866f..1d6f0b29be6 100644 --- a/tests/unit/test-class-wc-payments-token-service.php +++ b/tests/unit/test-class-wc-payments-token-service.php @@ -114,7 +114,7 @@ public function test_add_token_to_user_for_sepa() { $token = $this->token_service->add_token_to_user( $mock_payment_method, wp_get_current_user() ); - $this->assertEquals( 'woocommerce_payments', $token->get_gateway_id() ); + $this->assertEquals( 'woocommerce_payments_sepa_debit', $token->get_gateway_id() ); $this->assertEquals( 1, $token->get_user_id() ); $this->assertEquals( 'pm_mock', $token->get_token() ); $this->assertEquals( '3000', $token->get_last4() ); diff --git a/tests/unit/test-class-woopay-tracker.php b/tests/unit/test-class-woopay-tracker.php index 408d9cb56f5..94df55b5e11 100644 --- a/tests/unit/test-class-woopay-tracker.php +++ b/tests/unit/test-class-woopay-tracker.php @@ -43,6 +43,7 @@ public function set_up() { $this->_cache = WC_Payments::get_database_cache(); $this->mock_cache = $this->createMock( WCPay\Database_Cache::class ); WC_Payments::set_database_cache( $this->mock_cache ); + WC_Payments::get_gateway()->enable(); $this->mock_account = $this->getMockBuilder( WC_Payments_Account::class ) ->disableOriginalConstructor() diff --git a/tests/unit/woopay/services/test-checkout-service.php b/tests/unit/woopay/services/test-checkout-service.php index 3cd514ab0e2..2fdab51f567 100644 --- a/tests/unit/woopay/services/test-checkout-service.php +++ b/tests/unit/woopay/services/test-checkout-service.php @@ -39,7 +39,7 @@ public function set_up() { $this->checkout_service = new Checkout_Service(); $this->request = new Create_And_Confirm_Intention( $this->createMock( WC_Payments_API_Client::class ), $this->createMock( WC_Payments_Http_Interface::class ) ); - $this->payment_information = new Payment_Information( 'pm_mock', wc_create_order(), Payment_Type::SINGLE(), null ); + $this->payment_information = new Payment_Information( 'pm_mock', wc_create_order(), Payment_Type::SINGLE(), null, null, null, null, '', 'card' ); } public function test_exception_will_throw_if_base_request_parameter_is_invalid() { From 48573837c3fd24027a5528761a3dd2cf0ba450e1 Mon Sep 17 00:00:00 2001 From: Allie Mims <60988591+allie500@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:24:06 -0500 Subject: [PATCH 24/61] Fix setting options on non-production environments (#7483) --- .../fix-6961-setting-options-non-prod-envs | 4 +++ includes/class-wc-payments.php | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 changelog/fix-6961-setting-options-non-prod-envs diff --git a/changelog/fix-6961-setting-options-non-prod-envs b/changelog/fix-6961-setting-options-non-prod-envs new file mode 100644 index 00000000000..6795a358bcc --- /dev/null +++ b/changelog/fix-6961-setting-options-non-prod-envs @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Adds WCPay options to Woo Core option allow list to avoid 403 responses from Options API when getting and updating options in non-prod env. diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 23a19991182..aac3cbe6036 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -591,6 +591,7 @@ public static function init() { add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ], 2 ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ], 3 ); add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'replace_wcpay_gateway_with_payment_methods' ], 4 ); + add_filter( 'woocommerce_rest_api_option_permissions', [ __CLASS__, 'add_wcpay_options_to_woocommerce_permissions_list' ], 5 ); add_filter( 'woocommerce_admin_get_user_data_fields', [ __CLASS__, 'add_user_data_fields' ] ); // Add note query support for source. @@ -1673,6 +1674,39 @@ public static function enqueue_dev_runtime_scripts() { } } + /** + * Adds WCPay options to Woo Core option allow list. + * + * @param array $permissions Array containing the permissions. + * + * @return array An array containing the modified permissions. + */ + public static function add_wcpay_options_to_woocommerce_permissions_list( $permissions ) { + $wcpay_permissions_list = array_fill_keys( + [ + 'wcpay_frt_discover_banner_settings', + 'wcpay_multi_currency_setup_completed', + 'woocommerce_dismissed_todo_tasks', + 'woocommerce_remind_me_later_todo_tasks', + 'woocommerce_deleted_todo_tasks', + 'wcpay_fraud_protection_welcome_tour_dismissed', + 'wcpay_capability_request_dismissed_notices', + 'wcpay_onboarding_eligibility_modal_dismissed', + ], + true + ); + + if ( is_array( $permissions ) ) { + return array_merge( + $permissions, + $wcpay_permissions_list + ); + } + + return $wcpay_permissions_list; + } + + /** * Creates a new request object for a server call. * From 06730ada61d9259c295a8511936f7c608eee14f5 Mon Sep 17 00:00:00 2001 From: mordeth Date: Mon, 20 Nov 2023 13:59:28 +0200 Subject: [PATCH 25/61] Add channel, country and risk level filters --- changelog/add-7596-transactions-filters | 4 + client/data/transactions/hooks.ts | 36 ++++ client/data/transactions/resolvers.js | 6 + client/deposits/details/test/index.tsx | 8 + client/transactions/declarations.d.ts | 6 + client/transactions/filters/config.ts | 179 +++++++++++++++++- .../filters/test/__snapshots__/index.tsx.snap | 55 ++++++ client/transactions/filters/test/index.tsx | 164 ++++++++++++++++ client/transactions/list/index.tsx | 20 ++ client/transactions/strings.ts | 13 ++ ...-rest-payments-transactions-controller.php | 6 + .../server/request/class-list-transactions.md | 46 +++-- .../request/class-list-transactions.php | 72 +++++++ tests/js/jest-test-file-setup.js | 5 + .../test-class-list-transactions-request.php | 102 ++++++---- 15 files changed, 671 insertions(+), 51 deletions(-) create mode 100644 changelog/add-7596-transactions-filters diff --git a/changelog/add-7596-transactions-filters b/changelog/add-7596-transactions-filters new file mode 100644 index 00000000000..f465724d789 --- /dev/null +++ b/changelog/add-7596-transactions-filters @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Introduce filters for channel, customer country, and risk level on the transactions list page. diff --git a/client/data/transactions/hooks.ts b/client/data/transactions/hooks.ts index a93f43c8ff4..d0a983e75f9 100644 --- a/client/data/transactions/hooks.ts +++ b/client/data/transactions/hooks.ts @@ -148,6 +148,12 @@ export const useTransactions = ( type_is_not: typeIsNot, source_device_is: sourceDeviceIs, source_device_is_not: sourceDeviceIsNot, + channel_is: channelIs, + channel_is_not: channelIsNot, + customer_country_is: customerCountryIs, + customer_country_is_not: customerCountryIsNot, + risk_level_is: riskLevelIs, + risk_level_is_not: riskLevelIsNot, store_currency_is: storeCurrencyIs, customer_currency_is: customerCurrencyIs, customer_currency_is_not: customerCurrencyIsNot, @@ -188,6 +194,12 @@ export const useTransactions = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, search, @@ -215,6 +227,12 @@ export const useTransactions = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, JSON.stringify( search ), @@ -234,6 +252,12 @@ export const useTransactionsSummary = ( store_currency_is: storeCurrencyIs, customer_currency_is: customerCurrencyIs, customer_currency_is_not: customerCurrencyIsNot, + channel_is: channelIs, + channel_is_not: channelIsNot, + customer_country_is: customerCountryIs, + customer_country_is_not: customerCountryIsNot, + risk_level_is: riskLevelIs, + risk_level_is_not: riskLevelIsNot, loan_id_is: loanIdIs, search, }: Query, @@ -257,6 +281,12 @@ export const useTransactionsSummary = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, search, @@ -279,6 +309,12 @@ export const useTransactionsSummary = ( storeCurrencyIs, customerCurrencyIs, customerCurrencyIsNot, + channelIs, + channelIsNot, + customerCountryIs, + customerCountryIsNot, + riskLevelIs, + riskLevelIsNot, loanIdIs, depositId, JSON.stringify( search ), diff --git a/client/data/transactions/resolvers.js b/client/data/transactions/resolvers.js index 864e1289d2d..8f5c3ea3a96 100644 --- a/client/data/transactions/resolvers.js +++ b/client/data/transactions/resolvers.js @@ -42,6 +42,12 @@ export const formatQueryFilters = ( query ) => ( { type_is_not: query.typeIsNot, source_device_is: query.sourceDeviceIs, source_device_is_not: query.sourceDeviceIsNot, + channel_is: query.channelIs, + channel_is_not: query.channelIsNot, + customer_country_is: query.customerCountryIs, + customer_country_is_not: query.customerCountryIsNot, + risk_level_is: query.riskLevelIs, + risk_level_is_not: query.riskLevelIsNot, store_currency_is: query.storeCurrencyIs, loan_id_is: query.loanIdIs, deposit_id: query.depositId, diff --git a/client/deposits/details/test/index.tsx b/client/deposits/details/test/index.tsx index cd1bd3abc75..d3657a6b6ca 100644 --- a/client/deposits/details/test/index.tsx +++ b/client/deposits/details/test/index.tsx @@ -39,6 +39,7 @@ declare const global: { country: string; }; }; + wcSettings: { countries: Record< string, string > }; }; describe( 'Deposit overview', () => { @@ -60,6 +61,13 @@ describe( 'Deposit overview', () => { }, }, }; + global.wcSettings = { + countries: { + US: 'United States of America', + CA: 'Canada', + UK: 'United Kingdom', + }, + }; } ); test( 'renders automatic deposit correctly', () => { diff --git a/client/transactions/declarations.d.ts b/client/transactions/declarations.d.ts index 3ff75a40ef2..0dfe270d2e6 100644 --- a/client/transactions/declarations.d.ts +++ b/client/transactions/declarations.d.ts @@ -129,6 +129,12 @@ declare module '@woocommerce/navigation' { type_is_not?: unknown; source_device_is?: unknown; source_device_is_not?: unknown; + channel_is?: unknown; + channel_is_not?: unknown; + customer_country_is?: unknown; + customer_country_is_not?: unknown; + risk_level_is?: unknown; + risk_level_is_not?: unknown; customer_currency_is?: unknown; customer_currency_is_not?: unknown; store_currency_is?: string; diff --git a/client/transactions/filters/config.ts b/client/transactions/filters/config.ts index a79bce31560..b6e3c34f534 100644 --- a/client/transactions/filters/config.ts +++ b/client/transactions/filters/config.ts @@ -7,7 +7,12 @@ import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies */ -import { displayType, sourceDevice } from 'transactions/strings'; +import { + displayType, + sourceDevice, + channel, + riskLevel, +} from 'transactions/strings'; interface TransactionsFilterEntryType { label: string; @@ -59,6 +64,24 @@ const transactionSourceDeviceOptions = Object.entries( sourceDevice ).map( } ); +const transactionChannelOptions = Object.entries( channel ).map( + ( [ type, label ] ) => { + return { label, value: type }; + } +); + +const transactionRiskLevelOptions = Object.entries( riskLevel ).map( + ( [ type, label ] ) => { + return { label, value: type }; + } +); + +const transactionCustomerCounryOptions = Object.entries( + wcSettings.countries +).map( ( [ type, label ] ) => { + return { label, value: type }; +} ); + export const getFilters = ( depositCurrencyOptions: TransactionsFilterEntryType[], showDepositCurrencyFilter: boolean @@ -81,6 +104,12 @@ export const getFilters = ( 'date_between', 'source_device_is', 'source_device_is_not', + 'channel_is', + 'channel_is_not', + 'customer_country_is', + 'customer_country_is_not', + 'risk_level_is', + 'risk_level_is_not', ], showFilters: () => showDepositCurrencyFilter, filters: [ @@ -368,6 +397,154 @@ export const getAdvancedFilters = ( options: transactionSourceDeviceOptions, }, }, + channel: { + labels: { + add: __( 'Channel', 'woocommerce-payments' ), + remove: __( + 'Remove transaction channel filter', + 'woocommerce-payments' + ), + rule: __( + 'Select a transaction channel filter match', + 'woocommerce-payments' + ), + /* translators: A sentence describing a Transaction Channel filter. */ + title: + wooCommerceVersion < 7.8 + ? __( + '{{title}}Channel{{/title}} {{rule /}} {{filter /}}', + 'woocommerce-payments' + ) + : __( + 'Channel ', + 'woocommerce-payments' + ), + filter: __( + 'Select a transaction channel', + 'woocommerce-payments' + ), + }, + rules: [ + { + value: 'is', + /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction channel type. */ + label: _x( 'Is', 'Channel', 'woocommerce-payments' ), + }, + { + value: 'is_not', + /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction channel type. */ + label: _x( + 'Is not', + 'Channel', + 'woocommerce-payments' + ), + }, + ], + input: { + component: 'SelectControl', + options: transactionChannelOptions, + }, + }, + customer_country: { + labels: { + add: __( 'Customer Country', 'woocommerce-payments' ), + remove: __( + 'Remove transaction customer country filter', + 'woocommerce-payments' + ), + rule: __( + 'Select a transaction customer country filter match', + 'woocommerce-payments' + ), + /* translators: A sentence describing a Transaction customer country. */ + title: + wooCommerceVersion < 7.8 + ? __( + '{{title}}Customer country{{/title}} {{rule /}} {{filter /}}', + 'woocommerce-payments' + ) + : __( + 'Customer country ', + 'woocommerce-payments' + ), + filter: __( + 'Select a transaction customer country', + 'woocommerce-payments' + ), + }, + rules: [ + { + value: 'is', + /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction customer country. */ + label: _x( + 'Is', + 'Customer Country', + 'woocommerce-payments' + ), + }, + { + value: 'is_not', + /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction customer country. */ + label: _x( + 'Is not', + 'Customer Country', + 'woocommerce-payments' + ), + }, + ], + input: { + component: 'SelectControl', + options: transactionCustomerCounryOptions, + }, + }, + risk_level: { + labels: { + add: __( 'Risk Level', 'woocommerce-payments' ), + remove: __( + 'Remove transaction Risk Level filter', + 'woocommerce-payments' + ), + rule: __( + 'Select a transaction Risk Level filter match', + 'woocommerce-payments' + ), + /* translators: A sentence describing a Transaction Risk Level filter. */ + title: + wooCommerceVersion < 7.8 + ? __( + '{{title}}Risk Level{{/title}} {{rule /}} {{filter /}}', + 'woocommerce-payments' + ) + : __( + 'Risk Level ', + 'woocommerce-payments' + ), + filter: __( + 'Select a transaction Risk Level', + 'woocommerce-payments' + ), + }, + rules: [ + { + value: 'is', + /* translators: Sentence fragment, logical, "Is" refers to searching for transactions matching a chosen transaction risk level. */ + label: _x( 'Is', 'Risk Level', 'woocommerce-payments' ), + }, + { + value: 'is_not', + /* translators: Sentence fragment, logical, "Is not" refers to searching for transactions that don\'t match a chosen transaction risk level. */ + label: _x( + 'Is not', + 'Risk Level', + 'woocommerce-payments' + ), + }, + ], + input: { + component: 'SelectControl', + options: transactionRiskLevelOptions, + }, + }, }, }; }; diff --git a/client/transactions/filters/test/__snapshots__/index.tsx.snap b/client/transactions/filters/test/__snapshots__/index.tsx.snap index c1956c8960a..b124ea5379b 100644 --- a/client/transactions/filters/test/__snapshots__/index.tsx.snap +++ b/client/transactions/filters/test/__snapshots__/index.tsx.snap @@ -1,5 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Transactions filters when filtering by channel should render all types 1`] = ` +HTMLOptionsCollection [ + , + , +] +`; + +exports[`Transactions filters when filtering by customer country should render all types 1`] = ` +HTMLOptionsCollection [ + , + , + , +] +`; + exports[`Transactions filters when filtering by customer currency should render all types 1`] = ` HTMLOptionsCollection [ , + , + , +] +`; + exports[`Transactions filters when filtering by source device should render all types 1`] = ` HTMLOptionsCollection [
`; -exports[`CustomerLink renders a link to a customer with name and email 1`] = ` +exports[`CustomerLink renders a link to a customer from billing details 1`] = ` + +`; + +exports[`CustomerLink renders a link to a customer from order details 1`] = `
); +function renderCustomer( customer: ChargeBillingDetails, order: OrderDetails ) { + return render( + + ); } describe( 'CustomerLink', () => { - test( 'renders a link to a customer with name and email', () => { - const { container: customerLink } = renderCustomer( { - name: 'Some Name', - email: 'some@email.com', - } as any ); + test( 'renders a link to a customer from billing details', () => { + const { container: customerLink } = renderCustomer( + { + name: 'Some Name', + email: 'some@email.com', + } as any, + null as any + ); + expect( customerLink ).toMatchSnapshot(); + } ); + + test( 'renders a link to a customer from order details', () => { + const { container: customerLink } = renderCustomer( + null as any, + { + customer_name: 'Some Name', + customer_email: 'some@email.com', + } as any + ); expect( customerLink ).toMatchSnapshot(); } ); test( 'renders a dash if customer name is undefined', () => { - const { container: customerLink1 } = renderCustomer( null as any ); + const { container: customerLink1 } = renderCustomer( + null as any, + null as any + ); expect( customerLink1 ).toMatchSnapshot(); - const { container: customerLink2 } = renderCustomer( {} as any ); + const { container: customerLink2 } = renderCustomer( + {} as any, + {} as any + ); expect( customerLink2 ).toMatchSnapshot(); } ); } ); diff --git a/client/data/payment-intents/test/hooks.ts b/client/data/payment-intents/test/hooks.ts index 68513573c36..370816ac23a 100644 --- a/client/data/payment-intents/test/hooks.ts +++ b/client/data/payment-intents/test/hooks.ts @@ -57,6 +57,8 @@ export const chargeMock: Charge = { number: Number( '67' ), url: 'http://order.url', customer_url: 'customer.url', + customer_name: '', + customer_email: '', subscriptions: [], }, outcome: { @@ -89,6 +91,8 @@ export const paymentIntentMock: PaymentIntent = { number: 123, url: 'http://order.url', customer_url: 'customer.url', + customer_name: '', + customer_email: '', fraud_meta_box_type: 'review', }, }; diff --git a/client/payment-details/order-details/test/index.test.tsx b/client/payment-details/order-details/test/index.test.tsx index e4ca6cd55be..e90265721d6 100644 --- a/client/payment-details/order-details/test/index.test.tsx +++ b/client/payment-details/order-details/test/index.test.tsx @@ -65,6 +65,8 @@ const chargeFromOrderMock = { url: 'http://wcpay.test/wp-admin/post.php?post=776&action=edit', customer_url: 'admin.php?page=wc-admin&path=/customers&filter=single_customer&customers=55', + customer_name: '', + customer_email: '', subscriptions: [], fraud_meta_box_type: 'succeeded', }, diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index cab3e5ce37b..6112a27e86f 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -125,7 +125,12 @@ const composePaymentSummaryItems = ( { }, { title: __( 'Customer', 'woocommerce-payments' ), - content: , + content: ( + + ), }, { title: __( 'Order', 'woocommerce-payments' ), diff --git a/client/transactions/list/test/index.tsx b/client/transactions/list/test/index.tsx index be1bf5d9fe5..d2706511578 100644 --- a/client/transactions/list/test/index.tsx +++ b/client/transactions/list/test/index.tsx @@ -102,6 +102,8 @@ const getMockTransactions: () => Transaction[] = () => [ url: 'https://example.com/order/123', // eslint-disable-next-line camelcase customer_url: 'https://example.com/customer/my-name', + customer_name: '', + customer_email: '', }, channel: 'online', source_identifier: '1234', @@ -131,6 +133,8 @@ const getMockTransactions: () => Transaction[] = () => [ url: 'https://example.com/order/125', // eslint-disable-next-line camelcase customer_url: 'https://example.com/customer/my-name', + customer_name: '', + customer_email: '', }, channel: 'online', source_identifier: '1234', @@ -160,6 +164,8 @@ const getMockTransactions: () => Transaction[] = () => [ url: 'https://example.com/order/335', // eslint-disable-next-line camelcase customer_url: 'https://example.com/customer/my-name', + customer_name: '', + customer_email: '', }, channel: 'in_person', source_identifier: '1234', diff --git a/client/types/orders.d.ts b/client/types/orders.d.ts index 216f1caa13a..1865fd23c5a 100644 --- a/client/types/orders.d.ts +++ b/client/types/orders.d.ts @@ -15,6 +15,8 @@ interface OrderDetails { number: number; url: string; customer_url: null | string; + customer_email: null | string; + customer_name: null | string; subscriptions?: SubscriptionDetails[]; fraud_meta_box_type?: string; } diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 13b7f43e3be..5456d2f1744 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -2097,6 +2097,8 @@ public function build_order_info( WC_Order $order ): array { 'number' => $order->get_order_number(), 'url' => $order->get_edit_order_url(), 'customer_url' => $this->get_customer_url( $order ), + 'customer_name' => $order->get_formatted_billing_full_name(), + 'customer_email' => $order->get_billing_email(), 'fraud_meta_box_type' => $order->get_meta( '_wcpay_fraud_meta_box_type' ), ]; From 7555461d76ce686511669da181f7efbf8c6cd2e6 Mon Sep 17 00:00:00 2001 From: Hsing-yu Flowers Date: Wed, 22 Nov 2023 14:22:47 -0500 Subject: [PATCH 35/61] Pass the pay-for-order params to the first-party auth flow (#7763) --- .../fix-pass-pay-for-order-params-to-first-party-auth-flow | 4 ++++ .../woopay/express-button/woopay-express-checkout-button.js | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-pass-pay-for-order-params-to-first-party-auth-flow diff --git a/changelog/fix-pass-pay-for-order-params-to-first-party-auth-flow b/changelog/fix-pass-pay-for-order-params-to-first-party-auth-flow new file mode 100644 index 00000000000..19c4ba5ad2e --- /dev/null +++ b/changelog/fix-pass-pay-for-order-params-to-first-party-auth-flow @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Pass the pay-for-order params to the first-party auth flow diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index 22f30bd79d6..094d0856ad5 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -18,6 +18,7 @@ import request from 'wcpay/checkout/utils/request'; import { showErrorMessage } from 'wcpay/checkout/woopay/express-button/utils'; import { buildAjaxURL } from 'wcpay/payment-request/utils'; import interpolateComponents from '@automattic/interpolate-components'; +import { appendRedirectionParams } from 'wcpay/checkout/woopay/utils'; const BUTTON_WIDTH_THRESHOLD = 140; @@ -331,7 +332,9 @@ export const WoopayExpressCheckoutButton = ( { } if ( isSessionDataSuccess ) { - window.location.href = event.data.value.redirect_url; + window.location.href = appendRedirectionParams( + event.data.value.redirect_url + ); } else if ( isSessionDataError ) { onClickFallback( null ); From e26044de250fd4478440b2c801883533fc24d9f2 Mon Sep 17 00:00:00 2001 From: Dan Paun <82826872+dpaun1985@users.noreply.github.com> Date: Thu, 23 Nov 2023 10:05:42 +0200 Subject: [PATCH 36/61] Add date_between filter for Authorization Reporting API (#7707) Co-authored-by: Dan Paun Co-authored-by: Cvetan Cvetanov --- changelog/fix-7445-authorization-filters | 4 ++++ docs/rest-api/source/includes/wp-api-v3/reports.md | 3 +++ ...-payments-reports-authorizations-controller.php | 14 ++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-7445-authorization-filters diff --git a/changelog/fix-7445-authorization-filters b/changelog/fix-7445-authorization-filters new file mode 100644 index 00000000000..d9979e600c5 --- /dev/null +++ b/changelog/fix-7445-authorization-filters @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add date_between filter for Authorization Reporting API diff --git a/docs/rest-api/source/includes/wp-api-v3/reports.md b/docs/rest-api/source/includes/wp-api-v3/reports.md index 6e78cd6ee60..38d8dba5f55 100644 --- a/docs/rest-api/source/includes/wp-api-v3/reports.md +++ b/docs/rest-api/source/includes/wp-api-v3/reports.md @@ -155,6 +155,9 @@ Fetch a detailed overview of authorizations. ### Optional parameters +- `date_before`: Filter authorizations before this date. +- `date_after`: Filter authorizations after this date. If it's not provided the default it will be 7 days before today. +- `date_between`: Filter authorizations between these dates. - `order_id`: Filter authorizations based on the associated order ID. - `deposit_id`: Filter authorizations based on the associated deposit ID. - `customer_email`: Filter authorizations based on the customer email. diff --git a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php index 09c2e191816..a9abef775e8 100644 --- a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php +++ b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php @@ -62,14 +62,20 @@ public function get_authorizations( $request ) { $wcpay_request = List_Authorizations::from_rest_request( $request ); $wcpay_request->set_page_size( $request->get_param( 'per_page' ) ?? 25 ); - $date_between_filter = $request->get_param( 'date_between' ); - $user_timezone = $request->get_param( 'user_timezone' ); - $filters = [ + $user_timezone = $request->get_param( 'user_timezone' ); + $filters = [ 'match' => $request->get_param( 'match' ), 'order_id_is' => $request->get_param( 'order_id' ), 'customer_email_is' => $request->get_param( 'customer_email' ), 'source_is' => $request->get_param( 'payment_method_type' ), ]; + + if ( $request->get_param( 'date_between' ) ) { + $date_between_filter = $request->get_param( 'date_between' ); + $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $date_between_filter[0], $user_timezone ) ); + $filters['to_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $date_between_filter[1], $user_timezone ) ); + } + if ( $request->get_param( 'date_before' ) ) { $filters['from_date'] = strtotime( Request_Utils::format_transaction_date_by_timezone( $request->get_param( 'date_before' ), $user_timezone ) ); } @@ -238,7 +244,7 @@ public function get_collection_params() { 'description' => __( 'Field on which to sort.', 'woocommerce-payments' ), 'type' => 'string', 'required' => false, - 'default' => 'date', + 'default' => 'created', ], 'direction' => [ 'description' => __( 'Direction on which to sort.', 'woocommerce-payments' ), From 818fe822d150d409a7ac9b9e5732acd7ec9d1da1 Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Thu, 23 Nov 2023 10:06:10 +0100 Subject: [PATCH 37/61] Buyer details reference: handle empty billing name. (#7772) --- changelog/fix-7740-customer-reference-rendering-no-name | 4 ++++ includes/wc-payment-api/class-wc-payments-api-client.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-7740-customer-reference-rendering-no-name diff --git a/changelog/fix-7740-customer-reference-rendering-no-name b/changelog/fix-7740-customer-reference-rendering-no-name new file mode 100644 index 00000000000..ca84c5fb827 --- /dev/null +++ b/changelog/fix-7740-customer-reference-rendering-no-name @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +When rendering customer reference on transaction details page, handle case with name being not provided in the order. diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 5456d2f1744..24585d57e81 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -2097,7 +2097,7 @@ public function build_order_info( WC_Order $order ): array { 'number' => $order->get_order_number(), 'url' => $order->get_edit_order_url(), 'customer_url' => $this->get_customer_url( $order ), - 'customer_name' => $order->get_formatted_billing_full_name(), + 'customer_name' => trim( $order->get_formatted_billing_full_name() ), 'customer_email' => $order->get_billing_email(), 'fraud_meta_box_type' => $order->get_meta( '_wcpay_fraud_meta_box_type' ), ]; From 2af85e4821bbe4860946f2d0adc8c2a7f309c5e8 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:09:37 +0200 Subject: [PATCH 38/61] Prevent merchants to access onboarding again after starting it in new flow (#7767) --- ...0-refresh-account-data-when-left-kyc-early | 4 ++++ includes/admin/class-wc-payments-admin.php | 6 ++++++ includes/class-wc-payments-account.php | 8 ++++++++ includes/class-wc-payments-utils.php | 14 +++++++++++++ .../admin/test-class-wc-payments-admin.php | 20 +++++++++++++++++++ 5 files changed, 52 insertions(+) create mode 100644 changelog/fix-6520-refresh-account-data-when-left-kyc-early diff --git a/changelog/fix-6520-refresh-account-data-when-left-kyc-early b/changelog/fix-6520-refresh-account-data-when-left-kyc-early new file mode 100644 index 00000000000..a64a6fd23ac --- /dev/null +++ b/changelog/fix-6520-refresh-account-data-when-left-kyc-early @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Prevent merchants to access onboarding again after starting it in new flow diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 12e727d69a5..88f78a11fa4 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -343,6 +343,12 @@ public function add_payments_menu() { } global $submenu; + // If the user is redirected to the page after Stripe KYC with an error, refresh the account data. + // The GET parameter accessed here comes from server and is just to indicate that some error occured. For this reason we're not using a nonce. + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['wcpay-connection-error'] ) ) { + $this->account->refresh_account_data(); + } try { // Render full payments menu with sub-items only if the merchant completed the KYC (details_submitted = true). $should_render_full_menu = $this->account->is_account_fully_onboarded(); diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 8491400b098..03684fa31fa 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -901,6 +901,14 @@ public function maybe_redirect_onboarding_flow_to_overview(): bool { return false; } + // We check it here after refreshing the cache, because merchant might have clicked back in browser (after Stripe KYC). + // That will mean that no redirect from Stripe happened and user might be able to go through onboarding again if no webhook processed yet. + // That might cause issues if user selects dev onboarding after live one. + // Shouldn't be called with force disconnected option enabled, otherwise we'll get current account data. + if ( ! WC_Payments_Utils::force_disconnected_enabled() ) { + $this->refresh_account_data(); + } + // Don't redirect merchants that have no Stripe account connected. if ( ! $this->is_stripe_connected() ) { return false; diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index a4c164ddbca..e8e1d398de3 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -26,6 +26,11 @@ class WC_Payments_Utils { */ const ORDER_INTENT_CURRENCY_META_KEY = '_wcpay_intent_currency'; + /** + * Force disconnected flag name. + */ + const FORCE_DISCONNECTED_FLAG_NAME = 'wcpaydev_force_disconnected'; + /** * Mirrors JS's createInterpolateElement functionality. * Returns a string where angle brackets expressions are replaced with unescaped html while the rest is escaped. @@ -735,6 +740,15 @@ public static function should_use_progressive_onboarding_flow(): bool { return false; } + /** + * Checks whether the Force disconnected option is enabled. + * + * @return bool + */ + public static function force_disconnected_enabled(): bool { + return '1' === get_option( self::FORCE_DISCONNECTED_FLAG_NAME, '0' ); + } + /** * Check to see if the current user is in progressive onboarding experiment treatment mode. * diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 6c2734f248e..ea7aaaed164 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -173,6 +173,26 @@ public function test_it_does_not_render_payments_badge_if_stripe_is_connected() $this->assertArrayNotHasKey( 'wc-admin&path=/payments/connect', $item_names_by_urls ); } + public function test_it_refreshes_the_cache_if_get_param_exists() { + global $menu; + $this->mock_current_user_is_admin(); + $_GET = [ + 'page' => 'wc-admin', + 'path' => '/payments/overview', + 'wcpay-connection-error' => '1', + ]; + + // Make sure we render the menu with submenu items. + $this->mock_account->method( 'is_account_fully_onboarded' )->willReturn( true ); + $this->mock_account->method( 'is_stripe_connected' )->willReturn( true ); + $this->mock_account->expects( $this->once() )->method( 'refresh_account_data' ); + $this->payments_admin->add_payments_menu(); + + $item_names_by_urls = wp_list_pluck( $menu, 0, 2 ); + $this->assertEquals( 'Payments', $item_names_by_urls['wc-admin&path=/payments/overview'] ); + $this->assertArrayNotHasKey( 'wc-admin&path=/payments/connect', $item_names_by_urls ); + } + public function test_it_renders_payments_badge_if_activation_date_is_older_than_3_days_and_stripe_is_not_connected() { global $menu; $this->mock_current_user_is_admin(); From 81c4e3ecb465e158f23506e464f33d160a2ee500 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Fri, 24 Nov 2023 11:52:11 +0200 Subject: [PATCH 39/61] Allow requests with IDs to be extended (#7775) --- changelog/fix-7508-request-extension | 4 ++++ includes/core/server/class-request.php | 2 +- .../request/test-class-core-request.php | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-7508-request-extension diff --git a/changelog/fix-7508-request-extension b/changelog/fix-7508-request-extension new file mode 100644 index 00000000000..916c1557194 --- /dev/null +++ b/changelog/fix-7508-request-extension @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Allow requests with item IDs to be extended without exceptions. diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index 68195675987..80e35dba0de 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -460,7 +460,7 @@ final public static function extend( Request $base_request ) { 'wcpay_core_extend_class_incorrectly' ); } - $obj = new $current_class( $base_request->api_client, $base_request->http_interface ); + $obj = new $current_class( $base_request->api_client, $base_request->http_interface, $base_request->id ?? null ); $obj->set_params( array_merge( static::DEFAULT_PARAMS, $base_request->params ) ); // Carry over the base class and protected mode into the child request. diff --git a/tests/unit/core/server/request/test-class-core-request.php b/tests/unit/core/server/request/test-class-core-request.php index 8f7e48d016b..c71dce2c3f3 100644 --- a/tests/unit/core/server/request/test-class-core-request.php +++ b/tests/unit/core/server/request/test-class-core-request.php @@ -8,6 +8,7 @@ use WCPay\Core\Server\Request; use WCPay\Core\Server\Request\Paginated; use WCPay\Core\Server\Request\List_Transactions; +use WCPay\Core\Server\Request\Update_Intention; // phpcs:disable class My_Request extends Request { @@ -54,6 +55,10 @@ public function set_param_4( int $value ) { $this->set_param( 'param_4', $value ); } } + +class Request_With_Id extends Update_Intention { + +} // phpcs:enable // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound @@ -134,4 +139,21 @@ function( $request ) { $result ); } + + public function test_extension_works_with_ids() { + $intent_id = 'pi_XYZ'; + $hook = 'some_request_class_with_id'; + $base = Update_Intention::create( $intent_id ); + + add_filter( + $hook, + function( $base ) { + return Request_With_Id::extend( $base ); + } + ); + + $filtered = $base->apply_filters( $hook ); + $this->assertInstanceOf( Request_With_Id::class, $filtered ); + $this->assertStringContainsString( $intent_id, $filtered->get_api() ); + } } From 529f9f75dcbad67b4f22fdca84151125332b26b0 Mon Sep 17 00:00:00 2001 From: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:08:28 +0200 Subject: [PATCH 40/61] Enable the new onboarding flow as default for all users (#7773) Co-authored-by: Daniel Mallory --- ...ble-progressive-onboarding-as-default-flow | 4 ++ includes/admin/class-wc-payments-admin.php | 2 +- includes/class-wc-payments-account.php | 8 ++-- includes/class-wc-payments-features.php | 11 ----- includes/class-wc-payments-utils.php | 29 +++----------- .../merchant-progressive-onboarding.spec.js | 2 - tests/e2e/utils/flows.js | 40 ------------------- .../unit/test-class-wc-payments-features.php | 32 +-------------- 8 files changed, 15 insertions(+), 113 deletions(-) create mode 100644 changelog/update-7738-enable-progressive-onboarding-as-default-flow diff --git a/changelog/update-7738-enable-progressive-onboarding-as-default-flow b/changelog/update-7738-enable-progressive-onboarding-as-default-flow new file mode 100644 index 00000000000..44266d4198b --- /dev/null +++ b/changelog/update-7738-enable-progressive-onboarding-as-default-flow @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Enable the new onboarding flow as default for all users diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 88f78a11fa4..c44adb42ac8 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -385,7 +385,7 @@ public function add_payments_menu() { } if ( ! $this->account->is_stripe_connected() ) { - if ( WC_Payments_Utils::should_use_progressive_onboarding_flow() ) { + if ( WC_Payments_Utils::should_use_new_onboarding_flow() ) { wc_admin_register_page( [ 'id' => 'wc-payments-onboarding', diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 03684fa31fa..00d21ab7a79 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -534,7 +534,7 @@ public function get_progressive_onboarding_details(): array { return [ 'isEnabled' => $account['progressive_onboarding']['is_enabled'] ?? false, 'isComplete' => $account['progressive_onboarding']['is_complete'] ?? false, - 'isNewFlowEnabled' => WC_Payments_Utils::should_use_progressive_onboarding_flow(), + 'isNewFlowEnabled' => WC_Payments_Utils::should_use_new_onboarding_flow(), 'isEligibilityModalDismissed' => get_option( WC_Payments_Onboarding_Service::ONBOARDING_ELIGIBILITY_MODAL_OPTION, false ), ]; } @@ -757,7 +757,7 @@ public function maybe_redirect_to_onboarding() { return false; } - // Redirect directly to onboarding page if come from WC Admin task and are in treatment mode. + // Redirect directly to onboarding page if come from WC Admin task. $http_referer = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) ); if ( 0 < strpos( $http_referer, 'task=payments' ) ) { $this->redirect_to_onboarding_flow_page(); @@ -1885,13 +1885,13 @@ public function is_card_testing_protection_eligible(): bool { } /** - * Redirects to the onboarding flow page if the Progressive Onboarding feature flag is enabled or in the experiment treatment mode. + * Redirects to the onboarding flow page. * Also checks if the server is connected and try to connect it otherwise. * * @return void */ private function redirect_to_onboarding_flow_page() { - if ( ! WC_Payments_Utils::should_use_progressive_onboarding_flow() ) { + if ( ! WC_Payments_Utils::should_use_new_onboarding_flow() ) { return; } diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 9302f1face2..e7193fa2a86 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -21,7 +21,6 @@ class WC_Payments_Features { const WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME = '_wcpay_feature_woopay_express_checkout'; const WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME = '_wcpay_feature_woopay_first_party_auth'; const AUTH_AND_CAPTURE_FLAG_NAME = '_wcpay_feature_auth_and_capture'; - const PROGRESSIVE_ONBOARDING_FLAG_NAME = '_wcpay_feature_progressive_onboarding'; const PAY_FOR_ORDER_FLOW = '_wcpay_feature_pay_for_order_flow'; const DEFERRED_UPE_SERVER_FLAG_NAME = 'is_deferred_intent_creation_upe_enabled'; const DISPUTE_ISSUER_EVIDENCE = '_wcpay_feature_dispute_issuer_evidence'; @@ -354,15 +353,6 @@ public static function is_auth_and_capture_enabled() { return '1' === get_option( self::AUTH_AND_CAPTURE_FLAG_NAME, '1' ); } - /** - * Checks whether Progressive Onboarding is enabled. - * - * @return bool - */ - public static function is_progressive_onboarding_enabled(): bool { - return '1' === get_option( self::PROGRESSIVE_ONBOARDING_FLAG_NAME, '0' ); - } - /** * Checks whether the Fraud and Risk Tools feature flag is enabled. * @@ -470,7 +460,6 @@ public static function to_array() { 'clientSecretEncryption' => self::is_client_secret_encryption_enabled(), 'woopayExpressCheckout' => self::is_woopay_express_checkout_enabled(), 'isAuthAndCaptureEnabled' => self::is_auth_and_capture_enabled(), - 'progressiveOnboarding' => self::is_progressive_onboarding_enabled(), 'isPayForOrderFlowEnabled' => self::is_pay_for_order_flow_enabled(), 'isDisputeIssuerEvidenceEnabled' => self::is_dispute_issuer_evidence_enabled(), 'isRefundControlsEnabled' => self::is_streamline_refunds_enabled(), diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php index e8e1d398de3..181f5837940 100644 --- a/includes/class-wc-payments-utils.php +++ b/includes/class-wc-payments-utils.php @@ -728,16 +728,16 @@ public static function get_last_refund_from_order_id( $order_id ) { } /** - * Helper function to check whether the user is either in the PO experiment, or has manually enabled PO via the dev tools. + * Helper function to check whether to show default new onboarding flow or as an exception disable it (if specific constant is set) . * * @return boolean */ - public static function should_use_progressive_onboarding_flow(): bool { - if ( self::is_in_progressive_onboarding_treatment_mode() || WC_Payments_Features::is_progressive_onboarding_enabled() ) { - return true; + public static function should_use_new_onboarding_flow(): bool { + if ( defined( 'WCPAY_DISABLE_NEW_ONBOARDING' ) && WCPAY_DISABLE_NEW_ONBOARDING ) { + return false; } - return false; + return true; } /** @@ -749,25 +749,6 @@ public static function force_disconnected_enabled(): bool { return '1' === get_option( self::FORCE_DISCONNECTED_FLAG_NAME, '0' ); } - /** - * Check to see if the current user is in progressive onboarding experiment treatment mode. - * - * @return bool - */ - public static function is_in_progressive_onboarding_treatment_mode(): bool { - if ( ! isset( $_COOKIE['tk_ai'] ) ) { - return false; - } - - $abtest = new \WCPay\Experimental_Abtest( - sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ), - 'woocommerce', - 'yes' === get_option( 'woocommerce_allow_tracking' ) - ); - - return 'treatment' === $abtest->get_variation( 'woocommerce_payments_onboarding_progressive_express_2023_v3' ); - } - /** * Return the currency format based on the symbol position. * Similar to get_woocommerce_price_format but with an input. diff --git a/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js b/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js index e7cf88553e1..ed2382dcd2c 100644 --- a/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js +++ b/tests/e2e/specs/wcpay/merchant/merchant-progressive-onboarding.spec.js @@ -11,12 +11,10 @@ import { merchantWCP, uiLoaded } from '../../../utils'; describe( 'Admin merchant progressive onboarding', () => { beforeAll( async () => { await merchant.login(); - await merchantWCP.enableProgressiveOnboarding(); await merchantWCP.enableActAsDisconnectedFromWCPay(); } ); afterAll( async () => { - await merchantWCP.disableProgressiveOnboarding(); await merchantWCP.disableActAsDisconnectedFromWCPay(); await merchant.logout(); } ); diff --git a/tests/e2e/utils/flows.js b/tests/e2e/utils/flows.js index eac8f167785..f568c536938 100644 --- a/tests/e2e/utils/flows.js +++ b/tests/e2e/utils/flows.js @@ -462,46 +462,6 @@ export const merchantWCP = { } ); }, - enableProgressiveOnboarding: async () => { - await page.goto( WCPAY_DEV_TOOLS, { - waitUntil: 'networkidle0', - } ); - - if ( - ! ( await page.$( - '#_wcpay_feature_progressive_onboarding:checked' - ) ) - ) { - await expect( page ).toClick( - 'label[for="_wcpay_feature_progressive_onboarding"]' - ); - } - - await expect( page ).toClick( 'input#submit' ); - await page.waitForNavigation( { - waitUntil: 'networkidle0', - } ); - }, - - disableProgressiveOnboarding: async () => { - await page.goto( WCPAY_DEV_TOOLS, { - waitUntil: 'networkidle0', - } ); - - if ( - await page.$( '#_wcpay_feature_progressive_onboarding:checked' ) - ) { - await expect( page ).toClick( - 'label[for="_wcpay_feature_progressive_onboarding"]' - ); - } - - await expect( page ).toClick( 'input#submit' ); - await page.waitForNavigation( { - waitUntil: 'networkidle0', - } ); - }, - enableActAsDisconnectedFromWCPay: async () => { await page.goto( WCPAY_DEV_TOOLS, { waitUntil: 'networkidle0', diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index 3a0756d5bdc..4e530feaf78 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -25,7 +25,6 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase { '_wcpay_feature_customer_multi_currency' => 'multiCurrency', '_wcpay_feature_documents' => 'documents', '_wcpay_feature_auth_and_capture' => 'isAuthAndCaptureEnabled', - '_wcpay_feature_progressive_onboarding' => 'progressiveOnboarding', 'is_deferred_intent_creation_upe_enabled' => 'upeDeferred', ]; @@ -233,7 +232,7 @@ function ( $pre_option, $option, $default ) { public function test_is_woopay_express_checkout_enabled_returns_false_when_woopay_eligible_is_false() { add_filter( - 'pre_option_' . WC_Payments_Features::PROGRESSIVE_ONBOARDING_FLAG_NAME, + 'pre_option_' . WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, function ( $pre_option, $option, $default ) { return '1'; }, @@ -244,35 +243,6 @@ function ( $pre_option, $option, $default ) { $this->assertFalse( WC_Payments_Features::is_woopay_express_checkout_enabled() ); } - public function test_is_progressive_onboarding_enabled_returns_true() { - add_filter( - 'pre_option_' . WC_Payments_Features::PROGRESSIVE_ONBOARDING_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '1'; - }, - 10, - 3 - ); - $this->assertTrue( WC_Payments_Features::is_progressive_onboarding_enabled() ); - } - - public function test_is_progressive_onboarding_enabled_returns_false_when_flag_is_false() { - add_filter( - 'pre_option_' . WC_Payments_Features::PROGRESSIVE_ONBOARDING_FLAG_NAME, - function ( $pre_option, $option, $default ) { - return '0'; - }, - 10, - 3 - ); - $this->assertFalse( WC_Payments_Features::is_progressive_onboarding_enabled() ); - $this->assertArrayNotHasKey( 'progressiveOnboarding', WC_Payments_Features::to_array() ); - } - - public function test_is_progressive_onboarding_enabled_returns_false_when_flag_is_not_set() { - $this->assertFalse( WC_Payments_Features::is_progressive_onboarding_enabled() ); - } - public function test_deferred_upe_enabled_with_sepa() { $this->mock_cache->method( 'get' )->willReturn( [ From 58a061deb0de9dd88165df57c28580c3076719d1 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Fri, 24 Nov 2023 16:25:49 -0300 Subject: [PATCH 41/61] Display notice when clicking the WooPay button if variable product selection is incomplete (#7735) --- changelog/fix-7616-variable-product-notice | 4 + .../checkout/woopay/express-button/index.js | 7 +- .../woopay-express-checkout-button.test.js | 21 ++-- .../use-express-checkout-product-handler.js | 91 +-------------- .../woopay-express-checkout-button.js | 105 +++++++++++++----- client/payment-request/index.js | 19 +++- 6 files changed, 107 insertions(+), 140 deletions(-) create mode 100644 changelog/fix-7616-variable-product-notice diff --git a/changelog/fix-7616-variable-product-notice b/changelog/fix-7616-variable-product-notice new file mode 100644 index 00000000000..6bbb29e1b15 --- /dev/null +++ b/changelog/fix-7616-variable-product-notice @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Display notice when clicking the WooPay button if variable product selection is incomplete. diff --git a/client/checkout/woopay/express-button/index.js b/client/checkout/woopay/express-button/index.js index bd2aeff02c2..6058d71a8f4 100644 --- a/client/checkout/woopay/express-button/index.js +++ b/client/checkout/woopay/express-button/index.js @@ -1,4 +1,3 @@ -/* global jQuery */ /** * External dependencies */ @@ -58,16 +57,16 @@ const renderWooPayExpressCheckoutButtonWithCallbacks = () => { renderWooPayExpressCheckoutButton( listenForCartChanges ); }; -jQuery( ( $ ) => { +document.addEventListener( 'DOMContentLoaded', () => { listenForCartChanges = { start: () => { - $( document.body ).on( + document.body.addEventListener( 'updated_cart_totals', renderWooPayExpressCheckoutButtonWithCallbacks ); }, stop: () => { - $( document.body ).off( + document.body.removeEventListener( 'updated_cart_totals', renderWooPayExpressCheckoutButtonWithCallbacks ); diff --git a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js index f37f92efac2..5fd5b1d3235 100644 --- a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js +++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js @@ -38,9 +38,6 @@ jest.mock( '../use-express-checkout-product-handler', () => jest.fn() ); jest.spyOn( window, 'alert' ).mockImplementation( () => {} ); global.fetch = jest.fn( () => Promise.resolve( { json: () => ( {} ) } ) ); -global.window.wc_add_to_cart_variation_params = { - i18n_make_a_selection_text: 'Mock text', -}; describe( 'WoopayExpressCheckoutButton', () => { const buttonSettings = { @@ -62,7 +59,6 @@ describe( 'WoopayExpressCheckoutButton', () => { }; useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, - isAddToCartDisabled: false, } ) ); } ); @@ -214,15 +210,21 @@ describe( 'WoopayExpressCheckoutButton', () => { } ); } ); - test( 'should shown an alert when clicking the button when add to cart button is disabled', () => { + test( 'should show an alert when clicking the button when add to cart button is disabled', () => { getConfig.mockImplementation( ( v ) => { return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo'; } ); useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, - isAddToCartDisabled: true, } ) ); + // Add a disabled add to cart button to the DOM. + const addToCartButton = document.createElement( 'button' ); + addToCartButton.classList.add( 'single_add_to_cart_button' ); + addToCartButton.classList.add( 'disabled' ); + addToCartButton.classList.add( 'wc-variation-selection-needed' ); + document.body.appendChild( addToCartButton ); + render( { userEvent.click( expressButton ); expect( window.alert ).toBeCalledWith( - window.wc_add_to_cart_variation_params - .i18n_make_a_selection_text + 'Please select your product options before proceeding.' ); + + document.body.removeChild( addToCartButton ); } ); test( 'call `addToCart` and `expressCheckoutIframe` on express button click on product page', async () => { @@ -252,7 +255,6 @@ describe( 'WoopayExpressCheckoutButton', () => { useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, getProductData: jest.fn().mockReturnValue( {} ), - isAddToCartDisabled: false, } ) ); render( { useExpressCheckoutProductHandler.mockImplementation( () => ( { addToCart: mockAddToCart, getProductData: jest.fn().mockReturnValue( false ), - isAddToCartDisabled: false, } ) ); render( { - const [ isAddToCartDisabled, setIsAddToCartDisabled ] = useState( false ); - +const useExpressCheckoutProductHandler = ( api ) => { const getAttributes = () => { const select = document .querySelector( '.variations_form' ) @@ -142,95 +139,9 @@ const useExpressCheckoutProductHandler = ( api, isProductPage = false ) => { return api.expressCheckoutAddToCart( data ); }; - useEffect( () => { - if ( ! isProductPage ) { - return; - } - - const getIsAddToCartDisabled = () => { - const addToCartButton = document.querySelector( - '.single_add_to_cart_button' - ); - - return ( - addToCartButton.disabled || - addToCartButton.classList.contains( 'disabled' ) - ); - }; - - setIsAddToCartDisabled( getIsAddToCartDisabled() ); - - const enableAddToCartButton = () => { - setIsAddToCartDisabled( false ); - }; - - const disableAddToCartButton = () => { - setIsAddToCartDisabled( true ); - }; - - const bundleForm = document.querySelector( '.bundle_form' ); - const mixAndMatchForm = document.querySelector( '.mnm_form' ); - const variationForm = document.querySelector( '.variations_form' ); - - if ( bundleForm ) { - // eslint-disable-next-line no-undef - jQuery( bundleForm ) - .on( 'woocommerce-product-bundle-show', enableAddToCartButton ) - .on( - 'woocommerce-product-bundle-hide', - disableAddToCartButton - ); - } else if ( mixAndMatchForm ) { - // eslint-disable-next-line no-undef - jQuery( mixAndMatchForm ) - .on( - 'wc-mnm-display-add-to-cart-button', - enableAddToCartButton - ) - .on( 'wc-mnm-hide-add-to-cart-button', disableAddToCartButton ); - } else if ( variationForm ) { - // eslint-disable-next-line no-undef - jQuery( variationForm ) - .on( 'show_variation', enableAddToCartButton ) - .on( 'hide_variation', disableAddToCartButton ); - } - - return () => { - if ( bundleForm ) { - // eslint-disable-next-line no-undef - jQuery( bundleForm ) - .off( - 'woocommerce-product-bundle-show', - enableAddToCartButton - ) - .off( - 'woocommerce-product-bundle-hide', - disableAddToCartButton - ); - } else if ( mixAndMatchForm ) { - // eslint-disable-next-line no-undef - jQuery( mixAndMatchForm ) - .off( - 'wc-mnm-display-add-to-cart-button', - enableAddToCartButton - ) - .off( - 'wc-mnm-hide-add-to-cart-button', - disableAddToCartButton - ); - } else if ( variationForm ) { - // eslint-disable-next-line no-undef - jQuery( variationForm ) - .off( 'show_variation', enableAddToCartButton ) - .off( 'hide_variation', disableAddToCartButton ); - } - }; - }, [ isProductPage, setIsAddToCartDisabled ] ); - return { addToCart, getProductData, - isAddToCartDisabled, }; }; diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index 094d0856ad5..a7b26ef4853 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -58,11 +58,9 @@ export const WoopayExpressCheckoutButton = ( { const ThemedWooPayIcon = theme === 'dark' ? WoopayIcon : WoopayIconLight; - const { - addToCart, - getProductData, - isAddToCartDisabled, - } = useExpressCheckoutProductHandler( api, isProductPage ); + const { addToCart, getProductData } = useExpressCheckoutProductHandler( + api + ); const getProductDataRef = useRef( getProductData ); const addToCartRef = useRef( addToCart ); @@ -89,14 +87,64 @@ export const WoopayExpressCheckoutButton = ( { } }, [ isPreview, context ] ); - const defaultOnClick = useCallback( ( event ) => { - // This will only be called if user clicks the button too quickly. - // It saves the event for later use. - initialOnClickEventRef.current = event; - // Set isLoadingRef to true to prevent multiple clicks. - isLoadingRef.current = true; - setIsLoading( true ); - }, [] ); + const canAddProductToCart = useCallback( () => { + if ( ! isProductPage ) { + return true; + } + + const addToCartButton = document.querySelector( + '.single_add_to_cart_button' + ); + + if ( + addToCartButton && + ( addToCartButton.disabled || + addToCartButton.classList.contains( 'disabled' ) ) + ) { + if ( + addToCartButton.classList.contains( + 'wc-variation-is-unavailable' + ) + ) { + window.alert( + window?.wc_add_to_cart_variation_params + ?.i18n_unavailable_text || + __( + 'Sorry, this product is unavailable. Please choose a different combination.', + 'woocommerce-payments' + ) + ); + } else { + window.alert( + __( + 'Please select your product options before proceeding.', + 'woocommerce-payments' + ) + ); + } + + return false; + } + + return true; + }, [ isProductPage ] ); + + const defaultOnClick = useCallback( + ( event ) => { + event?.preventDefault(); + + if ( ! canAddProductToCart() ) { + return; + } + // This will only be called if user clicks the button too quickly. + // It saves the event for later use. + initialOnClickEventRef.current = event; + // Set isLoadingRef to true to prevent multiple clicks. + isLoadingRef.current = true; + setIsLoading( true ); + }, + [ canAddProductToCart ] + ); const onClickFallback = useCallback( // OTP flow @@ -114,19 +162,11 @@ export const WoopayExpressCheckoutButton = ( { } ); - if ( isProductPage ) { - if ( isAddToCartDisabled ) { - alert( - window.wc_add_to_cart_variation_params - ?.i18n_make_a_selection_text || - __( - 'Please select all required options to continue.', - 'woocommerce-payments' - ) - ); - return; - } + if ( ! canAddProductToCart() ) { + return; + } + if ( isProductPage ) { const productData = getProductDataRef.current(); if ( ! productData ) { return; @@ -152,9 +192,9 @@ export const WoopayExpressCheckoutButton = ( { api, context, emailSelector, - isAddToCartDisabled, isPreview, isProductPage, + canAddProductToCart, ] ); @@ -201,10 +241,6 @@ export const WoopayExpressCheckoutButton = ( { return; } - // Set isLoadingRef to true to prevent multiple clicks. - isLoadingRef.current = true; - setIsLoading( true ); - wcpayTracks.recordUserEvent( wcpayTracks.events.WOOPAY_BUTTON_CLICK, { @@ -212,6 +248,14 @@ export const WoopayExpressCheckoutButton = ( { } ); + if ( ! canAddProductToCart() ) { + return; + } + + // Set isLoadingRef to true to prevent multiple clicks. + isLoadingRef.current = true; + setIsLoading( true ); + if ( isProductPage ) { const productData = getProductDataRef.current(); @@ -297,6 +341,7 @@ export const WoopayExpressCheckoutButton = ( { isPreview, listenForCartChanges, onClickFallback, + canAddProductToCart, ] ); useEffect( () => { diff --git a/client/payment-request/index.js b/client/payment-request/index.js index a837efa902c..9578b42517c 100644 --- a/client/payment-request/index.js +++ b/client/payment-request/index.js @@ -1,7 +1,8 @@ -/* global jQuery, wcpayPaymentRequestParams, wcpayPaymentRequestPayForOrderParams, wc_add_to_cart_variation_params */ +/* global jQuery, wcpayPaymentRequestParams, wcpayPaymentRequestPayForOrderParams */ /** * External dependencies */ +import { __ } from '@wordpress/i18n'; import { doAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -383,13 +384,19 @@ jQuery( ( $ ) => { addToCartButton.is( '.wc-variation-is-unavailable' ) ) { window.alert( - wc_add_to_cart_variation_params.i18n_unavailable_text + window?.wc_add_to_cart_variation_params + ?.i18n_unavailable_text || + __( + 'Sorry, this product is unavailable. Please choose a different combination.', + 'woocommerce-payments' + ) ); - } else if ( - addToCartButton.is( '.wc-variation-selection-needed' ) - ) { + } else { window.alert( - wc_add_to_cart_variation_params.i18n_make_a_selection_text + __( + 'Please select your product options before proceeding.', + 'woocommerce-payments' + ) ); } return; From 30019b583d4c477978256b11ff7fe783d4fb4fa5 Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:51:15 -0600 Subject: [PATCH 42/61] Sanitize the Tracks properties (#7755) --- changelog/fix-tracks-sanitization | 5 +++++ includes/class-woopay-tracker.php | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-tracks-sanitization diff --git a/changelog/fix-tracks-sanitization b/changelog/fix-tracks-sanitization new file mode 100644 index 00000000000..83d5fec780a --- /dev/null +++ b/changelog/fix-tracks-sanitization @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Sanitize Tracks properties + + diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php index a1c638bb36d..123ec606a7b 100644 --- a/includes/class-woopay-tracker.php +++ b/includes/class-woopay-tracker.php @@ -278,8 +278,7 @@ private function tracks_build_event_obj( $user, $event_name, $properties = [] ) $identity = $this->tracks_get_identity( $user->ID ); $site_url = get_option( 'siteurl' ); - //phpcs:ignore WordPress.Security.ValidatedSanitizedInput - $properties['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; + $properties['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ): ''; $properties['blog_url'] = $site_url; $properties['blog_id'] = \Jetpack_Options::get_option( 'id' ); $properties['user_lang'] = $user->get( 'WPLANG' ); @@ -290,7 +289,7 @@ private function tracks_build_event_obj( $user, $event_name, $properties = [] ) // Add client's user agent to the event properties. if ( !empty( $_SERVER['HTTP_USER_AGENT'] ) ) { - $properties['_via_ua'] = $_SERVER['HTTP_USER_AGENT']; + $properties['_via_ua'] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ); } $blog_details = [ From b552162fffa7cbe0ae2564cd5b0df1919aa583ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Qui=C3=B1ones?= Date: Mon, 27 Nov 2023 09:28:26 +0000 Subject: [PATCH 43/61] Allow PO accounts to continue the first part of onboarding (#7716) Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com> --- changelog/fix-7065-continue-po-onboarding | 4 ++++ includes/class-wc-payments-account.php | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog/fix-7065-continue-po-onboarding diff --git a/changelog/fix-7065-continue-po-onboarding b/changelog/fix-7065-continue-po-onboarding new file mode 100644 index 00000000000..5da02af6ca0 --- /dev/null +++ b/changelog/fix-7065-continue-po-onboarding @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Allow Gradual signup accounts to continue with the Gradual KYC after abandoning it diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index 00d21ab7a79..d417de02838 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -954,6 +954,12 @@ public function maybe_handle_onboarding() { if ( $this->is_account_partially_onboarded() ) { $args = $_GET; $args['type'] = 'complete_kyc_link'; + + // Allow progressive onboarding accounts to continue onboarding without payout collection. + if ( $this->is_progressive_onboarding_in_progress() ) { + $args['is_progressive_onboarding'] = $this->is_progressive_onboarding_in_progress() ?? false; + } + $this->redirect_to_account_link( $args ); } From 10854dc1c7147df694d596aa82d31864ccfa50ff Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Mon, 27 Nov 2023 15:22:17 +0100 Subject: [PATCH 44/61] Streamline UPE & non-UPE branching across the codebase & improve test coverage (#7770) Co-authored-by: Timur Karimov --- .../partially-cleanup-legacy-upe-and-card | 4 + includes/class-wc-payment-gateway-wcpay.php | 55 +---- includes/class-wc-payments-checkout.php | 14 +- includes/class-wc-payments-features.php | 60 +---- includes/class-wc-payments-token-service.php | 10 +- ...-wc-payments-upe-blocks-payment-method.php | 39 ---- includes/class-wc-payments-upe-checkout.php | 52 ++--- includes/class-wc-payments.php | 113 ++++------ .../class-upe-payment-gateway.php | 111 --------- .../class-upe-split-payment-gateway.php | 62 ----- .../services/class-checkout-service.php | 2 +- ...s-wc-rest-payments-settings-controller.php | 16 -- .../test-class-upe-payment-gateway.php | 145 ------------ .../test-class-upe-split-payment-gateway.php | 68 +----- .../test-class-wc-payment-gateway-wcpay.php | 212 ++++++++++++++++++ .../unit/test-class-wc-payments-features.php | 14 -- .../test-class-wc-payments-token-service.php | 20 ++ 17 files changed, 321 insertions(+), 676 deletions(-) create mode 100644 changelog/partially-cleanup-legacy-upe-and-card delete mode 100644 includes/class-wc-payments-upe-blocks-payment-method.php diff --git a/changelog/partially-cleanup-legacy-upe-and-card b/changelog/partially-cleanup-legacy-upe-and-card new file mode 100644 index 00000000000..8e3bae1034b --- /dev/null +++ b/changelog/partially-cleanup-legacy-upe-and-card @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Cleanup the deprecated payment gateway processing - part I diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 97f4377cfa4..df4116e1cec 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -711,7 +711,6 @@ public function should_use_stripe_platform_on_checkout_page() { if ( WC_Payments_Features::is_woopay_eligible() && 'yes' === $this->get_option( 'platform_checkout', 'no' ) && - ! $this->is_upe_incompatible_with_woopay() && ( is_checkout() || has_block( 'woocommerce/checkout' ) ) && ! is_wc_endpoint_url( 'order-pay' ) && WC()->cart instanceof WC_Cart && @@ -724,16 +723,6 @@ public function should_use_stripe_platform_on_checkout_page() { return false; } - /** - * The legacy UPE is incompatible with WooPay, whereas split UPE and deferred intent UPE are compatible. - * This method checks if there's incompatibility between WooPay and currently enabled UPE settings, applying the rule above. - * - * $return bool - true if UPE is incompatible with WooPay, false otherwise. - */ - private function is_upe_incompatible_with_woopay() { - return WC_Payments_Features::is_upe_legacy_enabled() && ! WC_Payments_Features::is_upe_deferred_intent_enabled(); - } - /** * Renders the credit card input fields needed to get the user's payment information on the checkout page. * @@ -946,10 +935,6 @@ public function process_payment( $order_id ) { ]; } - if ( WC_Payments_Features::is_upe_legacy_enabled() ) { - UPE_Payment_Gateway::remove_upe_payment_intent_from_session(); - } - $check_session_order = $this->duplicate_payment_prevention_service->check_against_session_processing_order( $order ); if ( is_array( $check_session_order ) ) { return $check_session_order; @@ -1050,10 +1035,6 @@ public function process_payment( $order_id ) { $order->add_order_note( $note ); } - if ( WC_Payments_Features::is_upe_legacy_enabled() ) { - UPE_Payment_Gateway::remove_upe_payment_intent_from_session(); - } - // Re-throw the exception after setting everything up. // This makes the error notice show up both in the regular and block checkout. throw new Exception( WC_Payments_Utils::get_filtered_error_message( $e ) ); @@ -1345,8 +1326,8 @@ public function process_payment_for_order( $cart, $payment_information, $schedul } } - // For Stripe Link & SEPA with deferred intent UPE, we must create mandate to acknowledge that terms have been shown to customer. - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() && $this->is_mandate_data_required() ) { + // For Stripe Link & SEPA, we must create mandate to acknowledge that terms have been shown to customer. + if ( $this->is_mandate_data_required() ) { $request->set_mandate_data( $this->get_mandate_data() ); } @@ -1420,7 +1401,6 @@ public function process_payment_for_order( $cart, $payment_information, $schedul $request->set_hook_args( $payment_information, false, $save_user_in_woopay ); if ( - WC_Payments_Features::is_upe_deferred_intent_enabled() && Payment_Method::CARD === $this->get_selected_stripe_payment_type_id() && in_array( Payment_Method::LINK, $this->get_upe_enabled_payment_method_ids(), true ) ) { @@ -1578,10 +1558,8 @@ protected function is_mandate_data_required() { * @return string|null Payment method to use for the intent. */ public function get_payment_method_to_use_for_intent() { - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification - return $this->get_payment_methods_from_gateway_id( $requested_payment_method )[0]; - } + $requested_payment_method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification + return $this->get_payment_methods_from_gateway_id( $requested_payment_method )[0]; } /** @@ -1627,20 +1605,14 @@ public function get_payment_methods_from_gateway_id( $gateway_id, $order_id = nu $eligible_payment_methods = WC_Payments::get_gateway()->get_payment_method_ids_enabled_at_checkout( $order_id, true ); - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - // If split or deferred intent UPE is enabled and $gateway_id is `woocommerce_payments`, this must be the CC gateway. - // We only need to return single `card` payment method, adding `link` since deferred intent UPE gateway is compatible with Link. - $payment_methods = [ Payment_Method::CARD ]; - if ( in_array( Payment_Method::LINK, $eligible_payment_methods, true ) ) { - $payment_methods[] = Payment_Method::LINK; - } - - return $payment_methods; + // If $gateway_id is `woocommerce_payments`, this must be the CC gateway. + // We only need to return single `card` payment method, adding `link` since Stripe Link is also supported. + $payment_methods = [ Payment_Method::CARD ]; + if ( in_array( Payment_Method::LINK, $eligible_payment_methods, true ) ) { + $payment_methods[] = Payment_Method::LINK; } - // $gateway_id must be `woocommerce_payments` and gateway is either legacy UPE or legacy card. - // Find the relevant gateway and return all available payment methods. - return $eligible_payment_methods; + return $payment_methods; } /** @@ -2106,13 +2078,6 @@ public function update_is_woopay_enabled( $is_woopay_enabled ) { if ( ! $is_woopay_enabled ) { WooPay_Order_Status_Sync::remove_webhook(); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - update_option( WC_Payments_Features::UPE_FLAG_NAME, '0' ); - update_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, '1' ); - - if ( function_exists( 'wc_admin_record_tracks_event' ) ) { - wc_admin_record_tracks_event( 'wcpay_deferred_intent_upe_enabled' ); - } } } } diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index a7f077e23a4..dc33fe4584d 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -145,14 +145,8 @@ public function register_scripts_for_zero_order_total() { ! has_block( 'woocommerce/checkout' ) ) { WC_Payments::get_gateway()->tokenization_script(); - $script_handle = 'WCPAY_CHECKOUT'; - $js_object = 'wcpayConfig'; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $script_handle = 'wcpay-upe-checkout'; - $js_object = 'wcpay_upe_config'; - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - $script_handle = 'wcpay-upe-checkout'; - } + $script_handle = 'wcpay-upe-checkout'; + $js_object = 'wcpay_upe_config'; wp_localize_script( $script_handle, $js_object, WC_Payments::get_wc_payments_checkout()->get_payment_fields_js_config() ); wp_enqueue_script( $script_handle ); } @@ -193,8 +187,8 @@ public function get_payment_fields_js_config() { 'locale' => WC_Payments_Utils::convert_to_stripe_locale( get_locale() ), 'isPreview' => is_preview(), 'isUPEEnabled' => WC_Payments_Features::is_upe_enabled(), - 'isUPESplitEnabled' => WC_Payments_Features::is_upe_split_enabled(), - 'isUPEDeferredEnabled' => WC_Payments_Features::is_upe_deferred_intent_enabled(), + 'isUPESplitEnabled' => false, + 'isUPEDeferredEnabled' => true, 'isSavedCardsEnabled' => $this->gateway->is_saved_cards_enabled(), 'isWooPayEnabled' => $this->woopay_util->should_enable_woopay( $this->gateway ) && $this->woopay_util->should_enable_woopay_on_cart_or_checkout(), 'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(), diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index e7193fa2a86..ef9ae19c263 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -22,7 +22,6 @@ class WC_Payments_Features { const WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME = '_wcpay_feature_woopay_first_party_auth'; const AUTH_AND_CAPTURE_FLAG_NAME = '_wcpay_feature_auth_and_capture'; const PAY_FOR_ORDER_FLOW = '_wcpay_feature_pay_for_order_flow'; - const DEFERRED_UPE_SERVER_FLAG_NAME = 'is_deferred_intent_creation_upe_enabled'; const DISPUTE_ISSUER_EVIDENCE = '_wcpay_feature_dispute_issuer_evidence'; const STREAMLINE_REFUNDS_FLAG_NAME = '_wcpay_feature_streamline_refunds'; @@ -32,7 +31,7 @@ class WC_Payments_Features { * @return bool */ public static function is_upe_enabled() { - return self::is_upe_legacy_enabled() || self::is_upe_split_enabled() || self::is_upe_deferred_intent_enabled(); + return true; } /** @@ -41,58 +40,7 @@ public static function is_upe_enabled() { * @return string */ public static function get_enabled_upe_type() { - if ( self::is_upe_deferred_intent_enabled() ) { - return 'deferred_intent'; - } - - if ( self::is_upe_split_enabled() ) { - return 'split'; - } - - if ( self::is_upe_legacy_enabled() ) { - return 'legacy'; - } - - return ''; - } - - /** - * Checks whether the legacy UPE gateway is enabled - * - * @return bool - */ - public static function is_upe_legacy_enabled() { - return '1' === get_option( self::UPE_FLAG_NAME, '0' ); - } - - /** - * Checks whether the Split-UPE gateway is enabled - */ - public static function is_upe_split_enabled() { - return '1' === get_option( self::UPE_SPLIT_FLAG_NAME, '0' ); - } - - /** - * Checks whether the Split UPE with deferred intent creation is enabled - */ - public static function is_upe_deferred_intent_enabled() { - $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); - if ( null === $account ) { - return true; - } - return is_array( $account ) && ( $account[ self::DEFERRED_UPE_SERVER_FLAG_NAME ] ?? true ); - } - - /** - * Checks for the requirements to have the split-UPE enabled. - */ - private static function is_upe_split_eligible() { - $account = WC_Payments::get_database_cache()->get( WCPay\Database_Cache::ACCOUNT_KEY, true ); - if ( empty( $account['capabilities']['sepa_debit_payments'] ) ) { - return true; - } - - return 'active' !== $account['capabilities']['sepa_debit_payments']; + return 'deferred_intent'; } /** @@ -451,8 +399,8 @@ public static function to_array() { return array_filter( [ 'upe' => self::is_upe_enabled(), - 'upeSplit' => self::is_upe_split_enabled(), - 'upeDeferred' => self::is_upe_deferred_intent_enabled(), + 'upeSplit' => false, + 'upeDeferred' => true, 'upeSettingsPreview' => self::is_upe_settings_preview_enabled(), 'multiCurrency' => self::is_customer_multi_currency_enabled(), 'woopay' => self::is_woopay_eligible(), diff --git a/includes/class-wc-payments-token-service.php b/includes/class-wc-payments-token-service.php index d075e69fde0..cb556ae2260 100644 --- a/includes/class-wc-payments-token-service.php +++ b/includes/class-wc-payments-token-service.php @@ -71,9 +71,7 @@ public function add_token_to_user( $payment_method, $user ) { switch ( $payment_method['type'] ) { case Payment_Method::SEPA: $token = new WC_Payment_Token_WCPay_SEPA(); - $gateway_id = WC_Payments_Features::is_upe_deferred_intent_enabled() ? - WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA : - CC_Payment_Gateway::GATEWAY_ID; + $gateway_id = WC_Payment_Gateway_WCPay::GATEWAY_ID . '_' . Payment_Method::SEPA; $token->set_gateway_id( $gateway_id ); $token->set_last4( $payment_method[ Payment_Method::SEPA ]['last4'] ); break; @@ -119,11 +117,7 @@ public function add_payment_method_to_user( $payment_method_id, $user ) { * @return bool True, if payment method type matches gateway, false if otherwise. */ public function is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id ) { - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id; - } else { - return WC_Payments::get_gateway()->id === $gateway_id; - } + return self::REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ] === $gateway_id; } /** diff --git a/includes/class-wc-payments-upe-blocks-payment-method.php b/includes/class-wc-payments-upe-blocks-payment-method.php deleted file mode 100644 index f354a27adc5..00000000000 --- a/includes/class-wc-payments-upe-blocks-payment-method.php +++ /dev/null @@ -1,39 +0,0 @@ -gateway, 'save_upe_appearance_ajax' ] ); add_action( 'switch_theme', [ $this->gateway, 'clear_upe_appearance_transient' ] ); add_action( 'woocommerce_woocommerce_payments_updated', [ $this->gateway, 'clear_upe_appearance_transient' ] ); - if ( ! WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - add_action( 'wc_ajax_wcpay_create_payment_intent', [ $this->gateway, 'create_payment_intent_ajax' ] ); - add_action( 'wc_ajax_wcpay_update_payment_intent', [ $this->gateway, 'update_payment_intent_ajax' ] ); - } add_action( 'wc_ajax_wcpay_init_setup_intent', [ $this->gateway, 'init_setup_intent_ajax' ] ); add_action( 'wc_ajax_wcpay_log_payment_error', [ $this->gateway, 'log_payment_error_ajax' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 ); - add_action( 'woocommerce_email_before_order_table', [ $this->gateway, 'set_payment_method_title_for_email' ], 10, 3 ); } /** @@ -137,11 +132,7 @@ public function register_scripts() { $script_dependencies[] = 'woocommerce-tokenization-form'; } - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $script = 'dist/upe_with_deferred_intent_creation_checkout'; - } else { - $script = 'dist/upe_checkout'; - } + $script = 'dist/upe_with_deferred_intent_creation_checkout'; WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', $script, $script_dependencies ); } @@ -166,16 +157,9 @@ public function get_payment_fields_js_config() { $payment_fields['upeAppearance'] = get_transient( UPE_Payment_Gateway::UPE_APPEARANCE_TRANSIENT ); $payment_fields['wcBlocksUPEAppearance'] = get_transient( UPE_Payment_Gateway::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); $payment_fields['cartContainsSubscription'] = $this->gateway->is_subscription_item_in_cart(); - - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $payment_fields['currency'] = get_woocommerce_currency(); - $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); - $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - $payment_fields['checkoutTitle'] = $this->gateway->get_checkout_title(); - $payment_fields['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session(); - $payment_fields['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session(); - } + $payment_fields['currency'] = get_woocommerce_currency(); + $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); + $payment_fields['cartTotal'] = WC_Payments_Utils::prepare_amount( $cart_total, get_woocommerce_currency() ); $enabled_billing_fields = []; foreach ( WC()->checkout()->get_checkout_fields( 'billing' ) as $billing_field => $billing_field_options ) { @@ -264,20 +248,18 @@ public function get_enabled_payment_method_config() { 'countries' => $payment_method->get_countries(), ]; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); - $settings[ $payment_method_id ]['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session( $payment_method_id ); - $settings[ $payment_method_id ]['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session( $payment_method_id ); - $settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html( - /* translators: link to Stripe testing page */ - $payment_method->get_testing_instructions(), - [ - 'strong' => '', - 'a' => '', - ] - ); - $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); - } + $gateway_for_payment_method = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); + $settings[ $payment_method_id ]['upePaymentIntentData'] = $this->gateway->get_payment_intent_data_from_session( $payment_method_id ); + $settings[ $payment_method_id ]['upeSetupIntentData'] = $this->gateway->get_setup_intent_data_from_session( $payment_method_id ); + $settings[ $payment_method_id ]['testingInstructions'] = WC_Payments_Utils::esc_interpolated_html( + /* translators: link to Stripe testing page */ + $payment_method->get_testing_instructions(), + [ + 'strong' => '', + 'a' => '', + ] + ); + $settings[ $payment_method_id ]['forceNetworkSavedCards'] = $gateway_for_payment_method->should_use_stripe_platform_on_checkout_page(); } return $settings; @@ -311,7 +293,7 @@ public function payment_fields() { * before `$this->saved_payment_methods()`. */ $payment_fields = $this->get_payment_fields_js_config(); - $upe_object_name = WC_Payments_Features::is_upe_deferred_intent_enabled() ? 'wcpay_upe_config' : 'wcpayConfig'; + $upe_object_name = 'wcpay_upe_config'; wp_enqueue_script( 'wcpay-upe-checkout' ); add_action( 'wp_footer', diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index aac3cbe6036..28775dc9599 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -523,41 +523,28 @@ public static function init() { Afterpay_Payment_Method::class, Klarna_Payment_Method::class, ]; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - $payment_methods = []; - foreach ( $payment_method_classes as $payment_method_class ) { - $payment_method = new $payment_method_class( self::$token_service ); - $payment_methods[ $payment_method->get_id() ] = $payment_method; - } - foreach ( $payment_methods as $payment_method ) { - self::$upe_payment_method_map[ $payment_method->get_id() ] = $payment_method; - $split_gateway = new UPE_Split_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service ); + $payment_methods = []; + foreach ( $payment_method_classes as $payment_method_class ) { + $payment_method = new $payment_method_class( self::$token_service ); + $payment_methods[ $payment_method->get_id() ] = $payment_method; + } + foreach ( $payment_methods as $payment_method ) { + self::$upe_payment_method_map[ $payment_method->get_id() ] = $payment_method; - // Card gateway hooks are registered once below. - if ( 'card' !== $payment_method->get_id() ) { - $split_gateway->init_hooks(); - } + $split_gateway = new UPE_Split_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service ); - self::$upe_payment_gateway_map[ $payment_method->get_id() ] = $split_gateway; + // Card gateway hooks are registered once below. + if ( 'card' !== $payment_method->get_id() ) { + $split_gateway->init_hooks(); } - self::$card_gateway = self::get_payment_gateway_by_id( 'card' ); - self::$wc_payments_checkout = new WC_Payments_UPE_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - $payment_methods = []; - foreach ( $payment_method_classes as $payment_method_class ) { - $payment_method = new $payment_method_class( self::$token_service ); - $payment_methods[ $payment_method->get_id() ] = $payment_method; - } - - self::$card_gateway = new UPE_Payment_Gateway( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service ); - self::$wc_payments_checkout = new WC_Payments_UPE_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); - } else { - self::$card_gateway = self::$legacy_card_gateway; - self::$wc_payments_checkout = new WC_Payments_Checkout( self::$legacy_card_gateway, self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); + self::$upe_payment_gateway_map[ $payment_method->get_id() ] = $split_gateway; } + self::$card_gateway = self::get_payment_gateway_by_id( 'card' ); + self::$wc_payments_checkout = new WC_Payments_UPE_Checkout( self::get_gateway(), self::$woopay_util, self::$account, self::$customer_service, self::$fraud_service ); + self::$card_gateway->init_hooks(); self::$wc_payments_checkout->init_hooks(); @@ -751,49 +738,41 @@ public static function get_plugin_headers() { * @return array The list of payment gateways that will be available, including WooPayments' Gateway class. */ public static function register_gateway( $gateways ) { - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { + $payment_methods = self::$card_gateway->get_payment_method_ids_enabled_at_checkout(); - $payment_methods = self::$card_gateway->get_payment_method_ids_enabled_at_checkout(); - - $key = array_search( 'link', $payment_methods, true ); - - if ( false !== $key && WC_Payments_Features::is_woopay_enabled() ) { - unset( $payment_methods[ $key ] ); - - self::get_gateway()->update_option( 'upe_enabled_payment_method_ids', $payment_methods ); - } + $key = array_search( 'link', $payment_methods, true ); - self::$registered_card_gateway = self::$card_gateway; + if ( false !== $key && WC_Payments_Features::is_woopay_enabled() ) { + unset( $payment_methods[ $key ] ); - $gateways[] = self::$registered_card_gateway; - $all_upe_gateways = []; - $reusable_methods = []; - foreach ( $payment_methods as $payment_method_id ) { - if ( 'card' === $payment_method_id || 'link' === $payment_method_id ) { - continue; - } - $upe_gateway = self::get_payment_gateway_by_id( $payment_method_id ); - $upe_payment_method = self::get_payment_method_by_id( $payment_method_id ); - - if ( $upe_payment_method->is_reusable() ) { - $reusable_methods[] = $upe_gateway; - } + self::get_gateway()->update_option( 'upe_enabled_payment_method_ids', $payment_methods ); + } - $all_upe_gateways[] = $upe_gateway; + self::$registered_card_gateway = self::$card_gateway; + $gateways[] = self::$registered_card_gateway; + $all_upe_gateways = []; + $reusable_methods = []; + foreach ( $payment_methods as $payment_method_id ) { + if ( 'card' === $payment_method_id || 'link' === $payment_method_id ) { + continue; } + $upe_gateway = self::get_payment_gateway_by_id( $payment_method_id ); + $upe_payment_method = self::get_payment_method_by_id( $payment_method_id ); - if ( is_add_payment_method_page() ) { - return array_merge( $gateways, $reusable_methods ); + if ( $upe_payment_method->is_reusable() ) { + $reusable_methods[] = $upe_gateway; } - return array_merge( $gateways, $all_upe_gateways ); - } elseif ( WC_Payments_Features::is_upe_enabled() ) { - self::$registered_card_gateway = self::$card_gateway; - } else { - self::$registered_card_gateway = self::$legacy_card_gateway; + $all_upe_gateways[] = $upe_gateway; + + } + + if ( is_add_payment_method_page() ) { + return array_merge( $gateways, $reusable_methods ); } - return array_merge( $gateways, [ self::$registered_card_gateway ] ); + + return array_merge( $gateways, $all_upe_gateways ); } /** @@ -1343,16 +1322,8 @@ public static function get_session_service() { */ public static function register_checkout_gateway( $payment_method_registry ) { require_once __DIR__ . '/class-wc-payments-blocks-payment-method.php'; - if ( WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - require_once __DIR__ . '/class-wc-payments-upe-split-blocks-payment-method.php'; - $payment_method_registry->register( new WC_Payments_UPE_Split_Blocks_Payment_Method() ); - } elseif ( WC_Payments_Features::is_upe_legacy_enabled() ) { - require_once __DIR__ . '/class-wc-payments-upe-blocks-payment-method.php'; - $payment_method_registry->register( new WC_Payments_UPE_Blocks_Payment_Method() ); - } else { - $payment_method_registry->register( new WC_Payments_Blocks_Payment_Method() ); - } - + require_once __DIR__ . '/class-wc-payments-upe-split-blocks-payment-method.php'; + $payment_method_registry->register( new WC_Payments_UPE_Split_Blocks_Payment_Method() ); } /** diff --git a/includes/payment-methods/class-upe-payment-gateway.php b/includes/payment-methods/class-upe-payment-gateway.php index 7023ba0ca2d..a8cfbbfced1 100644 --- a/includes/payment-methods/class-upe-payment-gateway.php +++ b/includes/payment-methods/class-upe-payment-gateway.php @@ -121,19 +121,6 @@ public function __construct( $this->payment_methods = $payment_methods; } - /** - * Initializes this class's WP hooks. - * - * @return void - */ - public function init_hooks() { - // Initializing a hook within this function increases the probability of multiple calls for each split UPE gateway. Consider adding the hook in the parent hook initialization. - if ( ! is_admin() ) { - add_filter( 'woocommerce_gateway_title', [ $this, 'maybe_filter_gateway_title' ], 10, 2 ); - } - parent::init_hooks(); - } - /** * Displays HTML tags for WC payment gateway radio button. */ @@ -273,52 +260,6 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null ]; } - /** - * Handle AJAX request for creating a payment intent for Stripe UPE. - * - * @throws Process_Payment_Exception - If nonce or setup intent is invalid. - */ - public function create_payment_intent_ajax() { - try { - $is_nonce_valid = check_ajax_referer( 'wcpay_create_payment_intent_nonce', false, false ); - if ( ! $is_nonce_valid ) { - throw new Process_Payment_Exception( - __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), - 'wcpay_upe_intent_error' - ); - } - - // If paying from order, we need to get the total from the order instead of the cart. - $order_id = isset( $_POST['wcpay_order_id'] ) ? absint( $_POST['wcpay_order_id'] ) : null; - $fingerprint = isset( $_POST['wcpay-fingerprint'] ) ? wc_clean( wp_unslash( $_POST['wcpay-fingerprint'] ) ) : ''; - - $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout( $order_id, true ); - - $response = $this->create_payment_intent( $enabled_payment_methods, $order_id, $fingerprint ); - - // Encrypt client secret before exposing it to the browser. - if ( $response['client_secret'] ) { - $response['client_secret'] = WC_Payments_Utils::encrypt_client_secret( $this->account->get_stripe_account_id(), $response['client_secret'] ); - } - - if ( strpos( $response['id'], 'pi_' ) === 0 ) { // response is a payment intent (could possibly be a setup intent). - $this->add_upe_payment_intent_to_session( $response['id'], $response['client_secret'] ); - } - - wp_send_json_success( $response, 200 ); - } catch ( Exception $e ) { - // Send back error so it can be displayed to the customer. - wp_send_json_error( - [ - 'error' => [ - 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), - ], - ], - WC_Payments_Utils::get_filtered_error_status_code( $e ), - ); - } - } - /** * Creates payment intent using current cart or order and store details. * @@ -1132,58 +1073,6 @@ public function clear_upe_appearance_transient() { delete_transient( self::WC_BLOCKS_UPE_APPEARANCE_TRANSIENT ); } - /** - * Sets the title on checkout correctly before the title is displayed. - * - * @param string $title The title of the gateway being filtered. - * @param string $id The id of the gateway being filtered. - * - * @return string Filtered gateway title. - */ - public function maybe_filter_gateway_title( $title, $id ) { - if ( ! WC_Payments_Features::is_upe_deferred_intent_enabled() && self::GATEWAY_ID === $id && $this->title === $title ) { - $title = $this->checkout_title; - $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout(); - - if ( 1 === count( $enabled_payment_methods ) ) { - $title = $this->payment_methods[ $enabled_payment_methods[0] ]->get_title(); - } - - if ( 0 === count( $enabled_payment_methods ) ) { - $title = $this->payment_methods['card']->get_title(); - } - } - return $title; - } - - /** - * Sets the payment method title on the order for emails. - * - * @param WC_Order $order WC Order object. - * - * @return void - */ - public function set_payment_method_title_for_email( $order ) { - $payment_gateway = wc_get_payment_gateway_by_order( $order ); - - if ( ! empty( $payment_gateway ) && self::GATEWAY_ID !== $payment_gateway->id || ! WC_Payments_Features::is_upe_legacy_enabled() ) { - return; - } - - $payment_method_id = $this->order_service->get_payment_method_id_for_order( $order ); - - if ( ! $payment_method_id ) { - $order->set_payment_method_title( $this->title ); - $order->save(); - - return; - } - - $payment_method_details = $this->payments_api_client->get_payment_method( $payment_method_id ); - $payment_method_type = $this->get_payment_method_type_from_payment_details( $payment_method_details ); - $this->set_payment_method_title_for_order( $order, $payment_method_type, $payment_method_details ); - } - /** * Validate order_id received from the request vs value saved in the intent metadata. * Throw an exception if they're not matched. diff --git a/includes/payment-methods/class-upe-split-payment-gateway.php b/includes/payment-methods/class-upe-split-payment-gateway.php index 9fe50bcdf75..e1fe2be2cff 100644 --- a/includes/payment-methods/class-upe-split-payment-gateway.php +++ b/includes/payment-methods/class-upe-split-payment-gateway.php @@ -104,10 +104,6 @@ public function __construct( * @return void */ public function init_hooks() { - if ( ! WC_Payments_Features::is_upe_deferred_intent_enabled() ) { - add_action( "wc_ajax_wcpay_create_payment_intent_$this->stripe_id", [ $this, 'create_payment_intent_ajax' ] ); - add_action( "wc_ajax_wcpay_update_payment_intent_$this->stripe_id", [ $this, 'update_payment_intent_ajax' ] ); - } add_action( "wc_ajax_wcpay_init_setup_intent_$this->stripe_id", [ $this, 'init_setup_intent_ajax' ] ); parent::init_hooks(); @@ -229,64 +225,6 @@ public function update_payment_intent_ajax() { } } - /** - * Handle AJAX request for creating a payment intent for Stripe UPE. - * - * @throws Process_Payment_Exception - If nonce or setup intent is invalid. - */ - public function create_payment_intent_ajax() { - try { - $is_nonce_valid = check_ajax_referer( 'wcpay_create_payment_intent_nonce', false, false ); - if ( ! $is_nonce_valid ) { - throw new Process_Payment_Exception( - __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), - 'wcpay_upe_intent_error' - ); - } - - // If paying from order, we need to get the total from the order instead of the cart. - $order_id = isset( $_POST['wcpay_order_id'] ) ? absint( $_POST['wcpay_order_id'] ) : null; - $fingerprint = isset( $_POST['wcpay-fingerprint'] ) ? wc_clean( wp_unslash( $_POST['wcpay-fingerprint'] ) ) : ''; - - $enabled_payment_methods = $this->get_payment_method_ids_enabled_at_checkout( $order_id, true ); - if ( ! in_array( $this->payment_method->get_id(), $enabled_payment_methods, true ) ) { - throw new Process_Payment_Exception( - __( "We're not able to process this payment. Please refresh the page and try again.", 'woocommerce-payments' ), - 'wcpay_upe_intent_error' - ); - } - $displayed_payment_methods = [ $this->payment_method->get_id() ]; - if ( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID === $this->payment_method->get_id() ) { - if ( in_array( Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $enabled_payment_methods, true ) ) { - $displayed_payment_methods[] = Link_Payment_Method::PAYMENT_METHOD_STRIPE_ID; - } - } - - $response = $this->create_payment_intent( $displayed_payment_methods, $order_id, $fingerprint ); - - // Encrypt client secret before exposing it to the browser. - if ( $response['client_secret'] ) { - $response['client_secret'] = WC_Payments_Utils::encrypt_client_secret( $this->account->get_stripe_account_id(), $response['client_secret'] ); - } - - if ( strpos( $response['id'], 'pi_' ) === 0 ) { // response is a payment intent (could possibly be a setup intent). - $this->add_upe_payment_intent_to_session( $response['id'], $response['client_secret'] ); - } - - wp_send_json_success( $response, 200 ); - } catch ( Exception $e ) { - // Send back error so it can be displayed to the customer. - wp_send_json_error( - [ - 'error' => [ - 'message' => WC_Payments_Utils::get_filtered_error_message( $e ), - ], - ], - WC_Payments_Utils::get_filtered_error_status_code( $e ), - ); - } - } - /** * Handle AJAX request for creating a setup intent without confirmation for Stripe UPE. * diff --git a/includes/woopay/services/class-checkout-service.php b/includes/woopay/services/class-checkout-service.php index d11d3135700..f10ca7bad7b 100644 --- a/includes/woopay/services/class-checkout-service.php +++ b/includes/woopay/services/class-checkout-service.php @@ -68,7 +68,7 @@ public function is_platform_payment_method( Payment_Information $payment_informa return false; } - $should_use_stripe_platform = WC_Payments_Features::is_upe_deferred_intent_enabled() ? \WC_Payments::get_payment_gateway_by_id( $payment_information->get_payment_method_stripe_id() )->should_use_stripe_platform_on_checkout_page() : \WC_Payments::get_gateway()->should_use_stripe_platform_on_checkout_page(); + $should_use_stripe_platform = \WC_Payments::get_payment_gateway_by_id( $payment_information->get_payment_method_stripe_id() )->should_use_stripe_platform_on_checkout_page(); // Make sure the payment method being charged was created in the platform. if ( diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php index 54f42f8de84..4d42756e1ef 100644 --- a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php +++ b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php @@ -665,22 +665,6 @@ public function test_update_settings_disables_saved_cards() { $this->assertEquals( 'no', $this->gateway->get_option( 'saved_cards' ) ); } - public function test_enable_woopay_converts_upe_flag() { - update_option( WC_Payments_Features::UPE_FLAG_NAME, '1' ); - update_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME, '0' ); - update_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME, '0' ); - $this->gateway->update_option( 'platform_checkout', 'no' ); - - $request = new WP_REST_Request(); - $request->set_param( 'is_woopay_enabled', true ); - - $this->controller->update_settings( $request ); - - $this->assertEquals( '0', get_option( WC_Payments_Features::UPE_FLAG_NAME ) ); - $this->assertEquals( '0', get_option( WC_Payments_Features::UPE_SPLIT_FLAG_NAME ) ); - $this->assertEquals( '1', get_option( WC_Payments_Features::UPE_DEFERRED_INTENT_FLAG_NAME ) ); - } - public function deposit_schedules_data_provider() { return [ [ diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php index 43069a5fb24..955fc151476 100644 --- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php @@ -1363,89 +1363,6 @@ public function test_correct_payment_method_title_for_order() { } } - public function test_set_payment_method_title_for_email_updates_title() { - $mock_visa_details = [ - 'type' => 'card', - 'card' => [ - 'network' => 'visa', - 'funding' => 'debit', - ], - ]; - - $this->mock_order_service - ->expects( $this->once() ) - ->method( 'get_payment_method_id_for_order' ) - ->will( - $this->returnValue( 'pm_XXXXXXX' ) - ); - - $this->mock_api_client - ->expects( $this->once() ) - ->method( 'get_payment_method' ) - ->will( - $this->returnValue( $mock_visa_details ) - ); - - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'woocommerce_payments' ); - $order->set_payment_method_title( 'Popular Payment Methods' ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( 'Visa debit card', $order->get_payment_method_title() ); - } - - public function test_correct_payment_method_title_for_order_when_set_for_email() { - $payment_methods = [ - 'cheque' => 'Check payments', - 'cod' => 'Cash on delivery', - 'bacs' => 'Direct bank transfer', - ]; - - // Emulates order creation which sets the payment method and title. - $order = WC_Helper_Order::create_order(); - - foreach ( $payment_methods as $method => $title ) { - $order->set_payment_method( $method ); - $order->set_payment_method_title( $title ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( $title, $order->get_payment_method_title() ); - } - } - - public function test_set_payment_method_title_for_email_fallback() { - $this->mock_order_service - ->expects( $this->once() ) - ->method( 'get_payment_method_id_for_order' ) - ->will( - $this->returnValue( '' ) - ); - - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'woocommerce_payments' ); - $order->set_payment_method_title( 'Popular Payment Methods' ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( 'WooPayments', $order->get_payment_method_title() ); - } - - public function test_set_payment_method_title_for_email_only_runs_for_legacy_upe() { - update_option( '_wcpay_feature_upe', '0' ); - update_option( '_wcpay_feature_upe_split', '1' ); - - // set_payment_method_title_for_email should return before this functions runs. - $this->mock_order_service - ->expects( $this->never() ) - ->method( 'get_payment_method_id_for_order' ); - - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'woocommerce_payments' ); - $order->set_payment_method_title( 'Credit / Debit Card' ); - - $this->mock_upe_gateway->set_payment_method_title_for_email( $order ); - $this->assertEquals( 'Credit / Debit Card', $order->get_payment_method_title() ); - } - public function test_payment_methods_show_correct_default_outputs() { $mock_token = WC_Helper_Token::create_token( 'pm_mock' ); $this->mock_token_service->expects( $this->any() ) @@ -1886,68 +1803,6 @@ function( $argument ) { } } - public function test_maybe_filter_gateway_title_skips_update_due_to_enabled_split_upe() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); - - $data = [ - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => 'WooPayments', - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'WooPayments', - ]; - - $default_option = $this->mock_upe_gateway->get_option( 'upe_enabled_payment_method_ids' ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $data['methods'] ); - - WC_Helper_Site_Currency::$mock_site_currency = $data['currency']; - $this->set_get_upe_enabled_payment_method_statuses_return_value( $data['statuses'] ); - $this->assertSame( $data['expected'], $this->mock_upe_gateway->maybe_filter_gateway_title( $data['title'], $data['id'] ) ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $default_option ); - } - - public function test_maybe_filter_gateway_title_skips_update_due_to_enabled_upe_with_deferred_intent_creation() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); - - $data = [ - 'methods' => [ - 'card', - 'bancontact', - ], - 'statuses' => [ - 'card_payments' => [ - 'status' => 'active', - ], - 'bancontact_payments' => [ - 'status' => 'active', - ], - ], - 'currency' => 'EUR', - 'title' => 'WooPayments', - 'id' => UPE_Payment_Gateway::GATEWAY_ID, - 'expected' => 'WooPayments', - ]; - - $default_option = $this->mock_upe_gateway->get_option( 'upe_enabled_payment_method_ids' ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $data['methods'] ); - - WC_Helper_Site_Currency::$mock_site_currency = $data['currency']; - $this->set_get_upe_enabled_payment_method_statuses_return_value( $data['statuses'] ); - $this->assertSame( $data['expected'], $this->mock_upe_gateway->maybe_filter_gateway_title( $data['title'], $data['id'] ) ); - $this->mock_upe_gateway->update_option( 'upe_enabled_payment_method_ids', $default_option ); - } - public function test_remove_link_payment_method_if_card_disabled() { $mock_upe_gateway = $this->getMockBuilder( UPE_Payment_Gateway::class ) diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php index 1559a659dd9..eb86595cd96 100644 --- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php +++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php @@ -2360,7 +2360,6 @@ public function test_get_payment_methods_without_request_context_or_token() { * @return void */ public function test_get_payment_methods_from_gateway_id_upe() { - $this->mock_cache->method( 'get' )->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => true ] ); WC_Helper_Order::create_order(); $mock_upe_gateway = $this->getMockBuilder( UPE_Split_Payment_Gateway::class ) ->setConstructorArgs( @@ -2395,80 +2394,23 @@ public function test_get_payment_methods_from_gateway_id_upe() { ->will( $this->returnValue( [ Payment_Method::CARD, Payment_Method::LINK ] ) ); - $mock_upe_gateway->expects( $this->any() ) - ->method( 'get_payment_method_ids_enabled_at_checkout' ) - ->will( - $this->returnValue( - [ Payment_Method::CARD, Payment_Method::LINK ] - ) - ); - - $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID ); - - $this->assertSame( [ Payment_Method::CARD, Payment_Method::LINK ], $payment_methods ); $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID . '_' . Payment_Method::BANCONTACT ); - $this->assertSame( [ Payment_Method::BANCONTACT ], $payment_methods ); - WC_Payments::set_gateway( $gateway ); - } - - /** - * Test get_payment_methods_from_gateway_id function with UPE disabled. - * - * @return void - */ - public function test_get_payment_methods_from_gateway_id_non_upe() { - $this->mock_cache - ->method( 'get' ) - ->willReturn( [ 'is_deferred_intent_creation_upe_enabled' => false ] ); - - $order = WC_Helper_Order::create_order(); - $mock_upe_gateway = $this->getMockBuilder( UPE_Split_Payment_Gateway::class ) - ->setConstructorArgs( - [ - $this->mock_api_client, - $this->mock_wcpay_account, - $this->mock_customer_service, - $this->mock_token_service, - $this->mock_action_scheduler_service, - $this->mock_payment_methods[ Payment_Method::CARD ], - $this->mock_payment_methods, - $this->mock_rate_limiter, - $this->order_service, - $this->mock_dpps, - $this->mock_localization_service, - $this->mock_fraud_service, - ] - ) - ->onlyMethods( - [ - 'get_upe_enabled_payment_method_ids', - 'get_payment_method_ids_enabled_at_checkout', - ] - ) - ->getMock(); - - $gateway = WC_Payments::get_gateway(); - WC_Payments::set_gateway( $mock_upe_gateway ); $mock_upe_gateway->expects( $this->any() ) ->method( 'get_payment_method_ids_enabled_at_checkout' ) ->will( - $this->returnValueMap( - [ - [ null, true, [ Payment_Method::CARD, Payment_Method::BANCONTACT ] ], - [ $order->get_id(), true, [ Payment_Method::CARD ] ], - ] + $this->onConsecutiveCalls( + [ Payment_Method::CARD, Payment_Method::LINK ], + [ Payment_Method::CARD ] ) ); $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID ); + $this->assertSame( [ Payment_Method::CARD, Payment_Method::LINK ], $payment_methods ); - $this->assertSame( [ Payment_Method::CARD, Payment_Method::BANCONTACT ], $payment_methods ); - - $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID, $order->get_id() ); - + $payment_methods = $mock_upe_gateway->get_payment_methods_from_gateway_id( UPE_Split_Payment_Gateway::GATEWAY_ID ); $this->assertSame( [ Payment_Method::CARD ], $payment_methods ); WC_Payments::set_gateway( $gateway ); diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index c23e4159896..82ee481b01f 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -28,6 +28,9 @@ use WCPay\Internal\Service\OrderService; use WCPay\Internal\Service\PaymentProcessingService; use WCPay\Payment_Information; +use WCPay\Payment_Methods\CC_Payment_Method; +use WCPay\Payment_Methods\Sepa_Payment_Method; +use WCPay\Payment_Methods\UPE_Split_Payment_Gateway; use WCPay\WooPay\WooPay_Utilities; use WCPay\Session_Rate_Limiter; use WCPay\WC_Payments_Checkout; @@ -1427,6 +1430,215 @@ public function test_process_payment_for_order_rejects_with_cached_minimum_amoun $this->wcpay_gateway->process_payment_for_order( WC()->cart, $pi ); } + public function test_mandate_data_not_added_to_payment_intent_if_not_required() { + // Mandate data is required for SEPA and Stripe Link only, hence initializing the gateway with a CC payment method should not add mandate data. + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new CC_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $payment_method = 'woocommerce_payments_sepa_debit'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->save(); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) ); + $request->expects( $this->never() ) + ->method( 'set_mandate_data' ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + + public function test_mandate_data_added_to_payment_intent_if_required() { + // Mandate data is required for SEPA and Stripe Link, hence initializing the gateway with a SEPA payment method should add mandate data. + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new Sepa_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $payment_method = 'woocommerce_payments_sepa_debit'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->save(); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) ); + + $request->expects( $this->once() ) + ->method( 'set_mandate_data' ) + ->with( + $this->callback( + function ( $data ) { + return isset( $data['customer_acceptance']['type'] ) && + 'online' === $data['customer_acceptance']['type'] && + isset( $data['customer_acceptance']['online'] ) && + is_array( $data['customer_acceptance']['online'] ); + } + ) + ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + + public function test_mandate_data_not_added_to_setup_intent_request_when_link_is_disabled() { + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new CC_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $gateway->settings['upe_enabled_payment_method_ids'] = [ 'card' ]; + + $payment_method = 'woocommerce_payments'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 0 ); + $order->save(); + $customer = 'cus_12345'; + + $this->mock_customer_service + ->expects( $this->once() ) + ->method( 'get_customer_id_by_user_id' ) + ->will( $this->returnValue( $customer ) ); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + $pi->must_save_payment_method_to_store(); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Setup_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_setup_intention( + [ 'id' => 'seti_mock_123' ] + ) + ); + $this->mock_token_service + ->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->willReturn( new WC_Payment_Token_CC() ); + + $request->expects( $this->never() ) + ->method( 'set_mandate_data' ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + + public function test_mandate_data_added_to_setup_intent_request_when_link_is_enabled() { + $gateway = new UPE_Split_Payment_Gateway( + $this->mock_api_client, + $this->mock_wcpay_account, + $this->mock_customer_service, + $this->mock_token_service, + $this->mock_action_scheduler_service, + new CC_Payment_Method( $this->mock_token_service ), + [], + $this->mock_rate_limiter, + $this->order_service, + $this->mock_dpps, + $this->mock_localization_service, + $this->mock_fraud_service + ); + WC_Payments::set_gateway( $gateway ); + $gateway->settings['upe_enabled_payment_method_ids'] = [ 'card', 'link' ]; + + $payment_method = 'woocommerce_payments'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 0 ); + $order->save(); + $customer = 'cus_12345'; + + $this->mock_customer_service + ->expects( $this->once() ) + ->method( 'get_customer_id_by_user_id' ) + ->will( $this->returnValue( $customer ) ); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' ); + $pi->must_save_payment_method_to_store(); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Setup_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( + WC_Helper_Intention::create_setup_intention( + [ 'id' => 'seti_mock_123' ] + ) + ); + $this->mock_token_service + ->expects( $this->once() ) + ->method( 'add_payment_method_to_user' ) + ->willReturn( new WC_Payment_Token_CC() ); + + $request->expects( $this->once() ) + ->method( 'set_mandate_data' ) + ->with( + $this->callback( + function ( $data ) { + return isset( $data['customer_acceptance']['type'] ) && + 'online' === $data['customer_acceptance']['type'] && + isset( $data['customer_acceptance']['online'] ) && + is_array( $data['customer_acceptance']['online'] ); + } + ) + ); + + $gateway->process_payment_for_order( WC()->cart, $pi ); + WC_Payments::set_gateway( $this->wcpay_gateway ); + } + public function test_process_payment_for_order_cc_payment_method() { $payment_method = 'woocommerce_payments'; $expected_upe_payment_method_for_pi_creation = 'card'; diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index 4e530feaf78..5176215dc59 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -20,7 +20,6 @@ class WC_Payments_Features_Test extends WCPAY_UnitTestCase { const FLAG_OPTION_NAME_TO_FRONTEND_KEY_MAPPING = [ '_wcpay_feature_upe' => 'upe', - '_wcpay_feature_upe_split' => 'upeSplit', '_wcpay_feature_upe_settings_preview' => 'upeSettingsPreview', '_wcpay_feature_customer_multi_currency' => 'multiCurrency', '_wcpay_feature_documents' => 'documents', @@ -243,19 +242,6 @@ function ( $pre_option, $option, $default ) { $this->assertFalse( WC_Payments_Features::is_woopay_express_checkout_enabled() ); } - public function test_deferred_upe_enabled_with_sepa() { - $this->mock_cache->method( 'get' )->willReturn( - [ - 'capabilities' => [ 'sepa_debit_payments' => 'active' ], - 'is_deferred_intent_creation_upe_enabled' => true, - ] - ); - - $this->assertTrue( WC_Payments_Features::is_upe_enabled() ); - $this->assertFalse( WC_Payments_Features::is_upe_legacy_enabled() ); - $this->assertTrue( WC_Payments_Features::is_upe_deferred_intent_enabled() ); - } - public function test_is_wcpay_frt_review_feature_active_returns_true() { add_filter( 'pre_option_wcpay_frt_review_feature_active', diff --git a/tests/unit/test-class-wc-payments-token-service.php b/tests/unit/test-class-wc-payments-token-service.php index 1d6f0b29be6..a5c24e5fb03 100644 --- a/tests/unit/test-class-wc-payments-token-service.php +++ b/tests/unit/test-class-wc-payments-token-service.php @@ -645,6 +645,26 @@ public function test_woocommerce_get_customer_payment_tokens_payment_methods_onl $this->token_service->woocommerce_get_customer_payment_tokens( $tokens, 1, $gateway_id ); } + /** + * @dataProvider valid_and_invalid_payment_methods_for_comparison_provider + */ + public function test_is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id, $expected_result ) { + $this->assertEquals( + $expected_result, + $this->token_service->is_valid_payment_method_type_for_gateway( $payment_method_type, $gateway_id ) + ); + } + + public function valid_and_invalid_payment_methods_for_comparison_provider() { + return [ + [ 'card', 'woocommerce_payments', true ], + [ 'sepa_debit', 'woocommerce_payments_sepa_debit', true ], + [ 'link', 'woocommerce_payments', true ], + [ 'card', 'card', false ], + [ 'card', 'woocommerce_payments_bancontact', false ], + ]; + } + private function generate_card_pm_response( $stripe_id ) { return [ 'type' => Payment_Method::CARD, From 09f8ca184d879efbaeaa2222714bec6f2fd18f1c Mon Sep 17 00:00:00 2001 From: Hsing-yu Flowers Date: Mon, 27 Nov 2023 17:52:55 -0500 Subject: [PATCH 45/61] Fix pay for order flow missing user session email (#7709) --- changelog/fix-pay-for-order-flow-missing-user-session-email | 4 ++++ ...ss-wc-payments-express-checkout-button-display-handler.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-pay-for-order-flow-missing-user-session-email diff --git a/changelog/fix-pay-for-order-flow-missing-user-session-email b/changelog/fix-pay-for-order-flow-missing-user-session-email new file mode 100644 index 00000000000..3a079a2f1e3 --- /dev/null +++ b/changelog/fix-pay-for-order-flow-missing-user-session-email @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Check that the email is set in the post global diff --git a/includes/class-wc-payments-express-checkout-button-display-handler.php b/includes/class-wc-payments-express-checkout-button-display-handler.php index cdc04b3fd69..ad8db375684 100644 --- a/includes/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/class-wc-payments-express-checkout-button-display-handler.php @@ -151,7 +151,7 @@ function( $js_config ) use ( $order ) { // Silence the filter_input warning because we are sanitizing the input with sanitize_email(). // nosemgrep: audit.php.lang.misc.filter-input-no-filter - $user_email = sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) ?? $session_email; + $user_email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( filter_input( INPUT_POST, 'email' ) ) ) : $session_email; $js_config['order_id'] = $order->get_id(); $js_config['pay_for_order'] = sanitize_text_field( wp_unslash( $_GET['pay_for_order'] ) ); From 69e52443a95eef5f9812d447992c7597dae42526 Mon Sep 17 00:00:00 2001 From: Anurag Bhandari Date: Tue, 28 Nov 2023 13:54:12 +0530 Subject: [PATCH 46/61] Upgrade the csv-export JS package to the latest version (#7729) --- .../update-7307-csv-export-package-version | 4 + docs/dependencies.md | 1 + package-lock.json | 74 +++++++++++++++++-- package.json | 2 +- 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 changelog/update-7307-csv-export-package-version diff --git a/changelog/update-7307-csv-export-package-version b/changelog/update-7307-csv-export-package-version new file mode 100644 index 00000000000..c1a26542d69 --- /dev/null +++ b/changelog/update-7307-csv-export-package-version @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Upgrade the csv-export JS package to the latest version. diff --git a/docs/dependencies.md b/docs/dependencies.md index 832feac7900..4b6357367df 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -69,6 +69,7 @@ to catalog our packages and provide guidance to a developer who wants to test an | [@types/react](https://www.npmjs.com/package/@types/react) | Contains type definitions for React. | JS unit tests are passing. | You should pick a version `x.y.z` with `x.y` similar to the version defined for React. | | [@woocommerce/components](https://www.npmjs.com/package/@woocommerce/components) | A library of components that can be used to create pages in the WooCommerce dashboard and reports pages. | JS unit tests are passing. | | | [@wordpress/components](https://www.npmjs.com/package/@wordpress/components) | A library of generic WordPress components to be used for creating common UI elements. | JS unit tests are passing and UI isn't affected at places of usage. | This package is one of the few `@wordpress/x` packages that doesn't come from WordPress directly, because we decided to bundle it ourselves (see our [dependency extractor webpack config](https://github.com/Automattic/woocommerce-payments/blob/b7634468560d905a479d50066233f807da62413f/webpack/shared.js#L132-L150)). | +| [@woocommerce/csv-export](https://www.npmjs.com/package/@woocommerce/csv-export) | A set of functions to convert data into CSV values, and enable a browser download of the CSV data. We use it to export reports for Transactions, Deposits, and Disputes. | JS unit tests are passing. | | ### PHP Runtime Dependencies | Package Name | Usage Summary | Testing | Notes | diff --git a/package-lock.json b/package-lock.json index 508c0e6ac4f..10e611e1e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "@typescript-eslint/parser": "4.15.2", "@woocommerce/api": "0.2.0", "@woocommerce/components": "12.0.0", - "@woocommerce/csv-export": "1.7.0", + "@woocommerce/csv-export": "1.8.0", "@woocommerce/currency": "4.2.0", "@woocommerce/date": "4.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", @@ -9881,6 +9881,16 @@ "@types/wordpress__rich-text": "*" } }, + "node_modules/@woocommerce/components/node_modules/@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "dependencies": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "node_modules/@woocommerce/components/node_modules/@wordpress/api-fetch": { "version": "6.30.0", "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.30.0.tgz", @@ -10380,15 +10390,26 @@ } }, "node_modules/@woocommerce/csv-export": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", - "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.8.0.tgz", + "integrity": "sha512-LfbwPWu1fyN3lNcuigiJ7g86Ute7rRQokbRkUP5m48wCrgxWmulmbXJD19642oXNLM1G8JY1UM0VFNIn7qMI3w==", "dev": true, "dependencies": { + "@types/node": "^16.18.18", "browser-filesaver": "^1.1.1", "moment": "^2.29.1" + }, + "engines": { + "node": "^16.14.1", + "pnpm": "^8.6.7" } }, + "node_modules/@woocommerce/csv-export/node_modules/@types/node": { + "version": "16.18.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz", + "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==", + "dev": true + }, "node_modules/@woocommerce/currency": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@woocommerce/currency/-/currency-4.2.0.tgz", @@ -11266,6 +11287,16 @@ "react-dom": "^17.0.0" } }, + "node_modules/@woocommerce/experimental/node_modules/@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "dependencies": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "node_modules/@woocommerce/experimental/node_modules/core-js": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", @@ -51349,6 +51380,16 @@ "@types/wordpress__rich-text": "*" } }, + "@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "requires": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "@wordpress/api-fetch": { "version": "6.30.0", "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.30.0.tgz", @@ -51768,13 +51809,22 @@ } }, "@woocommerce/csv-export": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", - "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.8.0.tgz", + "integrity": "sha512-LfbwPWu1fyN3lNcuigiJ7g86Ute7rRQokbRkUP5m48wCrgxWmulmbXJD19642oXNLM1G8JY1UM0VFNIn7qMI3w==", "dev": true, "requires": { + "@types/node": "^16.18.18", "browser-filesaver": "^1.1.1", "moment": "^2.29.1" + }, + "dependencies": { + "@types/node": { + "version": "16.18.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz", + "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==", + "dev": true + } } }, "@woocommerce/currency": { @@ -52497,6 +52547,16 @@ "react-transition-group": "^4.4.2" } }, + "@woocommerce/csv-export": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@woocommerce/csv-export/-/csv-export-1.7.0.tgz", + "integrity": "sha512-mBEYSc3WOibJ2thDE5KpQ8r7oD9ul6FHHDzm+N/ToqcJVdhjHOKqiR+9nrDOmpI9ut8jcD4uwZV8egzyP3xOuQ==", + "dev": true, + "requires": { + "browser-filesaver": "^1.1.1", + "moment": "^2.29.1" + } + }, "core-js": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", diff --git a/package.json b/package.json index 743122e5a15..5383d05252f 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@typescript-eslint/parser": "4.15.2", "@woocommerce/api": "0.2.0", "@woocommerce/components": "12.0.0", - "@woocommerce/csv-export": "1.7.0", + "@woocommerce/csv-export": "1.8.0", "@woocommerce/currency": "4.2.0", "@woocommerce/date": "4.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", From fa84c9ec4482c599c824eb1853f390281a8c03fe Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Wed, 29 Nov 2023 10:34:14 +1000 Subject: [PATCH 47/61] Deposit details - for instant deposits, replace the transaction list with a message (#7784) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> --- ...71-remove-instant-deposit-transaction-list | 4 + client/data/deposits/hooks.ts | 8 +- client/deposits/details/index.tsx | 98 ++++++++++++++----- client/deposits/details/style.scss | 6 ++ client/deposits/details/test/index.tsx | 21 +--- 5 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 changelog/fix-7771-remove-instant-deposit-transaction-list diff --git a/changelog/fix-7771-remove-instant-deposit-transaction-list b/changelog/fix-7771-remove-instant-deposit-transaction-list new file mode 100644 index 00000000000..0ad1152b15f --- /dev/null +++ b/changelog/fix-7771-remove-instant-deposit-transaction-list @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Replace the deposit overview transactions list with a "transaction history is unavailable for instant deposits" message. diff --git a/client/data/deposits/hooks.ts b/client/data/deposits/hooks.ts index 577cc7ef04d..bcb4878b06f 100644 --- a/client/data/deposits/hooks.ts +++ b/client/data/deposits/hooks.ts @@ -24,11 +24,15 @@ export const useDeposit = ( ): { deposit: CachedDeposit; isLoading: boolean } => useSelect( ( select ) => { - const { getDeposit, isResolving } = select( STORE_NAME ); + const { getDeposit, isResolving, hasFinishedResolution } = select( + STORE_NAME + ); return { deposit: getDeposit( id ), - isLoading: isResolving( 'getDeposit', [ id ] ), + isLoading: + ! hasFinishedResolution( 'getDeposit', [ id ] ) || + isResolving( 'getDeposit', [ id ] ), }; }, [ id ] diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 84968f1207c..7396aea2ff9 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -7,12 +7,21 @@ import React from 'react'; import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; import moment from 'moment'; -import { Card } from '@wordpress/components'; +import { + Card, + CardBody, + CardHeader, + ExternalLink, + // @ts-expect-error: Suppressing Module '"@wordpress/components"' has no exported member '__experimentalText'. + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- used by TableCard component which we replicate here. + __experimentalText as Text, +} from '@wordpress/components'; import { SummaryListPlaceholder, SummaryList, OrderStatus, } from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; import classNames from 'classnames'; /** @@ -65,15 +74,13 @@ const SummaryItem = ( { ); -export const DepositOverview = ( { - depositId, -}: { - depositId: string; -} ): JSX.Element => { - const { deposit = {} as CachedDeposit, isLoading } = useDeposit( - depositId - ); +interface DepositOverviewProps { + deposit: CachedDeposit; +} +export const DepositOverview: React.FC< DepositOverviewProps > = ( { + deposit, +} ) => { const depositDateLabel = deposit.automatic ? __( 'Deposit date', 'woocommerce-payments' ) : __( 'Instant deposit date', 'woocommerce-payments' ); @@ -94,7 +101,6 @@ export const DepositOverview = ( { /> ); - if ( isLoading ) return ; return (
{ deposit.automatic ? ( @@ -160,20 +166,64 @@ export const DepositOverview = ( { ); }; -export const DepositDetails = ( { +interface DepositDetailsProps { + query: { + id: string; + }; +} + +export const DepositDetails: React.FC< DepositDetailsProps > = ( { query: { id: depositId }, -}: { - query: { id: string }; -} ): JSX.Element => ( - - - - - - - - - -); +} ) => { + const { deposit, isLoading } = useDeposit( depositId ); + + const isInstantDeposit = ! isLoading && deposit && ! deposit.automatic; + + return ( + + + + { isLoading ? ( + + ) : ( + + ) } + + + + { isInstantDeposit ? ( + // If instant deposit, show a message instead of the transactions list. + // Matching the components used in @woocommerce/components TableCard for consistent UI. + + + + { __( + 'Deposit transactions', + 'woocommerce-payments' + ) } + + + + { interpolateComponents( { + /* Translators: {{learnMoreLink}} is a link element (). */ + mixedString: __( + `We're unable to show transaction history on instant deposits. {{learnMoreLink}}Learn more{{/learnMoreLink}}`, + 'woocommerce-payments' + ), + components: { + learnMoreLink: ( + + ), + }, + } ) } + + + ) : ( + + ) } + + + ); +}; export default DepositDetails; diff --git a/client/deposits/details/style.scss b/client/deposits/details/style.scss index 8f6fc09dda4..01e91c29955 100644 --- a/client/deposits/details/style.scss +++ b/client/deposits/details/style.scss @@ -63,4 +63,10 @@ line-height: 82px; } } + + &--instant { + &__transactions-list-message.components-card__body { + padding: 24px; + } + } } diff --git a/client/deposits/details/test/index.tsx b/client/deposits/details/test/index.tsx index cd1bd3abc75..bb50a9cd442 100644 --- a/client/deposits/details/test/index.tsx +++ b/client/deposits/details/test/index.tsx @@ -10,15 +10,8 @@ import React from 'react'; * Internal dependencies */ import { DepositOverview } from '../'; -import { useDeposit } from 'wcpay/data'; import { CachedDeposit } from 'wcpay/types/deposits'; -jest.mock( 'wcpay/data', () => ( { - useDeposit: jest.fn(), -} ) ); - -const mockUseDeposit = useDeposit as jest.MockedFunction< typeof useDeposit >; - const mockDeposit = { id: 'po_mock', date: '2020-01-02 17:46:02', @@ -63,25 +56,15 @@ describe( 'Deposit overview', () => { } ); test( 'renders automatic deposit correctly', () => { - mockUseDeposit.mockReturnValue( { - deposit: mockDeposit, - isLoading: false, - } ); - const { container: overview } = render( - + ); expect( overview ).toMatchSnapshot(); } ); test( 'renders instant deposit correctly', () => { - mockUseDeposit.mockReturnValue( { - deposit: { ...mockDeposit, automatic: false }, - isLoading: false, - } ); - const { container: overview } = render( - + ); expect( overview ).toMatchSnapshot(); } ); From 0cc48d5dd4863e54538c2b130d60911fc53f3ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Mart=C3=ADn=20Alabarce?= Date: Wed, 29 Nov 2023 09:27:35 +0100 Subject: [PATCH 48/61] Use supported countries instead of business types for PO (#7798) --- .../update-6841-supported-countries-fallback | 5 +++ client/onboarding/steps/business-details.tsx | 11 ++++-- .../steps/test/business-details.tsx | 36 +++++++++++++++++-- client/onboarding/utils.ts | 5 +++ 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 changelog/update-6841-supported-countries-fallback diff --git a/changelog/update-6841-supported-countries-fallback b/changelog/update-6841-supported-countries-fallback new file mode 100644 index 00000000000..ca093be719c --- /dev/null +++ b/changelog/update-6841-supported-countries-fallback @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Update PO onboarding country source to use available countries. + + diff --git a/client/onboarding/steps/business-details.tsx b/client/onboarding/steps/business-details.tsx index d7ffb3af0bb..8a9a3b762bb 100644 --- a/client/onboarding/steps/business-details.tsx +++ b/client/onboarding/steps/business-details.tsx @@ -14,14 +14,19 @@ import { OnboardingSelectField, OnboardingTextField, } from '../form'; -import { getBusinessTypes, getMccsFlatList } from 'onboarding/utils'; +import { + getAvailableCountries, + getBusinessTypes, + getMccsFlatList, +} from 'onboarding/utils'; import { BusinessType } from 'onboarding/types'; const BusinessDetails: React.FC = () => { const { data, setData } = useOnboardingContext(); - const countries = getBusinessTypes(); + const countries = getAvailableCountries(); + const businessTypes = getBusinessTypes(); - const selectedCountry = countries.find( + const selectedCountry = businessTypes.find( ( country ) => country.key === data.country ); const selectedBusinessType = selectedCountry?.types.find( diff --git a/client/onboarding/steps/test/business-details.tsx b/client/onboarding/steps/test/business-details.tsx index 15987f7f3e5..66e889f60c7 100644 --- a/client/onboarding/steps/test/business-details.tsx +++ b/client/onboarding/steps/test/business-details.tsx @@ -12,14 +12,39 @@ import { mocked } from 'ts-jest/utils'; import BusinessDetails from '../business-details'; import { OnboardingContextProvider } from '../../context'; import strings from '../../strings'; -import { getBusinessTypes, getMccsFlatList } from 'onboarding/utils'; +import { + getAvailableCountries, + getBusinessTypes, + getMccsFlatList, +} from 'onboarding/utils'; jest.mock( 'onboarding/utils', () => ( { + getAvailableCountries: jest.fn(), getBusinessTypes: jest.fn(), getMccsFlatList: jest.fn(), } ) ); const countries = [ + { + key: 'ES', + name: 'Spain', + types: [], + }, + { + key: 'US', + name: 'United States', + types: [], + }, + { + key: 'FR', + name: 'France', + types: [], + }, +]; + +mocked( getAvailableCountries ).mockReturnValue( countries ); + +const businessTypes = [ { key: 'US', name: 'United States', @@ -73,7 +98,7 @@ const countries = [ }, ]; -mocked( getBusinessTypes ).mockReturnValue( countries ); +mocked( getBusinessTypes ).mockReturnValue( businessTypes ); const mccsFlatList = [ { @@ -169,6 +194,13 @@ describe( 'BusinessDetails', () => { screen.queryByText( strings.placeholders[ 'company.structure' ] ) ).not.toBeInTheDocument(); + user.click( countryField ); + user.click( screen.getByText( 'Spain' ) ); + + expect( + screen.queryByText( strings.placeholders.business_type ) + ).not.toBeInTheDocument(); + user.click( countryField ); user.click( screen.getByText( 'United States' ) ); diff --git a/client/onboarding/utils.ts b/client/onboarding/utils.ts index c120232948a..4e5f392fb59 100644 --- a/client/onboarding/utils.ts +++ b/client/onboarding/utils.ts @@ -19,6 +19,11 @@ export const fromDotNotation = ( return value != null ? set( result, key, value ) : result; }, {} ); +export const getAvailableCountries = (): Country[] => + Object.entries( wcpaySettings?.connect.availableCountries || [] ) + .map( ( [ key, name ] ) => ( { key, name, types: [] } ) ) + .sort( ( a, b ) => a.name.localeCompare( b.name ) ); + export const getBusinessTypes = (): Country[] => { const data = wcpaySettings?.onboardingFieldsData?.business_types; From c0a286f53006015bd4f1a00a2ac2b63469d427c9 Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:42:25 -0600 Subject: [PATCH 49/61] Fix WooPay button location Tracks (#7765) --- changelog/fix-woopay-btn-location-updates | 4 ++++ includes/class-wc-payments-woopay-button-handler.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-woopay-btn-location-updates diff --git a/changelog/fix-woopay-btn-location-updates b/changelog/fix-woopay-btn-location-updates new file mode 100644 index 00000000000..714e061439f --- /dev/null +++ b/changelog/fix-woopay-btn-location-updates @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix a bug in WooPay button update Tracks diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index 18367161543..f3467f9cc8d 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -118,7 +118,7 @@ public function init() { $this->gateway->update_option( 'platform_checkout_button_locations', array_keys( $all_locations ) ); - WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, $all_locations ); + WC_Payments::woopay_tracker()->woopay_locations_updated( $all_locations, array_keys( $all_locations ) ); } add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] ); From 755c263d903f2a52c7e6cef34846fb34af4b279d Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Wed, 29 Nov 2023 20:27:09 +0000 Subject: [PATCH 50/61] Improve Dev Mode Indicators (#7768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Quiñones --- changelog/dev-7498-improve-dev-mode | 4 + client/capital/index.tsx | 6 +- client/components/banner-notice/index.tsx | 4 + client/components/banner-notice/style.scss | 2 +- client/components/test-mode-notice/index.js | 181 ------------- client/components/test-mode-notice/index.tsx | 172 ++++++++++++ .../test/__snapshots__/index.js.snap | 254 ------------------ .../test/__snapshots__/index.tsx.snap | 160 +++++++++++ .../components/test-mode-notice/test/index.js | 125 --------- .../test-mode-notice/test/index.tsx | 78 ++++++ client/deposits/details/index.tsx | 6 +- client/deposits/{index.js => index.tsx} | 7 +- client/disputes/evidence/index.js | 7 +- client/disputes/index.tsx | 4 +- client/documents/index.tsx | 4 +- client/overview/index.js | 59 ++-- .../modal/setup-live-payments/index.tsx | 73 +++++ .../modal/setup-live-payments/style.scss | 43 +++ .../setup-live-payments/test/index.test.tsx | 57 ++++ client/overview/setup-real-payments.tsx | 127 --------- client/overview/strings.tsx | 45 ++++ client/overview/style.scss | 44 --- client/overview/test/index.js | 26 -- .../test/setup-real-payments.test.tsx | 76 ------ .../payment-details/payment-details/index.tsx | 9 +- client/payment-details/readers/index.js | 10 +- client/settings/general-settings/index.js | 174 +++++++----- client/transactions/index.tsx | 11 +- includes/class-wc-payments-account.php | 11 +- .../class-wc-payments-api-client.php | 6 +- 30 files changed, 830 insertions(+), 955 deletions(-) create mode 100644 changelog/dev-7498-improve-dev-mode delete mode 100644 client/components/test-mode-notice/index.js create mode 100644 client/components/test-mode-notice/index.tsx delete mode 100644 client/components/test-mode-notice/test/__snapshots__/index.js.snap create mode 100644 client/components/test-mode-notice/test/__snapshots__/index.tsx.snap delete mode 100644 client/components/test-mode-notice/test/index.js create mode 100644 client/components/test-mode-notice/test/index.tsx rename client/deposits/{index.js => index.tsx} (58%) create mode 100644 client/overview/modal/setup-live-payments/index.tsx create mode 100644 client/overview/modal/setup-live-payments/style.scss create mode 100644 client/overview/modal/setup-live-payments/test/index.test.tsx delete mode 100644 client/overview/setup-real-payments.tsx create mode 100644 client/overview/strings.tsx delete mode 100644 client/overview/test/setup-real-payments.test.tsx diff --git a/changelog/dev-7498-improve-dev-mode b/changelog/dev-7498-improve-dev-mode new file mode 100644 index 00000000000..bd1bb4ac9f8 --- /dev/null +++ b/changelog/dev-7498-improve-dev-mode @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Improvements to the dev mode and test mode indicators. diff --git a/client/capital/index.tsx b/client/capital/index.tsx index a53b18b02ef..8d1ab3f4741 100644 --- a/client/capital/index.tsx +++ b/client/capital/index.tsx @@ -12,7 +12,7 @@ import { dateI18n } from '@wordpress/date'; * Internal dependencies. */ import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import ErrorBoundary from 'components/error-boundary'; import ActiveLoanSummary from 'components/active-loan-summary'; import { formatExplicitCurrency, isZeroDecimalCurrency } from 'utils/currency'; @@ -21,7 +21,6 @@ import ClickableCell from 'components/clickable-cell'; import Chip from 'components/chip'; import { useLoans } from 'wcpay/data'; import { getAdminUrl } from 'wcpay/utils'; - import './style.scss'; const columns = [ @@ -205,7 +204,8 @@ const CapitalPage = (): JSX.Element => { return ( - + + { wcpaySettings.accountLoans.has_active_loan && ( diff --git a/client/components/banner-notice/index.tsx b/client/components/banner-notice/index.tsx index dcba0e665fb..b7a7c4658f3 100644 --- a/client/components/banner-notice/index.tsx +++ b/client/components/banner-notice/index.tsx @@ -85,6 +85,7 @@ interface Props { * - `label`: `string` containing the text of the button/link * - `url`: `string` OR `onClick`: `( event: SyntheticEvent ) => void` to specify * what the action does. + * - `urlTarget`: `string` (optional) to specify the target attribute of the link. * - `className`: `string` (optional) to add custom classes to the button styles. * - `variant`: `'primary' | 'secondary' | 'link'` (optional) You can denote a * primary button action for a notice by passing a value of `primary`. @@ -101,6 +102,7 @@ interface Props { className?: string; variant?: Button.Props[ 'variant' ]; url?: string; + urlTarget?: string; onClick?: React.MouseEventHandler< HTMLAnchorElement >; } >; /** @@ -152,6 +154,7 @@ const BannerNotice: React.FC< Props > = ( { variant, onClick, url, + urlTarget, }, index ) => { @@ -169,6 +172,7 @@ const BannerNotice: React.FC< Props > = ( { variant={ computedVariant } onClick={ url ? undefined : onClick } className={ buttonCustomClasses } + target={ urlTarget } > { label } diff --git a/client/components/banner-notice/style.scss b/client/components/banner-notice/style.scss index 4ab24170454..2348891d528 100644 --- a/client/components/banner-notice/style.scss +++ b/client/components/banner-notice/style.scss @@ -41,7 +41,7 @@ &__actions { display: grid; grid-auto-flow: column; - grid-auto-columns: min-content; + grid-auto-columns: max-content; column-gap: $gap-small; margin-top: $gap-small; } diff --git a/client/components/test-mode-notice/index.js b/client/components/test-mode-notice/index.js deleted file mode 100644 index bdc028c741f..00000000000 --- a/client/components/test-mode-notice/index.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * External dependencies - */ -import { __, _n, sprintf } from '@wordpress/i18n'; -import { Notice } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { isInTestMode, getPaymentSettingsUrl } from 'utils'; - -// The topics (i.e. pages) that have test mode notices. -export const topics = { - overview: sprintf( - /* translators: %s: WooPayments */ - __( '%s is in test mode.', 'woocommerce-payments' ), - 'WooPayments' - ), - transactions: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test transactions. To view live transactions, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - paymentDetails: __( 'Test payment:', 'woocommerce-payments' ), - deposits: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test deposits. To view live deposits, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - depositDetails: __( 'Test deposit:', 'woocommerce-payments' ), - disputes: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test disputes. To view live disputes, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - disputeDetails: __( 'Test dispute:', 'woocommerce-payments' ), - documents: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test documents. To view live documents, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - documentDetails: __( 'Test document:', 'woocommerce-payments' ), - loans: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test loans. To view live loans, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - authorizations: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test authorizations. To view live authorizations, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - riskReviewTransactions: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test on review transactions. To view live on review transactions, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), - blockedTransactions: sprintf( - /* translators: %s: WooPayments */ - __( - 'Viewing test blocked transactions. To view live blocked transactions, disable test mode in %s settings.', - 'woocommerce-payments' - ), - 'WooPayments' - ), -}; - -// These are all the topics used for details pages where the notice is slightly different. -const detailsTopics = [ - topics.paymentDetails, - topics.disputeDetails, - topics.depositDetails, - topics.documentDetails, -]; - -/** - * Returns an tag with the href attribute set to the Payments settings - * page, and the provided text. - * - * @param {string} topic The notice message topic. - * - * @return {*} An HTML component with a link to wcpay settings page. - */ -export const getPaymentsSettingsUrlComponent = () => { - return ( - - { sprintf( - /* translators: %s: WooPayments */ - __( 'View %s settings', 'woocommerce-payments' ), - 'WooPayments' - ) } - - ); -}; - -/** - * Returns notice details depending on the topic provided. - * - * @param {string} topic The notice message topic. - * - * @return {string} The specific details the notice is supposed to contain. - */ -export const getTopicDetails = ( topic ) => { - return sprintf( - /* translators: %s: WooPayments */ - _n( - '%s was in test mode when this order was placed.', - '%s was in test mode when these orders were placed.', - topics.depositDetails === topic ? 2 : 1, - 'woocommerce-payments' - ), - 'WooPayments' - ); -}; - -/** - * Returns the correct notice message wrapped in a span for a given topic. - * - * The message is wrapped in a span to make it easier to apply styling to - * different parts of the text, i.e. to include multiple HTML elements. - * - * @param {string} topic The notice message topic. - * - * @return {string} The correct notice message. - */ -export const getNoticeMessage = ( topic ) => { - const { detailsSubmitted } = wcpaySettings.accountStatus; - const urlComponent = detailsSubmitted - ? getPaymentsSettingsUrlComponent() - : ''; - - if ( detailsTopics.includes( topic ) ) { - return ( - - { topic } { getTopicDetails( topic ) } { urlComponent } - - ); - } - - return ( - - { topic } { urlComponent } - - ); -}; - -export const TestModeNotice = ( { topic } ) => { - if ( isInTestMode() ) { - return ( - - { getNoticeMessage( topic ) } - - ); - } - return <>; -}; diff --git a/client/components/test-mode-notice/index.tsx b/client/components/test-mode-notice/index.tsx new file mode 100644 index 00000000000..80310707a46 --- /dev/null +++ b/client/components/test-mode-notice/index.tsx @@ -0,0 +1,172 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __, _n, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { getPaymentSettingsUrl, isInTestMode } from 'utils'; +import BannerNotice from '../banner-notice'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link } from '@woocommerce/components'; + +type CurrentPage = + | 'overview' + | 'documents' + | 'deposits' + | 'disputes' + | 'loans' + | 'payments' + | 'transactions'; + +interface Props { + currentPage: CurrentPage; + actions?: React.ComponentProps< typeof BannerNotice >[ 'actions' ]; + isDetailsView?: boolean; + isDevMode?: boolean; +} + +const nounToUse = { + documents: __( 'document', 'woocommerce-payments' ), + deposits: __( 'deposit', 'woocommerce-payments' ), + disputes: __( 'dispute', 'woocommerce-payments' ), + loans: __( 'loan', 'woocommerce-payments' ), + payments: __( 'order', 'woocommerce-payments' ), + transactions: __( 'order', 'woocommerce-payments' ), +}; + +const verbToUse = { + documents: __( 'created', 'woocommerce-payments' ), + deposits: __( 'created', 'woocommerce-payments' ), + disputes: __( 'created', 'woocommerce-payments' ), + loans: __( 'created', 'woocommerce-payments' ), + payments: __( 'placed', 'woocommerce-payments' ), + transactions: __( 'placed', 'woocommerce-payments' ), +}; + +const getNoticeContent = ( + currentPage: CurrentPage, + isDetailsView: boolean, + isDevMode: boolean +): JSX.Element => { + switch ( currentPage ) { + case 'overview': + return isDevMode ? ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{strong}}%1$s is in dev mode.{{/strong}} You need to set up a live %1$s account before you can accept real transactions.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + strong: , + }, + } ) } + + ) : ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{strong}}%1$s is in test mode.{{/strong}} All transactions will be simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + strong: , + learnMoreLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ); + case 'documents': + case 'deposits': + case 'disputes': + case 'payments': + case 'loans': + case 'transactions': + return isDetailsView ? ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + _n( + '%1$s was in test mode when this %2$s was %3$s. To view live %2$ss, disable test mode in {{settingsLink}}%1$s settings{{/settingsLink}}.', + '%1$s was in test mode when these %2$ss were %3$s. To view live %2$ss, disable test mode in {{settingsLink}}%1$s settings{{/settingsLink}}.', + 'deposits' === currentPage ? 2 : 1, + 'woocommerce-payments' + ), + 'WooPayments', + nounToUse[ currentPage ], + verbToUse[ currentPage ] + ), + components: { + settingsLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ) : ( + <> + { interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + 'Viewing test %1$s. To view live %1s, disable test mode in {{settingsLink}}%2s settings{{/settingsLink}}.', + 'woocommerce-payments' + ), + currentPage, + 'WooPayments' + ), + components: { + settingsLink: ( + // Link content is in the format string above. Consider disabling jsx-a11y/anchor-has-content. + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ); + } +}; + +export const TestModeNotice: React.FC< Props > = ( { + currentPage, + actions, + isDetailsView = false, + isDevMode = false, +} ) => { + if ( ! isInTestMode() ) return null; + + return ( + + { getNoticeContent( currentPage, isDetailsView, isDevMode ) } + + ); +}; diff --git a/client/components/test-mode-notice/test/__snapshots__/index.js.snap b/client/components/test-mode-notice/test/__snapshots__/index.js.snap deleted file mode 100644 index a2539f2bee4..00000000000 --- a/client/components/test-mode-notice/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,254 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Test mode notification Component is rendered correctly 1`] = ` -
- -`; - -exports[`Test mode notification Component is rendered correctly 2`] = ` -
-
-
- - Viewing test deposits. To view live deposits, disable test mode in WooPayments settings. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 3`] = ` -
-
-
- - Viewing test disputes. To view live disputes, disable test mode in WooPayments settings. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 4`] = ` -
-
-
- - Viewing test documents. To view live documents, disable test mode in WooPayments settings. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 5`] = ` -
-
-
- - - Test deposit: - - - WooPayments was in test mode when these orders were placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 6`] = ` -
-
-
- - - Test dispute: - - - WooPayments was in test mode when this order was placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 7`] = ` -
-
-
- - - Test payment: - - - WooPayments was in test mode when this order was placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 8`] = ` -
-
-
- - - Test document: - - - WooPayments was in test mode when this order was placed. - - - View WooPayments settings - - -
-
-
-
-`; - -exports[`Test mode notification Component is rendered correctly 9`] = `
`; - -exports[`Test mode notification Component is rendered correctly 10`] = `
`; - -exports[`Test mode notification Component is rendered correctly 11`] = `
`; - -exports[`Test mode notification Component is rendered correctly 12`] = `
`; - -exports[`Test mode notification Component is rendered correctly 13`] = `
`; - -exports[`Test mode notification Component is rendered correctly 14`] = `
`; - -exports[`Test mode notification Component is rendered correctly 15`] = `
`; - -exports[`Test mode notification Component is rendered correctly 16`] = `
`; - -exports[`Test mode notification Returns right notice message without URL component 1`] = ` -
-
-
- - WooPayments is in test mode. - - - -
-
-
-
-`; diff --git a/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap b/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap new file mode 100644 index 00000000000..6005f079030 --- /dev/null +++ b/client/components/test-mode-notice/test/__snapshots__/index.tsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test mode notification Returns empty div if not in test mode 1`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 2`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 3`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 4`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 5`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 6`] = `
`; + +exports[`Test mode notification Returns empty div if not in test mode 7`] = `
`; + +exports[`Test mode notification Returns valid component for deposits page 1`] = ` +
+
+
+ Viewing test deposits. To view live deposits, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for disputes page 1`] = ` +
+
+
+ Viewing test disputes. To view live disputes, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for documents page 1`] = ` +
+
+
+ Viewing test documents. To view live documents, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for loans page 1`] = ` +
+
+
+ Viewing test loans. To view live loans, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for overview page 1`] = ` +
+
+
+ + WooPayments is in test mode. + + All transactions will be simulated. + + Learn more + +
+
+
+`; + +exports[`Test mode notification Returns valid component for payments page 1`] = ` +
+
+
+ Viewing test payments. To view live payments, disable test mode in + + WooPayments settings + + . +
+
+
+`; + +exports[`Test mode notification Returns valid component for transactions page 1`] = ` +
+
+
+ Viewing test transactions. To view live transactions, disable test mode in + + WooPayments settings + + . +
+
+
+`; diff --git a/client/components/test-mode-notice/test/index.js b/client/components/test-mode-notice/test/index.js deleted file mode 100644 index da49b19a57b..00000000000 --- a/client/components/test-mode-notice/test/index.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import { getPaymentSettingsUrl, isInTestMode } from 'utils'; -import { - topics, - getPaymentsSettingsUrlComponent, - getTopicDetails, - getNoticeMessage, - TestModeNotice, -} from '../index'; - -jest.mock( 'utils', () => ( { - isInTestMode: jest.fn(), - getPaymentSettingsUrl: jest.fn().mockReturnValue( 'https://example.com/' ), -} ) ); - -describe( 'Test mode notification', () => { - beforeEach( () => { - global.wcpaySettings = { - accountStatus: { - detailsSubmitted: true, - }, - }; - } ); - // Set up easy to use lists containing test inputs. - const listTopics = [ - topics.transactions, - topics.deposits, - topics.disputes, - topics.documents, - ]; - const detailsTopics = [ - topics.depositDetails, - topics.disputeDetails, - topics.paymentDetails, - topics.documentDetails, - ]; - const allTopics = [ ...listTopics, ...detailsTopics ]; - - const topicsWithTestMode = [ - ...allTopics.map( ( topic ) => [ topic, true ] ), - ...allTopics.map( ( topic ) => [ topic, false ] ), - ]; - - test( 'Returns correct URL component', () => { - const expected = ( - - { 'View WooPayments settings' } - - ); - - expect( getPaymentsSettingsUrlComponent() ).toStrictEqual( expected ); - } ); - - test.each( listTopics )( - 'Returns right notice message for list topics', - ( topic ) => { - const expected = ( - - { topic } { getPaymentsSettingsUrlComponent( topic ) } - - ); - - expect( getNoticeMessage( topic ) ).toStrictEqual( expected ); - } - ); - - test( 'Notice details are correct for details topics', () => { - expect( getTopicDetails( topics.depositDetails ) ).toBe( - 'WooPayments was in test mode when these orders were placed.' - ); - - expect( getTopicDetails( topics.disputeDetails ) ).toBe( - 'WooPayments was in test mode when this order was placed.' - ); - - expect( getTopicDetails( topics.paymentDetails ) ).toBe( - 'WooPayments was in test mode when this order was placed.' - ); - } ); - - test.each( detailsTopics )( - 'Returns right notice message for details topics', - ( topic ) => { - const topicDetails = getTopicDetails( topic ); - const urlComponent = getPaymentsSettingsUrlComponent( topic ); - const expected = ( - - { topic } { topicDetails } { urlComponent } - - ); - - expect( getNoticeMessage( topic ) ).toStrictEqual( expected ); - } - ); - - test( 'Returns right notice message without URL component', () => { - global.wcpaySettings.accountStatus.detailsSubmitted = false; - const topic = topics.overview; - isInTestMode.mockReturnValue( true ); - const { container: testModeNotice } = render( - - ); - - expect( testModeNotice ).toMatchSnapshot(); - } ); - - test.each( topicsWithTestMode )( - 'Component is rendered correctly', - ( topic, isTestMode ) => { - isInTestMode.mockReturnValue( isTestMode ); - const { container: testModeNotice } = render( - - ); - - expect( testModeNotice ).toMatchSnapshot(); - } - ); -} ); diff --git a/client/components/test-mode-notice/test/index.tsx b/client/components/test-mode-notice/test/index.tsx new file mode 100644 index 00000000000..7dabdb3afc1 --- /dev/null +++ b/client/components/test-mode-notice/test/index.tsx @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { isInTestMode } from 'utils'; +import { TestModeNotice } from '../index'; + +declare const global: { + wcSettings: { countries: Record< string, string > }; + wcpaySettings: { + accountStatus: { + detailsSubmitted: boolean; + }; + }; +}; + +jest.mock( 'utils', () => ( { + isInTestMode: jest.fn(), + getPaymentSettingsUrl: jest.fn().mockReturnValue( 'https://example.com/' ), +} ) ); + +const mockIsInTestMode = isInTestMode as jest.MockedFunction< + typeof isInTestMode +>; + +type CurrentPage = + | 'overview' + | 'documents' + | 'deposits' + | 'disputes' + | 'loans' + | 'payments' + | 'transactions'; + +describe( 'Test mode notification', () => { + beforeEach( () => { + global.wcpaySettings = { + accountStatus: { + detailsSubmitted: true, + }, + }; + } ); + + const pages: CurrentPage[] = [ + 'overview', + 'documents', + 'deposits', + 'disputes', + 'loans', + 'payments', + 'transactions', + ]; + + test.each( pages )( 'Returns valid component for %s page', ( page ) => { + mockIsInTestMode.mockReturnValue( true ); + + const { container: testModeNotice } = render( + + ); + + expect( testModeNotice ).toMatchSnapshot(); + } ); + + test.each( pages )( 'Returns empty div if not in test mode', ( page ) => { + mockIsInTestMode.mockReturnValue( false ); + + const { container: testModeNotice } = render( + + ); + + expect( testModeNotice ).toMatchSnapshot(); + } ); +} ); diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 7396aea2ff9..76a03531b77 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -32,10 +32,10 @@ import { displayStatus } from '../strings'; import TransactionsList from 'transactions/list'; import Page from 'components/page'; import ErrorBoundary from 'components/error-boundary'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; import { formatCurrency, formatExplicitCurrency } from 'utils/currency'; -import './style.scss'; import { CachedDeposit } from 'wcpay/types/deposits'; +import { TestModeNotice } from 'wcpay/components/test-mode-notice'; +import './style.scss'; const Status = ( { status }: { status: string } ): JSX.Element => ( // Re-purpose order status indicator for deposit status. @@ -181,7 +181,7 @@ export const DepositDetails: React.FC< DepositDetailsProps > = ( { return ( - + { isLoading ? ( diff --git a/client/deposits/index.js b/client/deposits/index.tsx similarity index 58% rename from client/deposits/index.js rename to client/deposits/index.tsx index c1528092bc2..ae5ba238679 100644 --- a/client/deposits/index.js +++ b/client/deposits/index.tsx @@ -3,18 +3,19 @@ /** * External dependencies */ +import React from 'react'; /** * Internal dependencies. */ import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import DepositsList from './list'; -const DepositsPage = () => { +const DepositsPage: React.FC = () => { return ( - + ); diff --git a/client/disputes/evidence/index.js b/client/disputes/evidence/index.js index e25fa89f09f..0d4cf5b0c0a 100644 --- a/client/disputes/evidence/index.js +++ b/client/disputes/evidence/index.js @@ -27,11 +27,11 @@ import '../style.scss'; import { useDisputeEvidence } from 'wcpay/data'; import evidenceFields from './fields'; import { FileUploadControl, UploadedReadOnly } from 'components/file-upload'; +import { TestModeNotice } from 'components/test-mode-notice'; import Info from '../info'; import Page from 'components/page'; import ErrorBoundary from 'components/error-boundary'; import Loadable, { LoadableBlock } from 'components/loadable'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; import useConfirmNavigation from 'utils/use-confirm-navigation'; import wcpayTracks from 'tracks'; import { getAdminUrl } from 'wcpay/utils'; @@ -295,7 +295,6 @@ export const DisputeEvidencePage = ( props ) => { dispute.status !== 'needs_response' && dispute.status !== 'warning_needs_response'; const disputeIsAvailable = ! isLoading && dispute.id; - const testModeNotice = ; const readOnlyNotice = ( { if ( ! isLoading && ! disputeIsAvailable ) { return ( - { testModeNotice } +
{ __( 'Dispute not loaded', 'woocommerce-payments' ) }
@@ -323,7 +322,7 @@ export const DisputeEvidencePage = ( props ) => { return ( - { testModeNotice } + { readOnly && ! isLoading && readOnlyNotice } diff --git a/client/disputes/index.tsx b/client/disputes/index.tsx index 90880e592d2..ed2524d641a 100644 --- a/client/disputes/index.tsx +++ b/client/disputes/index.tsx @@ -30,7 +30,7 @@ import DisputeStatusChip from 'components/dispute-status-chip'; import ClickableCell from 'components/clickable-cell'; import DetailsLink, { getDetailsURL } from 'components/details-link'; import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import { reasons } from './strings'; import { formatStringValue } from 'utils'; import { formatExplicitCurrency } from 'utils/currency'; @@ -508,7 +508,7 @@ export const DisputesList = (): JSX.Element => { return ( - + { return ( - + ); diff --git a/client/overview/index.js b/client/overview/index.js index 1a8ddd196f9..124805902bc 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -3,7 +3,7 @@ /** * External dependencies */ -import React from 'react'; +import React, { useState } from 'react'; import { Card, Notice } from '@wordpress/components'; import { getQuery } from '@woocommerce/navigation'; import { __ } from '@wordpress/i18n'; @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies. */ import Page from 'components/page'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import AccountStatus from 'components/account-status'; import Welcome from 'components/welcome'; import AccountBalances from 'components/account-balances'; @@ -23,12 +23,13 @@ import TaskList from './task-list'; import { getTasks, taskSort } from './task-list/tasks'; import InboxNotifications from './inbox-notifications'; import ConnectionSuccessNotice from './connection-sucess-notice'; -import SetupRealPayments from './setup-real-payments'; import ProgressiveOnboardingEligibilityModal from './modal/progressive-onboarding-eligibility'; import JetpackIdcNotice from 'components/jetpack-idc-notice'; import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; import { useDisputes, useGetSettings, useSettings } from 'wcpay/data'; +import strings from './strings'; import './style.scss'; +import SetupLivePaymentsModal from './modal/setup-live-payments'; const OverviewPageError = () => { const queryParams = getQuery(); @@ -60,7 +61,11 @@ const OverviewPage = () => { enabledPaymentMethods, } = wcpaySettings; + const isDevMode = wcpaySettings.devMode; const { isLoading: settingsIsLoading } = useSettings(); + const [ livePaymentsModalVisible, setLivePaymentsModalVisible ] = useState( + false + ); const settings = useGetSettings(); const { disputes: activeDisputes } = useDisputes( { @@ -116,9 +121,7 @@ const OverviewPage = () => { return ( - - { showLoanOfferError && ( { __( @@ -127,7 +130,6 @@ const OverviewPage = () => { ) } ) } - { showServerLinkError && ( { __( @@ -136,15 +138,31 @@ const OverviewPage = () => { ) } ) } - - - + + setLivePaymentsModalVisible( true ), + }, + { + label: strings.notice.actions.learnMore, + url: + 'https://woo.com/document/woopayments/testing-and-troubleshooting/dev-mode/', + urlTarget: '_blank', + }, + ] + : [] + } + /> - { showConnectionSuccess && } - { ! accountRejected && ( <> @@ -176,37 +194,36 @@ const OverviewPage = () => { ) } - - { wcpaySettings.onboardingTestMode && ( - - - - ) } - - { wcpaySettings.accountLoans.has_active_loan && ( ) } - { ! accountRejected && ( ) } - { showProgressiveOnboardingEligibilityModal && ( ) } + { livePaymentsModalVisible && ( + + + setLivePaymentsModalVisible( false ) + } + /> + + ) } ); }; diff --git a/client/overview/modal/setup-live-payments/index.tsx b/client/overview/modal/setup-live-payments/index.tsx new file mode 100644 index 00000000000..6977892bb9b --- /dev/null +++ b/client/overview/modal/setup-live-payments/index.tsx @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import React from 'react'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; +import { Button, Modal } from '@wordpress/components'; +import { Icon, currencyDollar } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import BlockEmbedIcon from 'components/icons/block-embed'; +import BlockPostAuthorIcon from 'components/icons/block-post-author'; +import './style.scss'; + +interface Props { + closeModal: () => void; +} + +const SetupLivePaymentsModal: React.FC< Props > = ( { closeModal }: Props ) => { + const handleSetup = () => { + window.location.href = addQueryArgs( wcpaySettings.connectUrl, { + 'wcpay-disable-onboarding-test-mode': true, + } ); + }; + + return ( + +

+ { __( + 'Before proceeding, please take note of the following information:', + 'woocommerce-payments' + ) } +

+
+ + { __( + 'Your test account will be deactivated and your transaction records will be preserved for future reference.', + 'woocommerce-payments' + ) } + + { __( + 'The owner, business and contact information will be required.', + 'woocommerce-payments' + ) } + + { __( + 'We will need your banking details in order to process any deposits to you.', + 'woocommerce-payments' + ) } +
+
+ + +
+
+ ); +}; + +export default SetupLivePaymentsModal; diff --git a/client/overview/modal/setup-live-payments/style.scss b/client/overview/modal/setup-live-payments/style.scss new file mode 100644 index 00000000000..b4067be8ba1 --- /dev/null +++ b/client/overview/modal/setup-live-payments/style.scss @@ -0,0 +1,43 @@ +.wcpay-setup-real-payments-modal { + color: $gray-900; + fill: $studio-woocommerce-purple-50; + + .components-modal__content { + box-sizing: border-box; + max-width: 600px; + margin: auto; + padding: $gap-smaller $gap-larger $gap-larger; + } + + .components-modal__header { + position: initial; + padding: 0; + border: 0; + + h1 { + @include wp-title-small; + margin-bottom: $gap-smaller; + } + } + + &__title { + @include wp-title-small; + } + + &__headline { + font-weight: 600; + } + + &__content { + display: grid; + grid-template-columns: auto 1fr; + gap: $gap; + padding: $gap-smallest; + align-items: center; + margin-bottom: $gap-large; + } + + &__footer { + @include modal-footer-buttons; + } +} diff --git a/client/overview/modal/setup-live-payments/test/index.test.tsx b/client/overview/modal/setup-live-payments/test/index.test.tsx new file mode 100644 index 00000000000..be83254beda --- /dev/null +++ b/client/overview/modal/setup-live-payments/test/index.test.tsx @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import SetupLivePaymentsModal from '../index'; + +jest.mock( '@wordpress/data', () => ( { + useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), +} ) ); + +declare const global: { + wcpaySettings: { + connectUrl: string; + }; +}; + +describe( 'Setup Live Payments Modal', () => { + global.wcpaySettings = { + connectUrl: 'https://wcpay.test/connect', + }; + + it( 'modal is open by default', () => { + render( jest.fn() } /> ); + + expect( + screen.queryByText( + 'Before proceeding, please take note of the following information:' + ) + ).toBeInTheDocument(); + } ); + + it( 'calls `handleSetup` when setup button is clicked', () => { + Object.defineProperty( window, 'location', { + configurable: true, + enumerable: true, + value: new URL( window.location.href ), + } ); + + render( jest.fn() } /> ); + + user.click( + screen.getByRole( 'button', { + name: 'Continue setup', + } ) + ); + + expect( window.location.href ).toBe( + `https://wcpay.test/connect?wcpay-disable-onboarding-test-mode=true` + ); + } ); +} ); diff --git a/client/overview/setup-real-payments.tsx b/client/overview/setup-real-payments.tsx deleted file mode 100644 index 50a08d8c069..00000000000 --- a/client/overview/setup-real-payments.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/** - * External dependencies - */ -import React, { useState } from 'react'; -import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; -import { - Button, - Card, - CardBody, - CardFooter, - CardHeader, - Modal, - Flex, -} from '@wordpress/components'; -import { Icon, payment, globe, currencyDollar } from '@wordpress/icons'; -import ScheduledIcon from 'gridicons/dist/scheduled'; - -/** - * Internal dependencies - */ -import BlockEmbedIcon from 'components/icons/block-embed'; -import BlockPostAuthorIcon from 'components/icons/block-post-author'; - -const SetupRealPayments: React.FC = () => { - const [ modalVisible, setModalVisible ] = useState( false ); - - const handleContinue = () => { - window.location.href = addQueryArgs( wcpaySettings.connectUrl, { - 'wcpay-disable-onboarding-test-mode': true, - } ); - }; - - return ( - <> - - - { __( - 'Set up real payments on your store', - 'woocommerce-payments' - ) } - - -
- - { __( - 'Offer a wide range of card payments', - 'woocommerce-payments' - ) } -
-
- - { __( - '135 different currencies and local payment methods', - 'woocommerce-payments' - ) } -
-
- - { __( - 'Enjoy direct deposits into your bank account', - 'woocommerce-payments' - ) } -
-
- - - - - -
- { modalVisible && ( - setModalVisible( false ) } - > -

- { __( - 'Before proceeding, please take note of the following information:', - 'woocommerce-payments' - ) } -

-
- - { __( - 'Your test account will be deactivated and your transaction records will be preserved for future reference.', - 'woocommerce-payments' - ) } - - { __( - 'The owner, business and contact information will be required.', - 'woocommerce-payments' - ) } - - { __( - 'We will need your banking details in order to process any payouts to you.', - 'woocommerce-payments' - ) } -
-
- - -
-
- ) } - - ); -}; - -export default SetupRealPayments; diff --git a/client/overview/strings.tsx b/client/overview/strings.tsx new file mode 100644 index 00000000000..43ab5d47bb5 --- /dev/null +++ b/client/overview/strings.tsx @@ -0,0 +1,45 @@ +/* eslint-disable max-len */ +/** + * External dependencies + */ +import interpolateComponents from '@automattic/interpolate-components'; +import { __, sprintf } from '@wordpress/i18n'; +import React from 'react'; + +export default { + notice: { + content: { + test: interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{bold}}%1s is in test mode.{{bold /}}. All transactions will be simulated.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + bold: , + }, + } ), + dev: interpolateComponents( { + mixedString: sprintf( + /* translators: %1$s: WooPayments */ + __( + '{{bold}}%1s is in dev mode.{{bold /}}. You need to set up a live %1s account before you can accept real transactions.', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + bold: , + }, + } ), + }, + actions: { + goLive: __( 'Ready to go live?', 'woocommerce-payments' ), + setUpPayments: __( 'Set up payments', 'woocommerce-payments' ), + learnMore: __( 'Learn more', 'woocommerce-payments' ), + }, + }, +}; diff --git a/client/overview/style.scss b/client/overview/style.scss index 201452431f5..4dbe66d79e7 100644 --- a/client/overview/style.scss +++ b/client/overview/style.scss @@ -112,47 +112,3 @@ } } } - -.wcpay-setup-real-payments-modal { - color: $gray-900; - fill: $studio-woocommerce-purple-50; - - .components-modal__content { - box-sizing: border-box; - max-width: 600px; - margin: auto; - padding: $gap-smaller $gap-larger $gap-larger; - } - - .components-modal__header { - position: initial; - padding: 0; - border: 0; - - h1 { - @include wp-title-small; - margin-bottom: $gap-smaller; - } - } - - &__title { - @include wp-title-small; - } - - &__headline { - font-weight: 600; - } - - &__content { - display: grid; - grid-template-columns: auto 1fr; - gap: $gap; - padding: $gap-smallest; - align-items: center; - margin-bottom: $gap-large; - } - - &__footer { - @include modal-footer-buttons; - } -} diff --git a/client/overview/test/index.js b/client/overview/test/index.js index adc214b993a..2c677e27e31 100644 --- a/client/overview/test/index.js +++ b/client/overview/test/index.js @@ -295,32 +295,6 @@ describe( 'Overview page', () => { ).toBeInTheDocument(); } ); - it( 'displays SetupRealPayments if onboardingTestMode is true', () => { - global.wcpaySettings = { - ...global.wcpaySettings, - onboardingTestMode: true, - }; - - render( ); - - expect( - screen.getByText( 'Set up real payments on your store' ) - ).toBeInTheDocument(); - } ); - - it( 'does not displays SetupRealPayments if onboardingTestMode is false', () => { - global.wcpaySettings = { - ...global.wcpaySettings, - onboardingTestMode: false, - }; - - render( ); - - expect( - screen.queryByText( 'Set up real payments on your store' ) - ).not.toBeInTheDocument(); - } ); - it( 'displays ProgressiveOnboardingEligibilityModal if showProgressiveOnboardingEligibilityModal is true', () => { getQuery.mockReturnValue( { 'wcpay-connection-success': '1' } ); diff --git a/client/overview/test/setup-real-payments.test.tsx b/client/overview/test/setup-real-payments.test.tsx deleted file mode 100644 index 25ce482ddfb..00000000000 --- a/client/overview/test/setup-real-payments.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * External dependencies - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import user from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import SetupRealPayments from '../setup-real-payments'; - -declare const global: { - wcpaySettings: { - connectUrl: string; - }; -}; - -describe( 'Setup Real Payments', () => { - it( 'opens modal when set up payments button is clicked', () => { - render( ); - - const queryHeading = () => - screen.queryByRole( 'heading', { - name: 'Set up live payments on your store', - } ); - - expect( queryHeading() ).not.toBeInTheDocument(); - - user.click( screen.getByRole( 'button' ) ); - - expect( queryHeading() ).toBeInTheDocument(); - } ); - - it( 'closes modal when cancel button is clicked', () => { - render( ); - - user.click( screen.getByRole( 'button' ) ); - user.click( - screen.getByRole( 'button', { - name: 'Cancel', - } ) - ); - - expect( - screen.queryByRole( 'heading', { - name: 'Set up live payments on your store', - } ) - ).not.toBeInTheDocument(); - } ); - - it( 'calls handleContinue when continue setup button is clicked', () => { - global.wcpaySettings = { - connectUrl: 'https://wcpay.test/connect', - }; - - Object.defineProperty( window, 'location', { - configurable: true, - enumerable: true, - value: new URL( window.location.href ), - } ); - - render( ); - - user.click( screen.getByRole( 'button' ) ); - user.click( - screen.getByRole( 'button', { - name: 'Continue setup', - } ) - ); - - expect( window.location.href ).toBe( - `https://wcpay.test/connect?wcpay-disable-onboarding-test-mode=true` - ); - } ); -} ); diff --git a/client/payment-details/payment-details/index.tsx b/client/payment-details/payment-details/index.tsx index d6a55279a8a..cdf568520f2 100644 --- a/client/payment-details/payment-details/index.tsx +++ b/client/payment-details/payment-details/index.tsx @@ -7,7 +7,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { TestModeNotice, topics } from '../../components/test-mode-notice'; +import { TestModeNotice } from '../../components/test-mode-notice'; import Page from '../../components/page'; import { Card, CardBody } from '@wordpress/components'; import ErrorBoundary from '../../components/error-boundary'; @@ -37,13 +37,11 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { showTimeline = true, paymentIntent, } ) => { - const testModeNotice = ; - // Check instance of error because its default value is empty object if ( ! isLoading && error instanceof Error ) { return ( - { testModeNotice } + { __( @@ -58,8 +56,7 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { return ( - { testModeNotice } - + { @@ -26,13 +26,12 @@ const PaymentCardReaderChargeDetails = ( props ) => { props.chargeId, props.transactionId ); - const testModeNotice = ; // Check instance of chargeError because its default value is empty object if ( ! isLoading && chargeError instanceof Error ) { return ( - { testModeNotice } + { __( @@ -56,7 +55,6 @@ const PaymentCardReaderChargeDetails = ( props ) => { const RenderPaymentCardReaderChargeDetails = ( props ) => { const readers = props.readers; const isLoading = props.isLoading; - const testModeNotice = ; const headers = [ { @@ -128,7 +126,7 @@ const RenderPaymentCardReaderChargeDetails = ( props ) => { const downloadable = !! rows.length; return ( - { testModeNotice } + { const [ isWCPayEnabled, setIsWCPayEnabled ] = useIsWCPayEnabled(); const [ isEnabled, updateIsTestModeEnabled ] = useTestMode(); + const [ modalVisible, setModalVisible ] = useState( false ); const isDevModeEnabled = useDevMode(); return ( - - - + + + + { ! isDevModeEnabled && ( + <> +

+ { __( 'Test mode', 'woocommerce-payments' ) } +

+ + ), + learnMoreLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + /> + ) } - help={ sprintf( - /* translators: %s: WooPayments */ - __( - 'When enabled, payment methods powered by %s will appear on checkout.', - 'woocommerce-payments' - ), - 'WooPayments' + { isDevModeEnabled && ( + { + setModalVisible( true ); + }, + }, + ] } + className="wcpay-general-settings__notice" + > + + { interpolateComponents( { + mixedString: sprintf( + /* translators: %s: WooPayments */ + __( + '{{b}}%1$s is in dev mode.{{/b}} You need to set up a live %1$s account before ' + + 'you can accept real transactions. {{learnMoreLink}}Learn more{{/learnMoreLink}}', + 'woocommerce-payments' + ), + 'WooPayments' + ), + components: { + b: , + learnMoreLink: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }, + } ) } + + ) } +
+
+ { modalVisible && ( + setModalVisible( false ) } /> -

{ __( 'Test mode', 'woocommerce-payments' ) }

- - ), - learnMoreLink: ( - // eslint-disable-next-line jsx-a11y/anchor-has-content -
- ), - }, - } ) } - /> -
-
+ ) } + ); }; diff --git a/client/transactions/index.tsx b/client/transactions/index.tsx index 1623d0fd733..55395647f51 100644 --- a/client/transactions/index.tsx +++ b/client/transactions/index.tsx @@ -4,7 +4,6 @@ * External dependencies */ import React, { useContext } from 'react'; -import { Experiment } from '@woocommerce/explat'; import { TabPanel } from '@wordpress/components'; import { getQuery, updateQueryString } from '@woocommerce/navigation'; import { __, sprintf } from '@wordpress/i18n'; @@ -14,7 +13,7 @@ import { __, sprintf } from '@wordpress/i18n'; */ import Page from 'components/page'; import TransactionsList from './list'; -import { TestModeNotice, topics } from 'components/test-mode-notice'; +import { TestModeNotice } from 'components/test-mode-notice'; import Authorizations from './uncaptured'; import './style.scss'; import { @@ -51,25 +50,25 @@ export const TransactionsPage: React.FC = () => { const tabsComponentMap = { 'transactions-page': ( <> - + ), 'uncaptured-page': ( <> - + ), 'review-page': ( <> - + ), 'blocked-page': ( <> - + ), diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index d417de02838..a5b35128d0d 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -1017,11 +1017,16 @@ public function maybe_handle_onboarding() { } } + // Handle the flow for a builder moving from test to live. if ( isset( $_GET['wcpay-disable-onboarding-test-mode'] ) ) { - // Delete the account if the dev mode is enabled otherwise it'll cause issues to onboard again. - if ( WC_Payments::mode()->is_dev() ) { - $this->payments_api_client->delete_account(); + $test_mode = WC_Payments_Onboarding_Service::is_test_mode_enabled(); + + // Delete the account if the test mode is enabled otherwise it'll cause issues to onboard again. + if ( $test_mode ) { + $this->payments_api_client->delete_account( $test_mode ); } + + // Set the test mode to false now that we are handling a real onboarding. WC_Payments_Onboarding_Service::set_test_mode( false ); $this->redirect_to_onboarding_flow_page(); return; diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 24585d57e81..4416906767d 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -2398,13 +2398,15 @@ public function get_woopay_compatibility() { /** * Delete account. * + * @param bool $test_mode Whether we are in test mode or not. + * * @return array * @throws API_Exception */ - public function delete_account() { + public function delete_account( bool $test_mode = false ) { return $this->request( [ - 'test_mode' => WC_Payments::mode()->is_dev(), // only send a test mode request if in dev mode. + 'test_mode' => $test_mode, ], self::ACCOUNTS_API . '/delete', self::POST, From 00df31ea882ffe287f9e6c2522cb13be2a40eeba Mon Sep 17 00:00:00 2001 From: Eric Jinks <3147296+Jinksi@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:24:51 +1000 Subject: [PATCH 51/61] Code quality improvements for the Deposit Details component (#7786) Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> --- .../dev-improve-deposit-details-code-quality | 3 ++ client/deposits/details/index.tsx | 32 +++++++++++-------- client/deposits/details/test/index.tsx | 2 +- 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 changelog/dev-improve-deposit-details-code-quality diff --git a/changelog/dev-improve-deposit-details-code-quality b/changelog/dev-improve-deposit-details-code-quality new file mode 100644 index 00000000000..b1ef48f7c48 --- /dev/null +++ b/changelog/dev-improve-deposit-details-code-quality @@ -0,0 +1,3 @@ +Significance: patch +Type: dev +Comment: No user-facing changes. Code quality improvements only. diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 76a03531b77..1ab114e27e9 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -27,32 +27,38 @@ import classNames from 'classnames'; /** * Internal dependencies. */ -import { useDeposit } from 'wcpay/data'; -import { displayStatus } from '../strings'; +import type { CachedDeposit } from 'types/deposits'; +import { useDeposit } from 'data'; import TransactionsList from 'transactions/list'; import Page from 'components/page'; import ErrorBoundary from 'components/error-boundary'; +import { TestModeNotice } from 'components/test-mode-notice'; import { formatCurrency, formatExplicitCurrency } from 'utils/currency'; -import { CachedDeposit } from 'wcpay/types/deposits'; -import { TestModeNotice } from 'wcpay/components/test-mode-notice'; +import { displayStatus } from '../strings'; import './style.scss'; -const Status = ( { status }: { status: string } ): JSX.Element => ( - // Re-purpose order status indicator for deposit status. +/** + * Renders the deposit status indicator UI, re-purposing the OrderStatus component from @woocommerce/components. + */ +const Status: React.FC< { status: string } > = ( { status } ) => ( ); -// Custom SummaryNumber with custom value className reusing @woocommerce/components styles. -const SummaryItem = ( { - label, - value, - valueClass, - detail, -}: { +interface SummaryItemProps { label: string; value: string | JSX.Element; valueClass?: string | false; detail?: string; +} + +/** + * A custom SummaryNumber with custom value className, reusing @woocommerce/components styles. + */ +const SummaryItem: React.FC< SummaryItemProps > = ( { + label, + value, + valueClass, + detail, } ) => (
  • diff --git a/client/deposits/details/test/index.tsx b/client/deposits/details/test/index.tsx index bb50a9cd442..936908c3dcc 100644 --- a/client/deposits/details/test/index.tsx +++ b/client/deposits/details/test/index.tsx @@ -9,8 +9,8 @@ import React from 'react'; /** * Internal dependencies */ +import type { CachedDeposit } from 'types/deposits'; import { DepositOverview } from '../'; -import { CachedDeposit } from 'wcpay/types/deposits'; const mockDeposit = { id: 'po_mock', From 77560f3a846f037520d8f5d5779052b87ba789f2 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Thu, 30 Nov 2023 16:27:22 +0000 Subject: [PATCH 52/61] Update to handle redirect to new onboarding from WCAdmin Promo (#7812) --- changelog/4247-fix-incorrect-redirect | 4 ++++ includes/class-wc-payments-account.php | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog/4247-fix-incorrect-redirect diff --git a/changelog/4247-fix-incorrect-redirect b/changelog/4247-fix-incorrect-redirect new file mode 100644 index 00000000000..10384e5bfbd --- /dev/null +++ b/changelog/4247-fix-incorrect-redirect @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fixes a redirect to show the new onboarding when coming from WC Core. diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index a5b35128d0d..740d1dc9e00 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -1005,9 +1005,10 @@ public function maybe_handle_onboarding() { $wcpay_connect_param = sanitize_text_field( wp_unslash( $_GET['wcpay-connect'] ) ); - $from_wc_admin_task = 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param; - $from_wc_pay_connect_page = false !== strpos( wp_get_referer(), 'path=%2Fpayments%2Fconnect' ); - if ( ( $from_wc_admin_task || $from_wc_pay_connect_page ) ) { + $from_wc_admin_task = 'WCADMIN_PAYMENT_TASK' === $wcpay_connect_param; + $from_wc_admin_incentive_page = false !== strpos( wp_get_referer(), 'path=%2Fwc-pay-welcome-page' ); + $from_wc_pay_connect_page = false !== strpos( wp_get_referer(), 'path=%2Fpayments%2Fconnect' ); + if ( $from_wc_admin_task || $from_wc_pay_connect_page || $from_wc_admin_incentive_page ) { // Redirect non-onboarded account to the onboarding flow, otherwise to payments overview page. if ( ! $this->is_stripe_connected() ) { $this->redirect_to_onboarding_flow_page(); From 181b3e1796afac2d93a8a025a57eef6a0da4f401 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 30 Nov 2023 14:27:05 -0300 Subject: [PATCH 53/61] Bump WC tested up to version to 8.3.1 (#7807) --- changelog/dev-bump-wc-version-8-3-1 | 4 ++++ woocommerce-payments.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/dev-bump-wc-version-8-3-1 diff --git a/changelog/dev-bump-wc-version-8-3-1 b/changelog/dev-bump-wc-version-8-3-1 new file mode 100644 index 00000000000..d5bffe636aa --- /dev/null +++ b/changelog/dev-bump-wc-version-8-3-1 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Bump WC tested up to version to 8.3.1. diff --git a/woocommerce-payments.php b/woocommerce-payments.php index ebd28565578..dc99d86324e 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -9,7 +9,7 @@ * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 7.6 - * WC tested up to: 8.2.0 + * WC tested up to: 8.3.1 * Requires at least: 6.0 * Requires PHP: 7.3 * Version: 6.8.0 From 7849b433722f0b6922cad32b158ba1cefec00af1 Mon Sep 17 00:00:00 2001 From: Ricardo Metring Date: Thu, 30 Nov 2023 15:02:59 -0300 Subject: [PATCH 54/61] Add filter to disable WooPay checkout auto-redirect and email OTP iframe (#7805) --- .../add-2298-filter-disable-woopay-auto-redirect | 4 ++++ client/checkout/blocks/index.js | 1 + client/checkout/classic/index.js | 6 +++++- .../upe-deferred-intent-creation/event-handlers.js | 6 +++++- includes/class-wc-payments-checkout.php | 1 + includes/woopay/class-woopay-utilities.php | 13 +++++++++++++ 6 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 changelog/add-2298-filter-disable-woopay-auto-redirect diff --git a/changelog/add-2298-filter-disable-woopay-auto-redirect b/changelog/add-2298-filter-disable-woopay-auto-redirect new file mode 100644 index 00000000000..826af4bd0fe --- /dev/null +++ b/changelog/add-2298-filter-disable-woopay-auto-redirect @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Filter to disable WooPay checkout auto-redirect and email input hooks. diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 6b506531107..a9cf210c815 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -70,6 +70,7 @@ registerPaymentMethod( { if ( getConfig( 'isWooPayEnabled' ) ) { if ( document.querySelector( '[data-block-name="woocommerce/checkout"]' ) && + getConfig( 'isWooPayEmailInputEnabled' ) && ! isPreviewing() ) { handleWooPayEmailInput( '#email', api, true ); diff --git a/client/checkout/classic/index.js b/client/checkout/classic/index.js index f91fba53a72..2a421453337 100644 --- a/client/checkout/classic/index.js +++ b/client/checkout/classic/index.js @@ -551,7 +551,11 @@ jQuery( function ( $ ) { } } ); - if ( getConfig( 'isWooPayEnabled' ) && ! isPreviewing() ) { + if ( + getConfig( 'isWooPayEnabled' ) && + getConfig( 'isWooPayEmailInputEnabled' ) && + ! isPreviewing() + ) { handleWooPayEmailInput( '#billing_email', api ); } } ); diff --git a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js index 2738fa6537c..0f5c833e2c1 100644 --- a/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js +++ b/client/checkout/classic/upe-deferred-intent-creation/event-handlers.js @@ -107,7 +107,11 @@ jQuery( function ( $ ) { return processPaymentIfNotUsingSavedMethod( $( 'form#order_review' ) ); } ); - if ( getUPEConfig( 'isWooPayEnabled' ) && ! isPreviewing() ) { + if ( + getUPEConfig( 'isWooPayEnabled' ) && + getUPEConfig( 'isWooPayEmailInputEnabled' ) && + ! isPreviewing() + ) { handleWooPayEmailInput( '#billing_email', api ); } diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index dc33fe4584d..8815afe74f7 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -193,6 +193,7 @@ public function get_payment_fields_js_config() { 'isWooPayEnabled' => $this->woopay_util->should_enable_woopay( $this->gateway ) && $this->woopay_util->should_enable_woopay_on_cart_or_checkout(), 'isWoopayExpressCheckoutEnabled' => $this->woopay_util->is_woopay_express_checkout_enabled(), 'isWoopayFirstPartyAuthEnabled' => $this->woopay_util->is_woopay_first_party_auth_enabled(), + 'isWooPayEmailInputEnabled' => $this->woopay_util->is_woopay_email_input_enabled(), 'isClientEncryptionEnabled' => WC_Payments_Features::is_client_secret_encryption_enabled(), 'woopayHost' => WooPay_Utilities::get_woopay_url(), 'platformTrackerNonce' => wp_create_nonce( 'platform_tracks_nonce' ), diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php index a9837776291..dc13d5c9d46 100644 --- a/includes/woopay/class-woopay-utilities.php +++ b/includes/woopay/class-woopay-utilities.php @@ -87,6 +87,19 @@ public function is_woopay_express_checkout_enabled() { public function is_woopay_first_party_auth_enabled() { return WC_Payments_Features::is_woopay_first_party_auth_enabled() && $this->is_country_available( WC_Payments::get_gateway() ); // Feature flag. } + + /** + * Determines if the WooPay email input hooks should be enabled. + * + * This function doesn't affect the appearance of the email input, + * only whether or not the email exists check or auto-redirection should be enabled. + * + * @return bool + */ + public function is_woopay_email_input_enabled() { + return apply_filters( 'wcpay_is_woopay_email_input_enabled', true ); + } + /** * Generates a hash based on the store's blog token, merchant ID, and the time step window. * From 2aef1c67f1b112221857b1fb36f9dd8afcd3c039 Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Thu, 30 Nov 2023 17:43:13 -0300 Subject: [PATCH 55/61] Add E2E tests for shopper multi-currency checkout (#7804) --- .../e2e-7350-shopper-multicurrency-checkout | 5 + .../enabled-currencies-list/modal.js | 1 + .../test/__snapshots__/index.js.snap | 1 + .../shopper-checkout-multi-currency.spec.js | 147 ++++++++++++++++++ tests/e2e/utils/flows.js | 57 ++++++- 5 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 changelog/e2e-7350-shopper-multicurrency-checkout create mode 100644 tests/e2e/specs/wcpay/shopper/shopper-checkout-multi-currency.spec.js diff --git a/changelog/e2e-7350-shopper-multicurrency-checkout b/changelog/e2e-7350-shopper-multicurrency-checkout new file mode 100644 index 00000000000..9d68d013d41 --- /dev/null +++ b/changelog/e2e-7350-shopper-multicurrency-checkout @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: E2E test - Shopper facing multi-currency checkout + + diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js b/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js index 82bc78c1d52..619e51a01c5 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/modal.js @@ -212,6 +212,7 @@ const EnabledCurrenciesModal = ( { className } ) => { variant="secondary" className={ className } onClick={ handleEnabledCurrenciesAddButtonClick } + data-testid="enabled-currencies-add-button" > { __( 'Add/remove currencies', 'woocommerce-payments' ) } diff --git a/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap b/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap index 72c3958e9cf..b6ffaee1a8e 100644 --- a/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap +++ b/client/multi-currency/multi-currency-settings/enabled-currencies-list/test/__snapshots__/index.js.snap @@ -260,6 +260,7 @@ exports[`Multi-Currency enabled currencies list Enabled currencies list renders >