Merge pull request #2945 from woocommerce/PCP-4020-review-and-fix-the-sandbox-login

Review and fix the login logic - Frontend changes (4020)
This commit is contained in:
Philipp Stracker 2025-01-09 15:26:05 +01:00 committed by GitHub
commit 689463e243
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1750 additions and 1023 deletions

View file

@ -24,6 +24,7 @@ const BusyContext = createContext( false );
* @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state.
* @param {string} props.className - Additional class names for the wrapper.
* @param {Function} props.onBusy - Callback to process child props when busy.
* @param {boolean} props.isBusy - Optional. Additional condition to determine if the component is busy.
*/
const BusyStateWrapper = ( {
children,
@ -31,11 +32,12 @@ const BusyStateWrapper = ( {
busySpinner = true,
className = '',
onBusy = () => ( { disabled: true } ),
isBusy = false,
} ) => {
const { isBusy } = CommonHooks.useBusyState();
const { isBusy: globalIsBusy } = CommonHooks.useBusyState();
const hasBusyParent = useContext( BusyContext );
const isBusyComponent = isBusy && enabled;
const isBusyComponent = ( isBusy || globalIsBusy ) && enabled;
const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent;
const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, {

View file

@ -1,208 +1,13 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
import {
useRef,
useState,
useEffect,
useMemo,
useCallback,
} from '@wordpress/element';
import classNames from 'classnames';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import Separator from '../../../ReusableComponents/Separator';
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
import { CommonHooks } from '../../../../data';
import {
useSandboxConnection,
useManualConnection,
} from '../../../../hooks/useHandleConnections';
import ConnectionButton from './ConnectionButton';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const FORM_ERRORS = {
noClientId: __(
'Please enter your Client ID',
'woocommerce-paypal-payments'
),
noClientSecret: __(
'Please enter your Secret Key',
'woocommerce-paypal-payments'
),
invalidClientId: __(
'Please enter a valid Client ID',
'woocommerce-paypal-payments'
),
};
import SandboxConnectionForm from './SandboxConnectionForm';
import ManualConnectionForm from './ManualConnectionForm';
const AdvancedOptionsForm = () => {
const [ clientValid, setClientValid ] = useState( false );
const [ secretValid, setSecretValid ] = useState( false );
const { isBusy } = CommonHooks.useBusyState();
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
const {
handleConnectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = useManualConnection();
const refClientId = useRef( null );
const refClientSecret = useRef( null );
const validateManualConnectionForm = useCallback( () => {
const checks = [
{
ref: refClientId,
valid: () => clientId,
errorMessage: FORM_ERRORS.noClientId,
},
{
ref: refClientId,
valid: () => clientValid,
errorMessage: FORM_ERRORS.invalidClientId,
},
{
ref: refClientSecret,
valid: () => clientSecret && secretValid,
errorMessage: FORM_ERRORS.noClientSecret,
},
];
for ( const { ref, valid, errorMessage } of checks ) {
if ( valid() ) {
continue;
}
ref?.current?.focus();
throw new Error( errorMessage );
}
}, [ clientId, clientSecret, clientValid, secretValid ] );
const handleManualConnect = useCallback(
() =>
handleConnectViaIdAndSecret( {
validation: validateManualConnectionForm,
} ),
[ handleConnectViaIdAndSecret, validateManualConnectionForm ]
);
useEffect( () => {
setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) );
setSecretValid( clientSecret && clientSecret.length > 0 );
}, [ clientId, clientSecret ] );
const clientIdLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' )
: __( 'Live Client ID', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
const secretKeyLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' )
: __( 'Live Secret Key', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input'
);
return (
<>
<BusyStateWrapper>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
'woocommerce-paypal-payments'
) }
description={ __(
'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
'woocommerce-paypal-payments'
) }
isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode }
>
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
showIcon={ false }
variant="secondary"
className="small-button"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
<SandboxConnectionForm />
<Separator withLine={ false } />
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={ clientIdLabel }
value={ clientId }
onChange={ setClientId }
className={ classNames( {
'has-error': ! clientValid,
} ) }
/>
{ clientValid || (
<p className="client-id-error">
{ FORM_ERRORS.invalidClientId }
</p>
) }
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={ secretKeyLabel }
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button
variant="secondary"
className="small-button"
onClick={ handleManualConnect }
>
{ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
</Button>
</SettingsToggleBlock>
</BusyStateWrapper>
<ManualConnectionForm />
</>
);
};

View file

@ -1,15 +1,45 @@
import { Button } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import classNames from 'classnames';
import { CommonHooks } from '../../../../data';
import { openSignup } from '../../../ReusableComponents/Icons';
import {
useProductionConnection,
useSandboxConnection,
} from '../../../../hooks/useHandleConnections';
import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
/**
* Button component that outputs a placeholder button when no onboardingUrl is present yet - the
* placeholder button looks identical to the working button, but has no href, target, or
* custom connection attributes.
*
* @param {Object} props
* @param {string} props.className
* @param {string} props.variant
* @param {boolean} props.showIcon
* @param {?string} props.href
* @param {Element} props.children
*/
const ButtonOrPlaceholder = ( {
className,
variant,
showIcon,
href,
children,
} ) => {
const buttonProps = {
className,
variant,
icon: showIcon ? openSignup : null,
};
if ( href ) {
buttonProps.href = href;
buttonProps.target = 'PPFrame';
buttonProps[ 'data-paypal-button' ] = 'true';
buttonProps[ 'data-paypal-onboard-button' ] = 'true';
}
return <Button { ...buttonProps }>{ children }</Button>;
};
const ConnectionButton = ( {
title,
isSandbox = false,
@ -17,31 +47,45 @@ const ConnectionButton = ( {
showIcon = true,
className = '',
} ) => {
const { handleSandboxConnect } = useSandboxConnection();
const { handleProductionConnect } = useProductionConnection();
const {
onboardingUrl,
scriptLoaded,
setCompleteHandler,
removeCompleteHandler,
} = useHandleOnboardingButton( isSandbox );
const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
'sandbox-mode': isSandbox,
'live-mode': ! isSandbox,
} );
const environment = isSandbox ? 'sandbox' : 'production';
const handleConnectClick = async () => {
if ( isSandbox ) {
await handleSandboxConnect();
} else {
await handleProductionConnect();
useEffect( () => {
if ( scriptLoaded && onboardingUrl ) {
window.PAYPAL.apps.Signup.render();
setCompleteHandler( environment );
}
};
return () => {
removeCompleteHandler();
};
}, [
scriptLoaded,
onboardingUrl,
environment,
setCompleteHandler,
removeCompleteHandler,
] );
return (
<BusyStateWrapper>
<Button
<BusyStateWrapper isBusy={ ! onboardingUrl }>
<ButtonOrPlaceholder
className={ buttonClassName }
variant={ variant }
icon={ showIcon ? openSignup : null }
onClick={ handleConnectClick }
showIcon={ showIcon }
href={ onboardingUrl }
>
<span className="button-title">{ title }</span>
</Button>
</ButtonOrPlaceholder>
</BusyStateWrapper>
);
};

View file

@ -0,0 +1,188 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from '@wordpress/element';
import { Button, TextControl } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import {
useDirectAuthentication,
useSandboxConnection,
} from '../../../../hooks/useHandleConnections';
import { OnboardingHooks } from '../../../../data';
const FORM_ERRORS = {
noClientId: __(
'Please enter your Client ID',
'woocommerce-paypal-payments'
),
noClientSecret: __(
'Please enter your Secret Key',
'woocommerce-paypal-payments'
),
invalidClientId: __(
'Please enter a valid Client ID',
'woocommerce-paypal-payments'
),
};
const ManualConnectionForm = () => {
const [ clientValid, setClientValid ] = useState( false );
const [ secretValid, setSecretValid ] = useState( false );
const { isSandboxMode } = useSandboxConnection();
const {
manualClientId,
setManualClientId,
manualClientSecret,
setManualClientSecret,
} = OnboardingHooks.useManualConnectionForm();
const {
handleDirectAuthentication,
isManualConnectionMode,
setManualConnectionMode,
} = useDirectAuthentication();
const refClientId = useRef( null );
const refClientSecret = useRef( null );
// Form data validation and sanitation.
const getManualConnectionDetails = useCallback( () => {
const checks = [
{
ref: refClientId,
valid: () => manualClientId,
errorMessage: FORM_ERRORS.noClientId,
},
{
ref: refClientId,
valid: () => clientValid,
errorMessage: FORM_ERRORS.invalidClientId,
},
{
ref: refClientSecret,
valid: () => manualClientSecret && secretValid,
errorMessage: FORM_ERRORS.noClientSecret,
},
];
for ( const { ref, valid, errorMessage } of checks ) {
if ( valid() ) {
continue;
}
ref?.current?.focus();
throw new Error( errorMessage );
}
return {
clientId: manualClientId,
clientSecret: manualClientSecret,
isSandbox: isSandboxMode,
};
}, [
manualClientId,
manualClientSecret,
isSandboxMode,
clientValid,
secretValid,
] );
// On-the-fly form validation.
useEffect( () => {
setClientValid(
! manualClientId || /^A[\w-]{79}$/.test( manualClientId )
);
setSecretValid( manualClientSecret && manualClientSecret.length > 0 );
}, [ manualClientId, manualClientSecret ] );
// Environment-specific field labels.
const clientIdLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' )
: __( 'Live Client ID', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
const secretKeyLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' )
: __( 'Live Secret Key', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
// Translations with placeholders.
const advancedUsersDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, <a target="_blank" href="%s">click here</a>.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input'
);
// Button click handler.
const handleManualConnect = useCallback(
() => handleDirectAuthentication( getManualConnectionDetails ),
[ handleDirectAuthentication, getManualConnectionDetails ]
);
return (
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={ clientIdLabel }
value={ manualClientId }
onChange={ setManualClientId }
className={ classNames( {
'has-error': ! clientValid,
} ) }
/>
{ clientValid || (
<p className="client-id-error">
{ FORM_ERRORS.invalidClientId }
</p>
) }
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={ secretKeyLabel }
value={ manualClientSecret }
onChange={ setManualClientSecret }
type="password"
/>
<Button
variant="secondary"
className="small-button"
onClick={ handleManualConnect }
>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
</BusyStateWrapper>
);
};
export default ManualConnectionForm;

View file

@ -1,7 +1,6 @@
import { Button, Icon } from '@wordpress/components';
import { chevronLeft } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { OnboardingHooks } from '../../../../data';

View file

@ -0,0 +1,42 @@
import { __ } from '@wordpress/i18n';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import { useSandboxConnection } from '../../../../hooks/useHandleConnections';
import ConnectionButton from './ConnectionButton';
const SandboxConnectionForm = () => {
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
return (
<BusyStateWrapper>
<SettingsToggleBlock
label={ __(
'Enable Sandbox Mode',
'woocommerce-paypal-payments'
) }
description={ __(
'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.',
'woocommerce-paypal-payments'
) }
isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode }
>
<ConnectionButton
title={ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
showIcon={ false }
variant="secondary"
className="small-button"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
);
};
export default SandboxConnectionForm;

View file

@ -17,7 +17,7 @@ const TabOverview = () => {
const [ todosData, setTodosData ] = useState( todosDataDefault );
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo();
const { merchantFeatures } = useMerchantInfo();
const { refreshFeatureStatuses, setActiveModal } =
useDispatch( STORE_NAME );
@ -30,13 +30,13 @@ const TabOverview = () => {
// Map merchant features status to our config
const features = useMemo( () => {
return featuresData.map( ( feature ) => {
const merchantFeature = merchant?.features?.[ feature.id ];
const merchantFeature = merchantFeatures?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
}, [ featuresData, merchant?.features ] );
}, [ featuresData, merchantFeatures ] );
const refreshHandler = async () => {
setIsRefreshing( true );