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

Add Dispute Challenge and Accept buttons to Transaction Details #7093

Merged
merged 63 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
991dd84
Refactor isAwaitingResponse check to wrap entire <Card />
Jinksi Aug 30, 2023
46c3f31
Add dispute actions init
Jinksi Aug 30, 2023
59190f2
Add Dispute > EvidenceDetails type comments to improve devex
Jinksi Aug 30, 2023
b8ef1be
Show different challenge button label if has staged evidence
Jinksi Aug 30, 2023
026d905
Add missing `payment_intent` to Dispute TS interface
Jinksi Aug 30, 2023
10e42da
Add action func for accepting disputes on the Transaction Details screen
Jinksi Aug 30, 2023
1a53f0e
Add custom hook for accepting dispute
Jinksi Aug 30, 2023
7cc279b
Add accept dispute modal
Jinksi Aug 30, 2023
a85fea3
Improve isLoading state for accepting disputes and exceptions
Jinksi Aug 30, 2023
194677b
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Aug 31, 2023
3f7168d
Update accept dispute modal styles
Jinksi Aug 31, 2023
347198a
Add getDisputeFee util function
Jinksi Aug 31, 2023
484b60d
Populate dynamic content for accept dispute modal
Jinksi Aug 31, 2023
83474fe
Add changelog entry
Jinksi Aug 31, 2023
b64d04b
Fix modal text content overflow
Jinksi Aug 31, 2023
9ea2586
Update Dispute → Charge interface to accept `string`
Jinksi Aug 31, 2023
004aaf6
Move useDisputeAccept hook to `data/disputes/hooks`
Jinksi Aug 31, 2023
341d83d
Improve translators note with `<em>` element description
Jinksi Aug 31, 2023
da679bc
Add DisputeDetails tests
Jinksi Aug 31, 2023
62cd852
Add test for clicking challenge button
Jinksi Aug 31, 2023
8a349b6
Remove extraneous div
Jinksi Aug 31, 2023
3545ae9
Add test for dispute with staged evidence
Jinksi Aug 31, 2023
2fb3cfb
Add dispute action tracks events
Jinksi Aug 31, 2023
c355a77
Add missing full-stop to accept dispute modal text
Jinksi Aug 31, 2023
2f59597
Add currency rendering test for accept dispute modal
Jinksi Aug 31, 2023
0d0616d
Add test for Inquiry accept dispute modal
Jinksi Aug 31, 2023
b134595
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 4, 2023
39e0d7f
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 6, 2023
9d341a0
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 7, 2023
9091394
Fix formatting after merge conflict resolution
Jinksi Sep 7, 2023
163d427
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 11, 2023
1165d67
Fix duplicate import after merge conflict resolution
Jinksi Sep 11, 2023
dc3a79a
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 12, 2023
1b2bbc3
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 12, 2023
fe0955b
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 13, 2023
3412ae0
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 14, 2023
47a9324
Don't render dispute actions for inquiries
Jinksi Sep 14, 2023
cae2394
Add more info to acceptTransactionDetailsDispute comment
Jinksi Sep 14, 2023
db85448
Rename onSubmit → onAccept to name intent
Jinksi Sep 14, 2023
81738f0
Add dispute_status prop to tracks events
Jinksi Sep 14, 2023
c072af5
Update dispute hook comments
Jinksi Sep 14, 2023
85490fc
Cleanup tests and remove flaky test
Jinksi Sep 14, 2023
cfb3709
Move challenge click handler to func improve code clarity
Jinksi Sep 14, 2023
156c298
Remove redundant `null` return
Jinksi Sep 14, 2023
5961f5f
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 15, 2023
be9ff75
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 18, 2023
0bb32c9
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 19, 2023
c5a5ffe
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 19, 2023
dcc5d45
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 19, 2023
395a111
Move DisputeActions logic and markup into parent component
Jinksi Sep 19, 2023
49158aa
Allow native browser navigation for challenge button
Jinksi Sep 19, 2023
f7ffc1c
Add test for challenge button click
Jinksi Sep 19, 2023
ef03f65
Move accept click handler inline to improve readability
Jinksi Sep 19, 2023
b6eed3b
Use `showDisputeActions` variable to improve readability
Jinksi Sep 19, 2023
13a4fa3
Add descriptive comment about challenge link
Jinksi Sep 19, 2023
b52093d
Rename test `index.tsx` to `index.test.tsx` for consistency
Jinksi Sep 19, 2023
939c40d
Update styles on mobile screens to match design
Jinksi Sep 19, 2023
d8ca8e6
Add tests for when actions are not allowed
Jinksi Sep 19, 2023
3709cc0
Remove `Button` from `Link` to simplify code
Jinksi Sep 19, 2023
340b5a1
Fix tests after changing Button → Link
Jinksi Sep 19, 2023
203f161
Revert "Remove `Button` from `Link` to simplify code"
Jinksi Sep 19, 2023
163514b
Revert "Fix tests after changing Button → Link"
Jinksi Sep 19, 2023
eb23605
Merge branch 'develop' into fix/6926-transaction-dispute-details-actions
Jinksi Sep 19, 2023
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
5 changes: 5 additions & 0 deletions changelog/fix-6926-transaction-dispute-details-actions
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: add
Comment: Behind a feature flag: add challenge and accept action buttons to Transaction Details screen


61 changes: 61 additions & 0 deletions client/data/disputes/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NAMESPACE, STORE_NAME } from '../constants';
import TYPES from './action-types';
import wcpayTracks from 'tracks';
import { getAdminUrl } from 'wcpay/utils';
import { getPaymentIntent } from '../payment-intents/resolvers';

export function updateDispute( data ) {
return {
Expand Down Expand Up @@ -88,3 +89,63 @@ export function* acceptDispute( id ) {
yield controls.dispatch( 'core/notices', 'createErrorNotice', message );
}
}

// This function handles the dispute acceptance flow from the Transaction Details screen.
// It differs from the `acceptDispute` function above in that it also fetches and updates
// the payment intent associated with the dispute to reflect changes to the dispute
// on the Transaction Details screen.
//
// Once the '_wcpay_feature_dispute_on_transaction_page' is enabled by default,
// the `acceptDispute` function above can be removed and this function can be renamed
// to `acceptDispute`.
export function* acceptTransactionDetailsDispute( dispute ) {
Copy link
Member Author

@Jinksi Jinksi Aug 30, 2023

Choose a reason for hiding this comment

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

Rather than modify the existing acceptDispute action (used on the Dispute → Details screen), I've created a new one since there are some differences in the approach:

  1. The existing action will force a redirect to Payments → Disputes.
  2. This new action requires fetching and updating the payment intent to refresh the current Transaction Details page.

Once the feature flag is lifted, _wcpay_feature_dispute_on_transaction_page, we can remove and replace the previous action with the new one.

I've left a TODO item on #6883 to make this change.

const { id, payment_intent: paymentIntent } = dispute;

try {
yield controls.dispatch( STORE_NAME, 'startResolution', 'getDispute', [
id,
] );

const updatedDispute = yield apiFetch( {
path: `${ NAMESPACE }/disputes/${ id }/close`,
method: 'post',
} );

yield updateDispute( updatedDispute );

// Fetch and update the payment intent associated with the dispute
// to reflect changes to the dispute on the Transaction Details screen.
yield getPaymentIntent( paymentIntent );

yield controls.dispatch( STORE_NAME, 'finishResolution', 'getDispute', [
id,
] );

wcpayTracks.recordEvent( 'wcpay_dispute_accept_success' );
const message = updatedDispute.order
? sprintf(
/* translators: #%s is an order number, e.g. 15 */
__(
'You have accepted the dispute for order #%s.',
'woocommerce-payments'
),
updatedDispute.order.number
)
: __( 'You have accepted the dispute.', 'woocommerce-payments' );
yield controls.dispatch(
'core/notices',
'createSuccessNotice',
message
);
} catch ( e ) {
const message = __(
'There has been an error accepting the dispute. Please try again later.',
'woocommerce-payments'
);
wcpayTracks.recordEvent( 'wcpay_dispute_accept_failed' );
yield controls.dispatch( 'core/notices', 'createErrorNotice', message );
yield controls.dispatch( STORE_NAME, 'finishResolution', 'getDispute', [
id,
] );
}
}
29 changes: 29 additions & 0 deletions client/data/disputes/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import type {
import { STORE_NAME } from '../constants';
import { disputeAwaitingResponseStatuses } from 'wcpay/disputes/filters/config';

/**
* Returns the dispute object, loading state, and accept function.
* Fetches the dispute object if it is not already cached.
*/
export const useDispute = (
id: string
): {
Expand All @@ -43,6 +47,31 @@ export const useDispute = (
return { dispute, isLoading, doAccept };
};

/**
* Returns the dispute accept function and loading state.
* Does not return or fetch the dispute object.
*/
export const useDisputeAccept = (
Jinksi marked this conversation as resolved.
Show resolved Hide resolved
dispute: Dispute
): {
doAccept: () => void;
isLoading: boolean;
} => {
const { isLoading } = useSelect(
( select ) => {
const { isResolving } = select( STORE_NAME );

return {
isLoading: isResolving( 'getDispute', [ dispute.id ] ),
};
},
[ dispute.id ]
);
const { acceptTransactionDetailsDispute } = useDispatch( STORE_NAME );
const doAccept = () => acceptTransactionDetailsDispute( dispute );
return { doAccept, isLoading };
};

export const useDisputeEvidence = (): {
updateDispute: ( data: Dispute ) => void;
} => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,35 @@
/**
* External dependencies
*/
import React from 'react';
import React, { useState } from 'react';
import moment from 'moment';
import { __ } from '@wordpress/i18n';
import { Card, CardBody } from '@wordpress/components';
import { edit } from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n';
import { backup, edit, lock } from '@wordpress/icons';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
import {
Button,
Card,
CardBody,
Flex,
FlexItem,
Icon,
Modal,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import type { Dispute } from 'wcpay/types/disputes';
import { isAwaitingResponse } from 'wcpay/disputes/utils';
import wcpayTracks from 'tracks';
import { useDisputeAccept } from 'wcpay/data';
import {
getDisputeFee,
isAwaitingResponse,
isInquiry,
} from 'wcpay/disputes/utils';
import { getAdminUrl } from 'wcpay/utils';
import { formatCurrency } from 'wcpay/utils/currency';
import DisputeNotice from './dispute-notice';
import IssuerEvidenceList from './evidence-list';
import DisputeSummaryRow from './dispute-summary-row';
Expand All @@ -25,10 +43,19 @@ interface Props {
}

const DisputeAwaitingResponseDetails: React.FC< Props > = ( { dispute } ) => {
const { doAccept, isLoading } = useDisputeAccept( dispute );
const [ isModalOpen, setModalOpen ] = useState( false );

const now = moment();
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 disputeFee = getDisputeFee( dispute );
const showDisputeActions = ! isInquiry( dispute );

const onModalClose = () => {
setModalOpen( false );
};

return (
<div className="transaction-details-dispute-details-wrapper">
Expand Down Expand Up @@ -61,6 +88,147 @@ const DisputeAwaitingResponseDetails: React.FC< Props > = ( { dispute } ) => {
/>
</>
) }

{ /* Dispute Actions */ }
{ showDisputeActions && (
<div className="transaction-details-dispute-details-body__actions">
<Link
href={ getAdminUrl( {
page: 'wc-admin',
path: '/payments/disputes/challenge',
id: dispute.id,
} ) }
onClick={ () => {
wcpayTracks.recordEvent(
wcpayTracks.events
.DISPUTE_CHALLENGE_CLICK,
{
dispute_status: dispute.status,
}
);
} }
>
<span className="components-button is-primary">
{ hasStagedEvidence
? __(
'Continue with challenge',
'woocommerce-payments'
)
: __(
'Challenge dispute',
'woocommerce-payments'
) }
</span>
</Link>

<Button
variant="tertiary"
disabled={ isLoading }
onClick={ () => {
wcpayTracks.recordEvent(
wcpayTracks.events
.DISPUTE_ACCEPT_MODAL_VIEW,
{
dispute_status: dispute.status,
}
);
setModalOpen( true );
} }
>
{ __(
'Accept dispute',
'woocommerce-payments'
) }
</Button>

{ isModalOpen && (
<Modal
title="Accept the dispute?"
onRequestClose={ onModalClose }
className="transaction-details-dispute-accept-modal"
>
<p>
<strong>
{ __(
'Before proceeding, please take note of the following:',
'woocommerce-payments'
) }
</strong>
</p>
<Flex justify="start">
<FlexItem className="transaction-details-dispute-accept-modal__icon">
<Icon icon={ backup } size={ 24 } />
</FlexItem>
<FlexItem>
{ createInterpolateElement(
sprintf(
/* translators: %s: dispute fee, <em>: emphasis HTML element. */
__(
'Accepting the dispute marks it as <em>Lost</em>. The disputed amount will be returned to the cardholder, with a %s dispute fee deducted from your account.',
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to review this language. This implies the fee will only be deducted if they accept the dispute. When the fee has already been deducted.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll log a follow-up issue to allow this PR to be shipped and we can iterate on the wording.

Copy link
Member Author

Choose a reason for hiding this comment

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

New issue for this: #7254

'woocommerce-payments'
),
disputeFee &&
formatCurrency(
disputeFee.fee,
disputeFee.currency
)
),
{
em: <em />,
}
) }
</FlexItem>
</Flex>
<Flex justify="start">
<FlexItem className="transaction-details-dispute-accept-modal__icon">
<Icon icon={ lock } size={ 24 } />
</FlexItem>
<FlexItem>
{ __(
'Accepting the dispute is final and cannot be undone.',
'woocommerce-payments'
) }
</FlexItem>
</Flex>

<Flex
className="transaction-details-dispute-accept-modal__actions"
justify="end"
>
<Button
variant="tertiary"
onClick={ onModalClose }
>
{ __(
'Cancel',
'woocommerce-payments'
) }
</Button>
<Button
variant="primary"
onClick={ () => {
wcpayTracks.recordEvent(
wcpayTracks.events
.DISPUTE_ACCEPT_CLICK,
{
dispute_status:
dispute.status,
}
);
setModalOpen( false );
doAccept();
} }
>
{ __(
'Accept dispute',
'woocommerce-payments'
) }
</Button>
</Flex>
</Modal>
) }
</div>
) }
</CardBody>
</Card>
</div>
Expand Down
33 changes: 33 additions & 0 deletions client/payment-details/dispute-details/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@
}
}
}

&__actions {
display: flex;
justify-content: start;
gap: $grid-unit-10;

@media screen and ( max-width: $break-small ) {
flex-direction: column;

.components-button {
width: 100%;
justify-content: center;
}
}
}
}
}
.dispute-reason-tooltip {
Expand All @@ -49,6 +64,24 @@
}
}

.transaction-details-dispute-accept-modal {
max-width: 600px;

.components-modal__content {
padding-top: $grid-unit-30;
}

&__icon {
flex-shrink: 0;
padding: 6px;
margin-right: $grid-unit-10;
}

&__actions {
margin-top: $grid-unit-30;
}
}

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

Expand Down
Loading
Loading