diff --git a/changelog/fix-tokenized-cart-subscription-signup-fee b/changelog/fix-tokenized-cart-subscription-signup-fee new file mode 100644 index 00000000000..5abe9f0226b --- /dev/null +++ b/changelog/fix-tokenized-cart-subscription-signup-fee @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: fix: tokenized cart subscription signup fee price + + diff --git a/changelog/load-checkout-scripts-on-checkout-if-not-loaded b/changelog/load-checkout-scripts-on-checkout-if-not-loaded new file mode 100644 index 00000000000..4a684203a2e --- /dev/null +++ b/changelog/load-checkout-scripts-on-checkout-if-not-loaded @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Load checkout scripts when they are not previously loaded on checkout page. diff --git a/changelog/update-5713-failed-orders-should-include-more-info-on-transaction-details-page b/changelog/update-5713-failed-orders-should-include-more-info-on-transaction-details-page new file mode 100644 index 00000000000..daf90a1cd39 --- /dev/null +++ b/changelog/update-5713-failed-orders-should-include-more-info-on-transaction-details-page @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Add failure reason to failed payments in the timeline. diff --git a/client/payment-details/timeline/map-events.js b/client/payment-details/timeline/map-events.js index da4e275105c..b76f659ac63 100644 --- a/client/payment-details/timeline/map-events.js +++ b/client/payment-details/timeline/map-events.js @@ -30,7 +30,7 @@ import { import { formatFee } from 'utils/fees'; import { getAdminUrl } from 'wcpay/utils'; import { ShieldIcon } from 'wcpay/icons'; -import { fraudOutcomeRulesetMapping } from './mappings'; +import { fraudOutcomeRulesetMapping, paymentFailureMapping } from './mappings'; /** * Creates a timeline item about a payment status change @@ -772,6 +772,10 @@ const mapEventToTimelineItems = ( event ) => { ), ]; case 'failed': + const paymentFailureMessage = + paymentFailureMapping[ event.reason ] || + paymentFailureMapping.default; + return [ getStatusChangeTimelineItem( event, @@ -779,11 +783,14 @@ const mapEventToTimelineItems = ( event ) => { ), getMainTimelineItem( event, - stringWithAmount( - /* translators: %s is a monetary amount */ - __( 'A payment of %s failed.', 'woocommerce-payments' ), - event.amount, - true + sprintf( + /* translators: %1$s is the payment amount, %2$s is the failure reason message */ + __( + 'A payment of %1$s failed: %2$s.', + 'woocommerce-payments' + ), + formatExplicitCurrency( event.amount, event.currency ), + paymentFailureMessage ), ), diff --git a/client/payment-details/timeline/mappings.ts b/client/payment-details/timeline/mappings.ts index 80d2aceaa98..affcb62063a 100644 --- a/client/payment-details/timeline/mappings.ts +++ b/client/payment-details/timeline/mappings.ts @@ -65,3 +65,46 @@ export const fraudOutcomeRulesetMapping = { ), }, }; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const paymentFailureMapping = { + card_declined: __( + 'The card was declined by the bank', + 'woocommerce-payments' + ), + expired_card: __( 'The card has expired', 'woocommerce-payments' ), + incorrect_cvc: __( + 'The security code is incorrect', + 'woocommerce-payments' + ), + incorrect_number: __( + 'The card number is incorrect', + 'woocommerce-payments' + ), + incorrect_zip: __( 'The postal code is incorrect', 'woocommerce-payments' ), + invalid_cvc: __( 'The security code is invalid', 'woocommerce-payments' ), + invalid_expiry_month: __( + 'The expiration month is invalid', + 'woocommerce-payments' + ), + invalid_expiry_year: __( + 'The expiration year is invalid', + 'woocommerce-payments' + ), + invalid_number: __( 'The card number is invalid', 'woocommerce-payments' ), + processing_error: __( + 'An error occurred while processing the card', + 'woocommerce-payments' + ), + authentication_required: __( + 'The payment requires authentication', + 'woocommerce-payments' + ), + insufficient_funds: __( + 'The card has insufficient funds to complete the purchase', + 'woocommerce-payments' + ), + + // Default fallback + default: __( 'The payment was declined', 'woocommerce-payments' ), +}; diff --git a/client/payment-details/timeline/test/__snapshots__/index.js.snap b/client/payment-details/timeline/test/__snapshots__/index.js.snap index 95f1e8a5c9f..c7915e6aee9 100644 --- a/client/payment-details/timeline/test/__snapshots__/index.js.snap +++ b/client/payment-details/timeline/test/__snapshots__/index.js.snap @@ -933,7 +933,7 @@ exports[`PaymentDetailsTimeline renders correctly (with a mocked Timeline compon - A payment of $77.00 failed. + A payment of $77.00 failed: The card was declined by the bank. , diff --git a/client/payment-details/timeline/test/map-events.js b/client/payment-details/timeline/test/map-events.js index c3e42ceae8b..f1c0588d659 100644 --- a/client/payment-details/timeline/test/map-events.js +++ b/client/payment-details/timeline/test/map-events.js @@ -662,4 +662,59 @@ describe( 'mapTimelineEvents', () => { ).toMatchSnapshot(); } ); } ); + + test( 'formats payment failure events with different error codes', () => { + const testCases = [ + { + reason: 'insufficient_funds', + expectedMessage: + 'A payment of $77.00 USD failed: The card has insufficient funds to complete the purchase.', + }, + { + reason: 'expired_card', + expectedMessage: + 'A payment of $77.00 USD failed: The card has expired.', + }, + { + reason: 'invalid_cvc', + expectedMessage: + 'A payment of $77.00 USD failed: The security code is invalid.', + }, + { + reason: 'unknown_reason', + expectedMessage: + 'A payment of $77.00 USD failed: The payment was declined.', + }, + ]; + + testCases.forEach( ( { reason, expectedMessage } ) => { + const events = mapTimelineEvents( [ + { + amount: 7700, + currency: 'USD', + datetime: 1585712113, + reason, + type: 'failed', + }, + ] ); + + expect( events[ 1 ].headline ).toBe( expectedMessage ); + } ); + } ); + + test( 'formats payment failure events with different currencies', () => { + const events = mapTimelineEvents( [ + { + amount: 7700, + currency: 'EUR', + datetime: 1585712113, + reason: 'card_declined', + type: 'failed', + }, + ] ); + + expect( events[ 1 ].headline ).toBe( + 'A payment of €77.00 EUR failed: The card was declined by the bank.' + ); + } ); } ); diff --git a/client/tokenized-express-checkout/transformers/wc-to-stripe.js b/client/tokenized-express-checkout/transformers/wc-to-stripe.js index 7c4c9c14822..6d9b39035ce 100644 --- a/client/tokenized-express-checkout/transformers/wc-to-stripe.js +++ b/client/tokenized-express-checkout/transformers/wc-to-stripe.js @@ -40,8 +40,8 @@ export const transformPrice = ( price, priceObject ) => { export const transformCartDataForDisplayItems = ( cartData ) => { const displayItems = cartData.items.map( ( item ) => ( { amount: transformPrice( - parseInt( item.prices.price, 10 ), - item.prices + parseInt( item.totals?.line_subtotal || item.prices.price, 10 ), + item.totals || item.prices ), name: [ item.name, diff --git a/includes/class-wc-payments-checkout.php b/includes/class-wc-payments-checkout.php index ee7a161f3b1..44d92d10b23 100644 --- a/includes/class-wc-payments-checkout.php +++ b/includes/class-wc-payments-checkout.php @@ -103,6 +103,7 @@ public function init_hooks() { add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts_for_zero_order_total' ], 11 ); + add_action( 'woocommerce_after_checkout_form', [ $this, 'maybe_load_checkout_scripts' ] ); } /** @@ -151,11 +152,18 @@ public function register_scripts_for_zero_order_total() { ! has_block( 'woocommerce/checkout' ) && ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) { - WC_Payments::get_gateway()->tokenization_script(); - $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 ); + $this->load_checkout_scripts(); + } + } + + /** + * Sometimes the filters can remove the payment gateway from the checkout page which results in the payment fields not being displayed. + * This could prevent loading of the payment fields (checkout) scripts. + * This function ensures that these scripts are loaded. + */ + public function maybe_load_checkout_scripts() { + if ( is_checkout() && ! wp_script_is( 'wcpay-upe-checkout', 'enqueued' ) ) { + $this->load_checkout_scripts(); } } @@ -416,7 +424,7 @@ function () use ( $prepared_customer_data ) { } ?> -
gateway = $this->gateway->wc_payments_get_payment_gateway_by_id( $payment_method_id ); } } + + /** + * Load the checkout scripts. + */ + private function load_checkout_scripts() { + WC_Payments::get_gateway()->tokenization_script(); + $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 ); + } }