From 76caedf70728c0e58790daca770bca42707941be Mon Sep 17 00:00:00 2001 From: Naman Malhotra Date: Wed, 25 Oct 2023 18:05:50 +0530 Subject: [PATCH 1/5] Fixed tooltip alignment in mobile settings view (#7515) Made tooltip design aligned with Wordpress/tooltip and made it mobile-compatible --- changelog/fix-6950-fix-imp-mobile-tooltip | 4 ++++ client/components/tooltip/style.scss | 15 --------------- client/components/tooltip/tooltip-base.tsx | 11 ++++++++++- 3 files changed, 14 insertions(+), 16 deletions(-) create mode 100644 changelog/fix-6950-fix-imp-mobile-tooltip diff --git a/changelog/fix-6950-fix-imp-mobile-tooltip b/changelog/fix-6950-fix-imp-mobile-tooltip new file mode 100644 index 00000000000..651b5757222 --- /dev/null +++ b/changelog/fix-6950-fix-imp-mobile-tooltip @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed tooltip alignment for mobile view in payment settings diff --git a/client/components/tooltip/style.scss b/client/components/tooltip/style.scss index b0d053c2fcb..aa0cfe52062 100644 --- a/client/components/tooltip/style.scss +++ b/client/components/tooltip/style.scss @@ -52,21 +52,6 @@ padding: 10px; text-align: center; - &::after { - content: ' '; - position: absolute; - - // assuming all the tooltips are displayed at the top of the wrapped element. - // no need to complicate things since that's the only use case at the moment. - bottom: 0; - left: 50%; - transform: translate( -50%, 22px ); - border: solid 15px transparent; - border-top-color: $gray-900; - // Ensure the tooltip arrow does not obscure the mouse target element. - pointer-events: none; - } - a { color: var( --wp-admin-theme-color, $gutenberg-blue ); } diff --git a/client/components/tooltip/tooltip-base.tsx b/client/components/tooltip/tooltip-base.tsx index 7dbb818fba4..0a2c0f5ca92 100644 --- a/client/components/tooltip/tooltip-base.tsx +++ b/client/components/tooltip/tooltip-base.tsx @@ -223,9 +223,18 @@ const TooltipBase: React.FC< TooltipBaseProps > = ( { wrappedElement.offsetWidth / 2 + wrappedElementRect.left; const tooltipWidth = tooltipElement.offsetWidth; let tooltipLeft = elementMiddle - tooltipWidth / 2; + const tooltipRight = + window.innerWidth - + ( wrappedElementRect.left + tooltipElement.offsetWidth ); + if ( tooltipLeft < 0 ) { - tooltipLeft = 10; + // create a gap with the left edge if the element is out of view port + tooltipLeft = 45; + } else if ( tooltipRight < 0 ) { + // create a gap with the right edge if the element is out of view port + tooltipLeft = tooltipLeft - 85; } + tooltipElement.style.left = `${ tooltipLeft }px`; // make it visible only after all the calculations are done. From 98ed2053cc8f5294d9d4ce1ba9623edf21c67a75 Mon Sep 17 00:00:00 2001 From: Brian Borman <68524302+bborman22@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:28:02 -0400 Subject: [PATCH 2/5] Add clear both to payment request button wrapper (#7550) --- changelog/fix-express-checkout-wrapper-float | 4 ++++ client/checkout/express-checkout-buttons.scss | 1 + 2 files changed, 5 insertions(+) create mode 100644 changelog/fix-express-checkout-wrapper-float diff --git a/changelog/fix-express-checkout-wrapper-float b/changelog/fix-express-checkout-wrapper-float new file mode 100644 index 00000000000..398c084b0a0 --- /dev/null +++ b/changelog/fix-express-checkout-wrapper-float @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Clear floats for payment request button wrapper. diff --git a/client/checkout/express-checkout-buttons.scss b/client/checkout/express-checkout-buttons.scss index ef813873378..9cf1134ed5b 100644 --- a/client/checkout/express-checkout-buttons.scss +++ b/client/checkout/express-checkout-buttons.scss @@ -1,6 +1,7 @@ .wcpay-payment-request-wrapper { margin-top: 1em; width: 100%; + clear: both; &:first-child { margin-top: 0; From d22c2cef32b0e7bb824354e0b489048a0f89efde Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Wed, 25 Oct 2023 15:38:36 +0100 Subject: [PATCH 3/5] Fix PO Notice not appearing when adding APMs (#7552) --- changelog/dev-fix-po-notice-not-appearing | 4 ++++ .../payment-methods-list/payment-method.tsx | 22 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 changelog/dev-fix-po-notice-not-appearing diff --git a/changelog/dev-fix-po-notice-not-appearing b/changelog/dev-fix-po-notice-not-appearing new file mode 100644 index 00000000000..c6d74e1e348 --- /dev/null +++ b/changelog/dev-fix-po-notice-not-appearing @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Update to properly show tooltip on Payments > Settings page when account is in PO state. diff --git a/client/components/payment-methods-list/payment-method.tsx b/client/components/payment-methods-list/payment-method.tsx index 7f5bb405f37..57d7dd105d8 100644 --- a/client/components/payment-methods-list/payment-method.tsx +++ b/client/components/payment-methods-list/payment-method.tsx @@ -111,10 +111,15 @@ const PaymentMethod = ( { isPoEnabled, isPoComplete, }: PaymentMethodProps ): React.ReactElement => { + // We want to show a tooltip if PO is enabled and not yet complete. (We make an exception to not show this for card payments). + const isPoInProgress = + isPoEnabled && + ! isPoComplete && + status !== upeCapabilityStatuses.ACTIVE; + // APMs are disabled if they are inactive or if Progressive Onboarding is enabled and not yet complete. const disabled = - upeCapabilityStatuses.INACTIVE === status || - ( id !== 'card' && isPoEnabled && ! isPoComplete ); + upeCapabilityStatuses.INACTIVE === status || isPoInProgress; const { accountFees, }: { accountFees: Record< string, FeeStructure > } = useContext( @@ -122,12 +127,13 @@ const PaymentMethod = ( { ); const [ isManualCaptureEnabled ] = useManualCapture(); - const needsAttention = [ + const needsMoreInformation = [ upeCapabilityStatuses.INACTIVE, upeCapabilityStatuses.PENDING_APPROVAL, upeCapabilityStatuses.PENDING_VERIFICATION, ].includes( status ); + const needsAttention = needsMoreInformation || isPoInProgress; const shouldDisplayNotice = id === 'sofort'; const needsOverlay = @@ -190,9 +196,13 @@ const PaymentMethod = ( { 'woocommerce-payments' ) } /* eslint-disable-next-line max-len */ - href={ getDocumentationUrlForDisabledPaymentMethod( - paymentMethodId - ) } + href={ + isPoInProgress + ? 'https://woocommerce.com/document/woopayments/startup-guide/gradual-signup/#additional-payment-methods' + : getDocumentationUrlForDisabledPaymentMethod( + paymentMethodId + ) + } /> ), }, From 77b3acdb34c12a3fe739d8d09ac6c620dd9687e5 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Wed, 25 Oct 2023 19:35:40 +0300 Subject: [PATCH 4/5] RPP: Add order meta upon the AuthenticationRequired state (#7540) --- changelog/rpp-authentication-state-meta | 5 ++ src/Internal/Payment/State/InitialState.php | 1 + src/Internal/Service/OrderService.php | 28 +++++++++++ .../Payment/State/InitialStateTest.php | 16 ++++++- .../src/Internal/Service/OrderServiceTest.php | 47 +++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 changelog/rpp-authentication-state-meta diff --git a/changelog/rpp-authentication-state-meta b/changelog/rpp-authentication-state-meta new file mode 100644 index 00000000000..b35cdfbeb63 --- /dev/null +++ b/changelog/rpp-authentication-state-meta @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Storing the metadata, that was forgotten in 7471 + + diff --git a/src/Internal/Payment/State/InitialState.php b/src/Internal/Payment/State/InitialState.php index 7e3311d6819..87b59eec817 100644 --- a/src/Internal/Payment/State/InitialState.php +++ b/src/Internal/Payment/State/InitialState.php @@ -106,6 +106,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 ); return $this->create_state( AuthenticationRequiredState::class ); } diff --git a/src/Internal/Service/OrderService.php b/src/Internal/Service/OrderService.php index ca7b3ee9083..457cd9a0176 100644 --- a/src/Internal/Service/OrderService.php +++ b/src/Internal/Service/OrderService.php @@ -213,6 +213,34 @@ public function update_order_from_successful_intent( } } + /** + * Updates the order with the necessary details whenever an intent requires action. + * + * @param int $order_id ID of the order. + * @param WC_Payments_API_Abstract_Intention $intent Remote object. To be abstracted soon. + * @param PaymentContext $context Context for the payment. + * @throws Order_Not_Found_Exception + */ + public function update_order_from_intent_that_requires_action( + int $order_id, + WC_Payments_API_Abstract_Intention $intent, + PaymentContext $context + ) { + $order = $this->get_order( $order_id ); + + $this->legacy_service->attach_intent_info_to_order( + $order, + $intent->get_id(), + $intent->get_status(), + $context->get_payment_method()->get_id(), + $context->get_customer_id(), + '', + $context->get_currency() + ); + + $this->legacy_service->update_order_status_from_intent( $order, $intent ); + } + /** * Given the charge data, checks if there was an exchange and adds it to the given order as metadata * diff --git a/tests/unit/src/Internal/Payment/State/InitialStateTest.php b/tests/unit/src/Internal/Payment/State/InitialStateTest.php index 74687a167e6..f0df9ce0833 100644 --- a/tests/unit/src/Internal/Payment/State/InitialStateTest.php +++ b/tests/unit/src/Internal/Payment/State/InitialStateTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit_Utils; use WC_Order; +use WC_Payments_API_Payment_Intention; use WC_Payments_Customer_Service; use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception; use WCPay\Internal\Payment\State\InitialState; @@ -175,22 +176,35 @@ 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; $mock_request = $this->createMock( PaymentRequest::class ); $mock_auth_state = $this->createMock( AuthenticationRequiredState::class ); + // 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 ); $this->mock_payment_request_service->expects( $this->once() ) ->method( 'create_intent' ) ->with( $this->mock_context ) - ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => Intent_Status::REQUIRES_ACTION ] ) ); + ->willReturn( $mock_intent ); // Let's mock these services in order to prevent real execution of them. $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' ); + // Before the transition, the order service should update the order. + $this->mock_context->expects( $this->once() ) + ->method( 'get_order_id' ) + ->willReturn( $order_id ); + $this->mock_order_service->expects( $this->once() ) + ->method( 'update_order_from_intent_that_requires_action' ) + ->with( $order_id, $mock_intent, $this->mock_context ); + $this->mock_state_factory->expects( $this->once() ) ->method( 'create_state' ) ->with( AuthenticationRequiredState::class, $this->mock_context ) ->willReturn( $mock_auth_state ); + $result = $this->mocked_sut->start_processing( $mock_request ); $this->assertSame( $mock_auth_state, $result ); } diff --git a/tests/unit/src/Internal/Service/OrderServiceTest.php b/tests/unit/src/Internal/Service/OrderServiceTest.php index 362cb725fa9..168de422bf8 100644 --- a/tests/unit/src/Internal/Service/OrderServiceTest.php +++ b/tests/unit/src/Internal/Service/OrderServiceTest.php @@ -385,6 +385,53 @@ public function test_update_order_from_successful_intent( $intent ) { $this->sut->update_order_from_successful_intent( $this->order_id, $intent, $mock_context ); } + /** + * Test for the `update_order_from_intent_that_requires_action` method. + */ + public function test_update_order_from_intent_that_requires_action() { + $intent_id = 'pi_XYZ'; + $intent_status = 'success'; + $customer_id = 'cus_XYZ'; + $currency = 'usd'; + $payment_method_id = 'pm_XYZ'; + + // Prepare the context, and all needed getters. + $mock_context = $this->createMock( PaymentContext::class ); + $mock_context->expects( $this->once() )->method( 'get_payment_method' )->willReturn( new NewPaymentMethod( $payment_method_id ) ); + $mock_context->expects( $this->once() )->method( 'get_customer_id' )->willReturn( $customer_id ); + $mock_context->expects( $this->once() )->method( 'get_currency' )->willReturn( $currency ); + + // Create a mock order that will be used, and return it. + $mock_order = $this->createMock( WC_Order::class ); + $this->sut->expects( $this->once() ) + ->method( 'get_order' ) + ->with( $this->order_id ) + ->willReturn( $mock_order ); + + // Prepare the intent, and all expected getters. + $mock_intent = $this->createMock( WC_Payments_API_Payment_Intention::class ); + $mock_intent->expects( $this->once() )->method( 'get_id' )->willReturn( $intent_id ); + $mock_intent->expects( $this->once() )->method( 'get_status' )->willReturn( $intent_status ); + + $this->mock_legacy_service->expects( $this->once() ) + ->method( 'attach_intent_info_to_order' ) + ->with( + $mock_order, + $intent_id, + $intent_status, + $payment_method_id, + $customer_id, + null, + $currency + ); + + $this->mock_legacy_service->expects( $this->once() ) + ->method( 'update_order_status_from_intent' ) + ->with( $mock_order, $mock_intent ); + + $this->sut->update_order_from_intent_that_requires_action( $this->order_id, $mock_intent, $mock_context ); + } + public function provider_attach_exchange_info_to_order() { return [ 'Different store and account currencies' => [ 'USD', 'USD', 'EUR', null, null ], From ea5370017345fc2254f5011e4fdf381b3842e18a Mon Sep 17 00:00:00 2001 From: Miguel Gasca Date: Thu, 26 Oct 2023 09:45:23 +0200 Subject: [PATCH 5/5] Fix Afterpay checkout error when shipping information is missing (#7541) --- ...fails-when-shipping-information-is-missing | 4 ++ includes/class-wc-payment-gateway-wcpay.php | 32 +---------- includes/class-wc-payments-order-service.php | 56 +++++++++++++++++++ .../class-invalid-address-exception.php | 17 ++++++ .../class-upe-split-payment-gateway.php | 41 +++++++++++++- 5 files changed, 117 insertions(+), 33 deletions(-) create mode 100644 changelog/fix-7509-afterpay-checkout-fails-when-shipping-information-is-missing create mode 100644 includes/exceptions/class-invalid-address-exception.php diff --git a/changelog/fix-7509-afterpay-checkout-fails-when-shipping-information-is-missing b/changelog/fix-7509-afterpay-checkout-fails-when-shipping-information-is-missing new file mode 100644 index 00000000000..473e587b04b --- /dev/null +++ b/changelog/fix-7509-afterpay-checkout-fails-when-shipping-information-is-missing @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix Afterpay checkout error when shipping information is missing diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index fca52f46724..6e223ee1d4e 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -2890,34 +2890,6 @@ public function cancel_authorization( $order ) { ]; } - /** - * Create the shipping data array to send to Stripe when making a purchase. - * - * @param WC_Order $order The order that is being paid for. - * @return array The shipping data to send to Stripe. - */ - public function get_shipping_data_from_order( WC_Order $order ): array { - return [ - 'name' => implode( - ' ', - array_filter( - [ - $order->get_shipping_first_name(), - $order->get_shipping_last_name(), - ] - ) - ), - 'address' => [ - 'line1' => $order->get_shipping_address_1(), - 'line2' => $order->get_shipping_address_2(), - 'postal_code' => $order->get_shipping_postcode(), - 'city' => $order->get_shipping_city(), - 'state' => $order->get_shipping_state(), - 'country' => $order->get_shipping_country(), - ], - ]; - } - /** * Create the level 3 data array to send to Stripe when making a purchase. * @@ -3736,11 +3708,11 @@ private function upe_needs_redirection( $payment_methods ) { * * @param Create_And_Confirm_Intention $request The request object for creating and confirming intention. * @param Payment_Information $payment_information The payment information object. - * @param mixed $order The order object or data. + * @param WC_Order $order The order object. * * @return void */ - protected function modify_create_intent_parameters_when_processing_payment( Create_And_Confirm_Intention $request, Payment_Information $payment_information, $order ) { + protected function modify_create_intent_parameters_when_processing_payment( Create_And_Confirm_Intention $request, Payment_Information $payment_information, WC_Order $order ) { // Do nothing. } } diff --git a/includes/class-wc-payments-order-service.php b/includes/class-wc-payments-order-service.php index 3cf3d3cbdea..f7473dd71a0 100644 --- a/includes/class-wc-payments-order-service.php +++ b/includes/class-wc-payments-order-service.php @@ -741,6 +741,62 @@ public function attach_intent_info_to_order( $order, $intent_id, $intent_status, $order->save(); } + /** + * Create the shipping data array to send to Stripe when making a purchase. + * + * @param WC_Order $order The order that is being paid for. + * @return array The shipping data to send to Stripe. + */ + public function get_shipping_data_from_order( WC_Order $order ): array { + return [ + 'name' => implode( + ' ', + array_filter( + [ + $order->get_shipping_first_name(), + $order->get_shipping_last_name(), + ] + ) + ), + 'address' => [ + 'line1' => $order->get_shipping_address_1(), + 'line2' => $order->get_shipping_address_2(), + 'postal_code' => $order->get_shipping_postcode(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'country' => $order->get_shipping_country(), + ], + ]; + } + + /** + * Create the billing data array to send to Stripe when making a purchase, based on order's billing data. + * + * @param WC_Order $order The order that is being paid for. + * @return array The shipping data to send to Stripe. + */ + public function get_billing_data_from_order( WC_Order $order ): array { + return [ + 'name' => implode( + ' ', + array_filter( + [ + $order->get_billing_first_name(), + $order->get_billing_last_name(), + ] + ) + ), + 'address' => [ + 'line1' => $order->get_billing_address_1(), + 'line2' => $order->get_billing_address_2(), + 'postal_code' => $order->get_billing_postcode(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'country' => $order->get_billing_country(), + ], + ]; + } + /** * Updates an order to cancelled status, while adding a note with a link to the transaction. * diff --git a/includes/exceptions/class-invalid-address-exception.php b/includes/exceptions/class-invalid-address-exception.php new file mode 100644 index 00000000000..235ff953bbd --- /dev/null +++ b/includes/exceptions/class-invalid-address-exception.php @@ -0,0 +1,17 @@ +stripe_id; } + /** + * Handles the shipping requirement for Afterpay payments. + * + * This method extracts the shipping and billing data from the order and sets the appropriate + * shipping data for the Afterpay payment request. If neither shipping nor billing data is valid + * for shipping, an exception is thrown. + * + * @param WC_Order $order The order object containing shipping and billing information. + * @param Create_And_Confirm_Intention $request The Afterpay payment request object to set shipping data on. + * + * @throws Invalid_Address_Exception If neither shipping nor billing address is valid for Afterpay payments. + * @return void + */ + private function handle_afterpay_shipping_requirement( WC_Order $order, Create_And_Confirm_Intention $request ): void { + $check_if_usable = function( array $address ): bool { + return $address['country'] && $address['state'] && $address['city'] && $address['postal_code'] && $address['line1']; + }; + + $shipping_data = $this->order_service->get_shipping_data_from_order( $order ); + if ( $check_if_usable( $shipping_data['address'] ) ) { + $request->set_shipping( $shipping_data ); + return; + } + + $billing_data = $this->order_service->get_billing_data_from_order( $order ); + if ( $check_if_usable( $billing_data['address'] ) ) { + $request->set_shipping( $billing_data ); + return; + } + + throw new Invalid_Address_Exception( __( 'A valid shipping address is required for Afterpay payments.', 'woocommerce-payments' ) ); + } + /** * Modifies the create intent parameters when processing a payment. @@ -556,13 +591,13 @@ public function get_stripe_id() { * * @param Create_And_Confirm_Intention $request The request object for creating and confirming intention. * @param Payment_Information $payment_information The payment information object. - * @param mixed $order The order object or data. + * @param WC_Order $order The order object. * * @return void */ - protected function modify_create_intent_parameters_when_processing_payment( Create_And_Confirm_Intention $request, Payment_Information $payment_information, $order ) { + protected function modify_create_intent_parameters_when_processing_payment( Create_And_Confirm_Intention $request, Payment_Information $payment_information, WC_Order $order ): void { if ( Payment_Method::AFTERPAY === $this->get_selected_stripe_payment_type_id() ) { - $request->set_shipping( $this->get_shipping_data_from_order( $order ) ); + $this->handle_afterpay_shipping_requirement( $order, $request ); } } }