Merge trunk

This commit is contained in:
Emili Castells Guasch 2025-01-28 11:29:15 +01:00
commit 7b30568dd2
70 changed files with 1172 additions and 814 deletions

View file

@ -9,7 +9,12 @@ const ControlButton = ( {
buttonLabel,
} ) => (
<Action>
<Button isBusy={ isBusy } variant={ type } onClick={ onClick }>
<Button
className="small-button"
isBusy={ isBusy }
variant={ type }
onClick={ onClick }
>
{ buttonLabel }
</Button>
</Action>

View file

@ -10,6 +10,7 @@ const ControlTextInput = ( {
} ) => (
<Action>
<TextControl
__nextHasNoMarginBottom={ true }
className="ppcp-r-vertical-text-control"
placeholder={ placeholder }
value={ value }

View file

@ -24,6 +24,7 @@ const Checkbox = ( {
return (
<CheckboxControl
__nextHasNoMarginBottom={ true }
label={ label }
value={ value }
checked={ checked }

View file

@ -1,15 +1,39 @@
import { PayPalCheckbox } from './index';
import { useCallback } from '@wordpress/element';
const CheckboxGroup = ( { options, value, onChange } ) => {
const handleChange = ( key, checked ) => {
const getNewValue = () => {
if ( checked ) {
return [ ...value, key ];
}
return value.filter( ( val ) => val !== key );
};
const CheckboxGroup = ( { name, options, value, onChange } ) => {
const handleChange = useCallback(
( key, checked ) => {
const getNewValue = () => {
if ( 'boolean' === typeof value ) {
return checked;
}
onChange( getNewValue() );
if ( checked ) {
return [ ...value, key ];
}
return value.filter( ( val ) => val !== key );
};
onChange( getNewValue() );
},
[ onChange, value ]
);
const isItemChecked = ( checked, itemValue ) => {
if ( typeof checked === 'boolean' ) {
return checked;
}
if ( Array.isArray( value ) ) {
return value.includes( itemValue );
}
if ( typeof value === 'boolean' ) {
return value;
}
return value === itemValue;
};
return (
@ -21,16 +45,14 @@ const CheckboxGroup = ( { options, value, onChange } ) => {
checked,
disabled,
description,
tooltip,
} ) => (
<PayPalCheckbox
key={ itemValue }
key={ name + itemValue }
value={ itemValue }
label={ label }
checked={ checked }
checked={ isItemChecked( checked, itemValue ) }
disabled={ disabled }
description={ description }
tooltip={ tooltip }
changeCallback={ handleChange }
/>
)

View file

@ -49,10 +49,12 @@ const OptionItem = ( {
} ) => {
const boxClassName = classNames( 'ppcp-r-select-box', {
'ppcp--selected': isSelected,
'ppcp--multiselect': isMulti,
} );
return (
<div className={ boxClassName }>
// eslint-disable-next-line jsx-a11y/label-has-associated-control -- label has a nested input control.
<label className={ boxClassName }>
<InputField
value={ itemValue }
isRadio={ ! isMulti }
@ -60,22 +62,16 @@ const OptionItem = ( {
isSelected={ isSelected }
/>
<div className="ppcp-r-select-box__content">
<div className="ppcp-r-select-box__content-inner">
<span className="ppcp-r-select-box__title">
{ itemTitle }
</span>
<p className="ppcp-r-select-box__description">
{ itemDescription }
</p>
<div className="ppcp--box-content">
<div className="ppcp--box-content-inner">
<span className="ppcp--box-title">{ itemTitle }</span>
<p className="ppcp--box-description">{ itemDescription }</p>
{ children && (
<div className="ppcp-r-select-box__additional-content">
{ children }
</div>
<div className="ppcp--box-details">{ children }</div>
) }
</div>
</div>
</div>
</label>
);
};

View file

@ -1,26 +0,0 @@
/**
* Temporary component, until the experimental HStack block editor component is stable.
*
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs
* @file
*/
import classNames from 'classnames';
const HStack = ( { className, spacing = 3, children } ) => {
const wrapperClass = classNames(
'components-flex components-h-stack',
className
);
const styles = {
gap: `calc(${ 4 * spacing }px)`,
};
return (
<div className={ wrapperClass } style={ styles }>
{ children }
</div>
);
};
export default HStack;

View file

@ -1,15 +1,20 @@
import { Icon } from '@wordpress/components';
import data from '../../utils/data';
const PaymentMethodIcon = ( props ) => {
if (
( Array.isArray( props.icons ) &&
props.icons.includes( props.type ) ) ||
props.icons === 'all'
) {
return data().getImage( 'icon-button-' + props.type + '.svg' );
const PaymentMethodIcon = ( { icons, type } ) => {
const validIcon = Array.isArray( icons ) && icons.includes( type );
if ( validIcon || icons === 'all' ) {
return (
<Icon
icon={ data().getImage( 'icon-button-' + type + '.svg' ) }
className="ppcp--method-icon"
/>
);
}
return <></>;
return null;
};
export default PaymentMethodIcon;

View file

@ -15,22 +15,6 @@ const SettingsBlock = ( {
'ppcp--horizontal': horizontalLayout,
} );
const BlockTitle = ( { blockTitle, blockSuffix, blockDescription } ) => {
if ( ! blockTitle && ! blockDescription ) {
return null;
}
return (
<Header>
<Title>
{ blockTitle }
<TitleExtra>{ blockSuffix }</TitleExtra>
</Title>
<Description>{ blockDescription }</Description>
</Header>
);
};
return (
<div className={ blockClassName }>
<BlockTitle
@ -45,3 +29,19 @@ const SettingsBlock = ( {
};
export default SettingsBlock;
const BlockTitle = ( { blockTitle, blockSuffix, blockDescription } ) => {
if ( ! blockTitle && ! blockDescription ) {
return null;
}
return (
<Header>
<Title>
{ blockTitle }
<TitleExtra>{ blockSuffix }</TitleExtra>
</Title>
<Description>{ blockDescription }</Description>
</Header>
);
};

View file

@ -12,7 +12,7 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
}
return (
<span className="ppcp-r-feature-item__notes">
<span className="ppcp--item-notes">
{ notes.map( ( note, index ) => (
<span key={ index }>{ note }</span>
) ) }
@ -20,30 +20,39 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
);
};
const renderButton = ( button ) => {
const buttonElement = (
<Button
className={ button.class ? button.class : '' }
key={ button.text }
isBusy={ props.actionProps?.isBusy }
variant={ button.type }
onClick={ button.onClick }
>
{ button.text }
</Button>
);
const FeatureButton = ( {
className,
variant,
text,
isBusy,
url,
urls,
onClick,
} ) => {
const buttonProps = {
className,
isBusy,
variant,
};
// If there's a URL (either direct or in urls object), wrap in anchor tag
if ( button.url || button.urls ) {
const href = button.urls ? button.urls.live : button.url;
return (
<a href={ href } key={ button.text }>
{ buttonElement }
</a>
);
if ( url || urls ) {
buttonProps.href = urls ? urls.live : url;
buttonProps.target = '_blank';
}
if ( ! buttonProps.href ) {
buttonProps.onClick = onClick;
}
return buttonElement;
return <Button { ...buttonProps }>{ text }</Button>;
};
const renderDescription = () => {
return (
<span
className="ppcp-r-feature-item__description ppcp-r-settings-block__feature__description"
dangerouslySetInnerHTML={ { __html: description } }
/>
);
};
return (
@ -56,13 +65,33 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
) }
</Title>
<Description className="ppcp-r-settings-block__feature__description">
{ description }
{ renderDescription() }
{ printNotes() }
</Description>
</Header>
<Action>
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map( renderButton ) }
<div className="ppcp--action-buttons">
{ props.actionProps?.buttons.map(
( {
class: className,
type,
text,
url,
urls,
onClick,
} ) => (
<FeatureButton
key={ text }
className={ className }
variant={ type }
text={ text }
isBusy={ props.actionProps.isBusy }
url={ url }
urls={ urls }
onClick={ onClick }
/>
)
) }
</div>
</Action>
</SettingsBlock>

View file

@ -2,7 +2,6 @@ import { ToggleControl } from '@wordpress/components';
import SettingsBlock from '../SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon';
import data from '../../../utils/data';
const PaymentMethodItemBlock = ( {
paymentMethod,
@ -11,35 +10,35 @@ const PaymentMethodItemBlock = ( {
isSelected,
} ) => {
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">
{ paymentMethod?.icon && (
<PaymentMethodIcon
icons={ [ paymentMethod.icon ] }
type={ paymentMethod.icon }
/>
) }
<span className="ppcp-r-settings-block__payment-methods__item__title">
<SettingsBlock className="ppcp--method-item" separatorAndGap={ false }>
<div className="ppcp--method-inner">
<div className="ppcp--method-title-wrapper">
{ paymentMethod?.icon && (
<PaymentMethodIcon
icons={ [ paymentMethod.icon ] }
type={ paymentMethod.icon }
/>
) }
<span className="ppcp--method-title">
{ paymentMethod.itemTitle }
</span>
</div>
<p className="ppcp-r-settings-block__payment-methods__item__description">
<p className="ppcp--method-description">
{ paymentMethod.itemDescription }
</p>
<div className="ppcp-r-settings-block__payment-methods__item__footer">
<div className="ppcp--method-footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
checked={ isSelected }
onChange={ onSelect }
/>
{ paymentMethod?.fields && onTriggerModal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
<Button
className="ppcp--method-settings"
onClick={ onTriggerModal }
>
{ data().getImage( 'icon-settings.svg' ) }
</div>
<Icon icon={ cog } />
</Button>
) }
</div>
</div>

View file

@ -1,42 +1,38 @@
import SettingsBlock from '../SettingsBlock';
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
import { usePaymentMethods } from '../../../data/payment/hooks';
import { PaymentHooks } from '../../../data';
const PaymentMethodsBlock = ( {
paymentMethods,
className = '',
onTriggerModal,
} ) => {
const { setPersistent } = usePaymentMethods();
// TODO: This is not a reusable component, as it's connected to the Redux store.
const PaymentMethodsBlock = ( { paymentMethods = [], onTriggerModal } ) => {
const { changePaymentSettings } = PaymentHooks.useStore();
if ( ! paymentMethods?.length ) {
const handleSelect = ( methodId, isSelected ) =>
changePaymentSettings( methodId, {
enabled: isSelected,
} );
if ( ! paymentMethods.length ) {
return null;
}
const handleSelect = ( paymentMethod, isSelected ) => {
setPersistent( paymentMethod.id, {
...paymentMethod,
enabled: isSelected,
} );
};
return (
<SettingsBlock
className={ `ppcp-r-settings-block__payment-methods ${ className }` }
>
{ paymentMethods.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
paymentMethod={ paymentMethod }
isSelected={ paymentMethod.enabled }
onSelect={ ( checked ) =>
handleSelect( paymentMethod, checked )
}
onTriggerModal={ () =>
onTriggerModal?.( paymentMethod.id )
}
/>
) ) }
<SettingsBlock className="ppcp--grid ppcp-r-settings-block__payment-methods">
{ paymentMethods
// Remove empty/invalid payment method entries.
.filter( ( m ) => m.id )
.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
paymentMethod={ paymentMethod }
isSelected={ paymentMethod.enabled }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}
onTriggerModal={ () =>
onTriggerModal?.( paymentMethod.id )
}
/>
) ) }
</SettingsBlock>
);
};

View file

@ -1,55 +1,47 @@
import classNames from 'classnames';
import { Content, ContentWrapper } from './Elements';
import { Content } from './Elements';
const SettingsCard = ( {
id,
className: extraClassName,
className,
title,
description,
children,
contentItems,
contentContainer = true,
} ) => {
const className = classNames( 'ppcp-r-settings-card', extraClassName );
const renderContent = () => {
// If contentItems array is provided, wrap each item in Content component
if ( contentItems ) {
return (
<ContentWrapper>
{ contentItems.map( ( item ) => (
<Content key={ item.key } id={ item.key }>
{ item }
</Content>
) ) }
</ContentWrapper>
);
}
// Otherwise handle regular children with contentContainer prop
if ( contentContainer ) {
return <Content>{ children }</Content>;
}
return children;
const cardClassNames = classNames( 'ppcp-r-settings-card', className );
const cardProps = {
className: cardClassNames,
id,
};
return (
<div id={ id } className={ className }>
<div { ...cardProps }>
<div className="ppcp-r-settings-card__header">
<div className="ppcp-r-settings-card__content-inner">
<span className="ppcp-r-settings-card__title">
{ title }
</span>
<p className="ppcp-r-settings-card__description">
<div className="ppcp-r-settings-card__description">
{ description }
</p>
</div>
</div>
</div>
{ renderContent() }
<InnerContent showCards={ contentContainer }>
{ children }
</InnerContent>
</div>
);
};
export default SettingsCard;
const InnerContent = ( { showCards, children } ) => {
if ( showCards ) {
return <Content>{ children }</Content>;
}
return children;
};

View file

@ -9,9 +9,7 @@ const SpinnerOverlay = ( { message = null } ) => {
return (
<div className="ppcp-r-spinner-overlay">
{ message && (
<span className="ppcp-r-spinner-overlay__message">
{ message }
</span>
<span className="ppcp--spinner-message">{ message }</span>
) }
<Spinner />
</div>

View file

@ -0,0 +1,42 @@
/**
* Temporary component, until the experimental VStack/HStack block editor component is stable.
*
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-vstack--docs
* @file
*/
import classNames from 'classnames';
const Stack = ( { type, className, spacing, children } ) => {
const wrapperClass = classNames(
'components-flex',
`components-${ type }-stack`,
className
);
const styles = {
gap: `calc(${ 4 * spacing }px)`,
};
return (
<div className={ wrapperClass } style={ styles }>
{ children }
</div>
);
};
export const HStack = ( { className, spacing = 3, children } ) => {
return (
<Stack type="h" className={ className } spacing={ spacing }>
{ children }
</Stack>
);
};
export const VStack = ( { className, spacing = 3, children } ) => {
return (
<Stack type="v" className={ className } spacing={ spacing }>
{ children }
</Stack>
);
};

View file

@ -1,134 +0,0 @@
import { __ } from '@wordpress/i18n';
import SettingsCard from '../../ReusableComponents/SettingsCard';
import { PaymentMethodsBlock } from '../../ReusableComponents/SettingsBlocks';
import { PaymentHooks } from '../../../data';
import { useActiveModal } from '../../../data/common/hooks';
import Modal from './TabSettingsElements/Blocks/Modal';
import { usePaymentMethods } from '../../../data/payment/hooks';
const TabPaymentMethods = () => {
const { paymentMethodsPayPalCheckout } =
PaymentHooks.usePaymentMethodsPayPalCheckout();
const { paymentMethodsOnlineCardPayments } =
PaymentHooks.usePaymentMethodsOnlineCardPayments();
const { paymentMethodsAlternative } =
PaymentHooks.usePaymentMethodsAlternative();
const { setPersistent } = usePaymentMethods();
const { activeModal, setActiveModal } = useActiveModal();
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
}
const allMethods = [
...paymentMethodsPayPalCheckout,
...paymentMethodsOnlineCardPayments,
...paymentMethodsAlternative,
];
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.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-standard.svg"
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ paymentMethodsPayPalCheckout }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
<SettingsCard
id="ppcp-card-payments-card"
title={ __(
'Online Card Payments',
'woocommerce-paypal-payments'
) }
description={ __(
'Select your preferred card payment options for efficient payment processing.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-online-methods.svg"
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ paymentMethodsOnlineCardPayments }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
<SettingsCard
id="ppcp-alternative-payments-card"
title={ __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
) }
description={ __(
'With alternative payment methods, customers across the globe can pay with their bank accounts and other local payment methods.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-alternative-methods.svg"
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ paymentMethodsAlternative }
onTriggerModal={ setActiveModal }
/>
</SettingsCard>
{ activeModal && (
<Modal
method={ getActiveMethod() }
setModalIsVisible={ () => setActiveModal( null ) }
onSave={ ( methodId, settings ) => {
setPersistent( methodId, {
...getActiveMethod(),
title: settings.checkoutPageTitle,
description: settings.checkoutPageDescription,
} );
if ( 'paypalShowLogo' in settings ) {
setPersistent(
'paypalShowLogo',
settings.paypalShowLogo
);
}
if ( 'threeDSecure' in settings ) {
setPersistent(
'threeDSecure',
settings.threeDSecure
);
}
if ( 'fastlaneCardholderName' in settings ) {
setPersistent(
'fastlaneCardholderName',
settings.fastlaneCardholderName
);
}
if ( 'fastlaneDisplayWatermark' in settings ) {
setPersistent(
'fastlaneDisplayWatermark',
settings.fastlaneDisplayWatermark
);
}
setActiveModal( null );
} }
/>
) }
</div>
);
};
export default TabPaymentMethods;

View file

@ -1,4 +1,5 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import Container from '../ReusableComponents/Container';
import SettingsCard from '../ReusableComponents/SettingsCard';
@ -9,7 +10,7 @@ const SendOnlyMessage = () => {
return (
<>
<SettingsNavigation />
<SettingsNavigation canSave={ false } />
<Container page="settings">
<SettingsCard
title={ __(
@ -45,6 +46,19 @@ const SendOnlyMessage = () => {
),
} }
/>
<div>
<Button
href={ settingsPageUrl }
variant="primary"
className="small-button"
>
{ __(
'Go to WooCommerce settings',
'woocommerce-paypal-payments'
) }
</Button>
</div>
</SettingsCard>
</Container>
</>

View file

@ -2,21 +2,20 @@ import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import { useSaveSettings } from '../../../../hooks/useSaveSettings';
const SettingsNavigation = () => {
const SettingsNavigation = ( { canSave = true } ) => {
const { persistAll } = useSaveSettings();
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
return (
<TopNavigation title={ title } exitOnTitleClick={ true }>
<BusyStateWrapper>
{ canSave && (
<Button variant="primary" onClick={ persistAll }>
{ __( 'Save', 'woocommerce-paypal-payments' ) }
</Button>
</BusyStateWrapper>
) }
</TopNavigation>
);
};

View file

@ -1,8 +1,10 @@
import { __ } from '@wordpress/i18n';
import { TAB_IDS, selectTab } from '../../../../../utils/tabSelector';
import { payLaterMessaging } from './pay-later-messaging';
const Features = {
getFeatures: ( setActiveModal ) => [
export const getFeatures = ( setActiveModal ) => {
const storeCountry = ppcpSettings?.storeCountry;
const features = [
{
id: 'save_paypal_and_venmo',
title: __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
@ -39,7 +41,10 @@ const Features = {
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: 'https://developer.paypal.com/studio/checkout/standard',
urls: {
sandbox: '#',
live: '#',
},
class: 'small-button',
},
],
@ -230,7 +235,16 @@ const Features = {
},
],
},
{
];
const countryData = payLaterMessaging[ storeCountry ] || {};
// Add "Pay Later Messaging" to the feature list, if it's available.
if (
!! window.ppcpSettings?.isPayLaterConfiguratorAvailable &&
countryData
) {
features.push( {
id: 'pay_later_messaging',
title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
description: __(
@ -269,8 +283,8 @@ const Features = {
class: 'small-button',
},
],
},
],
};
} );
}
export default Features;
return features;
};

View file

@ -0,0 +1,106 @@
import { __, sprintf } from '@wordpress/i18n';
export const payLaterMessaging = {
US: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. PayPals Pay Later helps boost merchants\' conversion rates and increases cart sizes by 39%%.¹ You get paid in full up front. <a target="_blank" href="%s">More about Pay Later</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/us/business/accept-payments/checkout/installments'
),
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
GB: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. Pay in 3 gets a 216%% higher Average Order Value than a standard PayPal transaction.¹ Theres <strong>no extra cost</strong> and you get paid up front. <a target="_blank" href="%s">More about Pay in 3</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/uk/business/accept-payments/checkout/installments'
),
notes: [
__(
'Based on PayPal internal data from Q1 2022, results include Pay in 3 (UK).',
'woocommerce-paypal-payments'
),
],
},
FR: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. Pay in 4x gets a 65%% higher Average Order Value than a standard PayPal transaction.¹ <strong>There\'s no extra cost on top of your PayPal Checkout rate</strong>, and you get paid up front. <a target="_blank" href="%s">More about Pay in 4x</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/fr/business/accept-payments/checkout/installments'
),
notes: [
__(
'Internal Data Analysis of 1124 SMB across integrated partners and non-integrated partners, November 2022. SMB internally defined as up to 100,000€ in annual estimated ecommerce online payment volume.',
'woocommerce-paypal-payments'
),
],
},
AU: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. Pay in 4 gets more than a 100%% higher Average Order Value than a standard PayPal transaction.¹ Theres <strong>no extra cost</strong> and you get paid up front. <a target="_blank" href="%s">More about Pay in 4</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/au/business/accept-payments/checkout/installments'
),
notes: [
__(
'Based on PayPal internal data from Q1 2022, results include Pay in 4 (AU). Consumer eligibility applies.',
'woocommerce-paypal-payments'
),
],
},
IT: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. Pay in 3 installments gets about a 275%% higher Average Order Value than a standard PayPal transaction.¹ <strong>There\'s no extra cost on top of your PayPal Checkout rate</strong>, and you get paid up front. <a target="_blank" href="%s">More about Pay in 3 installments</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/it/business/accept-payments/checkout/installments'
),
notes: [
__(
'Based on PayPal internal data from Q1 2022, results include Pay in 3 installments (IT).',
'woocommerce-paypal-payments'
),
],
},
ES: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. Pay in 3 installments gets about a 275%% higher Average Order Value than a standard PayPal transaction.¹ <strong>There\'s no extra cost on top of your PayPal Checkout rate</strong>, and you get paid up front. <a target="_blank" href="%s">More about Pay in 3 installments</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/es/business/accept-payments/checkout/installments'
),
notes: [
__(
'Based on PayPal internal data from Q1 2022, results include Pay in 3 installments (ES).',
'woocommerce-paypal-payments'
),
],
},
DE: {
description: sprintf(
__(
'Your customers can already buy now and pay later with PayPal — add messaging to your site to let them know. When you offer your customers Pay Later options, 57%% will be more likely to buy from you again.¹ <strong>There\'s no extra cost</strong> and you get paid up front. <a target="_blank" href="%s">More about Pay Later</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/de/business/accept-payments/checkout/installments'
),
notes: [
__(
'Average order value in 2020 with PayPal installments compared to total PayPal sales.',
'woocommerce-paypal-payments'
),
],
},
};

View file

@ -6,20 +6,18 @@ import {
RadioControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import PaymentMethodModal from '../../../../ReusableComponents/PaymentMethodModal';
import {
usePaymentMethods,
usePaymentMethodsModal,
} from '../../../../../data/payment/hooks';
import { PaymentHooks } from '../../../../../data';
const Modal = ( { method, setModalIsVisible, onSave } ) => {
const { paymentMethods } = usePaymentMethods();
const { paymentMethods } = PaymentHooks.usePaymentMethods();
const {
paypalShowLogo,
threeDSecure,
fastlaneCardholderName,
fastlaneDisplayWatermark,
} = usePaymentMethodsModal();
} = PaymentHooks.usePaymentMethodsModal();
const [ settings, setSettings ] = useState( () => {
if ( ! method?.id ) {
@ -68,6 +66,7 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
return (
<div className="ppcp-r-modal__field-row">
<TextControl
__nextHasNoMarginBottom={ true }
className="ppcp-r-vertical-text-control"
label={ field.label }
value={ settings[ key ] }

View file

@ -4,9 +4,9 @@ import { Button } from '@wordpress/components';
import {
ControlTextInput,
ControlRadioGroup,
} from '../../../../ReusableComponents/Controls';
import Accordion from '../../../../ReusableComponents/AccordionSection';
import SettingsBlock from '../../../../ReusableComponents/SettingsBlock';
} from '../../../../../ReusableComponents/Controls';
import Accordion from '../../../../../ReusableComponents/AccordionSection';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
const ConnectionDetails = ( { settings, updateFormValue } ) => {
const isSandbox = settings.sandboxConnected;

View file

@ -4,7 +4,7 @@ import { CommonHooks } from '../../../../../../data';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
import { Title } from '../../../../../ReusableComponents/Elements';
const HooksTableBlock = () => {
const HooksListBlock = () => {
const { webhooks } = CommonHooks.useWebhooks();
const { url, events } = webhooks;
@ -13,7 +13,7 @@ const HooksTableBlock = () => {
}
return (
<SettingsBlock separatorAndGap={ false }>
<SettingsBlock separatorAndGap={ false } className="ppcp--webhooks">
<WebhookUrl url={ url } />
<WebhookEvents events={ events } />
</SettingsBlock>
@ -37,7 +37,7 @@ const WebhookEvents = ( { events } ) => {
<Title>
{ __( 'Subscribed Events', 'woocommerce-paypal-payments' ) }
</Title>
<ul>
<ul className="ppcp--webhook-list">
{ events.map( ( event, index ) => (
<li key={ index }>{ event }</li>
) ) }
@ -46,4 +46,4 @@ const WebhookEvents = ( { events } ) => {
);
};
export default HooksTableBlock;
export default HooksListBlock;

View file

@ -1,9 +1,9 @@
import { __ } from '@wordpress/i18n';
import Accordion from '../../../../ReusableComponents/AccordionSection';
import SettingsBlock from '../../../../ReusableComponents/SettingsBlock';
import { ControlSelect } from '../../../../ReusableComponents/Controls';
import { SettingsHooks } from '../../../../../data';
import Accordion from '../../../../../ReusableComponents/AccordionSection';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
import { ControlSelect } from '../../../../../ReusableComponents/Controls';
import { SettingsHooks } from '../../../../../../data';
const OtherSettings = () => {
const { disabledCards, setDisabledCards } = SettingsHooks.useSettings();

View file

@ -5,10 +5,10 @@ import {
ControlToggleButton,
ControlTextInput,
ControlSelect,
} from '../../../../ReusableComponents/Controls';
import SettingsBlock from '../../../../ReusableComponents/SettingsBlock';
import Accordion from '../../../../ReusableComponents/AccordionSection';
import { SettingsHooks } from '../../../../../data';
} from '../../../../../ReusableComponents/Controls';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
import Accordion from '../../../../../ReusableComponents/AccordionSection';
import { SettingsHooks } from '../../../../../../data';
const PaypalSettings = () => {
const {

View file

@ -59,6 +59,7 @@ const ResubscribeBlock = () => {
'woocommerce-paypal-payments'
) }
horizontalLayout={ true }
className="ppcp--webhook-resubscribe"
>
<ControlButton
type={ 'secondary' }

View file

@ -116,6 +116,7 @@ const SimulationBlock = () => {
'woocommerce-paypal-payments'
) }
horizontalLayout={ true }
className="ppcp--webhook-simulation"
>
<ControlButton
type={ 'secondary' }

View file

@ -6,7 +6,7 @@ import Accordion from '../../../../../ReusableComponents/AccordionSection';
import SimulationBlock from './SimulationBlock';
import ResubscribeBlock from './ResubscribeBlock';
import HooksTableBlock from './HooksTableBlock';
import HooksListBlock from './HooksListBlock';
import { SettingsHooks } from '../../../../../../data';
const Troubleshooting = () => {
@ -43,7 +43,7 @@ const Troubleshooting = () => {
'https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status'
) }
>
<HooksTableBlock />
<HooksListBlock />
<ResubscribeBlock />
<SimulationBlock />
</SettingsBlock>

View file

@ -4,10 +4,10 @@ import {
Content,
ContentWrapper,
} from '../../../../ReusableComponents/Elements';
import ConnectionDetails from '../../../Overview/TabSettingsElements/Blocks/ConnectionDetails';
import Troubleshooting from '../../../Overview/TabSettingsElements/Blocks/Troubleshooting/Troubleshooting';
import PaypalSettings from '../../../Overview/TabSettingsElements/Blocks/PaypalSettings';
import OtherSettings from '../../../Overview/TabSettingsElements/Blocks/OtherSettings';
import ConnectionDetails from './Blocks/ConnectionDetails';
import Troubleshooting from './Blocks/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';
const ExpertSettings = () => {
const settings = {}; // dummy object

View file

@ -49,7 +49,7 @@ const LocationSelector = ( { location, setLocation } ) => {
) }
</SelectStylingSection>
<CheckboxStylingSection
className="location-activation"
name="location-activation"
separatorAndGap={ false }
options={ [ activateCheckbox ] }
value={ isActive }

View file

@ -9,8 +9,8 @@ const SectionPaymentMethods = ( { location } ) => {
return (
<CheckboxStylingSection
name="payment-methods"
title={ __( 'Payment Methods', 'woocommerce-paypal-payments' ) }
className="payment-methods"
options={ choices }
value={ paymentMethods }
onChange={ setPaymentMethods }

View file

@ -21,7 +21,7 @@ const SectionTagline = ( { location } ) => {
return (
<CheckboxStylingSection
className="tagline"
name="tagline"
separatorAndGap={ false }
options={ [ checkbox ] }
value={ tagline }

View file

@ -20,7 +20,7 @@ const StylingSection = ( {
separatorAndGap={ separatorAndGap }
>
<Header>
<Title altStyle={ true } big={ bigTitle }>
<Title noCaps={ true } big={ bigTitle }>
{ title }
</Title>
<Description>{ description }</Description>

View file

@ -1,11 +1,12 @@
import classNames from 'classnames';
import { CheckboxGroup } from '../../../../../ReusableComponents/Fields';
import HStack from '../../../../../ReusableComponents/HStack';
import { VStack } from '../../../../../ReusableComponents/Stack';
import StylingSection from './StylingSection';
const StylingSectionWithCheckboxes = ( {
title,
name,
className = '',
description = '',
separatorAndGap = true,
@ -14,7 +15,14 @@ const StylingSectionWithCheckboxes = ( {
onChange,
children,
} ) => {
className = classNames( 'ppcp--has-checkboxes', className );
className = classNames( 'ppcp--has-checkboxes', name, className );
if ( ! name ) {
console.error(
'Checkbox sections need a unique name! No name given to:',
title
);
}
return (
<StylingSection
@ -23,13 +31,14 @@ const StylingSectionWithCheckboxes = ( {
description={ description }
separatorAndGap={ separatorAndGap }
>
<HStack spacing={ 6 }>
<VStack spacing={ 6 }>
<CheckboxGroup
name={ name }
options={ options }
value={ value }
onChange={ onChange }
/>
</HStack>
</VStack>
{ children }
</StylingSection>

View file

@ -1,7 +1,7 @@
import { RadioControl } from '@wordpress/components';
import classNames from 'classnames';
import HStack from '../../../../../ReusableComponents/HStack';
import { HStack } from '../../../../../ReusableComponents/Stack';
import StylingSection from './StylingSection';
const StylingSectionWithRadiobuttons = ( {

View file

@ -9,11 +9,12 @@ import {
TodoSettingsBlock,
FeatureSettingsBlock,
} from '../../../ReusableComponents/SettingsBlocks';
import { Content, ContentWrapper } from '../../../ReusableComponents/Elements';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge';
import { useMerchantInfo } from '../../../../data/common/hooks';
import { STORE_NAME } from '../../../../data/common';
import Features from '../Components/Overview/Features';
import { getFeatures } from '../Components/Overview/features-config';
import { todosData } from '../todo-items';
import {
@ -22,8 +23,34 @@ import {
} from '../../../ReusableComponents/Icons';
const TabOverview = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false );
return (
<div className="ppcp-r-tab-overview">
{ todosData.length > 0 && (
<SettingsCard
className="ppcp-r-tab-overview-todo"
title={ __(
'Things to do next',
'woocommerce-paypal-payments'
) }
description={ __(
'Complete these tasks to keep your store updated with the latest products and services.',
'woocommerce-paypal-payments'
) }
>
<TodoSettingsBlock todosData={ todosData } />
</SettingsCard>
) }
<OverviewFeatures />
<OverviewHelp />
</div>
);
};
export default TabOverview;
const OverviewFeatures = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant, features: merchantFeatures } = useMerchantInfo();
const { refreshFeatureStatuses, setActiveModal } =
useDispatch( STORE_NAME );
@ -32,7 +59,7 @@ const TabOverview = () => {
// Get the features data with access to setActiveModal
const featuresData = useMemo(
() => Features.getFeatures( setActiveModal ),
() => getFeatures( setActiveModal ),
[ setActiveModal ]
);
@ -49,6 +76,7 @@ const TabOverview = () => {
const refreshHandler = async () => {
setIsRefreshing( true );
try {
const result = await refreshFeatureStatuses();
if ( result && ! result.success ) {
@ -79,7 +107,6 @@ const TabOverview = () => {
icon: NOTIFICATION_SUCCESS,
}
);
console.log( 'Features refreshed successfully.' );
}
} finally {
setIsRefreshing( false );
@ -87,111 +114,132 @@ const TabOverview = () => {
};
return (
<div className="ppcp-r-tab-overview">
{ todosData.length > 0 && (
<SettingsCard
className="ppcp-r-tab-overview-todo"
title={ __(
'Things to do next',
'woocommerce-paypal-payments'
) }
description={ __(
'Complete these tasks to keep your store updated with the latest products and services.',
'woocommerce-paypal-payments'
) }
>
<TodoSettingsBlock todosData={ todosData } />
</SettingsCard>
) }
<SettingsCard
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<OverviewFeatureDescription
refreshHandler={ refreshHandler }
isRefreshing={ isRefreshing }
/>
}
contentContainer={ false }
>
<ContentWrapper>
{ features.map( ( { id, ...feature } ) => (
<OverviewFeatureItem
key={ id }
isBusy={ isRefreshing }
isSandbox={ merchant.isSandbox }
title={ feature.title }
description={ feature.description }
buttons={ feature.buttons }
enabled={ feature.enabled }
notes={ feature.notes }
/>
) ) }
</ContentWrapper>
</SettingsCard>
);
};
<SettingsCard
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<>
<p>
{ __(
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ isRefreshing
? __(
'Refreshing…',
'woocommerce-paypal-payments'
)
: __(
'Refresh',
'woocommerce-paypal-payments'
) }
</Button>
</>
}
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,
} }
/>
);
} ) }
const OverviewFeatureItem = ( {
isBusy,
isSandbox,
title,
description,
buttons,
enabled,
notes,
} ) => {
const getButtonUrl = ( button ) => {
if ( button.urls ) {
return isSandbox ? button.urls.sandbox : button.urls.live;
}
return button.url;
};
const visibleButtons = buttons.filter(
( button ) =>
! button.showWhen || // Learn more buttons
( enabled && button.showWhen === 'enabled' ) ||
( ! enabled && button.showWhen === 'disabled' )
);
const actionProps = {
isBusy,
enabled,
notes,
buttons: visibleButtons.map( ( button ) => ( {
...button,
url: getButtonUrl( button ),
} ) ),
};
if ( enabled ) {
actionProps.badge = {
text: __( 'Active', 'woocommerce-paypal-payments' ),
type: TITLE_BADGE_POSITIVE,
};
}
return (
<Content>
<FeatureSettingsBlock
title={ title }
description={ description }
actionProps={ actionProps }
/>
</Content>
);
};
<SettingsCard
className="ppcp-r-tab-overview-help"
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
description={ __(
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
const OverviewFeatureDescription = ( { refreshHandler, isRefreshing } ) => {
const buttonLabel = isRefreshing
? __( 'Refreshing…', 'woocommerce-paypal-payments' )
: __( 'Refresh', 'woocommerce-paypal-payments' );
return (
<>
<p>
{ __(
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
contentItems={ [
</p>
<p>
{ __(
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ buttonLabel }
</Button>
</>
);
};
const OverviewHelp = () => {
return (
<SettingsCard
className="ppcp-r-tab-overview-help"
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
description={ __(
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
'woocommerce-paypal-payments'
) }
contentContainer={ false }
>
<ContentWrapper>
<Content>
<FeatureSettingsBlock
key="documentation"
title={ __(
'Documentation',
'woocommerce-paypal-payments'
@ -212,9 +260,11 @@ const TabOverview = () => {
},
],
} }
/>,
/>
</Content>
<Content>
<FeatureSettingsBlock
key="support"
title={ __( 'Support', 'woocommerce-paypal-payments' ) }
description={ __(
'Need help? Access troubleshooting tips or contact our support team for personalized assistance.',
@ -232,11 +282,9 @@ const TabOverview = () => {
},
],
} }
/>,
] }
/>
</div>
/>
</Content>
</ContentWrapper>
</SettingsCard>
);
};
export default TabOverview;

View file

@ -0,0 +1,123 @@
import { __ } from '@wordpress/i18n';
import { useCallback } from '@wordpress/element';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { PaymentMethodsBlock } from '../../../ReusableComponents/SettingsBlocks';
import { PaymentHooks } from '../../../../data';
import { useActiveModal } from '../../../../data/common/hooks';
import Modal from '../Components/Payment/Modal';
const TabPaymentMethods = () => {
const methods = PaymentHooks.usePaymentMethods();
const { setPersistent, changePaymentSettings } = PaymentHooks.useStore();
const { activeModal, setActiveModal } = useActiveModal();
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
}
return methods.all.find( ( method ) => method.id === activeModal );
};
const handleSave = useCallback(
( methodId, settings ) => {
changePaymentSettings( methodId, {
title: settings.checkoutPageTitle,
description: settings.checkoutPageDescription,
} );
const persistentSettings = [
'paypalShowLogo',
'threeDSecure',
'fastlaneCardholderName',
'fastlaneDisplayWatermark',
];
persistentSettings.forEach( ( setting ) => {
if ( setting in settings ) {
// TODO: Create a dedicated setter for those values.
setPersistent( setting, settings[ setting ] );
}
} );
setActiveModal( null );
},
[ changePaymentSettings, setActiveModal, setPersistent ]
);
return (
<div className="ppcp-r-payment-methods">
<PaymentMethodCard
id="ppcp-paypal-checkout-card"
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
description={ __(
'Select your preferred checkout option with PayPal for easy payment processing.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-standard.svg"
methods={ methods.paypal }
onTriggerModal={ setActiveModal }
/>
<PaymentMethodCard
id="ppcp-card-payments-card"
title={ __(
'Online Card Payments',
'woocommerce-paypal-payments'
) }
description={ __(
'Select your preferred card payment options for efficient payment processing.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-online-methods.svg"
methods={ methods.cardPayment }
onTriggerModal={ setActiveModal }
/>
<PaymentMethodCard
id="ppcp-alternative-payments-card"
title={ __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
) }
description={ __(
'With alternative payment methods, customers across the globe can pay with their bank accounts and other local payment methods.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-alternative-methods.svg"
methods={ methods.apm }
onTriggerModal={ setActiveModal }
/>
{ activeModal && (
<Modal
method={ getActiveMethod() }
setModalIsVisible={ () => setActiveModal( null ) }
onSave={ handleSave }
/>
) }
</div>
);
};
export default TabPaymentMethods;
const PaymentMethodCard = ( {
id,
title,
description,
icon,
methods,
onTriggerModal,
} ) => (
<SettingsCard
id={ id }
title={ title }
description={ description }
icon={ icon }
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ methods }
onTriggerModal={ onTriggerModal }
/>
</SettingsCard>
);

View file

@ -1,7 +1,7 @@
import { __ } from '@wordpress/i18n';
import TabOverview from './TabOverview';
import TabPaymentMethods from '../../Overview/TabPaymentMethods';
import TabPaymentMethods from './TabPaymentMethods';
import TabSettings from './TabSettings';
import TabStyling from './TabStyling';
import TabPayLaterMessaging from '../../Overview/TabPayLaterMessaging';