Merge branch 'trunk' into fix/PCP-4072

# Conflicts:
#	modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/FeatureSettingsBlock.js
#	modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js
This commit is contained in:
carmenmaymo 2025-01-10 12:39:29 +01:00
commit 7b6ec6e9d6
No known key found for this signature in database
GPG key ID: 6023F686B0F3102E
75 changed files with 3427 additions and 2167 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

@ -18,7 +18,7 @@ const ButtonSettingsBlock = ( { title, description, ...props } ) => (
: undefined
}
>
{ props.actionProps.value }
{ props?.actionProps?.value }
</Button>
</Action>
</SettingsBlock>

View file

@ -19,6 +19,27 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
);
};
const renderButton = ( button ) => {
const buttonElement = (
<Button
className={ button.class ? button.class : '' }
key={ button.text }
variant={ button.type }
onClick={ button.onClick }
>
{ button.text }
</Button>
);
return button.urls ? (
<a href={ button.urls.live } key={ button.text }>
{ buttonElement }
</a>
) : (
buttonElement
);
};
return (
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature">
<Header>
@ -35,17 +56,7 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
</Header>
<Action>
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map( ( button ) => (
<Button
className={ button.class ? button.class : '' }
href={ button.url }
isBusy={ props.actionProps?.isBusy }
key={ button.text }
variant={ button.type }
>
{ button.text }
</Button>
) ) }
{ props.actionProps?.buttons.map( renderButton ) }
</div>
</Action>
</SettingsBlock>

View file

@ -1,51 +1,50 @@
import { useState } from '@wordpress/element';
import { ToggleControl } from '@wordpress/components';
import SettingsBlock from './SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon';
import data from '../../../utils/data';
import { hasSettings } from '../../Screens/Overview/TabSettingsElements/Blocks/PaymentMethods';
const PaymentMethodItemBlock = ( props ) => {
const [ toggleIsChecked, setToggleIsChecked ] = useState( false );
const [ modalIsVisible, setModalIsVisible ] = useState( false );
const Modal = props?.modal;
const PaymentMethodItemBlock = ( {
id,
title,
description,
icon,
onTriggerModal,
onSelect,
isSelected,
} ) => {
// Only show settings icon if this method has fields configured
const hasModal = hasSettings( id );
return (
<>
<SettingsBlock className="ppcp-r-settings-block__payment-methods__item">
<div className="ppcp-r-settings-block__payment-methods__item__inner">
<div className="ppcp-r-settings-block__payment-methods__item__title-wrapper">
<PaymentMethodIcon
icons={ [ props.icon ] }
type={ props.icon }
/>
<span className="ppcp-r-settings-block__payment-methods__item__title">
{ props.title }
</span>
</div>
<p className="ppcp-r-settings-block__payment-methods__item__description">
{ props.description }
</p>
<div className="ppcp-r-settings-block__payment-methods__item__footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ toggleIsChecked }
onChange={ setToggleIsChecked }
/>
{ Modal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
onClick={ () => setModalIsVisible( true ) }
>
{ data().getImage( 'icon-settings.svg' ) }
</div>
) }
</div>
<SettingsBlock className="ppcp-r-settings-block__payment-methods__item">
<div className="ppcp-r-settings-block__payment-methods__item__inner">
<div className="ppcp-r-settings-block__payment-methods__item__title-wrapper">
<PaymentMethodIcon icons={ [ icon ] } type={ icon } />
<span className="ppcp-r-settings-block__payment-methods__item__title">
{ title }
</span>
</div>
</SettingsBlock>
{ Modal && modalIsVisible && (
<Modal setModalIsVisible={ setModalIsVisible } />
) }
</>
<p className="ppcp-r-settings-block__payment-methods__item__description">
{ description }
</p>
<div className="ppcp-r-settings-block__payment-methods__item__footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ isSelected }
onChange={ onSelect }
/>
{ hasModal && onTriggerModal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
onClick={ onTriggerModal }
>
{ data().getImage( 'icon-settings.svg' ) }
</div>
) }
</div>
</div>
</SettingsBlock>
);
};

View file

@ -2,14 +2,21 @@ import { useState, useCallback } from '@wordpress/element';
import SettingsBlock from './SettingsBlock';
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
const [ selectedMethod, setSelectedMethod ] = useState( null );
const PaymentMethodsBlock = ( {
paymentMethods,
className = '',
onTriggerModal,
} ) => {
const [ selectedMethods, setSelectedMethods ] = useState( {} );
const handleSelect = useCallback( ( methodId, isSelected ) => {
setSelectedMethod( isSelected ? methodId : null );
setSelectedMethods( ( prev ) => ( {
...prev,
[ methodId ]: isSelected,
} ) );
}, [] );
if ( paymentMethods.length === 0 ) {
if ( ! paymentMethods?.length ) {
return null;
}
@ -21,10 +28,15 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
<PaymentMethodItemBlock
key={ paymentMethod.id }
{ ...paymentMethod }
isSelected={ selectedMethod === paymentMethod.id }
isSelected={ Boolean(
selectedMethods[ paymentMethod.id ]
) }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}
onTriggerModal={ () =>
onTriggerModal?.( paymentMethod.id )
}
/>
) ) }
</SettingsBlock>

View file

@ -33,8 +33,10 @@ export const Header = ( { children, className = '' } ) => (
);
// Card Elements
export const Content = ( { children } ) => (
<div className="ppcp-r-settings-card__content">{ children }</div>
export const Content = ( { children, id = '' } ) => (
<div id={ id } className="ppcp-r-settings-card__content">
{ children }
</div>
);
export const ContentWrapper = ( { children } ) => (

View file

@ -1,13 +1,4 @@
import { PayPalCheckbox, handleCheckboxState } from '../Fields';
import data from '../../../utils/data';
const TodoSettingsBlock = ( {
todos,
setTodos,
todosData,
setTodosData,
className = '',
} ) => {
const TodoSettingsBlock = ( { todosData, className = '' } ) => {
if ( todosData.length === 0 ) {
return null;
}
@ -16,54 +7,33 @@ const TodoSettingsBlock = ( {
<div
className={ `ppcp-r-settings-block__todo ppcp-r-todo-items ${ className }` }
>
{ todosData.map( ( todo ) => (
<TodoItem
name="todo_items"
key={ todo.value }
value={ todo.value }
currentValue={ todos }
changeCallback={ setTodos }
description={ todo.description }
changeTodos={ setTodosData }
todosData={ todosData }
/>
) ) }
{ todosData
.slice( 0, 5 )
.filter( ( todo ) => {
return ! todo.isCompleted();
} )
.map( ( todo ) => (
<TodoItem
key={ todo.id }
title={ todo.title }
onClick={ todo.onClick }
/>
) ) }
</div>
);
};
const TodoItem = ( props ) => {
return (
<div className="ppcp-r-todo-item">
<div className="ppcp-r-todo-item" onClick={ props.onClick }>
<div className="ppcp-r-todo-item__inner">
<PayPalCheckbox
{ ...{
...props,
handleCheckboxState,
} }
/>
<div className="ppcp-r-todo-item__icon"></div>
<div className="ppcp-r-todo-item__description">
{ props.description }
{ props.title }
</div>
</div>
<div
className="ppcp-r-todo-item__close"
onClick={ () =>
removeTodo(
props.value,
props.todosData,
props.changeTodos
)
}
>
{ data().getImage( 'icon-close.svg' ) }
</div>
</div>
);
};
const removeTodo = ( todoValue, todosData, changeTodos ) => {
changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) );
};
export default TodoSettingsBlock;

View file

@ -1,6 +1,7 @@
import { Content, ContentWrapper } from './SettingsBlocks';
const SettingsCard = ( {
id,
className: extraClassName,
title,
description,
@ -17,8 +18,10 @@ const SettingsCard = ( {
if ( contentItems ) {
return (
<ContentWrapper>
{ contentItems.map( ( item, index ) => (
<Content key={ index }>{ item }</Content>
{ contentItems.map( ( item ) => (
<Content key={ item.key } id={ item.key }>
{ item }
</Content>
) ) }
</ContentWrapper>
);
@ -33,7 +36,7 @@ const SettingsCard = ( {
};
return (
<div className={ className }>
<div id={ id } className={ className }>
<div className="ppcp-r-settings-card__header">
<div className="ppcp-r-settings-card__content-inner">
<span className="ppcp-r-settings-card__title">

View file

@ -1,4 +1,6 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
// TODO: Migrate to Tabs (TabPanel v2) once its API is publicly available, as it provides programmatic tab switching support: https://github.com/WordPress/gutenberg/issues/52997
import { TabPanel } from '@wordpress/components';
import { getQuery, updateQueryString } from '../../utils/navigation';

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

@ -1,62 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { RadioControl } from '@wordpress/components';
const ModalAcdc = ( { setModalIsVisible } ) => {
const [ threeDSecure, setThreeDSecure ] = useState( 'no-3d-secure' );
const acdcOptions = [
{
label: __( 'No 3D Secure', 'woocommerce-paypal-payments' ),
value: 'no-3d-secure',
},
{
label: __( 'Only when required', 'woocommerce-paypal-payments' ),
value: 'only-required-3d-secure',
},
{
label: __(
'Always require 3D Secure',
'woocommerce-paypal-payments'
),
value: 'always-3d-secure',
},
];
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-cards-big"
title={ __(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
) }
>
<strong className="ppcp-r-modal__content-title">
{ __( '3D Secure', 'woocommerce-paypal-payments' ) }
</strong>
<p className="ppcp-r-modal__description">
{ __(
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
'woocommerce-paypal-payments'
) }
</p>
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--acdc">
<RadioControl
onChange={ setThreeDSecure }
selected={ threeDSecure }
options={ acdcOptions }
/>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalAcdc;

View file

@ -1,63 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { Button, ToggleControl } from '@wordpress/components';
import { PayPalRdb } from '../../../ReusableComponents/Fields';
import { useState } from '@wordpress/element';
const ModalFastlane = ( { setModalIsVisible } ) => {
const [ fastlaneSettings, setFastlaneSettings ] = useState( {
cardholderName: false,
displayWatermark: false,
} );
const updateFormValue = ( key, value ) => {
setFastlaneSettings( { ...fastlaneSettings, [ key ]: value } );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-fastlane-big"
title={ __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ) }
size="small"
>
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--fastlane">
<div className="ppcp-r-modal__field-row">
<ToggleControl
className="ppcp-r-modal__inverted-toggle-control"
checked={ fastlaneSettings.cardholderName }
onChange={ ( newValue ) =>
updateFormValue( 'cardholderName', newValue )
}
label={ __(
'Display cardholder name',
'woocommerce-paypal-payments'
) }
id="ppcp-r-fastlane-settings-cardholder"
/>
</div>
<div className="ppcp-r-modal__field-row">
<ToggleControl
className="ppcp-r-modal__inverted-toggle-control"
checked={ fastlaneSettings.displayWatermark }
onChange={ ( newValue ) =>
updateFormValue( 'displayWatermark', newValue )
}
label={ __(
'Display Fastlane Watermark',
'woocommerce-paypal-payments'
) }
id="ppcp-r-fastlane-settings-watermark"
/>
</div>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalFastlane;

View file

@ -1,76 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { ToggleControl, Button, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
const ModalPayPal = ( { setModalIsVisible } ) => {
const [ paypalSettings, setPaypalSettings ] = useState( {
checkoutPageTitle: 'PayPal',
checkoutPageDescription: 'Pay via PayPal',
showLogo: false,
} );
const updateFormValue = ( key, value ) => {
setPaypalSettings( { ...paypalSettings, [ key ]: value } );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-paypal-big"
title={ __( 'PayPal', 'woocommerce-paypal-payments' ) }
>
<div className="ppcp-r-modal__field-rows">
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ __(
'Checkout page title',
'woocommerce-paypal-payments'
) }
value={ paypalSettings.checkoutPageTitle }
onChange={ ( newValue ) =>
updateFormValue( 'checkoutPageTitle', newValue )
}
/>
</div>
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ __(
'Checkout page description',
'woocommerce-paypal-payments'
) }
value={ paypalSettings.checkoutPageDescription }
onChange={ ( newValue ) =>
updateFormValue(
'checkoutPageDescription',
newValue
)
}
/>
</div>
<div className="ppcp-r-modal__field-row">
<ToggleControl
label={ __(
'Show logo',
'woocommerce-paypal-payments'
) }
id="ppcp-r-paypal-settings-show-logo"
checked={ paypalSettings.showLogo }
onChange={ ( newValue ) => {
updateFormValue( 'showLogo', newValue );
} }
/>
</div>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalPayPal;

View file

@ -1,5 +1,5 @@
import { __, sprintf } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useState, useMemo } from '@wordpress/element';
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { reusableBlock } from '@wordpress/icons';
@ -11,66 +11,76 @@ import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/Featur
import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
import { useMerchantInfo } from '../../../data/common/hooks';
import { STORE_NAME } from '../../../data/common';
import Features from './TabSettingsElements/Blocks/Features';
import { todosData } from '../../../data/settings/tab-overview-todos-data';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../ReusableComponents/Icons';
const TabOverview = () => {
const [ todos, setTodos ] = useState( [] );
const [ todosData, setTodosData ] = useState( todosDataDefault );
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo();
const { refreshFeatureStatuses } = useDispatch( STORE_NAME );
const { merchant, merchantFeatures } = useMerchantInfo();
const { refreshFeatureStatuses, setActiveModal } =
useDispatch( STORE_NAME );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const features = featuresDefault.map( ( feature ) => {
const merchantFeature = merchant?.features?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
// Get the features data with access to setActiveModal
const featuresData = useMemo(
() => Features.getFeatures( setActiveModal ),
[ setActiveModal ]
);
// Map merchant features status to our config
const features = useMemo( () => {
return featuresData.map( ( feature ) => {
const merchantFeature = merchantFeatures?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
}, [ featuresData, merchantFeatures ] );
const refreshHandler = async () => {
setIsRefreshing( true );
try {
const result = await refreshFeatureStatuses();
if ( result && ! result.success ) {
const errorMessage = sprintf(
/* translators: %s: error message */
__(
'Operation failed: %s Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
result.message ||
__( 'Unknown error', 'woocommerce-paypal-payments' )
);
const result = await refreshFeatureStatuses();
if ( result && ! result.success ) {
const errorMessage = sprintf(
/* translators: %s: error message */
__(
'Operation failed: %s Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
result.message ||
__( 'Unknown error', 'woocommerce-paypal-payments' )
);
createErrorNotice( errorMessage, {
icon: NOTIFICATION_ERROR,
} );
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
createSuccessNotice(
__(
'Features refreshed successfully.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
console.log( 'Features refreshed successfully.' );
createErrorNotice( errorMessage, {
icon: NOTIFICATION_ERROR,
} );
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
createSuccessNotice(
__(
'Features refreshed successfully.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
console.log( 'Features refreshed successfully.' );
}
} finally {
setIsRefreshing( false );
}
setIsRefreshing( false );
};
return (
@ -87,12 +97,7 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
>
<TodoSettingsBlock
todos={ todos }
setTodos={ setTodos }
todosData={ todosData }
setTodosData={ setTodosData }
/>
<TodoSettingsBlock todosData={ todosData } />
</SettingsCard>
) }
@ -131,28 +136,47 @@ const TabOverview = () => {
</Button>
</>
}
contentItems={ features.map( ( feature ) => (
<FeatureSettingsBlock
key={ feature.id }
title={ feature.title }
description={ feature.description }
actionProps={ {
buttons: feature.buttons,
isBusy: isRefreshing,
enabled: feature.enabled,
notes: feature.notes,
badge: feature.enabled
? {
text: __(
'Active',
'woocommerce-paypal-payments'
),
type: TITLE_BADGE_POSITIVE,
}
: undefined,
} }
/>
) ) }
contentItems={ features.map( ( feature ) => {
return (
<FeatureSettingsBlock
key={ feature.id }
title={ feature.title }
description={ feature.description }
actionProps={ {
buttons: feature.buttons
.filter(
( button ) =>
! button.showWhen || // Learn more buttons
( feature.enabled &&
button.showWhen ===
'enabled' ) ||
( ! feature.enabled &&
button.showWhen === 'disabled' )
)
.map( ( button ) => ( {
...button,
url: button.urls
? merchant?.isSandbox
? button.urls.sandbox
: button.urls.live
: button.url,
} ) ),
isBusy: isRefreshing,
enabled: feature.enabled,
notes: feature.notes,
badge: feature.enabled
? {
text: __(
'Active',
'woocommerce-paypal-payments'
),
type: TITLE_BADGE_POSITIVE,
}
: undefined,
} }
/>
);
} ) }
/>
<SettingsCard
@ -212,182 +236,4 @@ const TabOverview = () => {
);
};
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
const todosDataDefault = [
{
value: 'paypal_later_messaging',
description: __(
'Enable Pay Later messaging',
'woocommerce-paypal-payments'
),
},
{
value: 'capture_authorized_payments',
description: __(
'Capture authorized payments',
'woocommerce-paypal-payments'
),
},
{
value: 'enable_google_pay',
description: __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
},
{
value: 'paypal_shortcut',
description: __(
'Add PayPal shortcut to the Cart page',
'woocommerce-paypal-payments'
),
},
{
value: 'advanced_cards',
description: __(
'Add Advanced Cards to Blocks Checkout',
'woocommerce-paypal-payments'
),
},
];
// TODO: Hardcoding this list here is not the best idea. Can we move this to a REST API response?
const featuresDefault = [
{
id: 'save_paypal_and_venmo',
title: __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
description: __(
'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'advanced_credit_and_debit_cards',
title: __(
'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments'
),
description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'alternative_payment_methods',
title: __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
),
description: __(
'Offer global, country-specific payment options for your customers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'google_pay',
title: __( 'Google Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'apple_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Apple Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
},
{
id: 'pay_later_messaging',
title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
description: __(
'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
];
export default TabOverview;

View file

@ -4,12 +4,12 @@ import { useMemo } from '@wordpress/element';
import SettingsCard from '../../ReusableComponents/SettingsCard';
import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock';
import { CommonHooks } from '../../../data';
import ModalPayPal from './Modals/ModalPayPal';
import ModalFastlane from './Modals/ModalFastlane';
import ModalAcdc from './Modals/ModalAcdc';
import { useActiveModal } from '../../../data/common/hooks';
import Modal from './TabSettingsElements/Blocks/Modal';
const TabPaymentMethods = () => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const { activeModal, setActiveModal } = useActiveModal();
const filteredPaymentMethods = useMemo( () => {
const contextProps = { storeCountry, storeCurrency };
@ -30,9 +30,24 @@ const TabPaymentMethods = () => {
};
}, [ storeCountry, storeCurrency ] );
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
}
const allMethods = [
...filteredPaymentMethods.payPalCheckout,
...filteredPaymentMethods.onlineCardPayments,
...filteredPaymentMethods.alternative,
];
return allMethods.find( ( method ) => method.id === activeModal );
};
return (
<div className="ppcp-r-payment-methods">
<SettingsCard
id="ppcp-paypal-checkout-card"
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
description={ __(
'Select your preferred checkout option with PayPal for easy payment processing.',
@ -43,9 +58,11 @@ const TabPaymentMethods = () => {
>
<PaymentMethodsBlock
paymentMethods={ filteredPaymentMethods.payPalCheckout }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
<SettingsCard
id="ppcp-card-payments-card"
title={ __(
'Online Card Payments',
'woocommerce-paypal-payments'
@ -59,9 +76,11 @@ const TabPaymentMethods = () => {
>
<PaymentMethodsBlock
paymentMethods={ filteredPaymentMethods.onlineCardPayments }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
<SettingsCard
id="ppcp-alternative-payments-card"
title={ __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
@ -75,8 +94,24 @@ const TabPaymentMethods = () => {
>
<PaymentMethodsBlock
paymentMethods={ filteredPaymentMethods.alternative }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
{ activeModal && (
<Modal
method={ getActiveMethod() }
setModalIsVisible={ () => setActiveModal( null ) }
onSave={ ( methodId, settings ) => {
console.log(
'Saving settings for:',
methodId,
settings
);
setActiveModal( null );
} }
/>
) }
</div>
);
};
@ -98,7 +133,6 @@ const paymentMethodsPayPalCheckout = [
'woocommerce-paypal-payments'
),
icon: 'payment-method-paypal',
modal: ModalPayPal,
},
{
id: 'venmo',
@ -111,9 +145,9 @@ const paymentMethodsPayPalCheckout = [
},
{
id: 'paypal_credit',
title: __( 'PayPal Credit', 'woocommerce-paypal-payments' ),
title: __( 'Pay Later', 'woocommerce-paypal-payments' ),
description: __(
'Get paid in full at checkout while giving your customers the option to pay interest free if paid within 6 months on orders over $99.',
'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.',
'woocommerce-paypal-payments'
),
icon: 'payment-method-paypal',
@ -144,7 +178,6 @@ const paymentMethodsOnlineCardPayments = [
'woocommerce-paypal-payments'
),
icon: 'payment-method-advanced-cards',
modal: ModalAcdc,
},
{
id: 'fastlane',
@ -154,10 +187,9 @@ const paymentMethodsOnlineCardPayments = [
'woocommerce-paypal-payments'
),
icon: 'payment-method-fastlane',
modal: ModalFastlane,
},
{
id: 'apply_pay',
id: 'apple_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Allow customers to pay via their Apple Pay digital wallet.',

View file

@ -0,0 +1,41 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import {
AccordionSettingsBlock,
RadioSettingsBlock,
InputSettingsBlock,
} from '../../../../ReusableComponents/SettingsBlocks';
import {
sandboxData,
productionData,
} from '../../../../../data/settings/connection-details-data';
const ConnectionDetails = ( { settings, updateFormValue } ) => {
const isSandbox = settings.sandboxConnected;
const modeConfig = isSandbox
? productionData( { settings, updateFormValue } )
: sandboxData( { settings, updateFormValue } );
const modeKey = isSandbox ? 'productionMode' : 'sandboxMode';
return (
<AccordionSettingsBlock
title={ modeConfig.title }
description={ modeConfig.description }
>
<RadioSettingsBlock
title={ modeConfig.connectTitle }
description={ modeConfig.connectDescription }
options={ modeConfig.options }
actionProps={ {
key: modeKey,
currentValue: settings[ modeKey ],
callback: updateFormValue,
} }
/>
</AccordionSettingsBlock>
);
};
export default ConnectionDetails;

View file

@ -0,0 +1,297 @@
import { __ } from '@wordpress/i18n';
import { TAB_IDS, selectTab } from '../../../../../utils/tabSelector';
const Features = {
getFeatures: ( setActiveModal ) => [
{
id: 'save_paypal_and_venmo',
title: __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
description: __(
'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-paypal-checkout-card'
).then( () => {
setActiveModal( 'paypal' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
live: 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
},
{
id: 'advanced_credit_and_debit_cards',
title: __(
'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments'
),
description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal(
'advanced_credit_and_debit_card_payments'
);
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp',
live: 'https://www.paypal.com/bizsignup/entry?product=ppcp',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
},
{
id: 'alternative_payment_methods',
title: __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
),
description: __(
'Offer global, country-specific payment options for your customers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-alternative-payments-card'
);
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
},
{
id: 'google_pay',
title: __( 'Google Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'google_pay' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
notes: [
__(
'¹PayPal Q2 Earnings-2021.',
'woocommerce-paypal-payments'
),
],
},
{
id: 'apple_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Apple Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'apple_pay' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
),
urls: {
sandbox:
'https://www.sandbox.paypal.com/uccservicing/apm/applepay',
live: 'https://www.paypal.com/uccservicing/apm/applepay',
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
},
{
id: 'pay_later_messaging',
title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
description: __(
'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-paypal-checkout-card'
).then( () => {
setActiveModal( 'paypal' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
},
],
};
export default Features;

View file

@ -0,0 +1,131 @@
import { __ } from '@wordpress/i18n';
import {
Button,
TextControl,
ToggleControl,
RadioControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import PaymentMethodModal from '../../../../ReusableComponents/PaymentMethodModal';
import { getPaymentMethods } from './PaymentMethods';
const Modal = ( { method, setModalIsVisible, onSave } ) => {
const [ settings, setSettings ] = useState( () => {
if ( ! method?.id ) {
return {};
}
const methodConfig = getPaymentMethods( method );
if ( ! methodConfig?.fields ) {
return {};
}
const initialSettings = {};
Object.entries( methodConfig.fields ).forEach( ( [ key, field ] ) => {
initialSettings[ key ] = field.default;
} );
return initialSettings;
} );
if ( ! method?.id ) {
return null;
}
const methodConfig = getPaymentMethods( method );
if ( ! methodConfig?.fields ) {
return null;
}
const renderField = ( key, field ) => {
switch ( field.type ) {
case 'text':
return (
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ field.label }
value={ settings[ key ] }
onChange={ ( value ) =>
setSettings( ( prev ) => ( {
...prev,
[ key ]: value,
} ) )
}
/>
</div>
);
case 'toggle':
return (
<div className="ppcp-r-modal__field-row">
<ToggleControl
label={ field.label }
checked={ settings[ key ] }
onChange={ ( value ) =>
setSettings( ( prev ) => ( {
...prev,
[ key ]: value,
} ) )
}
/>
</div>
);
case 'radio':
return (
<>
<strong className="ppcp-r-modal__content-title">
{ field.label }
</strong>
{ field.description && (
<p className="ppcp-r-modal__description">
{ field.description }
</p>
) }
<div className="ppcp-r-modal__field-row">
<RadioControl
selected={ settings[ key ] }
options={ field.options }
onChange={ ( value ) =>
setSettings( ( prev ) => ( {
...prev,
[ key ]: value,
} ) )
}
/>
</div>
</>
);
default:
return null;
}
};
const handleSave = () => {
onSave?.( method.id, settings );
setModalIsVisible( false );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon={ methodConfig.icon }
title={ method.title }
>
<div className="ppcp-r-modal__field-rows">
{ Object.entries( methodConfig.fields ).map(
( [ key, field ] ) => renderField( key, field )
) }
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary" onClick={ handleSave }>
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default Modal;

View file

@ -0,0 +1,179 @@
import { __, sprintf } from '@wordpress/i18n';
const createStandardFields = ( methodId, defaultTitle ) => ( {
checkoutPageTitle: {
type: 'text',
default: defaultTitle,
label: __( 'Checkout page title', 'woocommerce-paypal-payments' ),
},
checkoutPageDescription: {
type: 'text',
default: sprintf(
/* translators: %s: payment method title */
__( 'Pay with %s', 'woocommerce-paypal-payments' ),
defaultTitle
),
label: __( 'Checkout page description', 'woocommerce-paypal-payments' ),
},
} );
const paymentMethods = {
// PayPal Checkout methods
paypal: {
fields: {
...createStandardFields( 'paypal', 'PayPal' ),
showLogo: {
type: 'toggle',
default: false,
label: __( 'Show logo', 'woocommerce-paypal-payments' ),
},
},
},
venmo: {
fields: createStandardFields( 'venmo', 'Venmo' ),
},
paypal_credit: {
fields: createStandardFields( 'paypal_credit', 'PayPal Credit' ),
},
credit_and_debit_card_payments: {
fields: createStandardFields(
'credit_and_debit_card_payments',
__(
'Credit and debit card payments',
'woocommerce-paypal-payments'
)
),
},
// Online Card Payments
advanced_credit_and_debit_card_payments: {
fields: {
...createStandardFields(
'advanced_credit_and_debit_card_payments',
__(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
)
),
threeDSecure: {
type: 'radio',
default: 'no-3d-secure',
label: __( '3D Secure', 'woocommerce-paypal-payments' ),
description: __(
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
'woocommerce-paypal-payments'
),
options: [
{
label: __(
'No 3D Secure',
'woocommerce-paypal-payments'
),
value: 'no-3d-secure',
},
{
label: __(
'Only when required',
'woocommerce-paypal-payments'
),
value: 'only-required-3d-secure',
},
{
label: __(
'Always require 3D Secure',
'woocommerce-paypal-payments'
),
value: 'always-3d-secure',
},
],
},
},
},
fastlane: {
fields: {
...createStandardFields( 'fastlane', 'Fastlane by PayPal' ),
cardholderName: {
type: 'toggle',
default: false,
label: __(
'Display cardholder name',
'woocommerce-paypal-payments'
),
},
displayWatermark: {
type: 'toggle',
default: false,
label: __(
'Display Fastlane Watermark',
'woocommerce-paypal-payments'
),
},
},
},
// Digital Wallets
apple_pay: {
fields: createStandardFields( 'apple_pay', 'Apple Pay' ),
},
google_pay: {
fields: createStandardFields( 'google_pay', 'Google Pay' ),
},
// Alternative Payment Methods
bancontact: {
fields: createStandardFields( 'bancontact', 'Bancontact' ),
},
ideal: {
fields: createStandardFields( 'ideal', 'iDEAL' ),
},
eps: {
fields: createStandardFields( 'eps', 'eps' ),
},
blik: {
fields: createStandardFields( 'blik', 'BLIK' ),
},
mybank: {
fields: createStandardFields( 'mybank', 'MyBank' ),
},
przelewy24: {
fields: createStandardFields( 'przelewy24', 'Przelewy24' ),
},
trustly: {
fields: createStandardFields( 'trustly', 'Trustly' ),
},
multibanco: {
fields: createStandardFields( 'multibanco', 'Multibanco' ),
},
pui: {
fields: createStandardFields( 'pui', 'Pay upon Invoice' ),
},
oxxo: {
fields: createStandardFields( 'oxxo', 'OXXO' ),
},
};
// Function to get configuration for a payment method
export const getPaymentMethods = ( method ) => {
if ( ! method?.id ) {
return null;
}
// If method has specific config, return it
if ( paymentMethods[ method.id ] ) {
return {
...paymentMethods[ method.id ],
icon: method.icon,
};
}
// Return standard config for new payment methods
return {
fields: createStandardFields( method.id, method.title ),
icon: method.icon,
};
};
// Function to check if a method has settings defined
export const hasSettings = ( methodId ) => {
return Boolean( methodId && paymentMethods[ methodId ] );
};

View file

@ -1,202 +0,0 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import {
AccordionSettingsBlock,
ButtonSettingsBlock,
RadioSettingsBlock,
ToggleSettingsBlock,
InputSettingsBlock,
} from '../../../../ReusableComponents/SettingsBlocks';
import TitleBadge, {
TITLE_BADGE_POSITIVE,
} from '../../../../ReusableComponents/TitleBadge';
import ConnectionInfo, {
connectionStatusDataDefault,
} from '../../../../ReusableComponents/ConnectionInfo';
const Sandbox = ( { settings, updateFormValue } ) => {
const className = settings.sandboxConnected
? 'ppcp-r-settings-block--sandbox-connected'
: 'ppcp-r-settings-block--sandbox-disconnected';
return (
<AccordionSettingsBlock
title={ __( 'Sandbox', 'woocommerce-paypal-payments' ) }
className={ className }
description={ __(
"Test your site in PayPal's Sandbox environment.",
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
} }
>
{ settings.sandboxConnected && (
<ButtonSettingsBlock
title={ __(
'Sandbox account credentials',
'woocommerce-paypal-payments'
) }
description={ __(
'Your account is connected to sandbox, no real charging takes place. To accept live payments, turn off sandbox mode and connect your live PayPal account.',
'woocommerce-paypal-payments'
) }
tag={
<TitleBadge
type={ TITLE_BADGE_POSITIVE }
text={ __(
'Connected',
'woocommerce-paypal-payments'
) }
/>
}
>
<div className="ppcp-r-settings-block--sandbox">
<ToggleSettingsBlock
title={ __(
'Enable sandbox mode',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'sandboxEnabled',
value: settings.sandboxEnabled,
} }
/>
<ConnectionInfo
connectionStatusDataDefault={
connectionStatusDataDefault
}
/>
<Button
variant="secondary"
onClick={ () =>
updateFormValue( 'sandboxConnected', false )
}
>
{ __(
'Disconnect Sandbox',
'woocommerce-paypal-payments'
) }
</Button>
</div>
</ButtonSettingsBlock>
) }
{ ! settings.sandboxConnected && (
<RadioSettingsBlock
title={ __(
'Connect Sandbox Account',
'woocommerce-paypal-payments'
) }
description={ __(
'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.',
'woocommerce-paypal-payments'
) }
options={ [
{
id: 'sandbox_mode',
value: 'sandbox_mode',
label: __(
'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'
),
additionalContent: (
<Button
variant="primary"
onClick={ () =>
updateFormValue(
'sandboxConnected',
true
)
}
>
{ __(
'Connect Sandbox Account',
'woocommerce-paypal-payments'
) }
</Button>
),
},
{
id: 'manual_connect',
value: 'manual_connect',
label: __(
'Manual Connect',
'woocommerce-paypal-payments'
),
description: sprintf(
__(
'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'
),
'#'
),
additionalContent: (
<>
<InputSettingsBlock
title={ __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
) }
actionProps={ {
value: settings.sandboxClientId, // Add this to settings if not present
callback: updateFormValue,
key: 'sandboxClientId',
placeholder: __(
'Enter Client ID',
'woocommerce-paypal-payments'
),
} }
/>
<InputSettingsBlock
title={ __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
) }
actionProps={ {
value: settings.sandboxSecretKey, // Add this to settings if not present
callback: updateFormValue,
key: 'sandboxSecretKey',
placeholder: __(
'Enter Secret Key',
'woocommerce-paypal-payments'
),
} }
/>
<Button
variant="primary"
onClick={ () =>
updateFormValue(
'sandboxManuallyConnected',
true
)
} // Add this handler if needed
>
{ __(
'Connect Account',
'woocommerce-paypal-payments'
) }
</Button>
</>
),
},
] }
actionProps={ {
name: 'paypal_connect_sandbox',
key: 'sandboxMode',
currentValue: settings.sandboxMode,
callback: updateFormValue,
} }
/>
) }
</AccordionSettingsBlock>
);
};
export default Sandbox;

View file

@ -1,36 +1,46 @@
import { __ } from '@wordpress/i18n';
import { CommonHooks } from '../../../../../../data';
import { Title } from '../../../../../ReusableComponents/SettingsBlocks';
const HooksTableBlock = () => {
const { webhooks } = CommonHooks.useWebhooks();
const { url, events } = webhooks;
if ( ! url || ! events?.length ) {
return <div>...</div>;
}
return (
<table className="ppcp-r-table">
<thead>
<tr>
<th className="ppcp-r-table__hooks-url">
{ __( 'URL', 'woocommerce-paypal-payments' ) }
</th>
<th className="ppcp-r-table__hooks-events">
{ __(
'Tracked events',
'woocommerce-paypal-payments'
) }
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="ppcp-r-table__hooks-url">
{ webhooks?.url }
</td>
<td
className="ppcp-r-table__hooks-events"
dangerouslySetInnerHTML={ { __html: webhooks?.events } }
></td>
</tr>
</tbody>
</table>
<>
<WebhookUrl url={ url } />
<WebhookEvents events={ events } />
</>
);
};
const WebhookUrl = ( { url } ) => {
return (
<div>
<Title>
{ __( 'Notification URL', 'woocommerce-paypal-payments' ) }
</Title>
<p>{ url }</p>
</div>
);
};
const WebhookEvents = ( { events } ) => {
return (
<div>
<Title>
{ __( 'Subscribed Events', 'woocommerce-paypal-payments' ) }
</Title>
<ul>
{ events.map( ( event, index ) => (
<li key={ index }>{ event }</li>
) ) }
</ul>
</div>
);
};

View file

@ -42,10 +42,7 @@ const Troubleshooting = ( { updateFormValue, settings } ) => {
<SettingsBlock>
<Header>
<Title>
{ __(
'Subscribed PayPal webhooks',
'woocommerce-paypal-payments'
) }
{ __( 'Webhooks', 'woocommerce-paypal-payments' ) }
</Title>
<Description>
{ __(

View file

@ -4,7 +4,7 @@ import {
Content,
ContentWrapper,
} from '../../../ReusableComponents/SettingsBlocks';
import Sandbox from './Blocks/Sandbox';
import ConnectionDetails from './Blocks/ConnectionDetails';
import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';
@ -27,7 +27,7 @@ const ExpertSettings = ( { updateFormValue, settings } ) => {
>
<ContentWrapper>
<Content>
<Sandbox
<ConnectionDetails
updateFormValue={ updateFormValue }
settings={ settings }
/>

View file

@ -19,11 +19,11 @@ export default {
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION',
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
DO_DIRECT_API_AUTHENTICATION: 'COMMON:DO_DIRECT_API_AUTHENTICATION',
DO_OAUTH_AUTHENTICATION: 'COMMON:DO_OAUTH_AUTHENTICATION',
DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES',
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
DO_CHECK_WEBHOOK_SIMULATION_STATE:

View file

@ -47,6 +47,17 @@ export const setIsReady = ( isReady ) => ( {
payload: { isReady },
} );
/**
* Transient. Sets the active settings tab.
*
* @param {string} activeModal
* @return {Action} The action.
*/
export const setActiveModal = ( activeModal ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { activeModal },
} );
/**
* Transient. Changes the "saving" flag.
*
@ -112,28 +123,6 @@ export const setManualConnectionMode = ( useManualConnection ) => ( {
payload: { useManualConnection },
} );
/**
* Persistent. Changes the "client ID" value.
*
* @param {string} clientId
* @return {Action} The action.
*/
export const setClientId = ( clientId ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { clientId },
} );
/**
* Persistent. Changes the "client secret" value.
*
* @param {string} clientSecret
* @return {Action} The action.
*/
export const setClientSecret = ( clientSecret ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { clientSecret },
} );
/**
* Side effect. Saves the persistent details to the WP database.
*
@ -150,8 +139,12 @@ export const persist = function* () {
*
* @return {Action} The action.
*/
export const connectToSandbox = function* () {
return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
export const sandboxOnboardingUrl = function* () {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: true,
products: [ 'EXPRESS_CHECKOUT' ],
};
};
/**
@ -160,27 +153,65 @@ export const connectToSandbox = function* () {
* @param {string[]} products Which products/features to display in the ISU popup.
* @return {Action} The action.
*/
export const connectToProduction = function* ( products = [] ) {
return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products };
export const productionOnboardingUrl = function* ( products = [] ) {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: false,
products,
};
};
/**
* Side effect. Initiates a manual connection attempt using the provided client ID and secret.
* Side effect. Initiates a direct connection attempt using the provided client ID and secret.
*
* This action accepts parameters instead of fetching data from the Redux state because the
* values (ID and secret) are not managed by a central redux store, but might come from private
* component state.
*
* @param {string} clientId - AP client ID (always 80-characters, starting with "A").
* @param {string} clientSecret - API client secret.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action.
*/
export const connectViaIdAndSecret = function* () {
const { clientId, clientSecret, useSandbox } =
yield select( STORE_NAME ).persistentData();
export const authenticateWithCredentials = function* (
clientId,
clientSecret,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION,
clientId,
clientSecret,
useSandbox,
};
};
/**
* Side effect. Completes the ISU login by authenticating the user via the one time sharedId and
* authCode provided by PayPal.
*
* This action accepts parameters instead of fetching data from the Redux state because all
* parameters are dynamically generated during the authentication process, and not managed by our
* Redux store.
*
* @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID.
* @param {string} authCode - OAuth authorization code provided during onboarding.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action.
*/
export const authenticateWithOAuth = function* (
sharedId,
authCode,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION,
sharedId,
authCode,
useSandbox,
};
};
/**
* Side effect. Clears and refreshes the merchant data via a REST request.
*

View file

@ -35,14 +35,25 @@ export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant';
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
/**
* REST path to perform the manual connection check, using client ID and secret,
* REST path to perform the manual connection authentication, using client ID and secret.
*
* Used by: Controls
* See: ConnectManualRestEndpoint.php
* See: AuthenticateRestEndpoint.php
*
* @type {string}
*/
export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
export const REST_DIRECT_AUTHENTICATION_PATH =
'/wc/v3/wc_paypal/authenticate/direct';
/**
* REST path to perform the ISU authentication check, using shared ID and authCode.
*
* Used by: Controls
* See: AuthenticateRestEndpoint.php
*
* @type {string}
*/
export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu';
/**
* REST path to generate an ISU URL for the PayPal-login.

View file

@ -10,11 +10,12 @@
import apiFetch from '@wordpress/api-fetch';
import {
REST_PERSIST_PATH,
REST_DIRECT_AUTHENTICATION_PATH,
REST_CONNECTION_URL_PATH,
REST_HYDRATE_MERCHANT_PATH,
REST_MANUAL_CONNECTION_PATH,
REST_PERSIST_PATH,
REST_REFRESH_FEATURES_PATH,
REST_ISU_AUTHENTICATION_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} from './constants';
@ -33,15 +34,15 @@ export const controls = {
}
},
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
async [ ACTION_TYPES.DO_GENERATE_ONBOARDING_URL ]( {
products,
useSandbox,
} ) {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
environment: 'sandbox',
products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT.
},
data: { useSandbox, products },
} );
} catch ( e ) {
return {
@ -51,32 +52,14 @@ export const controls = {
}
},
async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
environment: 'production',
products,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
async [ ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION ]( {
clientId,
clientSecret,
useSandbox,
} ) {
try {
return await apiFetch( {
path: REST_MANUAL_CONNECTION_PATH,
path: REST_DIRECT_AUTHENTICATION_PATH,
method: 'POST',
data: {
clientId,
@ -92,6 +75,29 @@ export const controls = {
}
},
async [ ACTION_TYPES.DO_OAUTH_AUTHENTICATION ]( {
sharedId,
authCode,
useSandbox,
} ) {
try {
return await apiFetch( {
path: REST_ISU_AUTHENTICATION_PATH,
method: 'POST',
data: {
sharedId,
authCode,
useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
try {
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );

View file

@ -28,32 +28,40 @@ const useHooks = () => {
persist,
setSandboxMode,
setManualConnectionMode,
setClientId,
setClientSecret,
connectToSandbox,
connectToProduction,
connectViaIdAndSecret,
sandboxOnboardingUrl,
productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
setActiveModal,
startWebhookSimulation,
checkWebhookSimulationState,
} = useDispatch( STORE_NAME );
// Transient accessors.
const isReady = useTransient( 'isReady' );
const activeModal = useTransient( 'activeModal' );
// Persistent accessors.
const clientId = usePersistent( 'clientId' );
const clientSecret = usePersistent( 'clientSecret' );
const isSandboxMode = usePersistent( 'useSandbox' );
const isManualConnectionMode = usePersistent( 'useManualConnection' );
const webhooks = usePersistent( 'webhooks' );
const merchant = useSelect(
( select ) => select( STORE_NAME ).merchant(),
[]
);
// Read-only properties.
const wooSettings = useSelect(
( select ) => select( STORE_NAME ).wooSettings(),
[]
);
const features = useSelect(
( select ) => select( STORE_NAME ).features(),
[]
);
const webhooks = useSelect(
( select ) => select( STORE_NAME ).webhooks(),
[]
);
const savePersistent = async ( setter, value ) => {
setter( value );
@ -62,6 +70,8 @@ const useHooks = () => {
return {
isReady,
activeModal,
setActiveModal,
isSandboxMode,
setSandboxMode: ( state ) => {
return savePersistent( setSandboxMode, state );
@ -70,19 +80,13 @@ const useHooks = () => {
setManualConnectionMode: ( state ) => {
return savePersistent( setManualConnectionMode, state );
},
clientId,
setClientId: ( value ) => {
return savePersistent( setClientId, value );
},
clientSecret,
setClientSecret: ( value ) => {
return savePersistent( setClientSecret, value );
},
connectToSandbox,
connectToProduction,
connectViaIdAndSecret,
sandboxOnboardingUrl,
productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
merchant,
wooSettings,
features,
webhooks,
startWebhookSimulation,
checkWebhookSimulationState,
@ -90,36 +94,30 @@ const useHooks = () => {
};
export const useSandbox = () => {
const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks();
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
return { isSandboxMode, setSandboxMode, connectToSandbox };
return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl };
};
export const useProduction = () => {
const { connectToProduction } = useHooks();
const { productionOnboardingUrl } = useHooks();
return { connectToProduction };
return { productionOnboardingUrl };
};
export const useManualConnection = () => {
export const useAuthentication = () => {
const {
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
authenticateWithCredentials,
authenticateWithOAuth,
} = useHooks();
return {
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
authenticateWithCredentials,
authenticateWithOAuth,
};
};
@ -146,7 +144,7 @@ export const useWebhooks = () => {
};
};
export const useMerchantInfo = () => {
const { merchant } = useHooks();
const { merchant, features } = useHooks();
const { refreshMerchantData } = useDispatch( STORE_NAME );
const verifyLoginStatus = useCallback( async () => {
@ -162,10 +160,16 @@ export const useMerchantInfo = () => {
return {
merchant, // Merchant details
features, // Eligible merchant features
verifyLoginStatus, // Callback
};
};
export const useActiveModal = () => {
const { activeModal, setActiveModal } = useHooks();
return { activeModal, setActiveModal };
};
// -- Not using the `useHooks()` data provider --
export const useBusyState = () => {

View file

@ -15,6 +15,7 @@ import ACTION_TYPES from './action-types';
const defaultTransient = Object.freeze( {
isReady: false,
activities: new Map(),
activeModal: '',
// Read only values, provided by the server via hydrate.
merchant: Object.freeze( {
@ -22,20 +23,36 @@ const defaultTransient = Object.freeze( {
isSandbox: false,
id: '',
email: '',
clientId: '',
clientSecret: '',
} ),
wooSettings: Object.freeze( {
storeCountry: '',
storeCurrency: '',
} ),
features: Object.freeze( {
save_paypal_and_venmo: {
enabled: false,
},
advanced_credit_and_debit_cards: {
enabled: false,
},
apple_pay: {
enabled: false,
},
google_pay: {
enabled: false,
},
} ),
webhooks: Object.freeze( [] ),
} );
const defaultPersistent = Object.freeze( {
useSandbox: false,
useManualConnection: false,
clientId: '',
clientSecret: '',
webhooks: [],
} );
// Reducer logic.
@ -83,22 +100,25 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( {
...state,
merchant: Object.freeze( { ...defaultTransient.merchant } ),
features: Object.freeze( { ...defaultTransient.features } ),
} ),
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
const newState = setPersistent( state, payload.data );
// Populate read-only properties.
[ 'wooSettings', 'merchant' ].forEach( ( key ) => {
if ( ! payload[ key ] ) {
return;
}
[ 'wooSettings', 'merchant', 'features', 'webhooks' ].forEach(
( key ) => {
if ( ! payload[ key ] ) {
return;
}
newState[ key ] = Object.freeze( {
...newState[ key ],
...payload[ key ],
} );
} );
newState[ key ] = Object.freeze( {
...newState[ key ],
...payload[ key ],
} );
}
);
return newState;
},

View file

@ -23,7 +23,9 @@ export const resolvers = {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
result.data = { ...result.data, ...webhooks.data };
if ( webhooks.success && webhooks.data ) {
result.webhooks = webhooks.data;
}
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );

View file

@ -16,8 +16,14 @@ export const persistentData = ( state ) => {
};
export const transientData = ( state ) => {
const { data, merchant, wooSettings, ...transientState } =
getState( state );
const {
data,
merchant,
features,
wooSettings,
webhooks,
...transientState
} = getState( state );
return transientState || EMPTY_OBJ;
};
@ -30,6 +36,10 @@ export const merchant = ( state ) => {
return getState( state ).merchant || EMPTY_OBJ;
};
export const features = ( state ) => {
return getState( state ).features || EMPTY_OBJ;
};
export const wooSettings = ( state ) => {
return getState( state ).wooSettings || EMPTY_OBJ;
};

View file

@ -47,6 +47,28 @@ export const setIsReady = ( isReady ) => ( {
payload: { isReady },
} );
/**
* Transient. Sets the "manualClientId" value.
*
* @param {string} manualClientId
* @return {Action} The action.
*/
export const setManualClientId = ( manualClientId ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { manualClientId },
} );
/**
* Transient. Sets the "manualClientSecret" value.
*
* @param {string} manualClientSecret
* @return {Action} The action.
*/
export const setManualClientSecret = ( manualClientSecret ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { manualClientSecret },
} );
/**
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
*

View file

@ -30,6 +30,8 @@ const useHooks = () => {
setStep,
setCompleted,
setIsCasualSeller,
setManualClientId,
setManualClientSecret,
setAreOptionalPaymentMethodsEnabled,
setProducts,
} = useDispatch( STORE_NAME );
@ -43,6 +45,8 @@ const useHooks = () => {
// Transient accessors.
const isReady = useTransient( 'isReady' );
const manualClientId = useTransient( 'manualClientId' );
const manualClientSecret = useTransient( 'manualClientSecret' );
// Persistent accessors.
const step = usePersistent( 'step' );
@ -73,6 +77,14 @@ const useHooks = () => {
setIsCasualSeller: ( value ) => {
return savePersistent( setIsCasualSeller, value );
},
manualClientId,
setManualClientId: ( value ) => {
return savePersistent( setManualClientId, value );
},
manualClientSecret,
setManualClientSecret: ( value ) => {
return savePersistent( setManualClientSecret, value );
},
areOptionalPaymentMethodsEnabled,
setAreOptionalPaymentMethodsEnabled: ( value ) => {
return savePersistent( setAreOptionalPaymentMethodsEnabled, value );
@ -88,6 +100,22 @@ const useHooks = () => {
};
};
export const useManualConnectionForm = () => {
const {
manualClientId,
setManualClientId,
manualClientSecret,
setManualClientSecret,
} = useHooks();
return {
manualClientId,
setManualClientId,
manualClientSecret,
setManualClientSecret,
};
};
export const useBusiness = () => {
const { isCasualSeller, setIsCasualSeller } = useHooks();

View file

@ -14,6 +14,8 @@ import ACTION_TYPES from './action-types';
const defaultTransient = Object.freeze( {
isReady: false,
manualClientId: '',
manualClientSecret: '',
// Read only values, provided by the server.
flags: Object.freeze( {

View file

@ -52,9 +52,6 @@ export const determineProducts = ( state ) => {
* The store uses the Express-checkout product.
*/
derivedProducts.push( 'EXPRESS_CHECKOUT' );
// TODO: Add the "BCDC" product/feature
// Requirement: "EXPRESS_CHECKOUT with BCDC"
} else {
/**
* Branch 3: Merchant is business, and can use CC payments.
@ -64,8 +61,7 @@ export const determineProducts = ( state ) => {
}
if ( canUseVaulting ) {
// TODO: Add the "Vaulting" product/feature
// Requirement: "... with Vault"
derivedProducts.push( 'ADVANCED_VAULTING' );
}
return derivedProducts;

View file

@ -0,0 +1,178 @@
import { __, sprintf } from '@wordpress/i18n';
import { InputSettingsBlock } from '../../Components/ReusableComponents/SettingsBlocks';
import { Button } from '@wordpress/components';
/**
* Generates options for the environment mode settings.
*
* @param {Object} config - Configuration for the mode.
* @param {Object} settings - Current settings.
* @param {Function} updateFormValue - Callback to update settings.
* @return {Array} Options array.
*/
const generateOptions = ( config, settings, updateFormValue ) => [
{
id: `${ config.mode }_mode`,
value: `${ config.mode }_mode`,
label: config.labelTitle,
description: config.labelDescription,
additionalContent: (
<Button
variant="primary"
onClick={ () => {
updateFormValue( `${ config.mode }Connected`, true );
if ( config.mode === 'production' ) {
global.ppcpSettings.startOnboarding();
}
} }
>
{ config.buttonText }
</Button>
),
},
{
id: 'manual_connect',
value: 'manual_connect',
label: __( 'Manual Connect', 'woocommerce-paypal-payments' ),
description: sprintf(
__(
'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'
),
'#'
),
additionalContent: (
<>
<InputSettingsBlock
title={ config.clientIdTitle }
actionProps={ {
value: settings[ `${ config.mode }ClientId` ],
callback: updateFormValue,
key: `${ config.mode }ClientId`,
placeholder: __(
'Enter Client ID',
'woocommerce-paypal-payments'
),
} }
/>
<InputSettingsBlock
title={ config.secretKeyTitle }
actionProps={ {
value: settings[ `${ config.mode }SecretKey` ],
callback: updateFormValue,
key: `${ config.mode }SecretKey`,
placeholder: __(
'Enter Secret Key',
'woocommerce-paypal-payments'
),
} }
/>
<Button
variant="primary"
onClick={ () =>
updateFormValue(
`${ config.mode }ManuallyConnected`,
true
)
}
>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</>
),
},
];
/**
* Generates data for a given mode (sandbox or production).
*
* @param {Object} config - Configuration for the mode.
* @param {Object} settings - Current settings.
* @param {Function} updateFormValue - Callback to update settings.
* @return {Object} Mode configuration.
*/
const generateModeData = ( config, settings, updateFormValue ) => ( {
title: config.title,
description: config.description,
connectTitle: __(
`Connect ${ config.label } Account`,
'woocommerce-paypal-payments'
),
connectDescription: config.connectDescription,
options: generateOptions( config, settings, updateFormValue ),
} );
export const sandboxData = ( { settings = {}, updateFormValue = () => {} } ) =>
generateModeData(
{
mode: 'sandbox',
label: 'Sandbox',
title: __( 'Sandbox', 'woocommerce-paypal-payments' ),
description: __(
"Test your site in PayPal's Sandbox environment.",
'woocommerce-paypal-payments'
),
connectDescription: __(
'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.',
'woocommerce-paypal-payments'
),
labelTitle: __( 'Sandbox Mode', 'woocommerce-paypal-payments' ),
labelDescription: __(
'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'
),
buttonText: __(
'Connect Sandbox Account',
'woocommerce-paypal-payments'
),
clientIdTitle: __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
),
secretKeyTitle: __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
),
},
settings,
updateFormValue
);
export const productionData = ( {
settings = {},
updateFormValue = () => {},
} ) =>
generateModeData(
{
mode: 'production',
label: 'Live',
title: __( 'Live Payments', 'woocommerce-paypal-payments' ),
description: __(
'Your site is currently configured in Sandbox mode to test payments. When you are ready, launch your site and receive live payments via PayPal.',
'woocommerce-paypal-payments'
),
connectDescription: __(
'Connect a live PayPal account to launch your site and receive live payments via PayPal. PayPal will guide you through the setup process.',
'woocommerce-paypal-payments'
),
labelTitle: __( 'Production Mode', 'woocommerce-paypal-payments' ),
labelDescription: __(
'Activate Production mode to connect your live account and receive live payments via PayPal. Stay connected in Sandbox mode to continue testing payments before going live.',
'woocommerce-paypal-payments'
),
buttonText: __(
'Set up and connect live PayPal Account',
'woocommerce-paypal-payments'
),
clientIdTitle: __(
'Live Account Client ID',
'woocommerce-paypal-payments'
),
secretKeyTitle: __(
'Live Account Secret Key',
'woocommerce-paypal-payments'
),
},
settings,
updateFormValue
);

View file

@ -0,0 +1,170 @@
import { __ } from '@wordpress/i18n';
import { selectTab, TAB_IDS } from '../../utils/tabSelector';
export const todosData = [
{
id: 'enable_fastlane',
title: __( 'Enable Fastlane', 'woocommerce-paypal-payments' ),
description: __(
'Accelerate your guest checkout with Fastlane by PayPal.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
selectTab( TAB_IDS.PAYMENT_METHODS, 'ppcp-card-payments-card' );
},
},
{
id: 'enable_credit_debit_cards',
title: __(
'Enable Credit and Debit Cards on your checkout',
'woocommerce-paypal-payments'
),
description: __(
'Credit and Debit Cards is now available for Blocks checkout pages.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
selectTab( TAB_IDS.PAYMENT_METHODS, 'ppcp-card-payments-card' );
},
},
{
id: 'enable_pay_later_messaging',
title: __(
'Enable Pay Later messaging',
'woocommerce-paypal-payments'
),
description: __(
'Show Pay Later messaging to boost conversion rate and increase cart size.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
selectTab( TAB_IDS.OVERVIEW, 'pay_later_messaging' );
},
},
{
id: 'configure_paypal_subscription',
title: __(
'Configure a PayPal Subscription',
'woocommerce-paypal-payments'
),
description: __(
'Connect a subscriptions-type product from WooCommerce with PayPal.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
console.log(
'Take merchant to product list, filtered with subscription-type products'
);
},
},
{
id: 'register_domain_apple_pay',
title: __(
'Register Domain for Apple Pay',
'woocommerce-paypal-payments'
),
description: __(
'To enable Apple Pay, you must register your domain with PayPal.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
selectTab( TAB_IDS.OVERVIEW, 'apple_pay' );
},
},
{
id: 'add_digital_wallets_to_account',
title: __(
'Add digital wallets to your account',
'woocommerce-paypal-payments'
),
description: __(
'Add the ability to accept Apple Pay & Google Pay to your PayPal account.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
console.log(
'Take merchant to PayPal to enable Apple Pay & Google Pay'
);
},
},
{
id: 'add_apple_pay_to_account',
title: __(
'Add Apple Pay to your account',
'woocommerce-paypal-payments'
),
description: __(
'Add the ability to accept Apple Pay to your PayPal account.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
console.log( 'Take merchant to PayPal to enable Apple Pay' );
},
},
{
id: 'add_google_pay_to_account',
title: __(
'Add Google Pay to your account',
'woocommerce-paypal-payments'
),
description: __(
'Add the ability to accept Google Pay to your PayPal account.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
console.log( 'Take merchant to PayPal to enable Google Pay' );
},
},
{
id: 'enable_apple_pay',
title: __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Allow your buyers to check out via Apple Pay.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
selectTab( TAB_IDS.OVERVIEW, 'apple_pay' );
},
},
{
id: 'enable_google_pay',
title: __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
description: __(
'Allow your buyers to check out via Google Pay.',
'woocommerce-paypal-payments'
),
isCompleted: () => {
return false;
},
onClick: () => {
selectTab( TAB_IDS.OVERVIEW, 'google_pay' );
},
},
];

View file

@ -1,9 +1,12 @@
import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks, OnboardingHooks } from '../data';
import { openPopup } from '../utils/window';
const PAYPAL_PARTNER_SDK_URL =
'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js';
const MESSAGES = {
CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ),
@ -32,35 +35,137 @@ const MESSAGES = {
const ACTIVITIES = {
CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX',
CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION',
CONNECT_ISU: 'ISU_LOGIN',
CONNECT_MANUAL: 'MANUAL_LOGIN',
};
const handlePopupWithCompletion = ( url, onError ) => {
return new Promise( ( resolve ) => {
const popup = openPopup( url );
export const useHandleOnboardingButton = ( isSandbox ) => {
const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
const { productionOnboardingUrl } = CommonHooks.useProduction();
const products = OnboardingHooks.useDetermineProducts();
const { withActivity } = CommonHooks.useBusyState();
const { authenticateWithOAuth } = CommonHooks.useAuthentication();
const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
const [ scriptLoaded, setScriptLoaded ] = useState( false );
const timerRef = useRef( null );
if ( ! popup ) {
onError( MESSAGES.POPUP_BLOCKED );
resolve( false );
useEffect( () => {
const fetchOnboardingUrl = async () => {
let res;
if ( isSandbox ) {
res = await sandboxOnboardingUrl();
} else {
res = await productionOnboardingUrl( products );
}
if ( res.success && res.data ) {
setOnboardingUrl( res.data );
} else {
console.error( 'Failed to fetch onboarding URL' );
}
};
fetchOnboardingUrl();
}, [ isSandbox, productionOnboardingUrl, products, sandboxOnboardingUrl ] );
useEffect( () => {
/**
* The partner.js script initializes all onboarding buttons in the onload event.
* When no buttons are present, a JS error is displayed; i.e. we should load this script
* only when the button is ready (with a valid href and data-attributes).
*/
if ( ! onboardingUrl ) {
return;
}
// Check popup state every 500ms
const checkPopup = setInterval( () => {
if ( popup.closed ) {
clearInterval( checkPopup );
resolve( true );
}
}, 500 );
const script = document.createElement( 'script' );
script.id = 'partner-js';
script.src = PAYPAL_PARTNER_SDK_URL;
script.onload = () => {
setScriptLoaded( true );
};
document.body.appendChild( script );
return () => {
clearInterval( checkPopup );
/**
* When the component is unmounted, remove the partner.js script, as well as the
* dynamic scripts it loaded (signup-js and rampConfig-js)
*
* This is important, as the onboarding button is only initialized during the onload
* event of those scripts; i.e. we need to load the scripts again, when the button is
* rendered again.
*/
const onboardingScripts = [
'partner-js',
'signup-js',
'rampConfig-js',
];
if ( popup && ! popup.closed ) {
popup.close();
}
onboardingScripts.forEach( ( id ) => {
const el = document.querySelector( `script[id="${ id }"]` );
if ( el?.parentNode ) {
el.parentNode.removeChild( el );
}
} );
};
} );
}, [ onboardingUrl ] );
const setCompleteHandler = useCallback(
( environment ) => {
const onComplete = async ( authCode, sharedId ) => {
/**
* Until now, the full page is blocked by PayPal's semi-transparent, black overlay.
* But at this point, the overlay is removed, while we process the sharedId and
* authCode via a REST call.
*
* Note: The REST response is irrelevant, since PayPal will most likely refresh this
* frame before the REST endpoint returns a value. Using "withActivity" is more of a
* visual cue to the user that something is still processing in the background.
*/
await withActivity(
ACTIVITIES.CONNECT_ISU,
'Validating the connection details',
async () => {
await authenticateWithOAuth(
sharedId,
authCode,
'sandbox' === environment
);
}
);
};
const addHandler = () => {
const MiniBrowser = window.PAYPAL?.apps?.Signup?.MiniBrowser;
if ( ! MiniBrowser || MiniBrowser.onOnboardComplete ) {
return;
}
MiniBrowser.onOnboardComplete = onComplete;
};
// Ensure the onComplete handler is not removed by a PayPal init script.
timerRef.current = setInterval( addHandler, 250 );
},
[ authenticateWithOAuth, withActivity ]
);
const removeCompleteHandler = useCallback( () => {
if ( timerRef.current ) {
clearInterval( timerRef.current );
timerRef.current = null;
}
delete window.PAYPAL?.apps?.Signup?.MiniBrowser?.onOnboardComplete;
}, [] );
return {
onboardingUrl,
scriptLoaded,
setCompleteHandler,
removeCompleteHandler,
};
};
const useConnectionBase = () => {
@ -92,104 +197,55 @@ const useConnectionBase = () => {
};
};
const useConnectionAttempt = ( connectFn, errorMessage ) => {
const { handleFailed, createErrorNotice, handleCompleted } =
useConnectionBase();
return async ( ...args ) => {
const res = await connectFn( ...args );
if ( ! res.success || ! res.data ) {
handleFailed( res, errorMessage );
return false;
}
const popupClosed = await handlePopupWithCompletion(
res.data,
createErrorNotice
);
if ( popupClosed ) {
await handleCompleted();
}
return popupClosed;
};
};
export const useSandboxConnection = () => {
const { connectToSandbox, isSandboxMode, setSandboxMode } =
CommonHooks.useSandbox();
const { withActivity } = CommonHooks.useBusyState();
const connectionAttempt = useConnectionAttempt(
connectToSandbox,
MESSAGES.SANDBOX_ERROR
);
const handleSandboxConnect = async () => {
return withActivity(
ACTIVITIES.CONNECT_SANDBOX,
'Connecting to sandbox account',
connectionAttempt
);
};
const { isSandboxMode, setSandboxMode } = CommonHooks.useSandbox();
return {
handleSandboxConnect,
isSandboxMode,
setSandboxMode,
};
};
export const useProductionConnection = () => {
const { connectToProduction } = CommonHooks.useProduction();
const { withActivity } = CommonHooks.useBusyState();
const products = OnboardingHooks.useDetermineProducts();
const connectionAttempt = useConnectionAttempt(
() => connectToProduction( products ),
MESSAGES.PRODUCTION_ERROR
);
const handleProductionConnect = async () => {
return withActivity(
ACTIVITIES.CONNECT_PRODUCTION,
'Connecting to production account',
connectionAttempt
);
};
return { handleProductionConnect };
};
export const useManualConnection = () => {
export const useDirectAuthentication = () => {
const { handleFailed, handleCompleted, createErrorNotice } =
useConnectionBase();
const { withActivity } = CommonHooks.useBusyState();
const {
connectViaIdAndSecret,
authenticateWithCredentials,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = CommonHooks.useManualConnection();
} = CommonHooks.useAuthentication();
const handleConnectViaIdAndSecret = async ( { validation } = {} ) => {
const handleDirectAuthentication = async ( connectionDetails ) => {
return withActivity(
ACTIVITIES.CONNECT_MANUAL,
'Connecting manually via Client ID and Secret',
async () => {
if ( 'function' === typeof validation ) {
let data;
if ( 'function' === typeof connectionDetails ) {
try {
validation();
data = connectionDetails();
} catch ( exception ) {
createErrorNotice( exception.message );
return;
}
} else if ( 'object' === typeof connectionDetails ) {
data = connectionDetails;
}
const res = await connectViaIdAndSecret();
if ( ! data || ! data.clientId || ! data.clientSecret ) {
createErrorNotice(
'Invalid connection details (clientID or clientSecret missing)'
);
return;
}
const res = await authenticateWithCredentials(
data.clientId,
data.clientSecret,
!! data.isSandbox
);
if ( res.success ) {
await handleCompleted();
@ -203,12 +259,8 @@ export const useManualConnection = () => {
};
return {
handleConnectViaIdAndSecret,
handleDirectAuthentication,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
};
};

View file

@ -0,0 +1,59 @@
// Tab panel IDs
export const TAB_IDS = {
OVERVIEW: 'tab-panel-0-overview',
PAYMENT_METHODS: 'tab-panel-0-payment-methods',
SETTINGS: 'tab-panel-0-settings',
STYLING: 'tab-panel-0-styling',
};
/**
* Select a tab by simulating a click event and scroll to specified element,
* accounting for navigation container height
*
* TODO: Once the TabPanel gets migrated to Tabs (TabPanel v2) we need to remove this in favor of programmatic tab switching: https://github.com/WordPress/gutenberg/issues/52997
*
* @param {string} tabId - The ID of the tab to select
* @param {string} [scrollToId] - Optional ID of the element to scroll to
* @return {Promise} - Resolves when tab switch and scroll are complete
*/
export const selectTab = ( tabId, scrollToId ) => {
return new Promise( ( resolve ) => {
const tab = document.getElementById( tabId );
if ( tab ) {
tab.click();
setTimeout( () => {
const scrollTarget = scrollToId
? document.getElementById( scrollToId )
: document.getElementById( 'ppcp-settings-container' );
if ( scrollTarget ) {
const navContainer = document.querySelector(
'.ppcp-r-navigation-container'
);
const navHeight = navContainer
? navContainer.offsetHeight
: 0;
// Get the current scroll position and element's position relative to viewport
const rect = scrollTarget.getBoundingClientRect();
// Calculate the final position with offset
const scrollPosition =
rect.top + window.scrollY - ( navHeight + 55 );
window.scrollTo( {
top: scrollPosition,
behavior: 'smooth',
} );
// Resolve after scroll animation
setTimeout( resolve, 300 );
} else {
resolve();
}
}, 100 );
} else {
resolve();
}
} );
};

View file

@ -1,42 +0,0 @@
/**
* Opens the provided URL, preferably in a popup window.
*
* Popups are usually only supported on desktop devices, when the browser is not in fullscreen mode.
*
* @param {string} url
* @param {Object} options
* @param {string} options.name
* @param {number} options.width
* @param {number} options.height
* @param {boolean} options.resizeable
* @return {null|Window} Popup window instance, or null.
*/
export const openPopup = (
url,
{ name = '_blank', width = 450, height = 720, resizeable = false } = {}
) => {
width = Math.max( 100, Math.min( window.screen.width - 40, width ) );
height = Math.max( 100, Math.min( window.screen.height - 40, height ) );
const left = ( window.screen.width - width ) / 2;
const top = ( window.screen.height - height ) / 2;
const features = [
`width=${ width }`,
`height=${ height }`,
`left=${ left }`,
`top=${ top }`,
`resizable=${ resizeable ? 'yes' : 'no' }`,
`scrollbars=yes`,
`status=no`,
];
const popup = window.open( url, name, features.join( ',' ) );
if ( popup && ! popup.closed ) {
popup.focus();
return popup;
}
return null;
};