mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
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:
commit
7c8532da62
5 changed files with 223 additions and 10 deletions
|
@ -1,9 +1,50 @@
|
||||||
.ppcp--static-value {
|
.ppcp--static-value {
|
||||||
@include font(13, 26, 400);
|
@include font(13, 26, 400);
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
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).
|
// Fix the checkbox layout (add gap between checkbox and label).
|
||||||
|
|
|
@ -1,9 +1,31 @@
|
||||||
import { Action } from '../Elements';
|
import { Action } from '../Elements';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import CopyButton from '../Elements/CopyButton';
|
||||||
|
|
||||||
const ControlStaticValue = ( { value } ) => (
|
const ControlStaticValue = ( {
|
||||||
<Action>
|
value,
|
||||||
<div className="ppcp--static-value">{ value }</div>
|
showCopy = false,
|
||||||
</Action>
|
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;
|
export default ControlStaticValue;
|
||||||
|
|
|
@ -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;
|
|
@ -37,17 +37,23 @@ const ConnectionStatus = () => {
|
||||||
title={ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
|
title={ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
|
||||||
className="ppcp--no-gap"
|
className="ppcp--no-gap"
|
||||||
>
|
>
|
||||||
<ControlStaticValue value={ merchant.id } />
|
<ControlStaticValue value={ merchant.id } showCopy={ true } />
|
||||||
</SettingsBlock>
|
</SettingsBlock>
|
||||||
<SettingsBlock
|
<SettingsBlock
|
||||||
title={ __( 'Email address', 'woocommerce-paypal-payments' ) }
|
title={ __( 'Email address', 'woocommerce-paypal-payments' ) }
|
||||||
>
|
>
|
||||||
<ControlStaticValue value={ merchant.email } />
|
<ControlStaticValue
|
||||||
|
value={ merchant.email }
|
||||||
|
showCopy={ true }
|
||||||
|
/>
|
||||||
</SettingsBlock>
|
</SettingsBlock>
|
||||||
<SettingsBlock
|
<SettingsBlock
|
||||||
title={ __( 'Client ID', 'woocommerce-paypal-payments' ) }
|
title={ __( 'Client ID', 'woocommerce-paypal-payments' ) }
|
||||||
>
|
>
|
||||||
<ControlStaticValue value={ merchant.clientId } />
|
<ControlStaticValue
|
||||||
|
value={ merchant.clientId }
|
||||||
|
showCopy={ true }
|
||||||
|
/>
|
||||||
</SettingsBlock>
|
</SettingsBlock>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
Loading…
Add table
Add a link
Reference in a new issue