Merge pull request #3561 from woocommerce/PCP-4483-add-a-button-to-copy-merchant-credentials-in-settings-tab

Add buttons to copy merchant credentials in the Settings tab (4483)
This commit is contained in:
Niklas Gutberlet 2025-08-04 19:33:44 +02:00 committed by GitHub
commit 7c8532da62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 223 additions and 10 deletions

View file

@ -1,9 +1,50 @@
.ppcp--static-value {
@include font(13, 26, 400);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.ppcp--static-value-with-copy {
display: inline-flex;
align-items: flex-start;
gap: 6px;
overflow: visible;
.ppcp--static-value-text {
flex: 1;
min-width: 0;
white-space: normal;
word-break: break-all;
overflow-wrap: break-word;
max-width: 37ch;
}
}
&:not(.ppcp--static-value-with-copy) {
overflow: hidden;
}
}
.ppcp-copy-button {
display: flex;
border: none;
background: transparent;
color: $color-gray-700;
cursor: pointer;
transition: color 0.2s ease;
flex-shrink: 0;
&:hover:not(:disabled) {
color: $color-blueberry;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
svg {
fill: currentColor;
}
}
// Fix the checkbox layout (add gap between checkbox and label).

View file

@ -1,9 +1,31 @@
import { Action } from '../Elements';
import classNames from 'classnames';
import CopyButton from '../Elements/CopyButton';
const ControlStaticValue = ( { value } ) => (
<Action>
<div className="ppcp--static-value">{ value }</div>
</Action>
);
const ControlStaticValue = ( {
value,
showCopy = false,
copyButtonProps = {},
className,
...props
} ) => {
const wrapperClass = classNames( 'ppcp--static-value', {
'ppcp--static-value-with-copy': showCopy,
'ppcp--has-copy': showCopy,
} );
return (
<Action className={ className } { ...props }>
{ showCopy ? (
<div className={ wrapperClass }>
<div className="ppcp--static-value-text">{ value }</div>
<CopyButton value={ value } { ...copyButtonProps } />
</div>
) : (
<div className={ wrapperClass }>{ value }</div>
) }
</Action>
);
};
export default ControlStaticValue;

View file

@ -0,0 +1,106 @@
import { __ } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
import { Tooltip } from '@wordpress/components';
import { SVG, Path } from '@wordpress/primitives';
import classNames from 'classnames';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
const COPY_CONFIRMATION_DURATION = 1000;
/**
* Copy button component with tooltip and icon transition
* @param {Object} props - Component props
* @param {string} props.value - The text value to copy to clipboard
* @param {string} [props.className] - Additional CSS class names
* @param {string} [props.ariaLabel] - Custom aria-label for the button
*/
const CopyButton = ( { value, className, ariaLabel, ...props } ) => {
const { copy, copied, error } = useCopyToClipboard( {
successDuration: COPY_CONFIRMATION_DURATION,
} );
const buttonClass = classNames( 'ppcp-copy-button', className );
const getTooltipText = () => {
if ( copied ) {
return __( 'Copied!', 'woocommerce-paypal-payments' );
}
if ( error ) {
return __( 'Failed to copy', 'woocommerce-paypal-payments' );
}
return __( 'Copy to clipboard', 'woocommerce-paypal-payments' );
};
const handleCopy = async () => {
if ( ! value ) {
return;
}
await copy( value );
if ( copied ) {
speak(
__( 'Copied to clipboard', 'woocommerce-paypal-payments' ),
'assertive'
);
return;
}
if ( error ) {
speak(
__(
'Failed to copy to clipboard',
'woocommerce-paypal-payments'
),
'assertive'
);
}
};
return (
<Tooltip
text={ getTooltipText() }
placement="top"
delay={ 100 }
hideOnClick={ false }
>
<button
type="button"
onClick={ handleCopy }
className={ buttonClass }
disabled={ ! value }
aria-label={ ariaLabel || getTooltipText() }
{ ...props }
>
{ copied ? <CheckIcon /> : <CopyIcon /> }
</button>
</Tooltip>
);
};
const CopyIcon = () => (
<SVG
width="20"
height="20"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<Path
fillRule="evenodd"
d="M16 16v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3V5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-3zm2.5-10.5v9H16V9a1 1 0 0 0-1-1H9.5V5.5h9z"
clipRule="evenodd"
/>
</SVG>
);
const CheckIcon = () => (
<SVG
width="20"
height="20"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<Path d="M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z" />
</SVG>
);
export default CopyButton;

View file

@ -37,17 +37,23 @@ const ConnectionStatus = () => {
title={ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
className="ppcp--no-gap"
>
<ControlStaticValue value={ merchant.id } />
<ControlStaticValue value={ merchant.id } showCopy={ true } />
</SettingsBlock>
<SettingsBlock
title={ __( 'Email address', 'woocommerce-paypal-payments' ) }
>
<ControlStaticValue value={ merchant.email } />
<ControlStaticValue
value={ merchant.email }
showCopy={ true }
/>
</SettingsBlock>
<SettingsBlock
title={ __( 'Client ID', 'woocommerce-paypal-payments' ) }
>
<ControlStaticValue value={ merchant.clientId } />
<ControlStaticValue
value={ merchant.clientId }
showCopy={ true }
/>
</SettingsBlock>
</SettingsCard>
);

View file

@ -0,0 +1,38 @@
import { useState, useRef } from '@wordpress/element';
/**
* Custom hook for handling copy to clipboard functionality
*
* @param {Object} options - Configuration options
* @param {number} options.successDuration - How long to show success state (ms)
* @return {Object} Copy functionality and state
*/
export const useCopyToClipboard = ( options = {} ) => {
const { successDuration = 1000 } = options;
const [ copied, setCopied ] = useState( false );
const [ error, setError ] = useState( false );
const timerRef = useRef( null );
const copy = async ( text ) => {
try {
await navigator.clipboard.writeText( text );
clearTimeout( timerRef.current );
setCopied( true );
setError( false );
timerRef.current = setTimeout(
() => setCopied( false ),
successDuration
);
} catch ( err ) {
console.error( 'Copy failed:', err );
setError( true );
setCopied( false );
}
};
return { copy, copied, error };
};
export default useCopyToClipboard;