diff --git a/assets/css/success.css b/assets/css/success.css index 8ada2af0ee6..f11e45dd18a 100644 --- a/assets/css/success.css +++ b/assets/css/success.css @@ -3,13 +3,13 @@ align-items: center; flex-wrap: wrap; line-height: 1; + padding-top: 4px; } .wc-payment-gateway-method-logo-wrapper img { margin-right: 0.5rem; - padding-top: 4px; } -.wc-payment-gateway-method-logo-wrapper.wc-payment-bnpl-logo img { - max-height: 30px; +.wc-payment-gateway-method-logo-wrapper.wc-payment-lpm-logo img { + max-height: 26px; } diff --git a/assets/css/success.rtl.css b/assets/css/success.rtl.css index 16c136a0b1e..c0778375319 100644 --- a/assets/css/success.rtl.css +++ b/assets/css/success.rtl.css @@ -3,13 +3,13 @@ align-items: center; flex-wrap: wrap; line-height: 1; + padding-top: 4px; } .wc-payment-gateway-method-logo-wrapper img { margin-left: 0.5rem; - padding-top: 4px; } -.wc-payment-gateway-method-logo-wrapper.wc-payment-bnpl-logo img { - max-height: 30px; +.wc-payment-gateway-method-logo-wrapper.wc-payment-lpm-logo img { + max-height: 26px; } diff --git a/changelog/fix-8423-klarna-cant-undo-manual-refunds-and-process-refunds-again-via-woopayments b/changelog/fix-8423-klarna-cant-undo-manual-refunds-and-process-refunds-again-via-woopayments new file mode 100644 index 00000000000..8cd309f823c --- /dev/null +++ b/changelog/fix-8423-klarna-cant-undo-manual-refunds-and-process-refunds-again-via-woopayments @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Include missing scripts that handle refunds for non credit card payments in the order details page. diff --git a/changelog/fix-9676-multi-currency-autoload b/changelog/fix-9676-multi-currency-autoload new file mode 100644 index 00000000000..04e7fcfbc58 --- /dev/null +++ b/changelog/fix-9676-multi-currency-autoload @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Keep multi-currency module in the same path to avoid autoload errors. + + diff --git a/changelog/fix-saving-duplicate-3ds-cards b/changelog/fix-saving-duplicate-3ds-cards new file mode 100644 index 00000000000..ee077e3e5e4 --- /dev/null +++ b/changelog/fix-saving-duplicate-3ds-cards @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix duplicate saving of 3DS card entry after checkout diff --git a/changelog/update-order-success-lpm-icon b/changelog/update-order-success-lpm-icon new file mode 100644 index 00000000000..d0f4ac443af --- /dev/null +++ b/changelog/update-order-success-lpm-icon @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +update: show LPM payment method icon on order success page diff --git a/changelog/update-payment-method-test-mode-label b/changelog/update-payment-method-test-mode-label new file mode 100644 index 00000000000..1c3874ebf5f --- /dev/null +++ b/changelog/update-payment-method-test-mode-label @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +update: payment method "test mode" label at checkout to be displayed only when payment method is selected diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 855cecf966b..4e79f191aa9 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -125,10 +125,10 @@ export default class WCPayAPI { * and displays the intent confirmation modal (if needed). * * @param {string} redirectUrl The redirect URL, returned from the server. - * @param {string} paymentMethodToSave The ID of a Payment Method if it should be saved (optional). + * @param {boolean} shouldSavePaymentMethod Whether the payment method should be saved. * @return {Promise|boolean} A redirect URL on success, or `true` if no confirmation is needed. */ - confirmIntent( redirectUrl, paymentMethodToSave ) { + confirmIntent( redirectUrl, shouldSavePaymentMethod = false ) { const partials = redirectUrl.match( /#wcpay-confirm-(pi|si):(.+):(.+):(.+)$/ ); @@ -219,7 +219,9 @@ export default class WCPayAPI { // order status call works when a guest user creates an account during checkout. _ajax_nonce: nonce, intent_id: intentId, - payment_method_id: paymentMethodToSave || null, + should_save_payment_method: shouldSavePaymentMethod + ? 'true' + : 'false', } ); return [ ajaxCall, result.error ]; diff --git a/client/checkout/blocks/confirm-card-payment.js b/client/checkout/blocks/confirm-card-payment.js index 92089853376..90d303ca062 100644 --- a/client/checkout/blocks/confirm-card-payment.js +++ b/client/checkout/blocks/confirm-card-payment.js @@ -13,12 +13,12 @@ export default async function confirmCardPayment( emitResponse, shouldSavePayment ) { - const { redirect, payment_method: paymentMethod } = paymentDetails; + const { redirect } = paymentDetails; try { const confirmationRequest = api.confirmIntent( redirect, - shouldSavePayment ? paymentMethod : null + shouldSavePayment ); // `true` means there is no intent to confirm. diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 924afccde1f..68593bababd 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -110,7 +110,10 @@ Object.entries( enabledPaymentMethodsConfig ) label: ( diff --git a/client/checkout/blocks/payment-method-label.js b/client/checkout/blocks/payment-method-label.js index 417007816aa..b329a165658 100644 --- a/client/checkout/blocks/payment-method-label.js +++ b/client/checkout/blocks/payment-method-label.js @@ -12,9 +12,50 @@ import './style.scss'; import { useEffect, useState } from '@wordpress/element'; import { getAppearance } from 'wcpay/checkout/upe-styles'; -export default ( { api, upeConfig, upeName, upeAppearanceTheme } ) => { +const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; +const PaymentMethodMessageWrapper = ( { + upeName, + countries, + currentCountry, + amount, + appearance, + children, +} ) => { + if ( ! bnplMethods.includes( upeName ) ) { + return null; + } + + if ( amount <= 0 ) { + return null; + } + + if ( ! currentCountry ) { + return null; + } + + if ( ! appearance ) { + return null; + } + + if ( countries.length !== 0 && ! countries.includes( currentCountry ) ) { + return null; + } + + return ( +
{ children }
+ ); +}; + +export default ( { + api, + title, + countries, + iconLight, + iconDark, + upeName, + upeAppearanceTheme, +} ) => { const cartData = wp.data.select( 'wc/store/cart' ).getCartData(); - const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; const isTestMode = getUPEConfig( 'testMode' ); const [ appearance, setAppearance ] = useState( getUPEConfig( 'wcBlocksUPEAppearance' ) @@ -35,8 +76,6 @@ export default ( { api, upeConfig, upeName, upeAppearanceTheme } ) => { window.wcBlocksCheckoutData?.storeCountry || 'US'; - const isCreditCard = upeName === 'card'; - useEffect( () => { async function generateUPEAppearance() { // Generate UPE input styles. @@ -56,10 +95,8 @@ export default ( { api, upeConfig, upeName, upeAppearanceTheme } ) => { return ( <>
- - { upeConfig.title } - - { isCreditCard && isTestMode && ( + { title } + { isTestMode && ( { __( 'Test Mode', 'woocommerce-payments' ) } @@ -67,39 +104,35 @@ export default ( { api, upeConfig, upeName, upeAppearanceTheme } ) => { {
- { bnplMethods.includes( upeName ) && - ( upeConfig.countries.length === 0 || - upeConfig.countries.includes( currentCountry ) ) && - amount > 0 && - currentCountry && - appearance && ( -
- - - -
- ) } + + + + + ); }; diff --git a/client/checkout/blocks/style.scss b/client/checkout/blocks/style.scss index b43b7217d9f..a6aa37953b3 100644 --- a/client/checkout/blocks/style.scss +++ b/client/checkout/blocks/style.scss @@ -43,6 +43,21 @@ button.wcpay-stripelink-modal-trigger:hover { } .wc-block-checkout__payment-method { + input:checked ~ div { + .wc-block-components-radio-control__label { + > .payment-method-label { + .test-mode.badge { + // hiding the badge when the payment method is not selected + display: inline-block; + } + + &__pmme-container { + display: none; + } + } + } + } + .wc-block-components-radio-control__label { width: 100%; display: block !important; @@ -77,6 +92,7 @@ button.wcpay-stripelink-modal-trigger:hover { color: #4d3716; justify-self: start; width: max-content; + display: none; } @include breakpoint( '<480px' ) { @@ -88,13 +104,14 @@ button.wcpay-stripelink-modal-trigger:hover { justify-self: end; } } - } - .bnpl-message { - width: 100%; + &__pmme-container { + width: 100%; + pointer-events: none; - @include breakpoint( '<480px' ) { - margin-top: 8px; + @include breakpoint( '<480px' ) { + margin-top: 8px; + } } } } @@ -112,11 +129,6 @@ button.wcpay-stripelink-modal-trigger:hover { } #payment-method { - label.wc-block-components-radio-control__option-checked { - .StripeElement { - display: none; - } - } /* stylelint-disable-next-line selector-id-pattern */ #radio-control-wc-payment-method-options-woocommerce_payments_affirm__label img { diff --git a/client/checkout/classic/3ds-flow-handling.js b/client/checkout/classic/3ds-flow-handling.js index 72a28c815c8..d737d52e40b 100644 --- a/client/checkout/classic/3ds-flow-handling.js +++ b/client/checkout/classic/3ds-flow-handling.js @@ -22,12 +22,9 @@ const cleanupURL = () => { }; export const showAuthenticationModalIfRequired = ( api ) => { - const paymentMethodId = document.querySelector( '#wcpay-payment-method' ) - ?.value; - const confirmationRequest = api.confirmIntent( window.location.href, - shouldSavePaymentPaymentMethod() ? paymentMethodId : null + shouldSavePaymentPaymentMethod() ); // Boolean `true` means that there is nothing to confirm. diff --git a/client/checkout/classic/event-handlers.js b/client/checkout/classic/event-handlers.js index 6e4b8fb43fe..714142cae62 100644 --- a/client/checkout/classic/event-handlers.js +++ b/client/checkout/classic/event-handlers.js @@ -159,55 +159,43 @@ jQuery( function ( $ ) { async function injectStripePMMEContainers() { const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; - const labelBase = 'payment_method_woocommerce_payments_'; const paymentMethods = getUPEConfig( 'paymentMethodsConfig' ); const paymentMethodsKeys = Object.keys( paymentMethods ); const cartData = await api.pmmeGetCartData(); for ( const method of paymentMethodsKeys ) { if ( bnplMethods.includes( method ) ) { - const targetLabel = document.querySelector( - `label[for="${ labelBase }${ method }"]` - ); const containerID = `stripe-pmme-container-${ method }`; + const container = document.getElementById( containerID ); - if ( document.getElementById( containerID ) ) { - document.getElementById( containerID ).innerHTML = ''; + if ( ! container ) { + continue; } - if ( targetLabel ) { - let container = document.getElementById( containerID ); - if ( ! container ) { - container = document.createElement( 'span' ); - container.id = containerID; - container.dataset.paymentMethodType = method; - container.classList.add( 'stripe-pmme-container' ); - targetLabel.appendChild( container ); - } - - const currentCountry = - cartData?.billing_address?.country || - getUPEConfig( 'storeCountry' ); - - if ( - paymentMethods[ method ]?.countries.length === 0 || - paymentMethods[ method ]?.countries?.includes( - currentCountry - ) - ) { - await mountStripePaymentMethodMessagingElement( - api, - container, - { - amount: cartData?.totals?.total_price, - currency: cartData?.totals?.currency_code, - decimalPlaces: - cartData?.totals?.currency_minor_unit, - country: currentCountry, - }, - 'shortcode_checkout' - ); - } + container.innerHTML = ''; + container.dataset.paymentMethodType = method; + + const currentCountry = + cartData?.billing_address?.country || + getUPEConfig( 'storeCountry' ); + if ( + paymentMethods[ method ]?.countries.length === 0 || + paymentMethods[ method ]?.countries?.includes( + currentCountry + ) + ) { + await mountStripePaymentMethodMessagingElement( + api, + container, + { + amount: cartData?.totals?.total_price, + currency: cartData?.totals?.currency_code, + decimalPlaces: + cartData?.totals?.currency_minor_unit, + country: currentCountry, + }, + 'shortcode_checkout' + ); } } } diff --git a/client/checkout/classic/style.scss b/client/checkout/classic/style.scss index 5b452accc83..36a3d29bd94 100644 --- a/client/checkout/classic/style.scss +++ b/client/checkout/classic/style.scss @@ -30,6 +30,11 @@ border: none; } +.woopayments-rich-payment-method-label { + // this will be displayed only on specific scenarios. Otherwise, the "legacy" label will be displayed. + display: none; +} + #payment .payment_methods { li[class*='payment_method_woocommerce_payments'] > label > img { float: right; @@ -41,32 +46,68 @@ &.wc_payment_methods, &.woocommerce-PaymentMethods { - li.payment_method_woocommerce_payments { + li[class*='payment_method_woocommerce_payments'] { display: grid; grid-template-columns: 0fr 0fr 1fr; grid-template-rows: max-content; + .woopayments-plain-payment-method-label { + display: none; + } + > input[name='payment_method'] { - align-self: center; + &:checked ~ label { + .payment-method-title { + margin-right: 8px; // 8px gap between .payment-method-title and .test-mode.badge + } + + .test-mode.badge { + display: inline-block; // hiding the badge when the payment method is not selected + } + + .stripe-pmme-container { + display: none; + } + } } + > label { grid-column: 3; + margin-bottom: 0; + } + + > div.payment_box { + grid-area: 2 / 1 / 3 / 4; + } + + > label:has( .woopayments-rich-payment-method-label ) { + display: inline-flex; + align-items: center; + width: 100%; + + > img { + display: none; // we'll display the image inside `.woopayments-rich-payment-method-label`, instead. + } + } + + .woopayments-rich-payment-method-label { display: grid; - grid-template-columns: 0fr auto; - grid-template-rows: max-content; - grid-gap: 0; + grid-template-columns: 1fr auto; + align-items: center; margin-bottom: 0; + flex-grow: 1; - > .label-title-container { - grid-area: 1 / 2 / 2 / 3; + .label-title-container { + display: block; } .payment-method-title { - margin-right: 8px; + white-space: normal; // Allows wrapping if text is too long + vertical-align: middle; } .test-mode.badge { - display: inline-block; + display: none; background-color: #fff2d7; border-radius: 4px; padding: 4px 6px; @@ -75,73 +116,31 @@ line-height: 16px; color: #4d3716; vertical-align: middle; + white-space: nowrap; // Prevents the badge text from wrapping } img { - float: none; - grid-area: 1 / 4 / 2 / 5; - align-self: baseline; + border: 0; + padding: 0; + height: 24px !important; + max-height: 24px !important; justify-self: end; - margin-left: 1em; + margin: 3px 0; // ensuring the images don't appear squished when all the payment methods are rendered next to each others, like in Elementor. + align-self: center; } - } - > div.payment_box { - grid-area: 2 / 1 / 3 / 4; - } - } - } -} - -li.wc_payment_method:has( .input-radio:not( :checked ) - + label - .stripe-pmme-container ) { - display: grid; - grid-template-columns: min-content 1fr; - grid-template-rows: auto auto; - align-items: baseline; - - .input-radio { - grid-row: 1; - grid-column: 1; - } - - label { - grid-column: 2; - grid-row: 1; - } - - img { - grid-row: 1 / span 2; - align-self: center; - } - .stripe-pmme-container { - width: 100%; - grid-column: 1; - grid-row-start: 2; - pointer-events: none; - } - - .payment_box { - flex: 0 0 100%; - grid-row: 2; - grid-column: 1 / span 2; - } -} - -li.wc_payment_method:has( .input-radio:checked - + label - .stripe-pmme-container ) { - display: block; - - .input-radio:checked { - + label { - .stripe-pmme-container { - display: none; - } - - img { - grid-column: 2; + .stripe-pmme-container { + &:empty { + display: none; // hides container if empty, without affecting alignment + } + + margin-left: 0.25em; // WooCommerce Core will add a   on the left of the payment method's label - this spacing ensures that at least it's consistently aligned. + pointer-events: none; + grid-column: 1 / 2; + grid-row: 2 / 3; + align-self: start; + width: 100%; + } } } } diff --git a/client/order/refund-confirm-modal/index.js b/client/order/refund-confirm-modal/index.js index fc589feb3c1..f7d8c66de07 100644 --- a/client/order/refund-confirm-modal/index.js +++ b/client/order/refund-confirm-modal/index.js @@ -115,7 +115,7 @@ const RefundConfirmationModal = ( { { sprintf( /* translators: %s: WooPayments */ __( - "Issue a full refund back to your customer's credit card using %s. " + + "Issue a full refund back to your customer's payment method using %s. " + 'This action can not be undone. To issue a partial refund, click "Cancel", and use ' + 'the "Refund" button in the order details below.', 'woocommerce-payments' diff --git a/composer.json b/composer.json index 497530e36e1..cdb679b4afb 100644 --- a/composer.json +++ b/composer.json @@ -88,7 +88,7 @@ }, "autoload": { "psr-4": { - "WCPay\\MultiCurrency\\": "multi-currency/src", + "WCPay\\MultiCurrency\\": "includes/multi-currency", "WCPay\\Vendor\\": "lib/packages", "WCPay\\": "src" }, diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 9c53cb48597..8b2713c4829 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -733,7 +733,7 @@ public function enqueue_payments_scripts() { if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) { $order = wc_get_order(); - if ( $order && WC_Payment_Gateway_WCPay::GATEWAY_ID === $order->get_payment_method() ) { + if ( $order && strpos( $order->get_payment_method(), WC_Payment_Gateway_WCPay::GATEWAY_ID ) !== false ) { $refund_amount = $order->get_remaining_refund_amount(); // Check if the order's test mode meta matches the site's current test mode state. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 2a50957df4d..84d30c82821 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -581,16 +581,32 @@ public function get_title() { $title = parent::get_title(); if ( - Payment_Method::CARD === $this->stripe_id && ( is_checkout() || is_add_payment_method_page() ) && ! isset( $_GET['change_payment_method'] ) // phpcs:ignore WordPress.Security.NonceVerification ) { + $test_mode_badge = ''; if ( WC_Payments::mode()->is_test() ) { $test_mode_badge = '' . __( 'Test Mode', 'woocommerce-payments' ) . ''; - } else { - $test_mode_badge = ''; } - return '
 ' . $title . '' . $test_mode_badge . '
'; + + $bnpl_messaging_container = ''; + if ( $this->payment_method->is_bnpl() ) { + $bnpl_messaging_container = ''; + } + + // the "plain" payment method label is displayed on some sections of the app + // - like "pay for order" when a payment method is pre-selected or a payment has previously failed. + $html = '' . $title . ''; + $html .= '
'; + $html .= '
'; + $html .= ' ' . $title . ''; + $html .= $test_mode_badge; + $html .= '
'; + $html .= $this->get_icon(); + $html .= $bnpl_messaging_container; + $html .= '
'; + + return $html; } return $title; @@ -3236,7 +3252,7 @@ public function update_fraud_rules_based_on_general_options() { * @return string */ public function get_icon() { - return '' . esc_attr( $this->payment_method->get_title() ) . ' payment method logo'; + return '' . esc_attr( $this->payment_method->get_title() ) . ' payment method logo'; } /** @@ -3608,7 +3624,9 @@ public function update_order_status() { wc_reduce_stock_levels( $order_id ); WC()->cart->empty_cart(); - if ( ! empty( $payment_method_id ) ) { + $is_subscription = function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order ); + $should_save_payment_method = $is_subscription || ( isset( $_POST['should_save_payment_method'] ) && 'true' === $_POST['should_save_payment_method'] ); + if ( $should_save_payment_method && ! empty( $payment_method_id ) ) { try { $token = $this->token_service->add_payment_method_to_user( $payment_method_id, wp_get_current_user() ); $this->add_token_to_order( $order, $token ); diff --git a/includes/class-wc-payments-blocks-payment-method.php b/includes/class-wc-payments-blocks-payment-method.php index 607bb3cafb0..877d4bf4270 100644 --- a/includes/class-wc-payments-blocks-payment-method.php +++ b/includes/class-wc-payments-blocks-payment-method.php @@ -58,7 +58,7 @@ public function get_payment_method_script_handles() { 'wc-blocks-checkout-style', plugins_url( 'dist/blocks-checkout.css', WCPAY_PLUGIN_FILE ), [], - '1.0', + WC_Payments::get_file_version( 'dist/checkout.css' ), 'all' ); } diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index 214d3178bb8..c4593f54b7e 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -134,9 +134,7 @@ public function register_scripts() { Fraud_Prevention_Service::maybe_append_fraud_prevention_token(); - $script = 'dist/checkout'; - - WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', $script, $script_dependencies ); + WC_Payments::register_script_with_dependencies( 'wcpay-upe-checkout', 'dist/checkout', $script_dependencies ); } /** @@ -417,32 +415,29 @@ function () use ( $prepared_customer_data ) { } // Output the form HTML. - ?> - gateway->get_description() ) ) : ?> + if ( ! empty( $this->gateway->get_description() ) ) : ?>

gateway->get_description() ); ?>

- + is_test() ) : ?> + if ( WC_Payments::mode()->is_test() && false !== $this->gateway->get_payment_method()->get_testing_instructions() ) : + ?>

- gateway->get_payment_method()->get_testing_instructions(); - if ( false !== $testing_instructions ) { + '', - 'strong' => '', - 'number' => '