diff --git a/changelog/add-9878-bank-ref-key-payout-details b/changelog/add-9878-bank-ref-key-payout-details new file mode 100644 index 00000000000..8d88b5cef46 --- /dev/null +++ b/changelog/add-9878-bank-ref-key-payout-details @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Show Bank reference key on top of the payout details page, whenever available. diff --git a/client/components/copy-button/index.tsx b/client/components/copy-button/index.tsx new file mode 100644 index 00000000000..0ec1fc3f76f --- /dev/null +++ b/client/components/copy-button/index.tsx @@ -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 ); + } + }; + }, [] ); + + const [ copied, setCopied ] = useState( false ); + + const copyToClipboard = () => { + navigator.clipboard.writeText( textToCopy ); + setCopied( true ); + timerRef.current = setTimeout( () => { + setCopied( false ); + }, 2000 ); + }; + + return ( + + ); +}; diff --git a/client/components/copy-button/style.scss b/client/components/copy-button/style.scss new file mode 100644 index 00000000000..bac8c2b6edd --- /dev/null +++ b/client/components/copy-button/style.scss @@ -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; + } +} diff --git a/client/components/copy-button/test/__snapshots__/index.test.tsx.snap b/client/components/copy-button/test/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..44cd5831dc5 --- /dev/null +++ b/client/components/copy-button/test/__snapshots__/index.test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyButton renders the button correctly 1`] = ` +
+ +
+`; diff --git a/client/components/copy-button/test/index.test.tsx b/client/components/copy-button/test/index.test.tsx new file mode 100644 index 00000000000..eaaf7844e50 --- /dev/null +++ b/client/components/copy-button/test/index.test.tsx @@ -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( + + ); + + 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( + + ); + + 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 ); + } ); + + expect( button ).not.toHaveClass( 'state--copied' ); + } ); + } ); +} ); diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 2176b079377..e87af290938 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -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'; @@ -70,7 +71,7 @@ interface SummaryItemProps { label: string; value: string | JSX.Element; valueClass?: string | false; - detail?: string; + detail?: string | JSX.Element; } /** @@ -102,35 +103,20 @@ const SummaryItem: React.FC< SummaryItemProps > = ( { ); -interface DepositOverviewProps { - deposit: CachedDeposit | undefined; +interface DepositDateItemProps { + deposit: CachedDeposit; } -export const DepositOverview: React.FC< DepositOverviewProps > = ( { - deposit, -} ) => { - if ( ! deposit ) { - return ( - - { __( - `The deposit you are looking for cannot be found.`, - 'woocommerce-payments' - ) } - - ); - } - - 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 ( = ( { ) } value={ } - detail={ deposit.bankAccount } + detail={ + <> + { deposit.bankAccount } +
+ Bank reference ID:{ ' ' } + { deposit.bank_reference_key ? ( + <> + + { deposit.bank_reference_key } + + + + ) : ( + 'N/A' + ) } + + } /> ); +}; +interface DepositOverviewProps { + deposit: CachedDeposit | undefined; +} + +export const DepositOverview: React.FC< DepositOverviewProps > = ( { + deposit, +} ) => { + if ( ! deposit ) { + return ( + + { __( + `The deposit you are looking for cannot be found.`, + 'woocommerce-payments' + ) } + + ); + } + + const isWithdrawal = deposit.type === 'withdrawal'; return (
{ deposit.automatic ? (
    - { depositDateItem } +
  • { formatExplicitCurrency( deposit.amount, @@ -161,7 +189,7 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( {
) : ( - = ( { } > { () => [ - depositDateItem, + , MOCK BANK •••• 1234 (USD) +
+ Bank reference ID: + + N/A
@@ -117,6 +121,10 @@ exports[`Deposit overview renders automatic withdrawal correctly 1`] = ` class="wcpay-summary__item-detail" > MOCK BANK •••• 1234 (USD) +
+ Bank reference ID: + + N/A @@ -195,6 +203,10 @@ exports[`Deposit overview renders instant deposit correctly 1`] = ` class="wcpay-summary__item-detail" > MOCK BANK •••• 1234 (USD) +
+ Bank reference ID: + + N/A