Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction Details → Show steps to resolve for inquiries #7292

Merged
merged 16 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: add
Comment: Behind feature flag: add steps to resolve section to the transaction dispute details


Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { getAdminUrl } from 'wcpay/utils';
import DisputeNotice from './dispute-notice';
import IssuerEvidenceList from './evidence-list';
import DisputeSummaryRow from './dispute-summary-row';
import DisputeSteps from './dispute-steps';
import { DisputeSteps, InquirySteps } from './dispute-steps';
import InlineNotice from 'components/inline-notice';
import './style.scss';

Expand All @@ -53,8 +53,8 @@ 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;
// This is a temporary restriction and can be removed once steps and actions for inquiries are implemented.
const showDisputeStepsAndActions = ! isInquiry( dispute );
// This is a temporary restriction and can be removed once actions for inquiries are implemented.
Jinksi marked this conversation as resolved.
Show resolved Hide resolved
const showDisputeActions = ! isInquiry( dispute );

const onModalClose = () => {
setModalOpen( false );
Expand All @@ -76,24 +76,28 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( {
) }
</InlineNotice>
) }
<DisputeSummaryRow
dispute={ dispute }
daysRemaining={ countdownDays }
/>
{ showDisputeStepsAndActions && (
<DisputeSummaryRow dispute={ dispute } />

{ isInquiry( dispute ) ? (
<InquirySteps
dispute={ dispute }
customer={ customer }
chargeCreated={ chargeCreated }
/>
) : (
<DisputeSteps
dispute={ dispute }
customer={ customer }
chargeCreated={ chargeCreated }
daysRemaining={ countdownDays }
/>
) }

<IssuerEvidenceList
issuerEvidence={ dispute.issuer_evidence }
/>

{ /* Dispute Actions */ }
{ showDisputeStepsAndActions && (
{ showDisputeActions && (
<div className="transaction-details-dispute-details-body__actions">
<Link
href={
Expand Down
57 changes: 57 additions & 0 deletions client/payment-details/dispute-details/dispute-due-by-date.tsx
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This UI component <DisputeDueByDate /> is now shared by 3 components on this screen, so I've moved it to it's own component file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* External dependencies
*/
import React from 'react';
import { dateI18n } from '@wordpress/date';
import { __, _n, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import moment from 'moment';

/**
* Internal dependencies
*/
import type { EvidenceDetails } from 'wcpay/types/disputes';

const DisputeDueByDate: React.FC< {
Jinksi marked this conversation as resolved.
Show resolved Hide resolved
dueBy: EvidenceDetails[ 'due_by' ];
Jinksi marked this conversation as resolved.
Show resolved Hide resolved
} > = ( { dueBy } ) => {
const daysRemaining = Math.floor(
moment.unix( dueBy ).diff( moment(), 'days', true )
);
const respondByDate = dateI18n(
'M j, Y, g:ia',
moment( dueBy * 1000 ).toISOString()
);
return (
<span className="dispute-steps__steps__response-date">
{ respondByDate }
<span
className={ classNames( {
'dispute-steps__steps__response-date--urgent':
daysRemaining < 3,
'dispute-steps__steps__response-date--warning':
daysRemaining < 7 && daysRemaining > 2,
} ) }
>
{ daysRemaining > 0 &&
sprintf(
// Translators: %s is the number of days left to respond to the dispute.
_n(
'(%s day left to respond)',
'(%s days left to respond)',
daysRemaining,
Jinksi marked this conversation as resolved.
Show resolved Hide resolved
'woocommerce-payments'
),
daysRemaining
) }

{ daysRemaining === 0 &&
__( '(Last day today)', 'woocommerce-payments' ) }
{ daysRemaining < 0 &&
__( '(Past due)', 'woocommerce-payments' ) }
</span>
</span>
);
};

export default DisputeDueByDate;
166 changes: 121 additions & 45 deletions client/payment-details/dispute-details/dispute-steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
* External dependencies
*/
import React from 'react';
import { __, _n, sprintf } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { ExternalLink } from '@wordpress/components';
import { dateI18n } from '@wordpress/date';
import moment from 'moment';
import HelpOutlineIcon from 'gridicons/dist/help-outline';
import classNames from 'classnames';

/**
* Internal dependencies
Expand All @@ -20,19 +19,18 @@ import { ChargeBillingDetails } from 'wcpay/types/charges';
import { formatExplicitCurrency } from 'utils/currency';
import { ClickTooltip } from 'wcpay/components/tooltip';
import { getDisputeFeeFormatted } from 'wcpay/disputes/utils';
import DisputeDueByDate from './dispute-due-by-date';

interface Props {
dispute: Dispute;
customer: ChargeBillingDetails | null;
chargeCreated: number;
daysRemaining: number;
}

const DisputeSteps: React.FC< Props > = ( {
export const DisputeSteps: React.FC< Props > = ( {
dispute,
customer,
chargeCreated,
daysRemaining,
} ) => {
let emailLink;
if ( customer?.email ) {
Expand Down Expand Up @@ -72,13 +70,6 @@ const DisputeSteps: React.FC< Props > = ( {
) }&body=${ encodeURIComponent( emailBody ) }`;
}

const respondByDate = dispute.evidence_details?.due_by
? dateI18n(
'M j, Y, g:ia',
moment( dispute.evidence_details?.due_by * 1000 ).toISOString()
)
: '–';

return (
<div className="dispute-steps">
<div className="dispute-steps__header">
Expand Down Expand Up @@ -124,11 +115,11 @@ const DisputeSteps: React.FC< Props > = ( {
<li>
{ createInterpolateElement(
__(
'Challenge <challengeicon/> or accept <accepticon/> the dispute by <disputeduedate/>.',
'Challenge <challengeIcon/> or accept <acceptIcon/> the dispute by <dueByDate/>.',
'woocommerce-payments'
),
{
challengeicon: (
challengeIcon: (
<ClickTooltip
buttonIcon={ <HelpOutlineIcon /> }
buttonLabel={ __(
Expand All @@ -141,7 +132,7 @@ const DisputeSteps: React.FC< Props > = ( {
) }
/>
),
accepticon: (
acceptIcon: (
<ClickTooltip
buttonIcon={ <HelpOutlineIcon /> }
buttonLabel={ __(
Expand All @@ -161,35 +152,10 @@ const DisputeSteps: React.FC< Props > = ( {
) }
/>
),
disputeduedate: (
<span className="dispute-steps__steps__response-date">
{ respondByDate }
<span
className={ classNames( {
'dispute-steps__steps__response-date--urgent':
daysRemaining < 3,
'dispute-steps__steps__response-date--warning':
daysRemaining < 7 &&
daysRemaining > 2,
} ) }
>
{ daysRemaining === 0
? __(
'(Last day today)',
'woocommerce-payments'
)
: sprintf(
// Translators: %s is the number of days left to respond to the dispute.
_n(
'(%s day left to respond)',
'(%s days left to respond)',
daysRemaining,
'woocommerce-payments'
),
daysRemaining
) }
</span>
</span>
dueByDate: (
<DisputeDueByDate
dueBy={ dispute.evidence_details.due_by }
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the way you've decoupled this into a self-contained component, and simplified props in the process.

),
}
) }
Expand All @@ -199,4 +165,114 @@ const DisputeSteps: React.FC< Props > = ( {
);
};

export default DisputeSteps;
export const InquirySteps: React.FC< Props > = ( {
dispute,
customer,
chargeCreated,
} ) => {
let emailLink;
if ( customer?.email ) {
const chargeDate = dateI18n(
'Y-m-d',
moment( chargeCreated * 1000 ).toISOString()
);
const disputeDate = dateI18n(
'Y-m-d',
moment( dispute.created * 1000 ).toISOString()
);
const emailSubject = sprintf(
// Translators: %1$s is the store name, %2$s is the charge date.
__(
`Problem with your purchase from %1$s on %2$s?`,
'woocommerce-payments'
),
wcpaySettings.storeName,
chargeDate
);
const customerName = customer?.name || '';
const emailBody = sprintf(
// Translators: %1$s is the customer name, %2$s is the dispute date, %3$s is the dispute amount with currency-code e.g. $15 USD, %4$s is the charge date.
__(
`Hello %1$s\n\n` +
Jinksi marked this conversation as resolved.
Show resolved Hide resolved
`We noticed that on %2$s, you disputed a %3$s charge on %4$s. We wanted to contact you to make sure everything was all right with your purchase and see if there's anything else we can do to resolve any problems you might have had.\n\n` +
`Alternatively, if the dispute was a mistake, you can easily withdraw it by calling the number on the back of your card. Thank you so much - we appreciate your business and look forward to working with you.`,
'woocommerce-payments'
),
customerName,
disputeDate,
formatExplicitCurrency( dispute.amount, dispute.currency ),
chargeDate
);
emailLink = `mailto:${ customer.email }?subject=${ encodeURIComponent(
emailSubject
) }&body=${ encodeURIComponent( emailBody ) }`;
}

return (
<div className="dispute-steps">
<div className="dispute-steps__header">
{ __( 'Steps to resolve:', 'woocommerce-payments' ) }
</div>
<ol className="dispute-steps__steps">
<li>
{ customer?.email
? createInterpolateElement(
__(
'<a>Email the customer</a> to identify the issue and work towards a resolution where possible.',
'woocommerce-payments'
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😭

<a
target="_blank"
rel="noopener noreferrer"
href={ emailLink }
/>
),
}
)
: __(
'Email the customer to identify the issue and work towards a resolution where possible.',
'woocommerce-payments'
) }
</li>
<li>
{ createInterpolateElement(
__(
'Submit evidence <submitEvidenceIcon/> or issue a refund by <dueByDate/>.',
'woocommerce-payments'
),
{
submitEvidenceIcon: (
<ClickTooltip
buttonIcon={ <HelpOutlineIcon /> }
buttonLabel={ __(
'Submit evidence',
'woocommerce-payments'
) }
content={ createInterpolateElement(
__(
"To submit evidence, provide documentation that supports your case. Keep in mind that submitting evidence doesn't ensure a favorable outcome. If the cardholder agrees to withdraw the inquiry, you'll still need to officially submit your evidence to prevent bank escalation. <learnMoreLink>Learn more</learnMoreLink>",
'woocommerce-payments'
),
{
learnMoreLink: (
<ExternalLink href="https://woocommerce.com/document/woopayments/fraud-and-disputes/managing-disputes/#inquiries" />
),
}
) }
/>
),
dueByDate: (
<DisputeDueByDate
dueBy={ dispute.evidence_details.due_by }
/>
),
}
) }
</li>
</ol>
</div>
);
};
Loading
Loading