mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-05 08:59:14 +08:00
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:
commit
689463e243
46 changed files with 1750 additions and 1023 deletions
|
@ -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, {
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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 );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue