From 2d2aeb002fa3a100ea861e4cb6fb8ce6f9865a9c Mon Sep 17 00:00:00 2001 From: Matt Allan Date: Mon, 15 Apr 2024 08:02:20 +1000 Subject: [PATCH 01/58] Fixes "Invalid recurring shipping method" errors when purchasing multiple subscriptions with Apple / Google Pay (#8618) --- ...8029-prb-invalid-recurring-shipping-method | 4 ++ ...ayments-payment-request-button-handler.php | 40 ++++++++++++++++++- psalm-baseline.xml | 4 +- 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 changelog/fix-8029-prb-invalid-recurring-shipping-method diff --git a/changelog/fix-8029-prb-invalid-recurring-shipping-method b/changelog/fix-8029-prb-invalid-recurring-shipping-method new file mode 100644 index 00000000000..2dd0f954958 --- /dev/null +++ b/changelog/fix-8029-prb-invalid-recurring-shipping-method @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Resolves "Invalid recurring shipping method" errors when purchasing multiple subscriptions with Apple Pay and Google Pay. diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 92ce89c7b94..d4f8c568a87 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -893,7 +893,7 @@ public function get_shipping_options( $shipping_address, $itemized_display_items $data = []; // Remember current shipping method before resetting. - $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); $this->calculate_shipping( apply_filters( 'wcpay_payment_request_shipping_posted_values', $shipping_address ) ); $packages = WC()->shipping->get_packages(); @@ -943,6 +943,8 @@ public function get_shipping_options( $shipping_address, $itemized_display_items WC()->cart->calculate_totals(); + $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods ); + $data += $this->express_checkout_helper->build_display_items( $itemized_display_items ); $data['result'] = 'success'; } catch ( Exception $e ) { @@ -1507,4 +1509,40 @@ private function get_taxes_like_cart( $product, $price ) { // Normally there should be a single tax, but `calc_tax` returns an array, let's use it. return WC_Tax::calc_tax( $price, $rates, false ); } + + /** + * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated + * during the Payment Request get_shipping_options flow. + * + * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping + * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's + * only concerned about handling the initial purchase. + * + * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore + * the previously chosen shipping methods for each recurring cart. + * + * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet. + * + * @param array $previous_chosen_methods The previously chosen shipping methods. + */ + private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) { + if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) { + return; + } + + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] ); + + foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) { + foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) { + $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index ); + + // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it. + if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) { + $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ]; + } + } + } + + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index ecb5ea1088e..17038230df6 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -25,11 +25,9 @@ - + WC_Pre_Orders_Product WC_Subscriptions_Product - WC_Subscriptions_Product - WC_Subscriptions_Cart WC_Subscriptions_Cart From 6f898adeb903dff4356e70b50e4627e811a6d08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Qui=C3=B1ones?= Date: Tue, 16 Apr 2024 07:44:18 -0500 Subject: [PATCH 02/58] Hide Fraud info banner until first transaction happens (#8643) --- .../8279-fix-fraud-banner-display-transaction | 4 ++ .../fraud-risk-tools-banner/index.tsx | 4 +- .../test/__snapshots__/index.test.tsx.snap | 2 + .../test/index.test.tsx | 16 ++++++++ client/overview/test/index.js | 39 +++++++++++++++---- 5 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 changelog/8279-fix-fraud-banner-display-transaction diff --git a/changelog/8279-fix-fraud-banner-display-transaction b/changelog/8279-fix-fraud-banner-display-transaction new file mode 100644 index 00000000000..8bf41b0760e --- /dev/null +++ b/changelog/8279-fix-fraud-banner-display-transaction @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Hide Fraud info banner until first transaction happens diff --git a/client/components/fraud-risk-tools-banner/index.tsx b/client/components/fraud-risk-tools-banner/index.tsx index 92b7c0429c9..04a6c3efc0e 100644 --- a/client/components/fraud-risk-tools-banner/index.tsx +++ b/client/components/fraud-risk-tools-banner/index.tsx @@ -18,7 +18,7 @@ interface BannerSettings { } const FRTDiscoverabilityBanner: React.FC = () => { - const { frtDiscoverBannerSettings } = wcpaySettings; + const { frtDiscoverBannerSettings, lifetimeTPV } = wcpaySettings; const { updateOptions } = useDispatch( 'wc/admin/options' ); const [ settings, setSettings ] = useState< BannerSettings >( () => { try { @@ -28,7 +28,7 @@ const FRTDiscoverabilityBanner: React.FC = () => { } } ); - const showBanner = ! settings.dontShowAgain; + const showBanner = lifetimeTPV > 0 && ! settings.dontShowAgain; const setDontShowAgain = () => { setSettings( { dontShowAgain: true } ); diff --git a/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap b/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap index fc16ede2d75..d22e4c5a584 100644 --- a/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap +++ b/client/components/fraud-risk-tools-banner/test/__snapshots__/index.test.tsx.snap @@ -2,6 +2,8 @@ exports[`FRTDiscoverabilityBanner does not render when dontShowAgain is true 1`] = `
`; +exports[`FRTDiscoverabilityBanner does not render when no transactions are processed 1`] = `
`; + exports[`FRTDiscoverabilityBanner renders 1`] = `
{ beforeEach( () => { global.wcpaySettings = { frtDiscoverBannerSettings: '', + lifetimeTPV: 100, }; } ); @@ -46,6 +48,20 @@ describe( 'FRTDiscoverabilityBanner', () => { frtDiscoverBannerSettings: JSON.stringify( { dontShowAgain: true, } ), + lifetimeTPV: 100, + }; + + const { container: frtBanner } = render( ); + + expect( frtBanner ).toMatchSnapshot(); + } ); + + it( 'does not render when no transactions are processed', () => { + global.wcpaySettings = { + frtDiscoverBannerSettings: JSON.stringify( { + dontShowAgain: false, + } ), + lifetimeTPV: 0, }; const { container: frtBanner } = render( ); diff --git a/client/overview/test/index.js b/client/overview/test/index.js index 1318b654b14..71277dd3f3e 100644 --- a/client/overview/test/index.js +++ b/client/overview/test/index.js @@ -264,12 +264,43 @@ describe( 'Overview page', () => { ).toBeVisible(); } ); + it( 'renders FRTDiscoverabilityBanner if store has transacted', () => { + global.wcpaySettings = { + ...global.wcpaySettings, + frtDiscoverBannerSettings: JSON.stringify( { + dontShowAgain: false, + } ), + lifetimeTPV: 100, + }; + render( ); + + expect( + screen.queryByText( 'Enhanced fraud protection for your store' ) + ).toBeInTheDocument(); + } ); + + it( 'does not render FRTDiscoverabilityBanner if store has not transacted', () => { + global.wcpaySettings = { + ...global.wcpaySettings, + frtDiscoverBannerSettings: JSON.stringify( { + dontShowAgain: false, + } ), + lifetimeTPV: 0, + }; + render( ); + + expect( + screen.queryByText( 'Enhanced fraud protection for your store' ) + ).not.toBeInTheDocument(); + } ); + it( 'dismisses the FRTDiscoverabilityBanner when dismiss button is clicked', async () => { global.wcpaySettings = { ...global.wcpaySettings, frtDiscoverBannerSettings: JSON.stringify( { dontShowAgain: false, } ), + lifetimeTPV: 100, }; render( ); @@ -287,14 +318,6 @@ describe( 'Overview page', () => { } ); } ); - it( 'renders FRTDiscoverabilityBanner', () => { - render( ); - - expect( - screen.queryByText( 'Enhanced fraud protection for your store' ) - ).toBeInTheDocument(); - } ); - it( 'displays ProgressiveOnboardingEligibilityModal if showProgressiveOnboardingEligibilityModal is true', () => { getQuery.mockReturnValue( { 'wcpay-connection-success': '1' } ); From 01e7a650a55b74ce6516fc71314f20258e9329eb Mon Sep 17 00:00:00 2001 From: Brett Shumaker Date: Tue, 16 Apr 2024 12:00:07 -0400 Subject: [PATCH 03/58] Only try to load Stripe's PaymentMethodMessagingElement on supported methods on block checkout (#8648) --- changelog/fix-only-try-to-load-pmme-on-bnpl-methods | 5 +++++ client/checkout/blocks/payment-method-label.js | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog/fix-only-try-to-load-pmme-on-bnpl-methods diff --git a/changelog/fix-only-try-to-load-pmme-on-bnpl-methods b/changelog/fix-only-try-to-load-pmme-on-bnpl-methods new file mode 100644 index 00000000000..7650d5ca075 --- /dev/null +++ b/changelog/fix-only-try-to-load-pmme-on-bnpl-methods @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: This is a quick fix to another item already in the changelog. + + diff --git a/client/checkout/blocks/payment-method-label.js b/client/checkout/blocks/payment-method-label.js index a97385df9ed..35cb4e019fa 100644 --- a/client/checkout/blocks/payment-method-label.js +++ b/client/checkout/blocks/payment-method-label.js @@ -15,6 +15,7 @@ export default ( { upeAppearanceTheme, } ) => { const cartData = wp.data.select( 'wc/store/cart' ).getCartData(); + const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ]; // Stripe expects the amount to be sent as the minor unit of 2 digits. const amount = normalizeCurrencyToMinorUnit( @@ -27,13 +28,11 @@ export default ( { cartData.billingAddress.country || window.wcBlocksCheckoutData.storeCountry; - // console.log( currentCountry ); - return ( <> { upeConfig.title } - { upeName !== 'card' && + { bnplMethods.includes( upeName ) && ( upeConfig.countries.length === 0 || upeConfig.countries.includes( currentCountry ) ) && ( <> From 1b9b52a5f51bab697acd551cb4d54ddb0cc07f0d Mon Sep 17 00:00:00 2001 From: Nagesh Pai <4162931+nagpai@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:40:26 +0530 Subject: [PATCH 04/58] Reporting: Fix spelling of `Payments` to `Payment` (#8630) Co-authored-by: Nagesh Pai --- .../update-rename-payment-activity-singular | 5 ++ .../index.tsx | 12 +-- .../payment-activity-data.tsx} | 42 +++++----- .../payment-data-tile.tsx} | 14 ++-- .../style.scss | 12 +-- .../test/__snapshots__/index.test.tsx.snap | 78 +++++++++---------- .../payment-data-tile.test.tsx.snap | 28 +++++++ .../test/index.test.tsx | 8 +- .../test/payment-data-tile.test.tsx} | 20 ++--- .../payments-data-tile.test.tsx.snap | 28 ------- client/overview/index.js | 4 +- includes/admin/class-wc-payments-admin.php | 2 +- includes/class-wc-payments-account.php | 4 +- 13 files changed, 131 insertions(+), 126 deletions(-) create mode 100644 changelog/update-rename-payment-activity-singular rename client/components/{payments-activity => payment-activity}/index.tsx (79%) rename client/components/{payments-activity/payments-activity-data.tsx => payment-activity/payment-activity-data.tsx} (69%) rename client/components/{payments-activity/payments-data-tile.tsx => payment-activity/payment-data-tile.tsx} (77%) rename client/components/{payments-activity => payment-activity}/style.scss (91%) rename client/components/{payments-activity => payment-activity}/test/__snapshots__/index.test.tsx.snap (77%) create mode 100644 client/components/payment-activity/test/__snapshots__/payment-data-tile.test.tsx.snap rename client/components/{payments-activity => payment-activity}/test/index.test.tsx (88%) rename client/components/{payments-activity/test/payments-data-tile.test.tsx => payment-activity/test/payment-data-tile.test.tsx} (86%) delete mode 100644 client/components/payments-activity/test/__snapshots__/payments-data-tile.test.tsx.snap diff --git a/changelog/update-rename-payment-activity-singular b/changelog/update-rename-payment-activity-singular new file mode 100644 index 00000000000..eb39062851c --- /dev/null +++ b/changelog/update-rename-payment-activity-singular @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This is a correction of spelling of "payment" on top of existing changes recently done for the Payment activity widget. + + diff --git a/client/components/payments-activity/index.tsx b/client/components/payment-activity/index.tsx similarity index 79% rename from client/components/payments-activity/index.tsx rename to client/components/payment-activity/index.tsx index 805e26b961d..24cbfac77d4 100644 --- a/client/components/payments-activity/index.tsx +++ b/client/components/payment-activity/index.tsx @@ -11,10 +11,10 @@ import { __ } from '@wordpress/i18n'; import EmptyStateAsset from 'assets/images/payment-activity-empty-state.svg?asset'; import interpolateComponents from '@automattic/interpolate-components'; -import PaymentsActivityData from './payments-activity-data'; +import PaymentActivityData from './payment-activity-data'; import './style.scss'; -const PaymentsActivity: React.FC = () => { +const PaymentActivity: React.FC = () => { const { lifetimeTPV } = wcpaySettings; const hasAtLeastOnePayment = lifetimeTPV > 0; @@ -25,11 +25,11 @@ const PaymentsActivity: React.FC = () => { { hasAtLeastOnePayment && <>{ /* Filters go here */ } } - + { hasAtLeastOnePayment ? ( - + ) : ( -
+

{ interpolateComponents( { @@ -54,4 +54,4 @@ const PaymentsActivity: React.FC = () => { ); }; -export default PaymentsActivity; +export default PaymentActivity; diff --git a/client/components/payments-activity/payments-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx similarity index 69% rename from client/components/payments-activity/payments-activity-data.tsx rename to client/components/payment-activity/payment-activity-data.tsx index ab1ead1e3ea..e6f82565322 100644 --- a/client/components/payments-activity/payments-activity-data.tsx +++ b/client/components/payment-activity/payment-activity-data.tsx @@ -8,29 +8,29 @@ import HelpOutlineIcon from 'gridicons/dist/help-outline'; /** * Internal dependencies. */ -import PaymentsDataTile from './payments-data-tile'; +import PaymentDataTile from './payment-data-tile'; import { ClickTooltip } from '../tooltip'; import { getAdminUrl } from 'wcpay/utils'; import './style.scss'; -const PaymentsActivityData: React.FC = () => { +const PaymentActivityData: React.FC = () => { return ( -

- + } buttonLabel={ __( - 'Total payments volume tooltip', + 'Total payment volume tooltip', 'woocommerce-payments' ) } content={ __( - 'test total payments volume content', + 'test total payment volume content', 'woocommerce-payments' ) } /> @@ -40,15 +40,15 @@ const PaymentsActivityData: React.FC = () => { path: '/payments/transactions', } ) } /> -
- + } buttonLabel={ __( 'Charges tooltip', @@ -64,8 +64,8 @@ const PaymentsActivityData: React.FC = () => { type_is: 'charge', } ) } /> - { type_is: 'refund', } ) } /> - { filter: 'awaiting_response', } ) } /> - } buttonLabel={ __( 'Fees tooltip', @@ -112,4 +112,4 @@ const PaymentsActivityData: React.FC = () => { ); }; -export default PaymentsActivityData; +export default PaymentActivityData; diff --git a/client/components/payments-activity/payments-data-tile.tsx b/client/components/payment-activity/payment-data-tile.tsx similarity index 77% rename from client/components/payments-activity/payments-data-tile.tsx rename to client/components/payment-activity/payment-data-tile.tsx index 3d2648c79b8..340b51c7e4a 100644 --- a/client/components/payments-activity/payments-data-tile.tsx +++ b/client/components/payment-activity/payment-data-tile.tsx @@ -12,7 +12,7 @@ import './style.scss'; import { formatCurrency } from 'wcpay/utils/currency'; import Loadable from '../loadable'; -interface PaymentsDataTileProps { +interface PaymentDataTileProps { /** * The id for the tile, can be used for CSS styling. */ @@ -43,7 +43,7 @@ interface PaymentsDataTileProps { reportLink?: string; } -const PaymentsDataTile: React.FC< PaymentsDataTileProps > = ( { +const PaymentDataTile: React.FC< PaymentDataTileProps > = ( { id, label, currencyCode, @@ -53,14 +53,14 @@ const PaymentsDataTile: React.FC< PaymentsDataTileProps > = ( { reportLink, } ) => { return ( -
-

+

+

{ label } { ! isLoading && tooltip }

-
+

= ( { ); }; -export default PaymentsDataTile; +export default PaymentDataTile; diff --git a/client/components/payments-activity/style.scss b/client/components/payment-activity/style.scss similarity index 91% rename from client/components/payments-activity/style.scss rename to client/components/payment-activity/style.scss index 255cb620ca6..acf8dd5a15c 100644 --- a/client/components/payments-activity/style.scss +++ b/client/components/payment-activity/style.scss @@ -13,7 +13,7 @@ line-height: 28px; } -.wcpay-payments-activity { +.wcpay-payment-activity { &__card { &__body { padding: 0 !important; @@ -26,7 +26,7 @@ } } -.wcpay-payments-activity-data { +.wcpay-payment-activity-data { display: grid; grid-template-columns: 1fr 1fr; width: 100%; @@ -36,7 +36,7 @@ padding: 24px; } - .wcpay-payments-data-highlights { + .wcpay-payment-data-highlights { display: grid; grid-template-columns: 1fr 1fr; @@ -68,7 +68,7 @@ } &:hover { - .wcpay-payments-data-highlights__item__wrapper a { + .wcpay-payment-data-highlights__item__wrapper a { opacity: 1; } } @@ -129,8 +129,8 @@ } } -#wcpay-payments-activity-data { - &__total-payments-volume { +#wcpay-payment-activity-data { + &__total-payment-volume { border-left: none; align-self: stretch; diff --git a/client/components/payments-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap similarity index 77% rename from client/components/payments-activity/test/__snapshots__/index.test.tsx.snap rename to client/components/payment-activity/test/__snapshots__/index.test.tsx.snap index b20840c408e..1320c6ca6ba 100644 --- a/client/components/payments-activity/test/__snapshots__/index.test.tsx.snap +++ b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PaymentsActivity component should render 1`] = ` +exports[`PaymentActivity component should render 1`] = `

- Total payments volume + Total payment volume

€1,563.73

@@ -82,14 +82,14 @@ exports[`PaymentsActivity component should render 1`] = `

Charges @@ -124,11 +124,11 @@ exports[`PaymentsActivity component should render 1`] = `

€3,143.00

@@ -141,22 +141,22 @@ exports[`PaymentsActivity component should render 1`] = `

Refunds

€1,532.00

@@ -169,22 +169,22 @@ exports[`PaymentsActivity component should render 1`] = `

Disputes

€47.27

@@ -197,11 +197,11 @@ exports[`PaymentsActivity component should render 1`] = `

Fees @@ -236,11 +236,11 @@ exports[`PaymentsActivity component should render 1`] = `

€94.29

@@ -266,7 +266,7 @@ exports[`PaymentsActivity component should render 1`] = `
`; -exports[`PaymentsActivity component should render an empty state 1`] = ` +exports[`PaymentActivity component should render an empty state 1`] = `
+
+

+ + Total payment volume + +

+
+

+ $0.00 +

+
+
+
+`; diff --git a/client/components/payments-activity/test/index.test.tsx b/client/components/payment-activity/test/index.test.tsx similarity index 88% rename from client/components/payments-activity/test/index.test.tsx rename to client/components/payment-activity/test/index.test.tsx index 2616e6be629..0dc16bb1737 100644 --- a/client/components/payments-activity/test/index.test.tsx +++ b/client/components/payment-activity/test/index.test.tsx @@ -7,7 +7,7 @@ import { render } from '@testing-library/react'; /** * Internal dependencies */ -import PaymentsActivity from '..'; +import PaymentActivity from '..'; declare const global: { wcpaySettings: { @@ -30,7 +30,7 @@ declare const global: { }; }; -describe( 'PaymentsActivity component', () => { +describe( 'PaymentActivity component', () => { beforeEach( () => { global.wcpaySettings = { lifetimeTPV: 1000, @@ -71,7 +71,7 @@ describe( 'PaymentsActivity component', () => { } ); it( 'should render', () => { - const { container } = render( ); + const { container } = render( ); expect( container ).toMatchSnapshot(); } ); @@ -79,7 +79,7 @@ describe( 'PaymentsActivity component', () => { it( 'should render an empty state', () => { global.wcpaySettings.lifetimeTPV = 0; - const { container, getByText } = render( ); + const { container, getByText } = render( ); expect( getByText( 'No payments…yet!' ) ).toBeInTheDocument(); expect( container ).toMatchSnapshot(); diff --git a/client/components/payments-activity/test/payments-data-tile.test.tsx b/client/components/payment-activity/test/payment-data-tile.test.tsx similarity index 86% rename from client/components/payments-activity/test/payments-data-tile.test.tsx rename to client/components/payment-activity/test/payment-data-tile.test.tsx index ada38673cd0..2409d4f634e 100644 --- a/client/components/payments-activity/test/payments-data-tile.test.tsx +++ b/client/components/payment-activity/test/payment-data-tile.test.tsx @@ -7,7 +7,7 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import PaymentsDataTile from '../payments-data-tile'; +import PaymentDataTile from '../payment-data-tile'; declare const global: { wcpaySettings: { @@ -20,7 +20,7 @@ declare const global: { }; }; -describe( 'PaymentsDataTile', () => { +describe( 'PaymentDataTile', () => { global.wcpaySettings = { accountDefaultCurrency: 'USD', zeroDecimalCurrencies: [], @@ -41,20 +41,20 @@ describe( 'PaymentsDataTile', () => { test( 'renders correctly', () => { const { container } = render( - ); expect( container ).toMatchSnapshot(); } ); test( 'renders label correctly', () => { - const label = 'Total Payments'; + const label = 'Total payment volume'; render( - @@ -67,7 +67,7 @@ describe( 'PaymentsDataTile', () => { const amount = 10000; const currencyCode = 'USD'; render( - { test( 'renders report link correctly', () => { const reportLink = 'https://example.com/report'; render( - -
-

- - Total Payments - -

-
-

- $0.00 -

-
-
-
-`; diff --git a/client/overview/index.js b/client/overview/index.js index c83d4a062aa..03d5816b879 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -20,7 +20,7 @@ import ErrorBoundary from 'components/error-boundary'; import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; import JetpackIdcNotice from 'components/jetpack-idc-notice'; import Page from 'components/page'; -import PaymentsActivity from 'wcpay/components/payments-activity'; +import PaymentActivity from 'wcpay/components/payment-activity'; import Welcome from 'components/welcome'; import { TestModeNotice } from 'components/test-mode-notice'; import InboxNotifications from './inbox-notifications'; @@ -263,7 +263,7 @@ const OverviewPage = () => { /* Show Payment Activity widget only when feature flag is set. To be removed before go live */ isPaymentOverviewWidgetEnabled && ( - + ) } diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index e93ac78686f..bb1ffc125e6 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -854,7 +854,7 @@ private function get_js_settings(): array { ], 'locale' => WC_Payments_Utils::get_language_data( get_locale() ), 'trackingInfo' => $this->account->get_tracking_info(), - 'lifetimeTPV' => $this->account->get_lifetime_total_payments_volume(), + 'lifetimeTPV' => $this->account->get_lifetime_total_payment_volume(), ]; return apply_filters( 'wcpay_js_settings', $this->wcpay_js_settings ); diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index a3177576050..f7b992a88b1 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -2104,9 +2104,9 @@ private function tracks_event( string $name, array $properties = [] ) { * * @return int The all-time total payment volume, or null if not available. */ - public function get_lifetime_total_payments_volume(): int { + public function get_lifetime_total_payment_volume(): int { $account = $this->get_cached_account_data(); - return (int) ! empty( $account ) && isset( $account['lifetime_total_payments_volume'] ) ? $account['lifetime_total_payments_volume'] : 0; + return (int) ! empty( $account ) && isset( $account['lifetime_total_payment_volume'] ) ? $account['lifetime_total_payment_volume'] : 0; } /** From 45719a0748e2e66394b2c8ebccab35a0ff46231e Mon Sep 17 00:00:00 2001 From: Jessy Pappachan <32092402+jessy-p@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:42:53 +0530 Subject: [PATCH 05/58] Use Reporting API in components (#8589) Co-authored-by: Jessy Co-authored-by: Naman Malhotra Co-authored-by: Nagesh Pai Co-authored-by: Shendy <73803630+shendy-a8c@users.noreply.github.com> Co-authored-by: Eric Jinks <3147296+Jinksi@users.noreply.github.com> --- changelog/add-8519-reporting-api-to-component | 4 + .../payment-activity-data.tsx | 66 ++++++++++++--- .../payment-activity/payment-data-tile.tsx | 4 +- .../test/__snapshots__/index.test.tsx.snap | 14 ++-- .../payment-activity/test/index.test.tsx | 7 ++ client/components/payment-activity/types.ts | 4 + client/data/index.ts | 1 + client/data/payment-activity/action-types.ts | 5 ++ client/data/payment-activity/actions.ts | 16 ++++ client/data/payment-activity/hooks.ts | 24 ++++++ client/data/payment-activity/index.ts | 12 +++ client/data/payment-activity/reducer.ts | 24 ++++++ client/data/payment-activity/resolvers.ts | 45 +++++++++++ client/data/payment-activity/selectors.ts | 11 +++ .../data/payment-activity/test/hooks.test.ts | 45 +++++++++++ .../payment-activity/test/reducer.test.ts | 30 +++++++ .../payment-activity/test/resolver.test.ts | 62 ++++++++++++++ client/data/payment-activity/types.d.ts | 30 +++++++ client/data/store.js | 5 ++ client/data/types.d.ts | 2 + ...-wc-rest-payments-reporting-controller.php | 53 ++++++++++++ includes/class-wc-payments.php | 5 ++ includes/core/server/class-request.php | 1 + .../class-get-reporting-payment-activity.php | 81 +++++++++++++++++++ .../class-wc-payments-api-client.php | 1 + 25 files changed, 533 insertions(+), 19 deletions(-) create mode 100644 changelog/add-8519-reporting-api-to-component create mode 100644 client/components/payment-activity/types.ts create mode 100644 client/data/payment-activity/action-types.ts create mode 100644 client/data/payment-activity/actions.ts create mode 100644 client/data/payment-activity/hooks.ts create mode 100644 client/data/payment-activity/index.ts create mode 100644 client/data/payment-activity/reducer.ts create mode 100644 client/data/payment-activity/resolvers.ts create mode 100644 client/data/payment-activity/selectors.ts create mode 100644 client/data/payment-activity/test/hooks.test.ts create mode 100644 client/data/payment-activity/test/reducer.test.ts create mode 100644 client/data/payment-activity/test/resolver.test.ts create mode 100644 client/data/payment-activity/types.d.ts create mode 100644 includes/admin/class-wc-rest-payments-reporting-controller.php create mode 100644 includes/core/server/request/class-get-reporting-payment-activity.php diff --git a/changelog/add-8519-reporting-api-to-component b/changelog/add-8519-reporting-api-to-component new file mode 100644 index 00000000000..be2cec2212b --- /dev/null +++ b/changelog/add-8519-reporting-api-to-component @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Not user-facing: hidden behind feature flag. Use Reporting API to fetch and populate data in the Payment Activity widget. diff --git a/client/components/payment-activity/payment-activity-data.tsx b/client/components/payment-activity/payment-activity-data.tsx index e6f82565322..0dccb682d32 100644 --- a/client/components/payment-activity/payment-activity-data.tsx +++ b/client/components/payment-activity/payment-activity-data.tsx @@ -2,6 +2,7 @@ * External dependencies */ import * as React from 'react'; +import moment from 'moment'; import { __ } from '@wordpress/i18n'; import HelpOutlineIcon from 'gridicons/dist/help-outline'; @@ -10,17 +11,43 @@ import HelpOutlineIcon from 'gridicons/dist/help-outline'; */ import PaymentDataTile from './payment-data-tile'; import { ClickTooltip } from '../tooltip'; +import { usePaymentActivityData } from 'wcpay/data'; import { getAdminUrl } from 'wcpay/utils'; +import type { DateRange } from './types'; + import './style.scss'; +/** + * This will be replaces in the future with a dynamic date range picker. + */ +const getDateRange = (): DateRange => { + return { + // Subtract 7 days from the current date. + date_start: moment() + .subtract( 7, 'd' ) + .format( 'YYYY-MM-DD\\THH:mm:ss' ), + date_end: moment().format( 'YYYY-MM-DD\\THH:mm:ss' ), + }; +}; + const PaymentActivityData: React.FC = () => { + const { paymentActivityData, isLoading } = usePaymentActivityData( + getDateRange() + ); + + const totalPaymentVolume = paymentActivityData?.total_payment_volume ?? 0; + const charges = paymentActivityData?.charges ?? 0; + const fees = paymentActivityData?.fees ?? 0; + const disputes = paymentActivityData?.disputes ?? 0; + const refunds = paymentActivityData?.refunds ?? 0; + const { storeCurrency } = wcpaySettings; + return (
{ ) } /> } + amount={ totalPaymentVolume } reportLink={ getAdminUrl( { page: 'wc-admin', path: '/payments/transactions', + 'date_between[0]': moment( + getDateRange().date_start + ).format( 'YYYY-MM-DD' ), + 'date_between[1]': moment( getDateRange().date_end ).format( + 'YYYY-MM-DD' + ), + filter: 'advanced', } ) } + isLoading={ isLoading } />
{ content={ __( 'test charge content' ) } /> } + amount={ charges } reportLink={ getAdminUrl( { page: 'wc-admin', path: '/payments/transactions', filter: 'advanced', type_is: 'charge', } ) } + isLoading={ isLoading } /> { ) } /> } + amount={ fees } + isLoading={ isLoading } />
diff --git a/client/components/payment-activity/payment-data-tile.tsx b/client/components/payment-activity/payment-data-tile.tsx index 340b51c7e4a..68a0cf7b01f 100644 --- a/client/components/payment-activity/payment-data-tile.tsx +++ b/client/components/payment-activity/payment-data-tile.tsx @@ -8,10 +8,10 @@ import { Link } from '@woocommerce/components'; /** * Internal dependencies */ -import './style.scss'; + import { formatCurrency } from 'wcpay/utils/currency'; import Loadable from '../loadable'; - +import './style.scss'; interface PaymentDataTileProps { /** * The id for the tile, can be used for CSS styling. diff --git a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap index 1320c6ca6ba..ae705b94c5d 100644 --- a/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap +++ b/client/components/payment-activity/test/__snapshots__/index.test.tsx.snap @@ -71,11 +71,11 @@ exports[`PaymentActivity component should render 1`] = ` aria-labelledby="wcpay-payment-activity-data__total-payment-volume" class="wcpay-payment-data-highlights__item__wrapper__amount" > - €1,563.73 + $0.00

View report @@ -130,7 +130,7 @@ exports[`PaymentActivity component should render 1`] = ` aria-labelledby="wcpay-payment-data-highlights__charges" class="wcpay-payment-data-highlights__item__wrapper__amount" > - €3,143.00 + $0.00

- €1,532.00 + $0.00

View report @@ -186,7 +186,7 @@ exports[`PaymentActivity component should render 1`] = ` aria-labelledby="wcpay-payment-data-highlights__disputes" class="wcpay-payment-data-highlights__item__wrapper__amount" > - €47.27 + $0.00

- €94.29 + $0.00

diff --git a/client/components/payment-activity/test/index.test.tsx b/client/components/payment-activity/test/index.test.tsx index 0dc16bb1737..241cb1b3b19 100644 --- a/client/components/payment-activity/test/index.test.tsx +++ b/client/components/payment-activity/test/index.test.tsx @@ -68,6 +68,13 @@ describe( 'PaymentActivity component', () => { }, }, }; + Date.now = jest.fn( () => + new Date( '2024-04-08T12:33:37.000Z' ).getTime() + ); + } ); + + afterEach( () => { + Date.now = () => new Date().getTime(); } ); it( 'should render', () => { diff --git a/client/components/payment-activity/types.ts b/client/components/payment-activity/types.ts new file mode 100644 index 00000000000..50276a89067 --- /dev/null +++ b/client/components/payment-activity/types.ts @@ -0,0 +1,4 @@ +export interface DateRange { + date_start: string; // Start date + date_end: string; // End date +} diff --git a/client/data/index.ts b/client/data/index.ts index 0d42f41ce93..d20938feb56 100644 --- a/client/data/index.ts +++ b/client/data/index.ts @@ -25,3 +25,4 @@ export * from './documents/hooks'; export * from './payment-intents/hooks'; export * from './authorizations/hooks'; export * from './files/hooks'; +export * from './payment-activity/hooks'; diff --git a/client/data/payment-activity/action-types.ts b/client/data/payment-activity/action-types.ts new file mode 100644 index 00000000000..149e7b3c333 --- /dev/null +++ b/client/data/payment-activity/action-types.ts @@ -0,0 +1,5 @@ +/** @format */ + +export default { + SET_PAYMENT_ACTIVITY_DATA: 'SET_PAYMENT_ACTIVITY_DATA', +}; diff --git a/client/data/payment-activity/actions.ts b/client/data/payment-activity/actions.ts new file mode 100644 index 00000000000..0d0fdc168c9 --- /dev/null +++ b/client/data/payment-activity/actions.ts @@ -0,0 +1,16 @@ +/** @format */ + +/** + * Internal Dependencies + */ +import TYPES from './action-types'; +import { PaymentActivityData, PaymentActivityAction } from './types'; + +export function updatePaymentActivity( + data: PaymentActivityData +): PaymentActivityAction { + return { + type: TYPES.SET_PAYMENT_ACTIVITY_DATA, + data, + }; +} diff --git a/client/data/payment-activity/hooks.ts b/client/data/payment-activity/hooks.ts new file mode 100644 index 00000000000..c737c3a87c6 --- /dev/null +++ b/client/data/payment-activity/hooks.ts @@ -0,0 +1,24 @@ +/** @format */ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from '../constants'; +import { PaymentActivityState, PaymentActivityQuery } from './types'; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const usePaymentActivityData = ( + query: PaymentActivityQuery +): PaymentActivityState => + useSelect( ( select ) => { + const { getPaymentActivityData, isResolving } = select( STORE_NAME ); + + return { + paymentActivityData: getPaymentActivityData( query ), + isLoading: isResolving( 'getPaymentActivityData', [ query ] ), + }; + }, [] ); diff --git a/client/data/payment-activity/index.ts b/client/data/payment-activity/index.ts new file mode 100644 index 00000000000..c524cca8b05 --- /dev/null +++ b/client/data/payment-activity/index.ts @@ -0,0 +1,12 @@ +/** @format */ + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; + +export { reducer, selectors, actions, resolvers }; +export * from './hooks'; diff --git a/client/data/payment-activity/reducer.ts b/client/data/payment-activity/reducer.ts new file mode 100644 index 00000000000..dfb53c7c95c --- /dev/null +++ b/client/data/payment-activity/reducer.ts @@ -0,0 +1,24 @@ +/** @format */ + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { PaymentActivityAction, PaymentActivityState } from './types'; + +const receivePaymentActivity = ( + state: PaymentActivityState = {}, + { type, data }: PaymentActivityAction +): PaymentActivityState => { + switch ( type ) { + case TYPES.SET_PAYMENT_ACTIVITY_DATA: + state = { + ...state, + paymentActivityData: data, + }; + break; + } + return state; +}; + +export default receivePaymentActivity; diff --git a/client/data/payment-activity/resolvers.ts b/client/data/payment-activity/resolvers.ts new file mode 100644 index 00000000000..9df0602577f --- /dev/null +++ b/client/data/payment-activity/resolvers.ts @@ -0,0 +1,45 @@ +/** @format */ + +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; +import { controls } from '@wordpress/data'; +import { addQueryArgs } from '@wordpress/url'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { NAMESPACE } from '../constants'; +import { updatePaymentActivity } from './actions'; +import { PaymentActivityData, QueryDate } from './types'; + +/** + * Retrieves payment activity data from the reporting API. + * + * @param {string} query Data on which to parameterize the selection. + */ +export function* getPaymentActivityData( + query: QueryDate +): Generator< unknown > { + const path = addQueryArgs( + `${ NAMESPACE }/reporting/payment_activity`, + query + ); + + try { + const results = yield apiFetch( { path } ); + + yield updatePaymentActivity( results as PaymentActivityData ); + } catch ( e ) { + yield controls.dispatch( + 'core/notices', + 'createErrorNotice', + __( + 'Error retrieving payment activity data.', + 'woocommerce-payments' + ) + ); + } +} diff --git a/client/data/payment-activity/selectors.ts b/client/data/payment-activity/selectors.ts new file mode 100644 index 00000000000..ab7a58201b7 --- /dev/null +++ b/client/data/payment-activity/selectors.ts @@ -0,0 +1,11 @@ +/** @format */ + +/** + * Internal Dependencies + */ +import { State } from 'wcpay/data/types'; +import { PaymentActivityData } from './types'; + +export const getPaymentActivityData = ( state: State ): PaymentActivityData => { + return state?.paymentActivity?.paymentActivityData || {}; +}; diff --git a/client/data/payment-activity/test/hooks.test.ts b/client/data/payment-activity/test/hooks.test.ts new file mode 100644 index 00000000000..a11822f17c9 --- /dev/null +++ b/client/data/payment-activity/test/hooks.test.ts @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { usePaymentActivityData } from '../hooks'; + +jest.mock( '@wordpress/data' ); + +describe( 'usePaymentActivityData', () => { + test( 'should return the correct payment activity data and loading state', () => { + const mockPaymentActivityData = { + total_payment_volume: 2500, + charges: 3000, + fees: 300, + disputes: 315, + refunds: 200, + }; + + const getPaymentActivityData = jest + .fn() + .mockReturnValue( mockPaymentActivityData ); + const isResolving = jest.fn().mockReturnValue( false ); + const select = jest.fn().mockReturnValue( { + getPaymentActivityData, + isResolving, + } ); + ( useSelect as jest.Mock ).mockImplementation( ( callback ) => + callback( select ) + ); + + const result = usePaymentActivityData( { + date_start: '2021-01-01', + date_end: '2021-01-31', + } ); + + expect( result ).toEqual( { + paymentActivityData: mockPaymentActivityData, + isLoading: false, + } ); + } ); +} ); diff --git a/client/data/payment-activity/test/reducer.test.ts b/client/data/payment-activity/test/reducer.test.ts new file mode 100644 index 00000000000..b5943426212 --- /dev/null +++ b/client/data/payment-activity/test/reducer.test.ts @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import receivePaymentActivity from '../reducer'; +import types from '../action-types'; +import { PaymentActivityData } from '../types'; + +describe( 'receivePaymentActivity', () => { + const mockPaymentActivityData: PaymentActivityData = { + total_payment_volume: 2500, + charges: 3000, + fees: 300, + disputes: 315, + refunds: 200, + }; + + test( 'should set payment activity data correctly', () => { + const initialState = {}; + const action = { + type: types.SET_PAYMENT_ACTIVITY_DATA, + data: mockPaymentActivityData, + }; + + const newState = receivePaymentActivity( initialState, action ); + + expect( newState ).toEqual( { + paymentActivityData: action.data, + } ); + } ); +} ); diff --git a/client/data/payment-activity/test/resolver.test.ts b/client/data/payment-activity/test/resolver.test.ts new file mode 100644 index 00000000000..8142b49b7c9 --- /dev/null +++ b/client/data/payment-activity/test/resolver.test.ts @@ -0,0 +1,62 @@ +/** @format */ + +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; +import { controls } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { updatePaymentActivity } from '../actions'; +import { getPaymentActivityData } from '../resolvers'; + +const query = { + date_start: '2020-04-29T04:00:00', + date_end: '2020-04-29T03:59:59', +}; + +describe( 'getPaymentActivityData resolver', () => { + const successfulResponse: any = { amount: 3000 }; + const expectedQueryString = + 'date_start=2020-04-29T04%3A00%3A00&date_end=2020-04-29T03%3A59%3A59'; + const errorResponse = new Error( + 'Error retrieving payment activity data.' + ); + + let generator: any = null; + + beforeEach( () => { + generator = getPaymentActivityData( query ); + expect( generator.next().value ).toEqual( + apiFetch( { + path: `/wc/v3/payments/reporting/payment_activity?${ expectedQueryString }`, + } ) + ); + } ); + + afterEach( () => { + expect( generator.next().done ).toStrictEqual( true ); + } ); + + describe( 'on success', () => { + test( 'should update state with payment activity data', () => { + expect( generator.next( successfulResponse ).value ).toEqual( + updatePaymentActivity( successfulResponse ) + ); + } ); + } ); + + describe( 'on error', () => { + test( 'should update state with error', () => { + expect( generator.throw( errorResponse ).value ).toEqual( + controls.dispatch( + 'core/notices', + 'createErrorNotice', + expect.any( String ) + ) + ); + } ); + } ); +} ); diff --git a/client/data/payment-activity/types.d.ts b/client/data/payment-activity/types.d.ts new file mode 100644 index 00000000000..d7c4315b16e --- /dev/null +++ b/client/data/payment-activity/types.d.ts @@ -0,0 +1,30 @@ +/** @format */ + +export interface PaymentActivityData { + total_payment_volume?: number; // Total payment volume + charges?: number; // Charges + fees?: number; // Fees + disputes?: number; // Disputes + refunds?: number; // Refunds +} + +export interface PaymentActivityState { + paymentActivityData?: PaymentActivityData; + isLoading?: boolean; +} + +export interface PaymentActivityAction { + type: string; + data: PaymentActivityData; +} + +export interface QueryDate { + date_start: string; + date_end: string; +} + +export interface PaymentActivityQuery { + date_start: string; + date_end: string; + timezone?: string; +} diff --git a/client/data/store.js b/client/data/store.js index 13e4c90733b..974efdb1e07 100644 --- a/client/data/store.js +++ b/client/data/store.js @@ -21,6 +21,7 @@ import * as documents from './documents'; import * as paymentIntents from './payment-intents'; import * as authorizations from './authorizations'; import * as files from './files'; +import * as paymentActivity from './payment-activity'; // Extracted into wrapper function to facilitate testing. export const initStore = () => @@ -39,6 +40,7 @@ export const initStore = () => paymentIntents: paymentIntents.reducer, authorizations: authorizations.reducer, files: files.reducer, + paymentActivity: paymentActivity.reducer, } ), actions: { ...deposits.actions, @@ -54,6 +56,7 @@ export const initStore = () => ...paymentIntents.actions, ...authorizations.actions, ...files.actions, + ...paymentActivity.actions, }, controls, selectors: { @@ -70,6 +73,7 @@ export const initStore = () => ...paymentIntents.selectors, ...authorizations.selectors, ...files.selectors, + ...paymentActivity.selectors, }, resolvers: { ...deposits.resolvers, @@ -85,5 +89,6 @@ export const initStore = () => ...paymentIntents.resolvers, ...authorizations.resolvers, ...files.resolvers, + ...paymentActivity.resolvers, }, } ); diff --git a/client/data/types.d.ts b/client/data/types.d.ts index ae79164fb81..ddddd37bc6e 100644 --- a/client/data/types.d.ts +++ b/client/data/types.d.ts @@ -6,9 +6,11 @@ import { CapitalState } from './capital/types'; import { PaymentIntentsState } from './payment-intents/types'; import { FilesState } from './files/types'; +import { PaymentActivityState } from './payment-activity/types'; export interface State { capital?: CapitalState; paymentIntents?: PaymentIntentsState; files?: FilesState; + paymentActivity?: PaymentActivityState; } diff --git a/includes/admin/class-wc-rest-payments-reporting-controller.php b/includes/admin/class-wc-rest-payments-reporting-controller.php new file mode 100644 index 00000000000..f2f292df683 --- /dev/null +++ b/includes/admin/class-wc-rest-payments-reporting-controller.php @@ -0,0 +1,53 @@ +namespace, + '/' . $this->rest_base . '/payment_activity', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_payment_activity' ], + 'permission_callback' => [ $this, 'check_permission' ], + ], + ] + ); + } + + /** + * Retrieve the Payment Activity data. + * + * @param WP_REST_Request $request The request. + */ + public function get_payment_activity( $request ) { + $wcpay_request = Get_Reporting_Payment_Activity::create(); + $wcpay_request->set_date_start( $request->get_param( 'date_start' ) ); + $wcpay_request->set_date_end( $request->get_param( 'date_end' ) ); + $wcpay_request->set_timezone( $request->get_param( 'timezone' ) ); + return $wcpay_request->handle_rest_request(); + } +} diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 22c21a3c4f4..c5823a9da3e 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -338,6 +338,7 @@ public static function init() { include_once __DIR__ . '/core/server/request/trait-use-test-mode-only-when-dev-mode.php'; include_once __DIR__ . '/core/server/request/class-generic.php'; include_once __DIR__ . '/core/server/request/class-get-intention.php'; + include_once __DIR__ . '/core/server/request/class-get-reporting-payment-activity.php'; include_once __DIR__ . '/core/server/request/class-get-payment-process-factors.php'; include_once __DIR__ . '/core/server/request/class-create-intention.php'; include_once __DIR__ . '/core/server/request/class-update-intention.php'; @@ -1004,6 +1005,10 @@ public static function init_rest_api() { $accounts_controller = new WC_REST_Payments_Terminal_Locations_Controller( self::$api_client ); $accounts_controller->register_routes(); + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-reporting-controller.php'; + $reporting_controller = new WC_REST_Payments_Reporting_Controller( self::$api_client ); + $reporting_controller->register_routes(); + include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-settings-controller.php'; $settings_controller = new WC_REST_Payments_Settings_Controller( self::$api_client, self::get_gateway(), self::$account ); $settings_controller->register_routes(); diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php index ddf1f042e8d..f42588573ce 100644 --- a/includes/core/server/class-request.php +++ b/includes/core/server/class-request.php @@ -162,6 +162,7 @@ abstract class Request { WC_Payments_API_Client::FRAUD_OUTCOMES_API => 'fraud_outcomes', WC_Payments_API_Client::FRAUD_RULESET_API => 'fraud_ruleset', WC_Payments_API_Client::COMPATIBILITY_API => 'compatibility', + WC_Payments_API_Client::REPORTING_API => 'reporting', ]; /** diff --git a/includes/core/server/request/class-get-reporting-payment-activity.php b/includes/core/server/request/class-get-reporting-payment-activity.php new file mode 100644 index 00000000000..d9be6cf33eb --- /dev/null +++ b/includes/core/server/request/class-get-reporting-payment-activity.php @@ -0,0 +1,81 @@ +set_param( 'date_start', $date_start ); + } + + /** + * Sets the end date for the payment activity data. + * + * @param string|null $date_end The end date in the format 'YYYY-MM-DDT00:00:00' or null. + * @return void + */ + public function set_date_end( string $date_end ) { + // TBD - validation. + $this->set_param( 'date_end', $date_end ); + } + + /** + * Sets the timezone for the reporting data. + * + * @param string|null $timezone The timezone to set or null. + * @return void + */ + public function set_timezone( ?string $timezone ) { + $this->set_param( 'timezone', $timezone ?? 'UTC' ); + } +} 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 ba66b392dec..5758cef5c08 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -79,6 +79,7 @@ class WC_Payments_API_Client { const FRAUD_OUTCOMES_API = 'fraud_outcomes'; const FRAUD_RULESET_API = 'fraud_ruleset'; const COMPATIBILITY_API = 'compatibility'; + const REPORTING_API = 'reporting/payment_activity'; /** * Common keys in API requests/responses that we might want to redact. From 166c0383432d88cdabd2e72b86c0837e011f628b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3fer=20Reykjal=C3=ADn?= <13835680+reykjalin@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:02:27 -0400 Subject: [PATCH 06/58] add currency code to cart and checkout block totals when mccy is enabled (#8636) --- ...-not-displayed-with-explicit-currency-code | 4 ++++ client/checkout/blocks/index.js | 21 ++++++++++++++++++- includes/multi-currency/MultiCurrency.php | 15 +++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-total-amounts-not-displayed-with-explicit-currency-code diff --git a/changelog/fix-total-amounts-not-displayed-with-explicit-currency-code b/changelog/fix-total-amounts-not-displayed-with-explicit-currency-code new file mode 100644 index 00000000000..beb7304b14d --- /dev/null +++ b/changelog/fix-total-amounts-not-displayed-with-explicit-currency-code @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Make sure an explicit currency code is present in the cart and checkout blocks when multi-currency is enabled diff --git a/client/checkout/blocks/index.js b/client/checkout/blocks/index.js index 5dd0bc9f0db..6baad3bdbea 100644 --- a/client/checkout/blocks/index.js +++ b/client/checkout/blocks/index.js @@ -11,7 +11,7 @@ import { /** * Internal dependencies */ -import { getUPEConfig } from 'utils/checkout'; +import { getUPEConfig, getConfig } from 'utils/checkout'; import { isLinkEnabled } from '../utils/upe'; import WCPayAPI from '../api'; import { SavedTokenHandler } from './saved-token-handler'; @@ -158,3 +158,22 @@ window.addEventListener( 'load', () => { enqueueFraudScripts( getUPEConfig( 'fraudServices' ) ); addCheckoutTracking(); } ); + +// If multi-currency is enabled, add currency code to total amount in cart and checkout blocks. +if ( getConfig( 'isMultiCurrencyEnabled' ) ) { + const { registerCheckoutFilters } = window.wc.blocksCheckout; + + const modifyTotalsPrice = ( defaultValue, extensions, args ) => { + const { cart } = args; + + if ( cart?.cartTotals?.currency_code ) { + return ` ${ cart.cartTotals.currency_code }`; + } + + return defaultValue; + }; + + registerCheckoutFilters( 'woocommerce-payments', { + totalValue: modifyTotalsPrice, + } ); +} diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php index 49c07c0fe8e..cc279200108 100644 --- a/includes/multi-currency/MultiCurrency.php +++ b/includes/multi-currency/MultiCurrency.php @@ -252,6 +252,8 @@ public function init_hooks() { add_action( 'woocommerce_created_customer', [ $this, 'set_new_customer_currency_meta' ] ); } + add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_props_to_wcpay_js_config' ] ); + $this->currency_switcher_block->init_hooks(); } @@ -388,6 +390,19 @@ public function enqueue_admin_scripts() { WC_Payments_Utils::enqueue_style( 'WCPAY_MULTI_CURRENCY_SETTINGS' ); } + /** + * Add multi-currency specific props to the WCPay JS config. + * + * @param array $config The JS config that will be loaded on the frontend. + * + * @return array The updated JS config. + */ + public function add_props_to_wcpay_js_config( $config ) { + $config['isMultiCurrencyEnabled'] = true; + + return $config; + } + /** * Wipes the cached currency data option, forcing to re-fetch the data from WPCOM. * From db3f524059c2fa08ab14ddd0cd2844f14cfa3601 Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Thu, 18 Apr 2024 12:06:57 +0200 Subject: [PATCH 07/58] Avoid updating billing details for legacy card objects (#8664) Co-authored-by: Timur Karimov --- changelog/2024-04-18-09-06-44-606607 | 4 + includes/class-wc-payment-gateway-wcpay.php | 5 +- .../test-class-wc-payment-gateway-wcpay.php | 79 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 changelog/2024-04-18-09-06-44-606607 diff --git a/changelog/2024-04-18-09-06-44-606607 b/changelog/2024-04-18-09-06-44-606607 new file mode 100644 index 00000000000..89a041cc4eb --- /dev/null +++ b/changelog/2024-04-18-09-06-44-606607 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Avoid updating billing details for legacy card objects. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 7ca12d90326..0c728fb0ff9 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -1514,7 +1514,10 @@ public function process_payment_for_order( $cart, $payment_information, $schedul if ( $payment_information->is_using_saved_payment_method() ) { $billing_details = WC_Payments_Utils::get_billing_details_from_order( $order ); - if ( ! empty( $billing_details ) ) { + $is_legacy_card_object = strpos( $payment_information->get_payment_method() ?? '', 'card_' ) === 0; + + // Not updating billing details for legacy card objects because they have a different structure and are no longer supported. + if ( ! empty( $billing_details ) && ! $is_legacy_card_object ) { $request->set_payment_method_update_data( [ 'billing_details' => $billing_details ] ); } } diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index b1dc008ac7d..1218eeef0a3 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -257,6 +257,18 @@ public function set_up() { ->method( 'get_payment_metadata' ) ->willReturn( [] ); wcpay_get_test_container()->replace( OrderService::class, $mock_order_service ); + $checkout_fields = [ + 'billing' => [ + 'billing_company' => '', + 'billing_country' => '', + 'billing_address_1' => '', + 'billing_address_2' => '', + 'billing_city' => '', + 'billing_state' => '', + 'billing_phone' => '', + ], + ]; + WC()->checkout()->checkout_fields = $checkout_fields; } /** @@ -292,6 +304,7 @@ public function tear_down() { } wcpay_get_test_container()->reset_all_replacements(); + WC()->checkout()->checkout_fields = null; } public function test_process_redirect_payment_intent_processing() { @@ -2429,6 +2442,72 @@ public function test_process_payment_for_order_not_from_request() { $this->card_gateway->process_payment_for_order( WC()->cart, $pi ); } + public function test_no_billing_details_update_for_legacy_card_object() { + $legacy_card = 'card_mock'; + + // There is no payment method data within the request. This is the case e.g. for the automatic subscription renewals. + $_POST['payment_method'] = ''; + + $token = WC_Helper_Token::create_token( $legacy_card ); + + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->add_payment_token( $token ); + $order->save(); + + $pi = new Payment_Information( $legacy_card, $order, null, $token, null, null, null, '', 'card' ); + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => 'success', + ] + ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->will( $this->returnValue( $payment_intent ) ); + + $request->expects( $this->never() ) + ->method( 'set_payment_method_update_data' ); + + $this->card_gateway->process_payment_for_order( WC()->cart, $pi ); + } + + public function test_billing_details_update_if_not_empty() { + // There is no payment method data within the request. This is the case e.g. for the automatic subscription renewals. + $_POST['payment_method'] = ''; + + $token = WC_Helper_Token::create_token( 'pm_mock' ); + + $expected_upe_payment_method = 'card'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->add_payment_token( $token ); + $order->save(); + + $pi = new Payment_Information( 'pm_mock', $order, null, $token, null, null, null, '', 'card' ); + + $payment_intent = WC_Helper_Intention::create_intention( + [ + 'status' => 'success', + ] + ); + + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->will( $this->returnValue( $payment_intent ) ); + + $request->expects( $this->once() ) + ->method( 'set_payment_method_update_data' ); + + $this->card_gateway->process_payment_for_order( WC()->cart, $pi ); + } + public function test_process_payment_for_order_rejects_with_cached_minimum_amount() { set_transient( 'wcpay_minimum_amount_usd', '50', DAY_IN_SECONDS ); From d3dc84bfff51e358e3bdb5e5f03d3f0d1432a0b7 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 18 Apr 2024 12:32:01 +0200 Subject: [PATCH 08/58] fix: BNPL announcement link (#8663) --- changelog/fix-bnpl-dialog-link | 4 ++++ client/bnpl-announcement/index.js | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog/fix-bnpl-dialog-link diff --git a/changelog/fix-bnpl-dialog-link b/changelog/fix-bnpl-dialog-link new file mode 100644 index 00000000000..412310cb58f --- /dev/null +++ b/changelog/fix-bnpl-dialog-link @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fix: BNPL announcement link. diff --git a/client/bnpl-announcement/index.js b/client/bnpl-announcement/index.js index 7a64c93d005..11d22c30051 100644 --- a/client/bnpl-announcement/index.js +++ b/client/bnpl-announcement/index.js @@ -14,6 +14,7 @@ import './style.scss'; import ConfirmationModal from 'wcpay/components/confirmation-modal'; import AfterpayBanner from 'assets/images/bnpl_announcement_afterpay.png?asset'; import ClearpayBanner from 'assets/images/bnpl_announcement_clearpay.png?asset'; +import { getAdminUrl } from 'wcpay/utils'; const BannerIcon = window.wcpayBnplAnnouncement?.accountCountry === 'GB' @@ -49,6 +50,11 @@ const Dialog = () => {
) } + + { ! isOverviewSurveySubmitted && hasAtLeastOnePayment && ( + + + + ) } ); }; diff --git a/client/components/payment-activity/survey/context.tsx b/client/components/payment-activity/survey/context.tsx new file mode 100644 index 00000000000..9919044b8e3 --- /dev/null +++ b/client/components/payment-activity/survey/context.tsx @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import React, { createContext, useState, useCallback, useContext } from 'react'; +import apiFetch from '@wordpress/api-fetch'; +import { useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { NAMESPACE } from 'data/constants'; +import type { OverviewSurveyFields } from './types'; + +type ResponseStatus = 'pending' | 'resolved' | 'error'; + +const useContextValue = ( initialState: OverviewSurveyFields = {} ) => { + const [ surveySubmitted, setSurveySubmitted ] = useState( false ); + const [ responseStatus, setResponseStatus ] = useState< ResponseStatus >( + 'resolved' + ); + const [ surveyAnswers, setSurveyAnswers ] = useState( initialState ); + + const { createErrorNotice } = useDispatch( 'core/notices' ); + + const submitSurvey = useCallback( + async ( answers: OverviewSurveyFields ) => { + setResponseStatus( 'pending' ); + try { + await apiFetch( { + path: `${ NAMESPACE }/upe_survey/payments-overview`, + method: 'POST', + data: answers, + } ); + setSurveySubmitted( true ); + setResponseStatus( 'resolved' ); + } catch ( e ) { + setResponseStatus( 'error' ); + setSurveySubmitted( false ); + createErrorNotice( + __( + 'An error occurred while submitting the survey. Please try again.', + 'woocommerce-payments' + ) + ); + } + }, + [ setResponseStatus, setSurveySubmitted, createErrorNotice ] + ); + + return { + setSurveySubmitted: submitSurvey, + responseStatus, + surveySubmitted, + surveyAnswers, + setSurveyAnswers, + }; +}; + +type ContextValue = ReturnType< typeof useContextValue >; + +const WcPayOverviewSurveyContext = createContext< ContextValue | null >( null ); + +export const WcPayOverviewSurveyContextProvider: React.FC< { + initialData?: OverviewSurveyFields; +} > = ( { children, initialData } ) => { + return ( + + { children } + + ); +}; + +export const useOverviewSurveyContext = (): ContextValue => { + const context = useContext( WcPayOverviewSurveyContext ); + if ( ! context ) { + throw new Error( 'An error occurred when using survey context' ); + } + return context; +}; + +export default WcPayOverviewSurveyContext; diff --git a/client/components/payment-activity/survey/emoticon.tsx b/client/components/payment-activity/survey/emoticon.tsx new file mode 100644 index 00000000000..67cf4d43a84 --- /dev/null +++ b/client/components/payment-activity/survey/emoticon.tsx @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import React from 'react'; +import classNames from 'classnames'; + +/** + * Internal dependencies + */ +import type { Rating } from './types'; + +const emoticons: Record< Rating, React.ReactElement > = { + 'very-unhappy': <>😞, + unhappy: <>🫤, + neutral: <>😑, + happy: <>🙂, + 'very-happy': <>😍, +}; + +interface Props { + rating: Rating; + onClick: ( event: React.MouseEvent< HTMLButtonElement > ) => void; + disabled: boolean; + isSelected: boolean; +} + +const Emoticon: React.FC< Props > = ( { + rating, + onClick, + disabled, + isSelected, +} ) => { + return ( + + ); +}; + +export default Emoticon; diff --git a/client/components/payment-activity/survey/index.tsx b/client/components/payment-activity/survey/index.tsx new file mode 100644 index 00000000000..0067159ff18 --- /dev/null +++ b/client/components/payment-activity/survey/index.tsx @@ -0,0 +1,199 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import { HorizontalRule } from '@wordpress/primitives'; +import { Button, CardFooter, TextareaControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { createInterpolateElement, useState } from '@wordpress/element'; +import { Icon, closeSmall } from '@wordpress/icons'; + +/** + * Internal dependencies. + */ +import type { Rating } from './types'; +import { useOverviewSurveyContext } from './context'; +import Emoticon from './emoticon'; +import './style.scss'; + +const Survey: React.FC = () => { + const { + responseStatus, + surveySubmitted, + surveyAnswers, + setSurveyAnswers, + setSurveySubmitted, + } = useOverviewSurveyContext(); + + const [ showComponent, setShowComponent ] = useState( true ); + + const currentRating = surveyAnswers.rating; + const ratingWithComment: Rating[] = [ + 'very-unhappy', + 'unhappy', + 'neutral', + ]; + const ratings: Rating[] = [ + 'very-unhappy', + 'unhappy', + 'neutral', + 'happy', + 'very-happy', + ]; + const showComment = + currentRating && ratingWithComment.includes( currentRating ); + const disableForm = 'pending' === responseStatus; + + const setReviewRating = function ( value?: Rating ) { + const answers = { + ...surveyAnswers, + rating: value, + }; + setSurveyAnswers( answers ); + + // If the user selects a rating that does not require a comment, submit the survey immediately. + if ( value && ! ratingWithComment.includes( value ) ) { + setSurveySubmitted( answers ); + } + }; + + if ( ! showComponent ) { + return null; + } + + if ( surveySubmitted ) { + return ( + +
+
+ + 🙌 + + { __( + 'We appreciate your feedback!', + 'woocommerce-payments' + ) } +
+ +
+ +
+
+
+ ); + } + + return ( + +
+
+ { __( + 'Are those metrics helpful?', + 'woocommerce-payments' + ) } + +
+ { ratings.map( ( rating ) => ( + setReviewRating( rating ) } + isSelected={ rating === currentRating } + /> + ) ) } +
+
+ + { showComment && ( + <> +
+ +
+ + + +
+ { + setSurveyAnswers( ( prev ) => ( { + ...prev, + comments: text, + } ) ); + } } + value={ surveyAnswers.comments ?? '' } + readOnly={ disableForm } + /> +

+ { createInterpolateElement( + __( + 'Your feedback will be only be shared with WooCommerce and treated pursuant to our privacy policy.', + 'woocommerce-payments' + ), + { + a: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + } + ) } +

+
+ +
+ + +
+ + ) } +
+
+ ); +}; +export default Survey; diff --git a/client/components/payment-activity/survey/style.scss b/client/components/payment-activity/survey/style.scss new file mode 100644 index 00000000000..33972d75bf8 --- /dev/null +++ b/client/components/payment-activity/survey/style.scss @@ -0,0 +1,56 @@ +.wcpay-payments-activity__survey { + position: relative; + width: 100%; + + .components-button.has-icon { + min-width: 32px; + height: 32px; + } + + .survey_container { + margin: 0 auto; + display: flex; + gap: 10px; + justify-content: center; + align-items: center; + + span[role='img'] { + line-height: 32px; + font-size: 20px; + } + + &__emoticons { + display: flex; + justify-content: flex-end; + + .selected { + background-color: $wp-blue-0; + } + } + + @include breakpoint( '<660px' ) { + flex-direction: column; + } + } + + .close_container { + position: absolute; + top: 0; + right: 0; + } + + hr { + margin: 16px auto; + } + + .comment_container { + max-width: 500px; + margin: 0 auto; + + &__disclaimer { + font-size: 11px; + color: $wp-gray-40; + font-style: italic; + } + } +} diff --git a/client/components/payment-activity/survey/test/__snapshots__/index.test.tsx.snap b/client/components/payment-activity/survey/test/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..aacb8aa6f5f --- /dev/null +++ b/client/components/payment-activity/survey/test/__snapshots__/index.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WcPayOverviewSurveyContextProvider test survey initial display 1`] = ` +
+ +
+`; + +exports[`WcPayOverviewSurveyContextProvider test survey with comments textbox 1`] = ` +
+