diff --git a/changelog/fix-7437-refactor-order-dispute-side-effects b/changelog/fix-7437-refactor-order-dispute-side-effects
new file mode 100644
index 00000000000..5e596f4d817
--- /dev/null
+++ b/changelog/fix-7437-refactor-order-dispute-side-effects
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Fire a tracks event for disputed order notice view.
diff --git a/client/components/disputed-order-notice/index.js b/client/components/disputed-order-notice/index.js
new file mode 100644
index 00000000000..13ea7af71e5
--- /dev/null
+++ b/client/components/disputed-order-notice/index.js
@@ -0,0 +1,234 @@
+import moment from 'moment';
+import React, { useEffect } from 'react';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { dateI18n } from '@wordpress/date';
+import { createInterpolateElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import InlineNotice from 'wcpay/components/inline-notice';
+import { formatExplicitCurrency } from 'utils/currency';
+import { reasons } from 'wcpay/disputes/strings';
+import { getDetailsURL } from 'wcpay/components/details-link';
+import {
+ isAwaitingResponse,
+ isInquiry,
+ isUnderReview,
+} from 'wcpay/disputes/utils';
+import { useCharge } from 'wcpay/data';
+import wcpayTracks from 'tracks';
+import './style.scss';
+
+const DisputedOrderNoticeHandler = ( { chargeId, onDisableOrderRefund } ) => {
+ const { data: charge } = useCharge( chargeId );
+ const disputeDetailsUrl = getDetailsURL( chargeId, 'transactions' );
+
+ // Disable the refund button if there's an active dispute.
+ useEffect( () => {
+ const { dispute } = charge;
+ if ( ! charge?.dispute ) {
+ return;
+ }
+ // Refunds are only allowed if the dispute is an inquiry or if it's won.
+ const isRefundable =
+ isInquiry( dispute ) || [ 'won' ].includes( dispute.status );
+ if ( ! isRefundable ) {
+ onDisableOrderRefund( dispute.status );
+ }
+ }, [ charge, onDisableOrderRefund ] );
+
+ const { dispute } = charge;
+ if ( ! charge?.dispute ) {
+ return null;
+ }
+ const isRefundable =
+ isInquiry( dispute ) || [ 'won' ].includes( dispute.status );
+
+ // Special case the dispute "under review" notice which is much simpler.
+ // (And return early.)
+ if ( isUnderReview( dispute.status ) && ! isInquiry( dispute ) ) {
+ return (
+
+ );
+ }
+
+ // Special case lost disputes.
+ // (And return early.)
+ // I suspect this is unnecessary, as any lost disputes will have already been
+ // refunded as part of `charge.dispute.closed` webhook handler.
+ // This may be dead code. Leaving in for now as this is consistent with
+ // the logic before this PR.
+ // https://github.com/Automattic/woocommerce-payments/pull/7557
+ if ( dispute.status === 'lost' && ! isRefundable ) {
+ return (
+
+ );
+ }
+
+ // Only show the notice if the dispute is awaiting a response.
+ if ( ! isAwaitingResponse( dispute.status ) ) {
+ return null;
+ }
+
+ // Bail if we don't have due_by for whatever reason.
+ if ( ! dispute.evidence_details?.due_by ) {
+ return null;
+ }
+
+ const now = moment();
+ const dueBy = moment.unix( dispute.evidence_details?.due_by );
+
+ // If the dispute is due in the past, don't show notice.
+ if ( ! now.isBefore( dueBy ) ) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+const DisputeNeedsResponseNotice = ( {
+ disputeReason,
+ formattedAmount,
+ isPreDisputeInquiry,
+ dueBy,
+ countdownDays,
+ disputeDetailsUrl,
+} ) => {
+ useEffect( () => {
+ wcpayTracks.recordEvent( 'wcpay_order_dispute_notice_view', {
+ is_inquiry: isPreDisputeInquiry,
+ dispute_reason: disputeReason,
+ due_by_days: countdownDays,
+ } );
+ }, [ isPreDisputeInquiry, disputeReason, countdownDays ] );
+
+ const titleStrings = {
+ // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
+ dispute_default: __(
+ // eslint-disable-next-line max-len
+ 'This order has been disputed in the amount of %1$s. The customer provided the following reason: %2$s. Please respond to this dispute before %3$s.',
+ 'woocommerce-payments'
+ ),
+ // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
+ inquiry_default: __(
+ // eslint-disable-next-line max-len
+ 'The card network involved in this order has opened an inquiry into the transaction with the following reason: %2$s. Please respond to this inquiry before %3$s, just like you would for a formal dispute.',
+ 'woocommerce-payments'
+ ),
+ // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
+ dispute_urgent: __(
+ 'Please resolve the dispute on this order for %1$s labeled "%2$s" by %3$s.',
+ 'woocommerce-payments'
+ ),
+ // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
+ inquiry_urgent: __(
+ 'Please resolve the inquiry on this order for %1$s labeled "%2$s" by %3$s.',
+ 'woocommerce-payments'
+ ),
+ };
+
+ let buttonLabel = __( 'Respond now', 'woocommerce-payments' );
+ let suffix = '';
+
+ let titleText = isPreDisputeInquiry
+ ? titleStrings.inquiry_default
+ : titleStrings.dispute_default;
+
+ // If the dispute is due within 7 days, adjust wording and highlight urgency.
+ if ( countdownDays < 7 ) {
+ titleText = isPreDisputeInquiry
+ ? titleStrings.inquiry_urgent
+ : titleStrings.dispute_urgent;
+
+ suffix = sprintf(
+ // Translators: %s is the number of days left to respond to the dispute.
+ _n(
+ '(%s day left)',
+ '(%s days left)',
+ countdownDays,
+ 'woocommerce-payments'
+ ),
+ countdownDays
+ );
+ }
+
+ const title = sprintf(
+ titleText,
+ formattedAmount,
+ reasons[ disputeReason ].display,
+ dateI18n( 'M j, Y', dueBy.local().toISOString() )
+ );
+
+ if ( countdownDays < 1 ) {
+ buttonLabel = __( 'Respond today', 'woocommerce-payments' );
+ suffix = __( '(Last day today)', 'woocommerce-payments' );
+ }
+
+ return (
+ {
+ wcpayTracks.recordEvent(
+ 'wcpay_order_dispute_notice_action_click',
+ {
+ due_by_days: countdownDays,
+ }
+ );
+ window.location = disputeDetailsUrl;
+ },
+ },
+ ] }
+ >
+ { { `${ title } ${ suffix }` } }
+
+ );
+};
+
+const DisputeOrderLockedNotice = ( { message, disputeDetailsUrl } ) => {
+ return (
+
+ { message }
+ { createInterpolateElement(
+ __( ' View details', 'woocommerce-payments' ),
+ {
+ // createInterpolateElement is incompatible with this eslint rule as the is decoupled from content.
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+ a: ,
+ }
+ ) }
+
+ );
+};
+
+export default DisputedOrderNoticeHandler;
diff --git a/client/order/style.scss b/client/components/disputed-order-notice/style.scss
similarity index 100%
rename from client/order/style.scss
rename to client/components/disputed-order-notice/style.scss
diff --git a/client/order/index.js b/client/order/index.js
index 42184ae92e8..4c983955c10 100644
--- a/client/order/index.js
+++ b/client/order/index.js
@@ -1,30 +1,58 @@
/* global jQuery */
-import { __, _n, sprintf } from '@wordpress/i18n';
-import { dateI18n } from '@wordpress/date';
import ReactDOM from 'react-dom';
+import React from 'react';
+import { __ } from '@wordpress/i18n';
import { dispatch } from '@wordpress/data';
-import moment from 'moment';
-import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { getConfig } from 'utils/order';
+import { isAwaitingResponse, isUnderReview } from 'wcpay/disputes/utils';
import RefundConfirmationModal from './refund-confirm-modal';
import CancelConfirmationModal from './cancel-confirm-modal';
-import InlineNotice from 'components/inline-notice';
-import { formatExplicitCurrency } from 'utils/currency';
-import { reasons } from 'wcpay/disputes/strings';
-import { getDetailsURL } from 'wcpay/components/details-link';
-import {
- isAwaitingResponse,
- isInquiry,
- isUnderReview,
-} from 'wcpay/disputes/utils';
-import { useCharge } from 'wcpay/data';
-import wcpayTracks from 'tracks';
-import './style.scss';
+import DisputedOrderNoticeHandler from 'wcpay/components/disputed-order-notice';
+
+function disableWooOrderRefundButton( disputeStatus ) {
+ const refundButton = document.querySelector( 'button.refund-items' );
+ if ( ! refundButton ) {
+ return;
+ }
+
+ refundButton.disabled = true;
+
+ // Show helpful info in order edit lock icon tooltip.
+
+ let tooltipText = '';
+ if ( isAwaitingResponse( disputeStatus ) ) {
+ tooltipText = __(
+ 'Refunds and order editing are disabled during disputes.',
+ 'woocommerce-payments'
+ );
+ } else if ( isUnderReview( disputeStatus ) ) {
+ tooltipText = __(
+ 'Refunds and order editing are disabled during an active dispute.',
+ 'woocommerce-payments'
+ );
+ } else if ( disputeStatus === 'lost' ) {
+ tooltipText = __(
+ 'Refunds and order editing have been disabled as a result of a lost dispute.',
+ 'woocommerce-payments'
+ );
+ }
+
+ jQuery( refundButton )
+ .parent()
+ .find( '.woocommerce-help-tip' )
+ .attr( {
+ // jQuery.tipTip uses the title attribute to generate the tooltip.
+ title: tooltipText,
+ 'aria-label': tooltipText,
+ } )
+ // Regenerate the tipTip tooltip.
+ .tipTip();
+}
jQuery( function ( $ ) {
const disableManualRefunds = getConfig( 'disableManualRefunds' ) ?? false;
@@ -135,221 +163,12 @@ jQuery( function ( $ ) {
return;
}
- ReactDOM.render( , container );
+ ReactDOM.render(
+ ,
+ container
+ );
}
} );
-
-const DisputeNotice = ( { chargeId } ) => {
- const { data: charge } = useCharge( chargeId );
-
- if ( ! charge?.dispute ) {
- return null;
- }
-
- const { dispute } = charge;
-
- let urgency = 'warning';
- let actions;
-
- // Refunds are only allowed if the dispute is an inquiry or if it's won.
- const isRefundable =
- isInquiry( dispute ) || [ 'won' ].includes( dispute.status );
- const shouldDisableRefund = ! isRefundable;
- let disableRefund = false;
-
- let refundDisabledNotice = '';
- if ( shouldDisableRefund ) {
- const refundButton = document.querySelector( 'button.refund-items' );
- if ( refundButton ) {
- disableRefund = true;
-
- // Disable the refund button.
- refundButton.disabled = true;
-
- const disputeDetailsLink = getDetailsURL(
- chargeId,
- 'transactions'
- );
-
- let tooltipText = '';
-
- if ( isAwaitingResponse( dispute.status ) ) {
- refundDisabledNotice = __(
- 'Refunds and order editing are disabled during disputes.',
- 'woocommerce-payments'
- );
- tooltipText = refundDisabledNotice;
- } else if ( isUnderReview( dispute.status ) ) {
- refundDisabledNotice = createInterpolateElement(
- __(
- // eslint-disable-next-line max-len
- 'This order has an active payment dispute. Refunds and order editing are disabled during this time. View details',
- 'woocommerce-payments'
- ),
- {
- // eslint-disable-next-line jsx-a11y/anchor-has-content
- a: ,
- }
- );
- tooltipText = __(
- 'Refunds and order editing are disabled during an active dispute.',
- 'woocommerce-payments'
- );
- } else if ( dispute.status === 'lost' ) {
- refundDisabledNotice = createInterpolateElement(
- __(
- 'Refunds and order editing have been disabled as a result of a lost dispute. View details',
- 'woocommerce-payments'
- ),
- {
- // eslint-disable-next-line jsx-a11y/anchor-has-content
- a: ,
- }
- );
- tooltipText = __(
- 'Refunds and order editing have been disabled as a result of a lost dispute.',
- 'woocommerce-payments'
- );
- }
-
- // Change refund tooltip's text copy.
- jQuery( refundButton )
- .parent()
- .find( '.woocommerce-help-tip' )
- .attr( {
- // jQuery.tipTip uses the title attribute to generate the tooltip.
- title: tooltipText,
- 'aria-label': tooltipText,
- } )
- // Regenerate the tipTip tooltip.
- .tipTip();
- }
- }
-
- let showWarning = false;
- let warningText = '';
-
- if (
- dispute.evidence_details?.due_by &&
- // Only show the notice if the dispute is awaiting a response.
- isAwaitingResponse( dispute.status )
- ) {
- const now = moment();
- const dueBy = moment.unix( dispute.evidence_details?.due_by );
- const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) );
-
- // If the dispute is due in the past, we don't want to show the notice.
- if ( now.isBefore( dueBy ) ) {
- showWarning = true;
-
- const titleStrings = {
- // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
- dispute_default: __(
- // eslint-disable-next-line max-len
- 'This order has been disputed in the amount of %1$s. The customer provided the following reason: %2$s. Please respond to this dispute before %3$s.',
- 'woocommerce-payments'
- ),
- // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
- inquiry_default: __(
- // eslint-disable-next-line max-len
- 'The card network involved in this order has opened an inquiry into the transaction with the following reason: %2$s. Please respond to this inquiry before %3$s, just like you would for a formal dispute.',
- 'woocommerce-payments'
- ),
- // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
- dispute_urgent: __(
- 'Please resolve the dispute on this order for %1$s labeled "%2$s" by %3$s.',
- 'woocommerce-payments'
- ),
- // Translators: %1$s is the formatted dispute amount, %2$s is the dispute reason, %3$s is the due date.
- inquiry_urgent: __(
- 'Please resolve the inquiry on this order for %1$s labeled "%2$s" by %3$s.',
- 'woocommerce-payments'
- ),
- };
- const amountFormatted = formatExplicitCurrency(
- dispute.amount,
- dispute.currency
- );
-
- let buttonLabel = __( 'Respond now', 'woocommerce-payments' );
- let suffix = '';
-
- let titleText = isInquiry( dispute )
- ? titleStrings.inquiry_default
- : titleStrings.dispute_default;
-
- // If the dispute is due within 7 days, use different wording.
- if ( countdownDays < 7 ) {
- titleText = isInquiry( dispute )
- ? titleStrings.inquiry_urgent
- : titleStrings.dispute_urgent;
-
- suffix = sprintf(
- // Translators: %s is the number of days left to respond to the dispute.
- _n(
- '(%s day left)',
- '(%s days left)',
- countdownDays,
- 'woocommerce-payments'
- ),
- countdownDays
- );
- }
-
- const title = sprintf(
- titleText,
- amountFormatted,
- reasons[ dispute.reason ].display,
- dateI18n( 'M j, Y', dueBy.local().toISOString() )
- );
-
- // If the dispute is due within 72 hours, we want to highlight it as urgent/red.
- if ( countdownDays < 3 ) {
- urgency = 'error';
- }
-
- if ( countdownDays < 1 ) {
- buttonLabel = __( 'Respond today', 'woocommerce-payments' );
- suffix = __( '(Last day today)', 'woocommerce-payments' );
- }
-
- actions = [
- {
- label: buttonLabel,
- variant: 'secondary',
- onClick: () => {
- wcpayTracks.recordEvent(
- wcpayTracks.events
- .ORDER_DISPUTE_NOTICE_BUTTON_CLICK,
- {
- due_by_days: parseInt( countdownDays, 10 ),
- }
- );
- window.location = getDetailsURL(
- chargeId,
- 'transactions'
- );
- },
- },
- ];
-
- warningText = `${ title } ${ suffix }`;
- }
- }
-
- if ( ! showWarning && ! disableRefund ) {
- return null;
- }
-
- return (
-
- { showWarning && { warningText } }
-
- { disableRefund && { refundDisabledNotice }
}
-
- );
-};
diff --git a/client/tracks/index.js b/client/tracks/index.js
index 866b3106a46..c02615f781d 100644
--- a/client/tracks/index.js
+++ b/client/tracks/index.js
@@ -76,8 +76,6 @@ const events = {
DISPUTE_INQUIRY_REFUND_MODAL_VIEW:
'wcpay_dispute_inquiry_refund_modal_view',
GOOGLEPAY_BUTTON_CLICK: 'gpay_button_click',
- ORDER_DISPUTE_NOTICE_BUTTON_CLICK:
- 'wcpay_order_dispute_notice_action_click',
OVERVIEW_BALANCES_CURRENCY_CLICK:
'wcpay_overview_balances_currency_tab_click',
OVERVIEW_DEPOSITS_VIEW_HISTORY_CLICK: