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..67d14eb4725 --- /dev/null +++ b/client/components/copy-button/index.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import React, { useState } 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, +} ) => { + const [ copied, setCopied ] = useState( false ); + + const copyToClipboard = () => { + navigator.clipboard.writeText( textToCopy ); + setCopied( true ); + }; + + return ( + + ); +}; diff --git a/client/components/copy-button/style.scss b/client/components/copy-button/style.scss new file mode 100644 index 00000000000..58b4555540c --- /dev/null +++ b/client/components/copy-button/style.scss @@ -0,0 +1,51 @@ +.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; + 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; + } + } +} 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..7a771a7fe39 --- /dev/null +++ b/client/components/copy-button/test/index.test.tsx @@ -0,0 +1,67 @@ +/** @format **/ + +/** + * External dependencies + */ +import React from 'react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +/** + * 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', () => { + it( 'copies the text to the clipboard and shows copied state', async () => { + render( + + ); + + const button = screen.queryByRole( 'button', { + name: /Copy bank reference ID to clipboard/i, + } ); + + if ( ! button ) { + throw new Error( 'Button not found' ); + } + + //Mock the clipboard API + Object.assign( navigator, { + clipboard: { + writeText: jest.fn().mockResolvedValueOnce( undefined ), + }, + } ); + + await act( async () => { + fireEvent.click( button ); + } ); + + expect( navigator.clipboard.writeText ).toHaveBeenCalledWith( + 'test_bank_reference_id' + ); + expect( button ).toHaveClass( 'state--copied' ); + + act( () => { + fireEvent.animationEnd( button ); + } ); + + expect( button ).not.toHaveClass( 'state--copied' ); + } ); + } ); +} ); diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 6f8ed023da1..c4c83154b0a 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -28,6 +28,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'; @@ -68,7 +69,7 @@ interface SummaryItemProps { label: string; value: string | JSX.Element; valueClass?: string | false; - detail?: string; + detail?: string | JSX.Element; } /** @@ -100,6 +101,30 @@ const SummaryItem: React.FC< SummaryItemProps > = ( { ); +interface DepositDateItemProps { + deposit: CachedDeposit; +} + +const DepositDateItem: React.FC< DepositDateItemProps > = ( { deposit } ) => { + let depositDateLabel = __( 'Payout date', 'woocommerce-payments' ); + if ( ! deposit.automatic ) { + depositDateLabel = __( 'Instant payout date', 'woocommerce-payments' ); + } + if ( deposit.type === 'withdrawal' ) { + depositDateLabel = __( 'Withdrawal date', 'woocommerce-payments' ); + } + + return ( + } + /> + ); +}; interface DepositOverviewProps { deposit: CachedDeposit | undefined; } @@ -120,32 +145,12 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( { const isWithdrawal = deposit.type === 'withdrawal'; - let depositDateLabel = __( 'Payout date', 'woocommerce-payments' ); - if ( ! deposit.automatic ) { - depositDateLabel = __( 'Instant payout date', 'woocommerce-payments' ); - } - if ( isWithdrawal ) { - depositDateLabel = __( 'Withdrawal date', 'woocommerce-payments' ); - } - - const depositDateItem = ( - } - detail={ deposit.bankAccount } - /> - ); - return (
{ deposit.automatic ? (
    - { depositDateItem } +
  • { formatExplicitCurrency( deposit.amount, @@ -155,7 +160,7 @@ export const DepositOverview: React.FC< DepositOverviewProps > = ( {
) : ( - = ( { } > { () => [ - depositDateItem, + , = ( { ] } ) } + + + + { isWithdrawal + ? __( 'Withdrawal details', 'woocommerce-payments' ) + : __( 'Payout details', 'woocommerce-payments' ) } + + + +
+
+

+ { __( 'Bank account', 'woocommerce-payments' ) } +

+
+ { deposit.bankAccount } +
+
+
+

+ { __( + 'Bank reference ID', + 'woocommerce-payments' + ) } +

+
+ { deposit.bank_reference_key ? ( + <> + + { deposit.bank_reference_key } + + + + ) : ( +
+ { __( + 'Not available', + 'woocommerce-payments' + ) } +
+ ) } +
+
+
+
+
); }; diff --git a/client/deposits/details/style.scss b/client/deposits/details/style.scss index 0031184d4cf..1e084484e81 100644 --- a/client/deposits/details/style.scss +++ b/client/deposits/details/style.scss @@ -37,6 +37,15 @@ font-size: 20px; line-height: 28px; } + + .wcpay-summary__item-detail { + color: $dark-gray-500; + word-wrap: break-word; + + .woopayments-payout-bank-reference-id { + font-family: monospace; + } + } } .wcpay-deposit-fee { @@ -47,11 +56,52 @@ color: $wp-green-70; } + .woopayments-payout-details-header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 0.75rem; + + &__bank-reference-id { + font-family: monospace; + color: $wp-gray-50; + } + + &__value { + color: $wp-gray-50; + align-self: center; + + @include breakpoint( '<660px' ) { + align-self: flex-start; + } + } + + &__item { + display: flex; + + @include breakpoint( '<660px' ) { + flex-direction: column; + } + } + + h2 { + font-size: 0.6875rem; + font-weight: 500; + color: $wp-gray-90; + min-width: 11rem; + text-transform: uppercase; + } + } + .wcpay-deposit-automatic ul { display: flex; margin: 0; list-style-type: none; + @include breakpoint( '<660px' ) { + flex-direction: column; + } + .woocommerce-summary__item { border-bottom: 0; background-color: inherit; @@ -59,6 +109,10 @@ .woocommerce-summary__item-label:hover { color: inherit; } + + @include breakpoint( '<660px' ) { + border-right: none; + } } .wcpay-deposit-amount { @@ -67,6 +121,12 @@ text-align: right; font-size: 36px; line-height: 82px; + + @include breakpoint( '<660px' ) { + line-height: 36px; + text-align: left; + border-top: 1px solid $studio-gray-5; + } } } diff --git a/client/deposits/details/test/__snapshots__/index.tsx.snap b/client/deposits/details/test/__snapshots__/index.tsx.snap index fecce68ae08..6b0973874ba 100644 --- a/client/deposits/details/test/__snapshots__/index.tsx.snap +++ b/client/deposits/details/test/__snapshots__/index.tsx.snap @@ -41,11 +41,6 @@ exports[`Deposit overview renders automatic payout correctly 1`] = ` -
- MOCK BANK •••• 1234 (USD) -
  • +
    +
    +
    + + Payout details + +
    +
    +
    +
    +

    + Bank account +

    +
    + MOCK BANK •••• 1234 (USD) +
    +
    +
    +

    + Bank reference ID +

    +
    +
    + Not available +
    +
    +
    +
    +
    +
    +
    `; @@ -113,11 +181,6 @@ exports[`Deposit overview renders automatic withdrawal correctly 1`] = ` -
    - MOCK BANK •••• 1234 (USD) -
  • +
    +
    +
    + + Withdrawal details + +
    +
    +
    +
    +

    + Bank account +

    +
    + MOCK BANK •••• 1234 (USD) +
    +
    +
    +

    + Bank reference ID +

    +
    +
    + Not available +
    +
    +
    +
    +
    +
    +
    `; @@ -191,11 +327,6 @@ exports[`Deposit overview renders instant deposit correctly 1`] = ` -
    - MOCK BANK •••• 1234 (USD) -
  • +
    +
    +
    + + Payout details + +
    +
    +
    +
    +

    + Bank account +

    +
    + MOCK BANK •••• 1234 (USD) +
    +
    +
    +

    + Bank reference ID +

    +
    +
    + Not available +
    +
    +
    +
    +
    +
    +
    `;