diff --git a/changelog/add-7193-dispute-cta-for-inquries b/changelog/add-7193-dispute-cta-for-inquries new file mode 100644 index 00000000000..3650c8263aa --- /dev/null +++ b/changelog/add-7193-dispute-cta-for-inquries @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Add CTA for Inquiries, behind a feature flag. + + diff --git a/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx b/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx index 18800f4c54d..394d014df33 100644 --- a/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx +++ b/client/payment-details/dispute-details/dispute-awaiting-response-details.tsx @@ -6,7 +6,8 @@ import React, { useState } from 'react'; import moment from 'moment'; import { __, sprintf } from '@wordpress/i18n'; -import { backup, edit, lock } from '@wordpress/icons'; +import { backup, edit, lock, arrowRight } from '@wordpress/icons'; +import { useDispatch } from '@wordpress/data'; import { createInterpolateElement } from '@wordpress/element'; import { Link } from '@woocommerce/components'; import { @@ -39,12 +40,121 @@ interface Props { dispute: Dispute; customer: ChargeBillingDetails | null; chargeCreated: number; + orderUrl: string | undefined; +} + +/** + * The lines of text to display in the modal to confirm acceptance / refunding of the dispute / inquiry. + */ +interface ModalLineItem { + icon: JSX.Element; + description: string | JSX.Element; +} + +interface AcceptDisputeProps { + /** + * The label for the button that opens the modal. + */ + acceptButtonLabel: string; + /** + * The event to track when the button that opens the modal is clicked. + */ + acceptButtonTracksEvent: string; + /** + * The title of the modal. + */ + modalTitle: string; + /** + * The lines of text to display in the modal. + */ + modalLines: ModalLineItem[]; + /** + * The label for the primary button in the modal to Accept / Refund the dispute / inquiry. + */ + modalButtonLabel: string; + /** + * The event to track when the primary button in the modal is clicked. + */ + modalButtonTracksEvent: string; +} + +/** + * Disputes and Inquiries have different text for buttons and the modal. + * They also have different icons and tracks events. This function returns the correct props. + * + * @param dispute + */ +function getAcceptDisputeProps( dispute: Dispute ): AcceptDisputeProps { + if ( isInquiry( dispute ) ) { + return { + acceptButtonLabel: __( 'Issue refund', 'woocommerce-payments' ), + acceptButtonTracksEvent: + wcpayTracks.events.DISPUTE_INQUIRY_REFUND_MODAL_VIEW, + modalTitle: __( 'Issue a refund?', 'woocommerce-payments' ), + modalLines: [ + { + icon: , + description: __( + 'Issuing a refund will close the inquiry, returning the amount in question back to the cardholder. No additional fees apply.', + 'woocommerce-payments' + ), + }, + { + icon: , + description: __( + 'You will be taken to the order, where you must complete the refund process manually.', + 'woocommerce-payments' + ), + }, + ], + modalButtonLabel: __( + 'View order to issue refund', + 'woocommerce-payments' + ), + modalButtonTracksEvent: + wcpayTracks.events.DISPUTE_INQUIRY_REFUND_CLICK, + }; + } + + return { + acceptButtonLabel: __( 'Accept dispute', 'woocommerce-payments' ), + acceptButtonTracksEvent: wcpayTracks.events.DISPUTE_ACCEPT_MODAL_VIEW, + modalTitle: __( 'Accept the dispute?', 'woocommerce-payments' ), + modalLines: [ + { + icon: , + description: createInterpolateElement( + sprintf( + /* translators: %s: dispute fee, : emphasis HTML element. */ + __( + 'Accepting the dispute marks it as Lost. The disputed amount will be returned to the cardholder, with a %s dispute fee deducted from your account.', + 'woocommerce-payments' + ), + getDisputeFeeFormatted( dispute, true ) ?? '-' + ), + { + em: , + } + ), + }, + { + icon: , + description: __( + 'This action is final and cannot be undone.', + 'woocommerce-payments' + ), + }, + ], + modalButtonLabel: __( 'Accept dispute', 'woocommerce-payments' ), + modalButtonTracksEvent: wcpayTracks.events.DISPUTE_ACCEPT_CLICK, + }; } const DisputeAwaitingResponseDetails: React.FC< Props > = ( { dispute, customer, chargeCreated, + orderUrl, } ) => { const { doAccept, isLoading } = useDisputeAccept( dispute ); const [ isModalOpen, setModalOpen ] = useState( false ); @@ -53,13 +163,34 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { const dueBy = moment.unix( dispute.evidence_details?.due_by ?? 0 ); const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) ); const hasStagedEvidence = dispute.evidence_details?.has_evidence; + const { createErrorNotice } = useDispatch( 'core/notices' ); // This is a temporary restriction and can be removed once steps and actions for inquiries are implemented. - const showDisputeStepsAndActions = ! isInquiry( dispute ); + const showDisputeSteps = ! isInquiry( dispute ); const onModalClose = () => { setModalOpen( false ); }; + const viewOrder = () => { + if ( orderUrl ) { + window.location.href = orderUrl; + return; + } + + createErrorNotice( + __( + 'Unable to view order. Order not found.', + 'woocommerce-payments' + ) + ); + }; + + const disputeAcceptAction = getAcceptDisputeProps( dispute ); + + const challengeButtonDefaultText = isInquiry( dispute ) + ? __( 'Submit evidence', 'woocommerce-payments' ) + : __( 'Challenge dispute', 'woocommerce-payments' ); + return (
@@ -80,7 +211,7 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { dispute={ dispute } daysRemaining={ countdownDays } /> - { showDisputeStepsAndActions && ( + { showDisputeSteps && ( = ( { /> { /* Dispute Actions */ } - { showDisputeStepsAndActions && ( + {
= ( { 'Continue with challenge', 'woocommerce-payments' ) - : __( - 'Challenge dispute', - 'woocommerce-payments' - ) } + : challengeButtonDefaultText } @@ -138,8 +266,7 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { disabled={ isLoading } onClick={ () => { wcpayTracks.recordEvent( - wcpayTracks.events - .DISPUTE_ACCEPT_MODAL_VIEW, + disputeAcceptAction.acceptButtonTracksEvent, { dispute_status: dispute.status, } @@ -147,15 +274,13 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { setModalOpen( true ); } } > - { __( - 'Accept dispute', - 'woocommerce-payments' - ) } + { disputeAcceptAction.acceptButtonLabel } + { /** Accept dispute modal */ } { isModalOpen && ( @@ -167,40 +292,19 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { ) }

- - - - - - { createInterpolateElement( - sprintf( - /* translators: %s: dispute fee, : emphasis HTML element. */ - __( - 'Accepting the dispute marks it as Lost. The disputed amount will be returned to the cardholder, with a %s dispute fee deducted from your account.', - 'woocommerce-payments' - ), - getDisputeFeeFormatted( - dispute, - true - ) ?? '-' - ), - { - em: , - } - ) } - - - - - - - - { __( - 'Accepting the dispute is final and cannot be undone.', - 'woocommerce-payments' - ) } - - + + { disputeAcceptAction.modalLines.map( + ( line, key ) => ( + + + { line.icon } + + + { line.description } + + + ) + ) } = ( { variant="primary" onClick={ () => { wcpayTracks.recordEvent( - wcpayTracks.events - .DISPUTE_ACCEPT_CLICK, + disputeAcceptAction.modalButtonTracksEvent, { dispute_status: dispute.status, } ); setModalOpen( false ); - doAccept(); + /** + * Handle the primary modal action. + * If it's an inquiry, redirect to the order page; otherwise, continue with the default dispute acceptance. + */ + if ( isInquiry( dispute ) ) { + viewOrder(); + } else { + doAccept(); + } } } > - { __( - 'Accept dispute', - 'woocommerce-payments' - ) } + { + disputeAcceptAction.modalButtonLabel + }
) }
- ) } + }
diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index 319b6adcab8..2cf9d668b75 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -470,6 +470,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { dispute={ charge.dispute } customer={ charge.billing_details } chargeCreated={ charge.created } + orderUrl={ charge.order?.url } /> ) : ( diff --git a/client/payment-details/summary/test/index.test.tsx b/client/payment-details/summary/test/index.test.tsx index 8f3647c643f..399f73ee1d8 100755 --- a/client/payment-details/summary/test/index.test.tsx +++ b/client/payment-details/summary/test/index.test.tsx @@ -50,6 +50,22 @@ jest.mock( 'wcpay/data', () => ( { } ) ), } ) ); +jest.mock( '@wordpress/data', () => ( { + createRegistryControl: jest.fn(), + dispatch: jest.fn( () => ( { + setIsMatching: jest.fn(), + onLoad: jest.fn(), + } ) ), + registerStore: jest.fn(), + select: jest.fn(), + useDispatch: jest.fn( () => ( { + createErrorNotice: jest.fn(), + } ) ), + useSelect: jest.fn( () => ( { getNotices: jest.fn() } ) ), + withDispatch: jest.fn( () => jest.fn() ), + withSelect: jest.fn( () => jest.fn() ), +} ) ); + const mockUseAuthorization = useAuthorization as jest.MockedFunction< typeof useAuthorization >; @@ -792,6 +808,29 @@ describe( 'PaymentDetailsSummary', () => { ).toBeNull(); } ); + test( 'correctly renders dispute details for "warning_needs_response" inquiry disputes', () => { + const charge = getBaseCharge(); + charge.disputed = true; + charge.dispute = getBaseDispute(); + charge.dispute.status = 'warning_needs_response'; + + renderCharge( charge ); + + // Dispute Notice + screen.getByText( + /The cardholder claims this is an unauthorized transaction/, + { ignore: '.a11y-speak-region' } + ); + + // Actions + screen.getByRole( 'button', { + name: /Submit evidence/i, + } ); + screen.getByRole( 'button', { + name: /Issue refund/i, + } ); + } ); + test( 'correctly renders dispute details for "warning_under_review" inquiry disputes', () => { const charge = getBaseCharge(); charge.disputed = true; diff --git a/client/tracks/index.js b/client/tracks/index.js index 4006c197e6e..4b065bceefd 100644 --- a/client/tracks/index.js +++ b/client/tracks/index.js @@ -71,6 +71,9 @@ const events = { DISPUTE_CHALLENGE_CLICK: 'wcpay_dispute_challenge_click', DISPUTE_ACCEPT_CLICK: 'wcpay_dispute_accept_click', DISPUTE_ACCEPT_MODAL_VIEW: 'wcpay_dispute_accept_modal_view', + DISPUTE_INQUIRY_REFUND_CLICK: 'wcpay_dispute_inquiry_refund_click', + DISPUTE_INQUIRY_REFUND_MODAL_VIEW: + 'wcpay_dispute_inquiry_refund_modal_view', ORDER_DISPUTE_NOTICE_BUTTON_CLICK: 'wcpay_order_dispute_notice_action_click', OVERVIEW_BALANCES_CURRENCY_CLICK: