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

Reporting: Show bank reference ID on Payout details page #9945

Open
wants to merge 45 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
33208a5
Add bank reference key to the deposit details header
Dec 5, 2024
98333d5
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 5, 2024
1939d4e
Add conditional
Dec 5, 2024
2d5a129
Changelog
Dec 5, 2024
e0f0dd6
CSS adjustments for mobile view
Dec 5, 2024
aea1f35
Css tweak
Dec 5, 2024
a935636
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 5, 2024
6386ab8
show n/a when ref key not available
Dec 6, 2024
3ace18a
Update test snapshots
Dec 6, 2024
d318c33
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 6, 2024
735fe94
Add copy button
Dec 6, 2024
e96ba95
Tweak appearance of button and snapshots
Dec 6, 2024
ed00ff5
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 6, 2024
d1339f9
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 6, 2024
292bfd9
Change Ref ID font to monospace
Dec 6, 2024
b503845
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
haszari Dec 9, 2024
8bda02c
CSS tweak
Dec 9, 2024
25b87b3
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 9, 2024
294a44d
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 11, 2024
34790c9
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 11, 2024
21b2e01
Move DepositDateItem to its own React FC
Dec 11, 2024
05ee6ca
WIP changes
Dec 11, 2024
4a4720a
Fix brackets
Dec 11, 2024
a159495
Update snapshots
Dec 11, 2024
aa7eb1b
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 12, 2024
af176bd
Simplify click handler
Dec 12, 2024
0d26f76
Move copy button to component
Dec 12, 2024
5c3c6aa
Add test and label prop
Dec 12, 2024
feb2746
Merge branch 'develop' into add/9878-bank-ref-key-payout-details
Dec 12, 2024
3b4bcc9
Use named exports as per TS guidelines
Dec 13, 2024
8a4f35e
Add comments for props
Dec 13, 2024
3be4a58
rough wip
Dec 14, 2024
ac2c8a9
Remove bank details and ref key from status header
Dec 16, 2024
17cd80d
WIP - Add styles
Dec 16, 2024
9ac8116
Merge branch 'develop' into add/1-9878-redesign-payout-details-header
Dec 16, 2024
d6ab75c
Update test snapshot
Dec 16, 2024
c0d01b0
Merge branch 'develop' into add/1-9878-redesign-payout-details-header
Dec 18, 2024
24dc671
Add translation tag
Dec 18, 2024
fd301fe
Use CSS for animation
Dec 18, 2024
459b83a
Merge branch 'develop' into add/1-9878-redesign-payout-details-header
Dec 19, 2024
83f7407
Update test.
Dec 19, 2024
68eb326
Merge branch 'develop' into add/1-9878-redesign-payout-details-header
Dec 20, 2024
f0cc6bf
Fix failing test
Dec 20, 2024
e1d5923
CSS adjustments
Dec 20, 2024
7c879f1
Fix conflicts and merge branch 'develop' into add/1-9878-redesign-pay…
Dec 23, 2024
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
4 changes: 4 additions & 0 deletions changelog/add-9878-bank-ref-key-payout-details
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Show Bank reference key on top of the payout details page, whenever available.
64 changes: 64 additions & 0 deletions client/components/copy-button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import React, { useState, useEffect, useRef } from 'react';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';

/**
* Internal dependencies
*/
import './style.scss';

interface CopyButtonProps {
/**
* The text to copy to the clipboard.
*/
textToCopy: string;

/**
* The label for the button. Also used as the aria-label.
*/
label: string;
}

export const CopyButton: React.FC< CopyButtonProps > = ( {
textToCopy,
label,
} ) => {
// useRef() is used to store the timer reference for the setTimeout() function.
const timerRef = useRef< NodeJS.Timeout | null >( null );

// useEffect() is used to clear the timer reference when the component is unmounted.
useEffect( () => {
return () => {
if ( timerRef.current ) {
clearTimeout( timerRef.current );
}
};
}, [] );
nagpai marked this conversation as resolved.
Show resolved Hide resolved

const [ copied, setCopied ] = useState( false );

const copyToClipboard = () => {
navigator.clipboard.writeText( textToCopy );
setCopied( true );
timerRef.current = setTimeout( () => {
setCopied( false );
}, 2000 );
};

return (
<button
type="button"
className={ classNames( 'woopayments-copy-button', {
'state--copied': copied,
} ) }
aria-label={ label }
title={ __( 'Copy to clipboard', 'woocommerce-payments' ) }
onClick={ copyToClipboard }
>
<i></i>
</button>
);
};
Copy link
Member

Choose a reason for hiding this comment

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

Nice work implementing this tricky bit of logic. Using the setTimeout and cleaning up in the useEffect return works well 🙌

I had a thought while looking at this, though – our intention with to show → 2s → hide a CSS change might be better served by a CSS animation. Since CSS animations already have "timer" logic, we can rely on it rather than implement a custom timer, whilst using React state to add and remove the classname.

The onAnimationEnd event handler will also abort if the element is unmounted, so we don't need to handle any cleanup of any timers.

Let me know what you think – I think it is a tidier and less complex approach (less code to understand and maintain), but there are tradeoffs. E.g. it might be less obvious to code readers where the timer is originating from (CSS, rather than JS).

export const CopyButton: React.FC< CopyButtonProps > = ( {
 	textToCopy,
 	label,
 } ) => {
-	// useRef() is used to store the timer reference for the setTimeout() function.
-	const timerRef = useRef< NodeJS.Timeout | null >( null );
-
-	// useEffect() is used to clear the timer reference when the component is unmounted.
-	useEffect( () => {
-		return () => {
-			if ( timerRef.current ) {
-				clearTimeout( timerRef.current );
-			}
-		};
-	}, [] );
-
 	const [ copied, setCopied ] = useState( false );
 
 	const copyToClipboard = () => {
 		navigator.clipboard.writeText( textToCopy );
 		setCopied( true );
-		timerRef.current = setTimeout( () => {
-			setCopied( false );
-		}, 2000 );
 	};
 
	return (
		<button
			type="button"
			className={ classNames( 'woopayments-copy-button', {
				'state--copied': copied,
			} ) }
			aria-label={ label }
			title={ __( 'Copy to clipboard', 'woocommerce-payments' ) }
			onClick={ copyToClipboard }
+			onAnimationEnd={ () => setCopied( false ) }
		>
			<i></i>
		</button>
	);

And then in the CSS:

	&.state--copied {
		animation: copy-indicator 2s forwards;
	}

	@keyframes copy-indicator {
		0% {
			opacity: 1;
		}
		95% {
			opacity: 1;
		}
		// a quick fade-out from 1%→0% at the end
		100% {
			opacity: 0;
		}
	}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The CSS suggestion is brilliant. I would prefer using CSS to additional JS/ React script. It is much cleaner and easier to maintain. I will try this out and update. Thanks so much!

37 changes: 37 additions & 0 deletions client/components/copy-button/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.woopayments-copy-button {
line-height: 1.2em;
display: inline-flex;
background: transparent;
border: none;
border-radius: 0;
vertical-align: middle;
font-weight: normal;
cursor: pointer;
color: inherit;
margin-left: 2px;
align-items: center;

i {
display: block;
width: 1.2em;
height: 1.2em;
mask-image: url( 'assets/images/icons/copy.svg?asset' );
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
background-color: currentColor;

&:hover {
opacity: 0.7;
}

&:active {
transform: scale( 0.9 );
}
}

&.state--copied i {
mask-image: url( 'assets/images/icons/check-green.svg?asset' );
background-color: $studio-green-50;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CopyButton renders the button correctly 1`] = `
<div>
<button
aria-label="Copy bank reference ID to clipboard"
class="woopayments-copy-button"
title="Copy to clipboard"
type="button"
>
<i />
</button>
</div>
`;
70 changes: 70 additions & 0 deletions client/components/copy-button/test/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/** @format **/

/**
* External dependencies
*/
import React from 'react';
import { act, render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import { CopyButton } from '..';

describe( 'CopyButton', () => {
it( 'renders the button correctly', () => {
const { container: copyButtonContainer } = render(
<CopyButton
textToCopy="test_bank_reference_id"
label="Copy bank reference ID to clipboard"
/>
);

expect( copyButtonContainer ).toMatchSnapshot();
} );

describe( 'when the button is clicked', () => {
beforeAll( () => {
jest.useFakeTimers();
} );

afterAll( () => {
jest.useRealTimers();
} );

it( 'copies the text to the clipboard and shows copied state', () => {
render(
<CopyButton
textToCopy="test_bank_reference_id"
label="Copy bank reference ID to clipboard"
/>
);

const button = screen.queryByRole( 'button', {
name: /Copy bank reference ID to clipboard/i,
} );

//Mock the clipboard API
Object.assign( navigator, {
clipboard: {
writeText: jest.fn(),
},
} );

act( () => {
button?.click();
} );

expect( navigator.clipboard.writeText ).toHaveBeenCalledWith(
'test_bank_reference_id'
);
expect( button ).toHaveClass( 'state--copied' );

act( () => {
jest.advanceTimersByTime( 2000 );
Copy link
Member

Choose a reason for hiding this comment

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

Ah, so it seems that the jest JSDOM env won't be running CSS animations, but this can be manually triggered using fireEvent:

Suggested change
jest.advanceTimersByTime( 2000 );
fireEvent.animationEnd( button );

Then we also won't need to override the jest timer setup and teardown.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks Eric!

} );

expect( button ).not.toHaveClass( 'state--copied' );
} );
} );
} );
102 changes: 77 additions & 25 deletions client/deposits/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import classNames from 'classnames';
import type { CachedDeposit } from 'types/deposits';
import { useDeposit } from 'data';
import TransactionsList from 'transactions/list';
import { CopyButton } from 'components/copy-button';
import Page from 'components/page';
import ErrorBoundary from 'components/error-boundary';
import { TestModeNotice } from 'components/test-mode-notice';
Expand Down Expand Up @@ -70,7 +71,7 @@ interface SummaryItemProps {
label: string;
value: string | JSX.Element;
valueClass?: string | false;
detail?: string;
detail?: string | JSX.Element;
}

/**
Expand Down Expand Up @@ -102,35 +103,20 @@ const SummaryItem: React.FC< SummaryItemProps > = ( {
</li>
);

interface DepositOverviewProps {
deposit: CachedDeposit | undefined;
interface DepositDateItemProps {
deposit: CachedDeposit;
}

export const DepositOverview: React.FC< DepositOverviewProps > = ( {
deposit,
} ) => {
if ( ! deposit ) {
return (
<InlineNotice icon status="error" isDismissible={ false }>
{ __(
`The deposit you are looking for cannot be found.`,
'woocommerce-payments'
) }
</InlineNotice>
);
}

const isWithdrawal = deposit.type === 'withdrawal';

const DepositDateItem: React.FC< DepositDateItemProps > = ( { deposit } ) => {
let depositDateLabel = __( 'Payout date', 'woocommerce-payments' );
if ( ! deposit.automatic ) {
depositDateLabel = __( 'Instant payout date', 'woocommerce-payments' );
}
if ( isWithdrawal ) {
if ( deposit.type === 'withdrawal' ) {
depositDateLabel = __( 'Withdrawal date', 'woocommerce-payments' );
}

const depositDateItem = (
return (
<SummaryItem
key="depositDate"
label={
Expand All @@ -142,16 +128,35 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( {
)
}
value={ <DepositStatusIndicator deposit={ deposit } /> }
detail={ deposit.bankAccount }
/>
);
};
interface DepositOverviewProps {
deposit: CachedDeposit | undefined;
}

export const DepositOverview: React.FC< DepositOverviewProps > = ( {
deposit,
} ) => {
if ( ! deposit ) {
return (
<InlineNotice icon status="error" isDismissible={ false }>
{ __(
`The deposit you are looking for cannot be found.`,
'woocommerce-payments'
) }
</InlineNotice>
);
}

const isWithdrawal = deposit.type === 'withdrawal';

return (
<div className="wcpay-deposit-overview">
{ deposit.automatic ? (
<Card className="wcpay-deposit-automatic">
<ul>
{ depositDateItem }
<DepositDateItem deposit={ deposit } />
<li className="wcpay-deposit-amount">
{ formatExplicitCurrency(
deposit.amount,
Expand All @@ -161,7 +166,7 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( {
</ul>
</Card>
) : (
<SummaryList
<SummaryList // For instant deposits only
label={
isWithdrawal
? __(
Expand All @@ -172,7 +177,7 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( {
}
>
{ () => [
depositDateItem,
<DepositDateItem key="dateItem" deposit={ deposit } />,
<SummaryItem
key="depositAmount"
label={
Expand Down Expand Up @@ -228,6 +233,53 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( {
] }
</SummaryList>
) }
<Card>
<CardHeader>
<Text size={ 16 } weight={ 600 }>
{ isWithdrawal
? __( 'Withdrawal details', 'woocommerce-payments' )
: __( 'Payout details', 'woocommerce-payments' ) }
</Text>
</CardHeader>
<CardBody>
<div className="woopayments-payout-details-header">
<h2>
{ __( 'Bank account', 'woocommerce-payments' ) }
</h2>
<div className="woopayments-payout-details-header__value">
{ deposit.bankAccount }
</div>
<h2>
{ __(
'Bank reference ID',
'woocommerce-payments'
) }
</h2>
<div>
{ deposit.bank_reference_key ? (
<>
<span className="woopayments-payout-details-header__bank-reference-id">
{ deposit.bank_reference_key }
</span>
<CopyButton
textToCopy={
deposit.bank_reference_key
}
label={ __(
'Copy bank reference ID to clipboard',
'woocommerce-payments'
) }
/>
</>
) : (
<div className="woopayments-payout-details-header__value">
Not available
nagpai marked this conversation as resolved.
Show resolved Hide resolved
</div>
) }
</div>
</div>
</CardBody>
</Card>
</div>
);
};
Expand Down
Loading
Loading