Skip to content

Commit

Permalink
Add Transaction Details dispute footer for disputes not awaiting a re…
Browse files Browse the repository at this point in the history
…sponse (#7047)

Co-authored-by: Bruce Aldridge <[email protected]>
Co-authored-by: Shendy <[email protected]>
Co-authored-by: Shendy Kurnia <[email protected]>
  • Loading branch information
4 people authored Sep 19, 2023
1 parent 1664440 commit e355f04
Show file tree
Hide file tree
Showing 12 changed files with 577 additions and 222 deletions.
4 changes: 4 additions & 0 deletions changelog/add-6927-dispute-details-not-awaiting-response
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: add

Behind a feature flag: dispute message added to transactions screen for disputes not needing a response.
29 changes: 29 additions & 0 deletions client/disputes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import type {
DisputeStatus,
EvidenceDetails,
} from 'wcpay/types/disputes';
import type { BalanceTransaction } from 'wcpay/types/balance-transactions';
import {
disputeAwaitingResponseStatuses,
disputeUnderReviewStatuses,
} from 'wcpay/disputes/filters/config';
import { formatExplicitCurrency } from 'wcpay/utils/currency';

interface IsDueWithinProps {
dueBy: CachedDispute[ 'due_by' ] | EvidenceDetails[ 'due_by' ];
Expand Down Expand Up @@ -69,3 +71,30 @@ export const isInquiry = ( dispute: Dispute | CachedDispute ): boolean => {
// Inquiry dispute statuses are one of `warning_needs_response`, `warning_under_review` or `warning_closed`.
return dispute.status.startsWith( 'warning' );
};

/**
* Returns the dispute fee balance transaction for a dispute if it exists.
*/
export const getDisputeFee = (
dispute: Dispute
): BalanceTransaction | undefined => {
const disputeFee = dispute.balance_transactions.find(
( transaction ) => transaction.reporting_category === 'dispute'
);
return disputeFee;
};

/**
* Returns the dispute fee balance transaction for a dispute if it exists
* formatted as a currency string.
*/
export const getDisputeFeeFormatted = (
dispute: Dispute
): string | undefined => {
const disputeFee = getDisputeFee( dispute );

return (
disputeFee &&
formatExplicitCurrency( disputeFee.fee, disputeFee.currency )
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import DisputeSummaryRow from './dispute-summary-row';
import InlineNotice from 'components/inline-notice';
import './style.scss';

interface DisputeDetailsProps {
interface Props {
dispute: Dispute;
}

const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => {
const DisputeAwaitingResponseDetails: React.FC< Props > = ( { dispute } ) => {
const now = moment();
const dueBy = moment.unix( dispute.evidence_details?.due_by ?? 0 );
const countdownDays = Math.floor( dueBy.diff( now, 'days', true ) );
Expand Down Expand Up @@ -67,4 +67,4 @@ const DisputeDetails: React.FC< DisputeDetailsProps > = ( { dispute } ) => {
);
};

export default DisputeDetails;
export default DisputeAwaitingResponseDetails;
292 changes: 292 additions & 0 deletions client/payment-details/dispute-details/dispute-resolution-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
/**
* External dependencies
*/
import React from 'react';
import moment from 'moment';
import { dateI18n } from '@wordpress/date';
import { __, sprintf } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
import { createInterpolateElement } from '@wordpress/element';
import { Button, CardFooter, Flex, FlexItem } from '@wordpress/components';

/**
* Internal dependencies
*/
import type { Dispute } from 'wcpay/types/disputes';
import wcpayTracks from 'tracks';
import { getAdminUrl } from 'wcpay/utils';
import { getDisputeFeeFormatted, isInquiry } from 'wcpay/disputes/utils';
import './style.scss';

const DisputeUnderReviewFooter: React.FC< {
dispute: Dispute;
} > = ( { dispute } ) => {
const submissionDateFormatted = dispute.metadata.__evidence_submitted_at
? dateI18n(
'M j, Y',
moment
.unix(
parseInt( dispute.metadata.__evidence_submitted_at, 10 )
)
.toISOString()
)
: '-';

return (
<CardFooter className="transaction-details-dispute-footer transaction-details-dispute-footer--primary">
<Flex justify="space-between">
<FlexItem>
{ createInterpolateElement(
sprintf(
isInquiry( dispute )
? /* Translators: %s - formatted date, <a> - link to documentation page */
__(
'You submitted evidence for this inquiry on %s. The cardholder’s bank is reviewing the case, which can take 120 days or more. You will be alerted when they make their final decision. <a>Learn more</a>.',
'woocommerce-payments'
)
: /* Translators: %s - formatted date, <a> - link to documentation page */
__(
'You submitted evidence for this dispute on %s. The cardholder’s bank is reviewing the case, which can take 60 days or more. You will be alerted when they make their final decision. <a>Learn more about the dispute process</a>.',
'woocommerce-payments'
),
submissionDateFormatted
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
<a
target="_blank"
rel="noopener noreferrer"
href="https://woocommerce.com/document/woopayments/fraud-and-disputes/"
/>
),
}
) }
</FlexItem>
<FlexItem className="transaction-details-dispute-footer__actions">
<Link
href={ getAdminUrl( {
page: 'wc-admin',
path: '/payments/disputes/challenge',
id: dispute?.id,
} ) }
>
<Button
variant="secondary"
onClick={ () => {
wcpayTracks.recordEvent(
wcpayTracks.events
.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICK,
{
dispute_status: dispute.status,
}
);
} }
>
{ __(
'View submitted evidence',
'woocommerce-payments'
) }
</Button>
</Link>
</FlexItem>
</Flex>
</CardFooter>
);
};

const DisputeWonFooter: React.FC< {
dispute: Dispute;
} > = ( { dispute } ) => {
const closedDateFormatted = dispute?.metadata.__dispute_closed_at
? dateI18n(
'M j, Y',
moment
.unix(
parseInt( dispute.metadata.__dispute_closed_at, 10 )
)
.toISOString()
)
: '-';

return (
<CardFooter className="transaction-details-dispute-footer">
<Flex justify="space-between">
<FlexItem>
{ createInterpolateElement(
sprintf(
/* Translators: %s - formatted date, <a> - link to documentation page */
__(
'Good news! You won this dispute on %s. The disputed amount and the dispute fee have been credited back to your account. <a>Learn more about preventing disputes</a>.',
'woocommerce-payments'
),
closedDateFormatted
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
<a
target="_blank"
rel="noopener noreferrer"
href="https://woocommerce.com/document/woopayments/fraud-and-disputes/"
/>
),
}
) }
</FlexItem>
<FlexItem className="transaction-details-dispute-footer__actions">
<Link
href={ getAdminUrl( {
page: 'wc-admin',
path: '/payments/disputes/challenge',
id: dispute?.id,
} ) }
>
<Button
variant="secondary"
onClick={ () => {
wcpayTracks.recordEvent(
wcpayTracks.events
.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICK,
{
dispute_status: dispute.status,
}
);
} }
>
{ __(
'View dispute details',
'woocommerce-payments'
) }
</Button>
</Link>
</FlexItem>
</Flex>
</CardFooter>
);
};

const DisputeLostFooter: React.FC< {
dispute: Dispute;
} > = ( { dispute } ) => {
const isSubmitted = !! dispute?.metadata.__evidence_submitted_at;
const isAccepted = dispute?.metadata.__closed_by_merchant === '1';
const disputeFeeFormatted = getDisputeFeeFormatted( dispute ) ?? '-';

const closedDateFormatted = dispute?.metadata.__dispute_closed_at
? dateI18n(
'M j, Y',
moment
.unix(
parseInt( dispute.metadata.__dispute_closed_at, 10 )
)
.toISOString()
)
: '-';

let messagePrefix = sprintf(
/* Translators: %1$s - formatted date */
__(
'This dispute was lost on %1$s due to non-response.',
'woocommerce-payments'
),
closedDateFormatted
);

if ( isAccepted ) {
messagePrefix = sprintf(
/* Translators: %1$s - formatted date */
__(
'This dispute was accepted and lost on %1$s.',
'woocommerce-payments'
),
closedDateFormatted
);
}

if ( isSubmitted ) {
messagePrefix = sprintf(
/* Translators: %1$s - formatted date */
__( 'This dispute was lost on %1$s.', 'woocommerce-payments' ),
closedDateFormatted
);
}

return (
<CardFooter className="transaction-details-dispute-footer">
<Flex justify="space-between">
<FlexItem>
{ messagePrefix }{ ' ' }
{ createInterpolateElement(
sprintf(
/* Translators: %1$s – the formatted dispute fee amount, <a> - link to documentation page */
__(
'The %1$s fee has been deducted from your account, and the disputed amount returned to the cardholder. <a>Learn more about preventing disputes</a>.',
'woocommerce-payments'
),
disputeFeeFormatted
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content -- Link content is provided by createInterpolateElement
<a
target="_blank"
rel="noopener noreferrer"
href="https://woocommerce.com/document/woopayments/fraud-and-disputes/"
/>
),
}
) }
</FlexItem>

{ isSubmitted && (
<FlexItem className="transaction-details-dispute-footer__actions">
<Link
href={ getAdminUrl( {
page: 'wc-admin',
path: '/payments/disputes/challenge',
id: dispute?.id,
} ) }
>
<Button
variant="secondary"
onClick={ () => {
wcpayTracks.recordEvent(
wcpayTracks.events
.PAYMENT_DETAILS_VIEW_DISPUTE_EVIDENCE_BUTTON_CLICK,
{
dispute_status: dispute.status,
}
);
} }
>
{ __(
'View dispute details',
'woocommerce-payments'
) }
</Button>
</Link>
</FlexItem>
) }
</Flex>
</CardFooter>
);
};

const DisputeResolutionFooter: React.FC< {
dispute: Dispute;
} > = ( { dispute } ) => {
if ( dispute.status === 'under_review' ) {
return <DisputeUnderReviewFooter dispute={ dispute } />;
}
if ( dispute.status === 'won' ) {
return <DisputeWonFooter dispute={ dispute } />;
}
if ( dispute.status === 'lost' ) {
return <DisputeLostFooter dispute={ dispute } />;
}

return null;
};

export default DisputeResolutionFooter;
23 changes: 23 additions & 0 deletions client/payment-details/dispute-details/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@
}
}

.transaction-details-dispute-footer {
background-color: #f2f4f5;

&__actions {
flex-shrink: 0;
}

&--primary {
background-color: $wp-blue-0;
}

@media screen and ( max-width: $break-small ) {
.components-flex {
flex-direction: column;
align-items: flex-start;
}

.components-flex-item {
margin: 10px 0;
}
}
}

.dispute-evidence {
// Override WordPress core PanelBody boxy styles. Ours is more inline content.
&.components-panel__body {
Expand Down
Loading

0 comments on commit e355f04

Please sign in to comment.