Merge branch 'trunk' into PCP-3917-things-to-do-next-component-functionality

This commit is contained in:
Emili Castells Guasch 2025-01-02 16:29:44 +01:00
commit 4d4ab689f5
121 changed files with 5004 additions and 3431 deletions

View file

@ -1,8 +1,6 @@
import { Icon } from '@wordpress/components';
import { chevronDown, chevronUp } from '@wordpress/icons';
import classNames from 'classnames';
import { useAccordionState } from '../../hooks/useAccordionState';
// Provide defaults for all layout components so the generic version just works.
@ -24,6 +22,13 @@ const DefaultDescription = ( { children } ) => (
<div className="ppcp-r-accordion__description">{ children }</div>
);
const AccordionContent = ( { isOpen, children } ) => {
if ( ! isOpen || ! children ) {
return null;
}
return <div className="ppcp-r-accordion__content">{ children }</div>;
};
const Accordion = ( {
title,
id = '',
@ -65,9 +70,7 @@ const Accordion = ( {
) }
</Header>
</button>
{ isOpen && children && (
<div className="ppcp-r-accordion__content">{ children }</div>
) }
<AccordionContent isOpen={ isOpen }>{ children }</AccordionContent>
</div>
);
};

View file

@ -1,6 +1,4 @@
import data from '../../utils/data';
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
import { __ } from '@wordpress/i18n';
const BadgeBox = ( props ) => {
const titleSize =
@ -29,12 +27,7 @@ const BadgeBox = ( props ) => {
</span>
) }
{ props.textBadge && (
<TitleBadge
type={ TITLE_BADGE_INFO }
text={ props.textBadge }
/>
) }
{ props.textBadge }
</span>
<div className="ppcp-r-badge-box__description">
{ props?.description && (

View file

@ -0,0 +1,68 @@
import {
Children,
isValidElement,
cloneElement,
useMemo,
createContext,
useContext,
} from '@wordpress/element';
import classNames from 'classnames';
import { CommonHooks } from '../../data';
import SpinnerOverlay from './SpinnerOverlay';
// Create context to track the busy state across nested wrappers
const BusyContext = createContext( false );
/**
* Wraps interactive child elements and modifies their behavior based on the global `isBusy` state.
* Allows custom processing of child props via the `onBusy` callback.
*
* @param {Object} props - Component properties.
* @param {Children} props.children - Child components to wrap.
* @param {boolean} props.enabled - Enables or disables the busy-state logic.
* @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.
*/
const BusyStateWrapper = ( {
children,
enabled = true,
busySpinner = true,
className = '',
onBusy = () => ( { disabled: true } ),
} ) => {
const { isBusy } = CommonHooks.useBusyState();
const hasBusyParent = useContext( BusyContext );
const isBusyComponent = isBusy && enabled;
const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent;
const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, {
'ppcp--is-loading': isBusyComponent,
} );
const memoizedChildren = useMemo(
() =>
Children.map( children, ( child ) =>
isValidElement( child )
? cloneElement(
child,
isBusyComponent ? onBusy( child.props ) : {}
)
: child
),
[ children, isBusyComponent, onBusy ]
);
return (
<BusyContext.Provider value={ isBusyComponent }>
<div className={ wrapperClassName }>
{ showSpinner && <SpinnerOverlay /> }
{ memoizedChildren }
</div>
</BusyContext.Provider>
);
};
export default BusyStateWrapper;

View file

@ -0,0 +1,3 @@
export { default as openSignup } from './Icons/open-signup';
export const NOTIFICATION_SUCCESS = '✔️';
export const NOTIFICATION_ERROR = '❌';

View file

@ -0,0 +1,12 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from '@wordpress/primitives';
const openSignup = (
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 24">
<Path d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z" />
</SVG>
);
export default openSignup;

View file

@ -1,14 +1,13 @@
import BadgeBox from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox from '../BadgeBox';
import Separator from '../Separator';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import PricingTitleBadge from '../PricingTitleBadge';
const AcdcOptionalPaymentMethods = ( {
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
return (
@ -24,11 +23,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'ccf',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="ccf" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -48,11 +43,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-apple-pay.svg',
'icon-button-google-pay.svg',
] }
textBadge={ generatePriceText(
'dw',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="dw" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -69,16 +60,11 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments'
) }
imageBadge={ [
'icon-button-sepa.svg',
'icon-button-ideal.svg',
'icon-button-blik.svg',
'icon-button-bancontact.svg',
] }
textBadge={ generatePriceText(
'apm',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="apm" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -92,11 +78,9 @@ const AcdcOptionalPaymentMethods = ( {
<BadgeBox
title={ __( '', 'woocommerce-paypal-payments' ) }
imageBadge={ [ 'icon-payment-method-fastlane-small.svg' ] }
textBadge={ generatePriceText(
'fastlane',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={
<PricingTitleBadge item="fast country currency=storeCurrency=storeCountrylane" />
}
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -124,11 +108,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'ccf',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="ccf" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -148,11 +128,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-apple-pay.svg',
'icon-button-google-pay.svg',
] }
textBadge={ generatePriceText(
'dw',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="dw" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -174,11 +150,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-blik.svg',
'icon-button-bancontact.svg',
] }
textBadge={ generatePriceText(
'apm',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="apm" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -205,11 +177,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'ccf',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="ccf" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -226,11 +194,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-apple-pay.svg',
'icon-button-google-pay.svg',
] }
textBadge={ generatePriceText(
'dw',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="dw" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
@ -252,11 +216,7 @@ const AcdcOptionalPaymentMethods = ( {
'icon-button-blik.svg',
'icon-button-bancontact.svg',
] }
textBadge={ generatePriceText(
'apm',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="apm" /> }
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(

View file

@ -1,13 +1,9 @@
import BadgeBox from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
const BcdcOptionalPaymentMethods = ( {
isPayLater,
storeCountry,
storeCurrency,
} ) => {
import BadgeBox from '../BadgeBox';
import PricingTitleBadge from '../PricingTitleBadge';
const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => {
if ( isPayLater && storeCountry === 'us' ) {
return (
<div className="ppcp-r-optional-payment-methods__wrapper">
@ -22,11 +18,9 @@ const BcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'standardCardFields',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={
<PricingTitleBadge item="standardCardFields" />
}
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
@ -53,11 +47,7 @@ const BcdcOptionalPaymentMethods = ( {
'icon-button-amex.svg',
'icon-button-discover.svg',
] }
textBadge={ generatePriceText(
'standardCardFields',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="standardCardFields" /> }
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(

View file

@ -6,7 +6,6 @@ const OptionalPaymentMethods = ( {
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
return (
<div className="ppcp-r-optional-payment-methods">
@ -15,13 +14,11 @@ const OptionalPaymentMethods = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) : (
<BcdcOptionalPaymentMethods
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) }
</div>

View file

@ -11,7 +11,6 @@ const PaymentMethodIcons = ( props ) => {
<PaymentMethodIcon type="discover" icons={ props.icons } />
<PaymentMethodIcon type="apple-pay" icons={ props.icons } />
<PaymentMethodIcon type="google-pay" icons={ props.icons } />
<PaymentMethodIcon type="sepa" icons={ props.icons } />
<PaymentMethodIcon type="ideal" icons={ props.icons } />
<PaymentMethodIcon type="bancontact" icons={ props.icons } />
</div>

View file

@ -0,0 +1,38 @@
import { __, sprintf } from '@wordpress/i18n';
import { countryPriceInfo } from '../../utils/countryPriceInfo';
import { CommonHooks } from '../../data';
const PricingDescription = () => {
const { storeCountry } = CommonHooks.useWooSettings();
if ( ! countryPriceInfo[ storeCountry ] ) {
return null;
}
const lastDate = 'October 25th, 2024'; // TODO -- needs to be the last plugin update date.
const detailsUrl =
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input';
const label = sprintf(
// translators: %1$s: Pricing date, %2$s Link to PayPal price-details page.
__(
'Prices based on domestic transactions as of %1$s. <a target="_blank" href="%2$s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
lastDate,
detailsUrl
);
return (
<p
className="ppcp-r-optional-payment-methods__description"
data-country={ storeCountry }
>
<sup>1</sup>
<span dangerouslySetInnerHTML={ { __html: label } } />
</p>
);
};
export default PricingDescription;

View file

@ -0,0 +1,46 @@
import { __, sprintf } from '@wordpress/i18n';
import { CommonHooks } from '../../data';
import { countryPriceInfo } from '../../utils/countryPriceInfo';
import { formatPrice } from '../../utils/formatPrice';
import TitleBadge, { TITLE_BADGE_INFO } from './TitleBadge';
const getFixedAmount = ( currency, priceList, itemFixedAmount ) => {
if ( priceList[ currency ] ) {
const sum = priceList[ currency ] + itemFixedAmount;
return formatPrice( sum, currency );
}
const [ defaultCurrency, defaultPrice ] = Object.entries( priceList )[ 0 ];
const sum = defaultPrice + itemFixedAmount;
return formatPrice( sum, defaultCurrency );
};
const PricingTitleBadge = ( { item } ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const infos = countryPriceInfo[ storeCountry ];
const itemKey = item.split(' ')[0]; // Extract the first word, fastlane has more than one
if ( ! infos || ! infos[ itemKey ] ) {
return null;
}
const percentage = typeof infos[itemKey] === 'number' ? infos[itemKey].toFixed(2) : infos[itemKey]['percentage'].toFixed(2);
const itemFixedAmount = infos[itemKey]['fixedFee'] ? infos[itemKey]['fixedFee'] : 0;
const fixedAmount = getFixedAmount( storeCurrency, infos.fixedFee, itemFixedAmount );
const label = sprintf(
__( 'from %1$s%% + %2$s', 'woocommerce-paypal-payments' ),
percentage,
fixedAmount
);
return (
<TitleBadge
type={ TITLE_BADGE_INFO }
text={ `${ label }<sup>1</sup>` }
/>
);
};
export default PricingTitleBadge;

View file

@ -9,25 +9,19 @@ import {
} from './SettingsBlockElements';
const SettingsAccordion = ( { title, description, children, ...props } ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__accordion"
components={ [
() => (
<Accordion
title={ title }
description={ description }
Header={ Header }
TitleWrapper={ TitleWrapper }
Title={ Title }
Action={ Action }
Description={ Description }
>
{ children }
</Accordion>
),
] }
/>
<SettingsBlock { ...props } className="ppcp-r-settings-block__accordion">
<Accordion
title={ title }
description={ description }
Header={ Header }
TitleWrapper={ TitleWrapper }
Title={ Title }
Action={ Action }
Description={ Description }
>
{ children }
</Accordion>
</SettingsBlock>
);
export default SettingsAccordion;

View file

@ -1,34 +1,27 @@
import { Button } from '@wordpress/components';
import SettingsBlock from './SettingsBlock';
import { Header, Title, Action, Description } from './SettingsBlockElements';
import { Action, Description, Header, Title } from './SettingsBlockElements';
const ButtonSettingsBlock = ( { title, description, ...props } ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__button"
components={ [
() => (
<>
<Header>
<Title>{ title }</Title>
<Description>{ description }</Description>
</Header>
<Action>
<Button
variant={ props.actionProps?.buttonType }
onClick={
props.actionProps?.callback
? () => props.actionProps.callback()
: undefined
}
>
{ props.actionProps.value }
</Button>
</Action>
</>
),
] }
/>
<SettingsBlock { ...props } className="ppcp-r-settings-block__button">
<Header>
<Title>{ title }</Title>
<Description>{ description }</Description>
</Header>
<Action>
<Button
isBusy={ props.actionProps?.isBusy }
variant={ props.actionProps?.buttonType }
onClick={
props.actionProps?.callback
? () => props.actionProps.callback()
: undefined
}
>
{ props.actionProps.value }
</Button>
</Action>
</SettingsBlock>
);
export default ButtonSettingsBlock;

View file

@ -11,56 +11,43 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
}
return (
<>
<span className="ppcp-r-feature-item__notes">
{ notes.map( ( note, index ) => (
<span key={ index }>{ note }</span>
) ) }
</span>
</>
<span className="ppcp-r-feature-item__notes">
{ notes.map( ( note, index ) => (
<span key={ index }>{ note }</span>
) ) }
</span>
);
};
return (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__feature"
components={ [
() => (
<>
<Header>
<Title>
{ title }
{ props.actionProps?.featureStatus && (
<TitleBadge
{ ...props.actionProps?.badge }
/>
) }
</Title>
<Description className="ppcp-r-settings-block__feature__description">
{ description }
{ printNotes() }
</Description>
</Header>
<Action>
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map(
( button ) => (
<Button
href={ button.url }
key={ button.text }
variant={ button.type }
>
{ button.text }
</Button>
)
) }
</div>
</Action>
</>
),
] }
/>
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature">
<Header>
<Title>
{ title }
{ props.actionProps?.enabled && (
<TitleBadge { ...props.actionProps?.badge } />
) }
</Title>
<Description className="ppcp-r-settings-block__feature__description">
{ description }
{ printNotes() }
</Description>
</Header>
<Action>
<div className="ppcp-r-feature-item__buttons">
{ props.actionProps?.buttons.map( ( button ) => (
<Button
className={ button.class ? button.class : '' }
href={ button.url }
key={ button.text }
variant={ button.type }
>
{ button.text }
</Button>
) ) }
</div>
</Action>
</SettingsBlock>
);
};

View file

@ -42,28 +42,20 @@ const InputSettingsBlock = ( {
order = DEFAULT_ELEMENT_ORDER,
...props
} ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__input"
components={ [
() => (
<>
{ order.map( ( elementKey ) => {
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
return RenderElement ? (
<RenderElement
key={ elementKey }
title={ title }
description={ description }
supplementaryLabel={ supplementaryLabel }
actionProps={ props.actionProps }
/>
) : null;
} ) }
</>
),
] }
/>
<SettingsBlock { ...props } className="ppcp-r-settings-block__input">
{ order.map( ( elementKey ) => {
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
return RenderElement ? (
<RenderElement
key={ elementKey }
title={ title }
description={ description }
supplementaryLabel={ supplementaryLabel }
actionProps={ props.actionProps }
/>
) : null;
} ) }
</SettingsBlock>
);
export default InputSettingsBlock;

View file

@ -5,56 +5,43 @@ import PaymentMethodIcon from '../PaymentMethodIcon';
import data from '../../../utils/data';
const PaymentMethodItemBlock = ( props ) => {
const [ paymentMethodState, setPaymentMethodState ] = useState();
const [ toggleIsChecked, setToggleIsChecked ] = useState( false );
const [ modalIsVisible, setModalIsVisible ] = useState( false );
const Modal = props?.modal;
const handleCheckboxState = ( checked ) => {
setPaymentMethodState( checked ? props.id : null );
};
return (
<>
<SettingsBlock
className="ppcp-r-settings-block__payment-methods__item"
components={ [
() => (
<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>
<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>
<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={ props.id === paymentMethodState }
onChange={ handleCheckboxState }
/>
{ Modal && (
<div
className="ppcp-r-settings-block__payment-methods__item__settings"
onClick={ () =>
setModalIsVisible( true )
}
>
{ data().getImage(
'icon-settings.svg'
) }
</div>
) }
</div>
</div>
),
] }
/>
) }
</div>
</div>
</SettingsBlock>
{ Modal && modalIsVisible && (
<Modal setModalIsVisible={ setModalIsVisible } />
) }

View file

@ -1,7 +1,14 @@
import { useState, useCallback } from '@wordpress/element';
import SettingsBlock from './SettingsBlock';
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
const [ selectedMethod, setSelectedMethod ] = useState( null );
const handleSelect = useCallback( ( methodId, isSelected ) => {
setSelectedMethod( isSelected ? methodId : null );
}, [] );
if ( paymentMethods.length === 0 ) {
return null;
}
@ -9,19 +16,18 @@ const PaymentMethodsBlock = ( { paymentMethods, className = '' } ) => {
return (
<SettingsBlock
className={ `ppcp-r-settings-block__payment-methods ${ className }` }
components={ [
() => (
<>
{ paymentMethods.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
{ ...paymentMethod }
/>
) ) }
</>
),
] }
/>
>
{ paymentMethods.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
{ ...paymentMethod }
isSelected={ selectedMethod === paymentMethod.id }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}
/>
) ) }
</SettingsBlock>
);
};

View file

@ -11,37 +11,32 @@ const RadioSettingsBlock = ( {
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__radio ppcp-r-settings-block--expert-rdb"
components={ [
() => (
<>
<Header>
<Title>{ title }</Title>
<Description>{ description }</Description>
</Header>
{ options.map( ( option ) => (
<PayPalRdbWithContent
key={ option.id }
id={ option.id }
name={ props.actionProps?.name }
value={ option.value }
currentValue={ props.actionProps?.currentValue }
handleRdbState={ ( newValue ) =>
props.actionProps?.callback(
props.actionProps?.key,
newValue
)
}
label={ option.label }
description={ option.description }
toggleAdditionalContent={ true }
>
{ option.additionalContent }
</PayPalRdbWithContent>
) ) }
</>
),
] }
/>
>
<Header>
<Title>{ title }</Title>
<Description>{ description }</Description>
</Header>
{ options.map( ( option ) => (
<PayPalRdbWithContent
key={ option.id }
id={ option.id }
name={ props.actionProps?.name }
value={ option.value }
currentValue={ props.actionProps?.currentValue }
handleRdbState={ ( newValue ) =>
props.actionProps?.callback(
props.actionProps?.key,
newValue
)
}
label={ option.label }
description={ option.description }
toggleAdditionalContent={ true }
>
{ option.additionalContent }
</PayPalRdbWithContent>
) ) }
</SettingsBlock>
);
export default RadioSettingsBlock;

View file

@ -35,27 +35,19 @@ const SelectSettingsBlock = ( {
order = DEFAULT_ELEMENT_ORDER,
...props
} ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__select"
components={ [
() => (
<>
{ order.map( ( elementKey ) => {
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
return RenderElement ? (
<RenderElement
key={ elementKey }
title={ title }
description={ description }
actionProps={ props.actionProps }
/>
) : null;
} ) }
</>
),
] }
/>
<SettingsBlock { ...props } className="ppcp-r-settings-block__select">
{ order.map( ( elementKey ) => {
const RenderElement = ELEMENT_RENDERERS[ elementKey ];
return RenderElement ? (
<RenderElement
key={ elementKey }
title={ title }
description={ description }
actionProps={ props.actionProps }
/>
) : null;
} ) }
</SettingsBlock>
);
export default SelectSettingsBlock;

View file

@ -1,15 +1,9 @@
const SettingsBlock = ( { className, components = [] } ) => {
const SettingsBlock = ( { className, children } ) => {
const blockClassName = [ 'ppcp-r-settings-block', className ].filter(
Boolean
);
return (
<div className={ blockClassName.join( ' ' ) }>
{ components.map( ( Component, index ) => (
<Component key={ index } />
) ) }
</div>
);
return <div className={ blockClassName.join( ' ' ) }>{ children }</div>;
};
export default SettingsBlock;

View file

@ -3,35 +3,25 @@ import SettingsBlock from './SettingsBlock';
import { Header, Title, Action, Description } from './SettingsBlockElements';
const ToggleSettingsBlock = ( { title, description, ...props } ) => (
<SettingsBlock
{ ...props }
className="ppcp-r-settings-block__toggle"
components={ [
() => (
<Action>
<ToggleControl
className="ppcp-r-settings-block__toggle-control"
__nextHasNoMarginBottom={ true }
checked={ props.actionProps?.value }
onChange={ ( newValue ) =>
props.actionProps?.callback(
props.actionProps?.key,
newValue
)
}
/>
</Action>
),
() => (
<Header>
{ title && <Title>{ title }</Title> }
{ description && (
<Description>{ description }</Description>
) }
</Header>
),
] }
/>
<SettingsBlock { ...props } className="ppcp-r-settings-block__toggle">
<Action>
<ToggleControl
className="ppcp-r-settings-block__toggle-control"
__nextHasNoMarginBottom={ true }
checked={ props.actionProps?.value }
onChange={ ( newValue ) =>
props.actionProps?.callback(
props.actionProps?.key,
newValue
)
}
/>
</Action>
<Header>
{ title && <Title>{ title }</Title> }
{ description && <Description>{ description }</Description> }
</Header>
</SettingsBlock>
);
export default ToggleSettingsBlock;

View file

@ -1,23 +1,17 @@
import { ToggleControl } from '@wordpress/components';
import { useRef } from '@wordpress/element';
import SpinnerOverlay from './SpinnerOverlay';
const SettingsToggleBlock = ( {
isToggled,
setToggled,
isLoading = false,
disabled = false,
...props
} ) => {
const toggleRef = useRef( null );
const blockClasses = [ 'ppcp-r-toggle-block' ];
if ( isLoading ) {
blockClasses.push( 'ppcp--is-loading' );
}
const handleLabelClick = () => {
if ( ! toggleRef.current || isLoading ) {
if ( ! toggleRef.current || disabled ) {
return;
}
@ -52,13 +46,12 @@ const SettingsToggleBlock = ( {
ref={ toggleRef }
checked={ isToggled }
onChange={ ( newState ) => setToggled( newState ) }
disabled={ isLoading }
disabled={ disabled }
/>
</div>
</div>
{ props.children && isToggled && (
<div className="ppcp-r-toggle-block__toggled-content">
{ isLoading && <SpinnerOverlay /> }
{ props.children }
</div>
) }

View file

@ -1,8 +1,13 @@
import { Spinner } from '@wordpress/components';
const SpinnerOverlay = () => {
const SpinnerOverlay = ( { message = '' } ) => {
return (
<div className="ppcp-r-spinner-overlay">
{ message && (
<span className="ppcp-r-spinner-overlay__message">
{ message }
</span>
) }
<Spinner />
</div>
);

View file

@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { TabPanel } from '@wordpress/components';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
import { getQuery, updateQueryString } from '../../utils/navigation';
const TabNavigation = ( { tabs } ) => {
const { panel } = getQuery();
@ -30,7 +31,7 @@ const TabNavigation = ( { tabs } ) => {
);
useEffect( () => {
updateQueryString( { panel: activePanel }, '/', getQuery() );
updateQueryString( { panel: activePanel } );
}, [ activePanel ] );
return (

View file

@ -1,17 +1,11 @@
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import Separator from '../Separator';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
import PricingTitleBadge from '../PricingTitleBadge';
const AcdcFlow = ( {
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => {
if ( isFastlane && isPayLater && storeCountry === 'US' ) {
return (
<div className="ppcp-r-welcome-docs__wrapper">
@ -22,11 +16,7 @@ const AcdcFlow = ( {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -63,10 +53,13 @@ const AcdcFlow = ( {
imageBadge={ [
'icon-payment-method-paypal-small.svg',
] }
textBadge={
<PricingTitleBadge item="plater" />
}
description={ sprintf(
// translators: %s: Link to PayPal business fees guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://www.paypal.com/us/business/paypal-business-fees'
@ -116,7 +109,6 @@ const AcdcFlow = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>
@ -133,11 +125,7 @@ const AcdcFlow = ( {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -201,7 +189,6 @@ const AcdcFlow = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>
@ -217,11 +204,7 @@ const AcdcFlow = ( {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -256,7 +239,7 @@ const AcdcFlow = ( {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -280,7 +263,6 @@ const AcdcFlow = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>

View file

@ -1,11 +1,11 @@
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import { __, sprintf } from '@wordpress/i18n';
import Separator from '../Separator';
import generatePriceText from '../../../utils/badgeBoxUtils';
import { countryPriceInfo } from '../../../utils/countryPriceInfo';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox';
import Separator from '../Separator';
import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods';
import PricingTitleBadge from '../PricingTitleBadge';
const BcdcFlow = ( { isPayLater, storeCountry } ) => {
if ( isPayLater && storeCountry === 'US' ) {
return (
<div className="ppcp-r-welcome-docs__wrapper">
@ -16,11 +16,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
'woocommerce-paypal-payments'
) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -60,7 +56,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -110,7 +106,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
isFastlane={ false }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
</div>
@ -122,11 +117,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
<BadgeBox
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
titleType={ BADGE_BOX_TITLE_BIG }
textBadge={ generatePriceText(
'checkout',
countryPriceInfo[ storeCountry ],
storeCurrency
) }
textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
@ -158,7 +149,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
description={ sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'Offer installment payment options and get paid upfront - at no extra cost to you. <a target="_blank" href="%s">Learn more</a>',
'Offer installment payment options and get paid upfront. <a target="_blank" href="%s">Learn more</a>',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
@ -181,7 +172,6 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => {
isFastlane={ false }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
</div>
);

View file

@ -1,24 +1,10 @@
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import PricingDescription from '../PricingDescription';
import AcdcFlow from './AcdcFlow';
import BcdcFlow from './BcdcFlow';
import { Button } from '@wordpress/components';
const WelcomeDocs = ( {
useAcdc,
isFastlane,
isPayLater,
storeCountry,
storeCurrency,
} ) => {
const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => {
return (
<div className="ppcp-r-welcome-docs">
<h2 className="ppcp-r-welcome-docs__title">
@ -32,19 +18,14 @@ const WelcomeDocs = ( {
isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) : (
<BcdcFlow
isPayLater={ isPayLater }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
) }
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ { __html: pricesBasedDescription } }
></p>
<PricingDescription />
</div>
);
};

View file

@ -1,96 +1,118 @@
import { __, sprintf } from '@wordpress/i18n';
import { Button, TextControl } from '@wordpress/components';
import { useRef, useMemo } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
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 { openPopup } from '../../../../utils/window';
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'
),
};
const AdvancedOptionsForm = () => {
const [ clientValid, setClientValid ] = useState( false );
const [ secretValid, setSecretValid ] = useState( false );
const AdvancedOptionsForm = ( { setCompleted } ) => {
const { isBusy } = CommonHooks.useBusyState();
const { isSandboxMode, setSandboxMode, connectViaSandbox } =
CommonHooks.useSandbox();
const { isSandboxMode, setSandboxMode } = useSandboxConnection();
const {
handleConnectViaIdAndSecret,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
} = CommonHooks.useManualConnection();
} = useManualConnection();
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const refClientId = useRef( null );
const refClientSecret = useRef( null );
const isValidClientId = useMemo( () => {
return /^A[\w-]{79}$/.test( clientId );
}, [ clientId ] );
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,
},
];
const isFormValid = useMemo( () => {
return isValidClientId && clientId && clientSecret;
}, [ isValidClientId, clientId, clientSecret ] );
for ( const { ref, valid, errorMessage } of checks ) {
if ( valid() ) {
continue;
}
const handleServerError = ( res, genericMessage ) => {
console.error( 'Connection error', res );
createErrorNotice( res?.message ?? genericMessage );
};
const handleServerSuccess = () => {
createSuccessNotice(
__( 'Connected to PayPal', 'woocommerce-paypal-payments' )
);
setCompleted( true );
};
const handleSandboxConnect = async () => {
const res = await connectViaSandbox();
if ( ! res.success || ! res.data ) {
handleServerError(
res,
__(
'Could not generate a Sandbox login link.',
'woocommerce-paypal-payments'
)
);
return;
ref?.current?.focus();
throw new Error( errorMessage );
}
}, [ clientId, clientSecret, clientValid, secretValid ] );
const connectionUrl = res.data;
const popup = openPopup( connectionUrl );
const handleManualConnect = useCallback(
() =>
handleConnectViaIdAndSecret( {
validation: validateManualConnectionForm,
} ),
[ handleConnectViaIdAndSecret, validateManualConnectionForm ]
);
if ( ! popup ) {
createErrorNotice(
__(
'Popup blocked. Please allow popups for this site to connect to PayPal.',
'woocommerce-paypal-payments'
)
);
}
};
useEffect( () => {
setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) );
setSecretValid( clientSecret && clientSecret.length > 0 );
}, [ clientId, clientSecret ] );
const handleManualConnect = async () => {
const res = await connectViaIdAndSecret();
const clientIdLabel = useMemo(
() =>
isSandboxMode
? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' )
: __( 'Live Client ID', 'woocommerce-paypal-payments' ),
[ isSandboxMode ]
);
if ( res.success ) {
handleServerSuccess();
} else {
handleServerError(
res,
__(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments'
)
);
}
};
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
@ -103,88 +125,84 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
return (
<>
<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 }
isLoading={ isBusy }
>
<Button onClick={ handleSandboxConnect } variant="secondary">
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
<Separator withLine={ false } />
<SettingsToggleBlock
label={
__( 'Manually Connect', 'woocommerce-paypal-payments' ) +
( isBusy ? ' ...' : '' )
}
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
isLoading={ isBusy }
>
<DataStoreControl
control={ TextControl }
ref={ refClientId }
label={
isSandboxMode
? __(
'Sandbox Client ID',
'woocommerce-paypal-payments'
)
: __(
'Live Client ID',
'woocommerce-paypal-payments'
)
}
value={ clientId }
onChange={ setClientId }
className={
clientId && ! isValidClientId ? 'has-error' : ''
}
/>
{ clientId && ! isValidClientId && (
<p className="client-id-error">
{ __(
'Please enter a valid Client ID',
<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'
) }
</p>
) }
<DataStoreControl
control={ TextControl }
ref={ refClientSecret }
label={
isSandboxMode
? __(
'Sandbox Secret Key',
'woocommerce-paypal-payments'
)
: __(
'Live Secret Key',
'woocommerce-paypal-payments'
)
}
value={ clientSecret }
onChange={ setClientSecret }
type="password"
/>
<Button
variant="secondary"
onClick={ handleManualConnect }
disabled={ ! isFormValid }
showIcon={ false }
variant="secondary"
className="small-button"
isSandbox={
true /* This button always connects to sandbox */
}
/>
</SettingsToggleBlock>
</BusyStateWrapper>
<Separator withLine={ false } />
<BusyStateWrapper
onBusy={ ( props ) => ( {
disabled: true,
label: props.label + ' ...',
} ) }
>
<SettingsToggleBlock
label={ __(
'Manually Connect',
'woocommerce-paypal-payments'
) }
description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode }
>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button>
</SettingsToggleBlock>
<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>
</>
);
};

View file

@ -0,0 +1,49 @@
import { Button } from '@wordpress/components';
import classNames from 'classnames';
import { CommonHooks } from '../../../../data';
import { openSignup } from '../../../ReusableComponents/Icons';
import {
useProductionConnection,
useSandboxConnection,
} from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const ConnectionButton = ( {
title,
isSandbox = false,
variant = 'primary',
showIcon = true,
className = '',
} ) => {
const { handleSandboxConnect } = useSandboxConnection();
const { handleProductionConnect } = useProductionConnection();
const buttonClassName = classNames( 'ppcp-r-connection-button', className, {
'sandbox-mode': isSandbox,
'live-mode': ! isSandbox,
} );
const handleConnectClick = async () => {
if ( isSandbox ) {
await handleSandboxConnect();
} else {
await handleProductionConnect();
}
};
return (
<BusyStateWrapper>
<Button
className={ buttonClassName }
variant={ variant }
icon={ showIcon ? openSignup : null }
onClick={ handleConnectClick }
>
<span className="button-title">{ title }</span>
</Button>
</BusyStateWrapper>
);
};
export default ConnectionButton;

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { OnboardingHooks } from '../../../../data';
import useIsScrolled from '../../../../hooks/useIsScrolled';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const { title, isFirst, percentage, showNext, canProceed } = stepDetails;
@ -20,7 +21,11 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
return (
<div className={ className }>
<div className="ppcp-r-navigation">
<div className="ppcp-r-navigation--left">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! isFirst }
>
<Button
variant="link"
onClick={ isFirst ? onExit : onPrev }
@ -31,7 +36,7 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
{ title }
</span>
</Button>
</div>
</BusyStateWrapper>
{ ! isFirst &&
NextButton( { showNext, isDisabled, onNext, onExit } ) }
<ProgressBar percent={ percentage } />
@ -42,7 +47,10 @@ const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => {
const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
return (
<div className="ppcp-r-navigation--right">
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
<Button variant="link" onClick={ onExit }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button>
@ -55,7 +63,7 @@ const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => {
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
) }
</div>
</BusyStateWrapper>
);
};

View file

@ -5,8 +5,7 @@ import { getSteps, getCurrentStep } from './availableSteps';
import Navigation from './Components/Navigation';
const Onboarding = () => {
const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
const { step, setStep, flags } = OnboardingHooks.useSteps();
const Steps = getSteps( flags );
const currentStep = getCurrentStep( step, Steps );
@ -30,7 +29,6 @@ const Onboarding = () => {
<currentStep.StepComponent
setStep={ setStep }
currentStep={ step }
setCompleted={ setCompleted }
stepperOrder={ Steps }
/>
</div>

View file

@ -1,28 +1,9 @@
import { __ } from '@wordpress/i18n';
import { Button, Icon } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import ConnectionButton from './Components/ConnectionButton';
const StepCompleteSetup = ( { setCompleted } ) => {
const ButtonIcon = () => (
<Icon
icon={ () => (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.4999 12.75V18.75C12.4999 18.9489 12.4209 19.1397 12.2803 19.2803C12.1396 19.421 11.9488 19.5 11.7499 19.5C11.551 19.5 11.3603 19.421 11.2196 19.2803C11.0789 19.1397 10.9999 18.9489 10.9999 18.75V14.5613L4.78055 20.7806C4.71087 20.8503 4.62815 20.9056 4.5371 20.9433C4.44606 20.981 4.34847 21.0004 4.24993 21.0004C4.15138 21.0004 4.0538 20.981 3.96276 20.9433C3.87171 20.9056 3.78899 20.8503 3.7193 20.7806C3.64962 20.7109 3.59435 20.6282 3.55663 20.5372C3.51892 20.4461 3.49951 20.3485 3.49951 20.25C3.49951 20.1515 3.51892 20.0539 3.55663 19.9628C3.59435 19.8718 3.64962 19.7891 3.7193 19.7194L9.93868 13.5H5.74993C5.55102 13.5 5.36025 13.421 5.2196 13.2803C5.07895 13.1397 4.99993 12.9489 4.99993 12.75C4.99993 12.5511 5.07895 12.3603 5.2196 12.2197C5.36025 12.079 5.55102 12 5.74993 12H11.7499C11.9488 12 12.1396 12.079 12.2803 12.2197C12.4209 12.3603 12.4999 12.5511 12.4999 12.75ZM19.9999 3H7.99993C7.6021 3 7.22057 3.15804 6.93927 3.43934C6.65796 3.72064 6.49993 4.10218 6.49993 4.5V9C6.49993 9.19891 6.57895 9.38968 6.7196 9.53033C6.86025 9.67098 7.05102 9.75 7.24993 9.75C7.44884 9.75 7.63961 9.67098 7.78026 9.53033C7.92091 9.38968 7.99993 9.19891 7.99993 9V4.5H19.9999V16.5H15.4999C15.301 16.5 15.1103 16.579 14.9696 16.7197C14.8289 16.8603 14.7499 17.0511 14.7499 17.25C14.7499 17.4489 14.8289 17.6397 14.9696 17.7803C15.1103 17.921 15.301 18 15.4999 18H19.9999C20.3978 18 20.7793 17.842 21.0606 17.5607C21.3419 17.2794 21.4999 16.8978 21.4999 16.5V4.5C21.4999 4.10218 21.3419 3.72064 21.0606 3.43934C20.7793 3.15804 20.3978 3 19.9999 3Z"
fill="white"
/>
</svg>
) }
/>
);
const StepCompleteSetup = () => {
return (
<div className="ppcp-r-page-products">
<OnboardingHeader
@ -37,18 +18,12 @@ const StepCompleteSetup = ( { setCompleted } ) => {
/>
<div className="ppcp-r-inner-container">
<div className="ppcp-r-onboarding-header__description">
<Button
variant="primary"
icon={ ButtonIcon }
onClick={ () => {
setCompleted( true );
} }
>
{ __(
<ConnectionButton
title={ __(
'Connect to PayPal',
'woocommerce-paypal-payments'
) }
</Button>
/>
</div>
</div>
</div>

View file

@ -1,10 +1,11 @@
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { CommonHooks, OnboardingHooks } from '../../../data';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox';
import { CommonHooks, OnboardingHooks } from '../../../data';
import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods';
import PricingDescription from '../../ReusableComponents/PricingDescription';
const OPM_RADIO_GROUP_NAME = 'optional-payment-methods';
@ -16,15 +17,6 @@ const StepPaymentMethods = ( {} ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const pricesBasedDescription = sprintf(
// translators: %s: Link to PayPal REST application guide
__(
'<sup>1</sup>Prices based on domestic transactions as of October 25th, 2024. <a target="_blank" href="%s">Click here</a> for full pricing details.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input '
);
return (
<div className="ppcp-r-page-optional-payment-methods">
<OnboardingHeader
@ -67,12 +59,7 @@ const StepPaymentMethods = ( {} ) => {
type="radio"
></SelectBox>
</SelectBoxWrapper>
<p
className="ppcp-r-optional-payment-methods__description"
dangerouslySetInnerHTML={ {
__html: pricesBasedDescription,
} }
></p>
<PricingDescription />
</div>
</div>
);

View file

@ -9,9 +9,11 @@ import AccordionSection from '../../ReusableComponents/AccordionSection';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
import { CommonHooks } from '../../../data';
import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper';
const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry } = CommonHooks.useWooSettings();
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
return (
<div className="ppcp-r-page-welcome">
<OnboardingHeader
@ -33,16 +35,18 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
'woocommerce-paypal-payments'
) }
</p>
<Button
className="ppcp-r-button-activate-paypal"
variant="primary"
onClick={ () => setStep( currentStep + 1 ) }
>
{ __(
'Activate PayPal Payments',
'woocommerce-paypal-payments'
) }
</Button>
<BusyStateWrapper>
<Button
className="ppcp-r-button-activate-paypal"
variant="primary"
onClick={ () => setStep( currentStep + 1 ) }
>
{ __(
'Activate PayPal Payments',
'woocommerce-paypal-payments'
) }
</Button>
</BusyStateWrapper>
</div>
<Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs
@ -50,7 +54,6 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
isFastlane={ true }
isPayLater={ true }
storeCountry={ storeCountry }
storeCurrency={ storeCurrency }
/>
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />
<AccordionSection
@ -61,7 +64,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
className="onboarding-advanced-options"
id="advanced-options"
>
<AdvancedOptionsForm setCompleted={ setCompleted } />
<AdvancedOptionsForm />
</AccordionSection>
</div>
);

View file

@ -1,15 +1,49 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { reusableBlock } from '@wordpress/icons';
import SettingsCard from '../../ReusableComponents/SettingsCard';
import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock';
import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock';
import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge';
import data from '../../../utils/data';
import { useMerchantInfo } from '../../../data/common/hooks';
import { STORE_NAME } from '../../../data/common';
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 features = featuresDefault.map( ( feature ) => {
const merchantFeature = merchant?.features?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
const refreshHandler = async () => {
setIsRefreshing( true );
const result = await refreshFeatureStatuses();
// TODO: Implement the refresh logic, remove this debug code -- PCP-4024
if ( result && ! result.success ) {
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
console.log( 'Features refreshed successfully.' );
}
setIsRefreshing( false );
};
return (
<div className="ppcp-r-tab-overview">
@ -38,39 +72,118 @@ const TabOverview = () => {
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<div>
<p>{ __( 'Enable additional features…' ) }</p>
<p>{ __( 'Click Refresh…' ) }</p>
<Button variant="tertiary">
{ data().getImage( 'icon-refresh.svg' ) }
{ __( 'Refresh', 'woocommerce-paypal-payments' ) }
<>
<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>
</div>
</>
}
contentItems={ featuresDefault.map( ( feature ) => (
contentItems={ features.map( ( feature ) => (
<FeatureSettingsBlock
key={ feature.id }
title={ feature.title }
description={ feature.description }
actionProps={ {
buttons: feature.buttons,
featureStatus: feature.featureStatus,
enabled: feature.enabled,
notes: feature.notes,
badge: {
text: __(
'Active',
'woocommerce-paypal-payments'
),
type: TITLE_BADGE_POSITIVE,
},
badge: feature.enabled
? {
text: __(
'Active',
'woocommerce-paypal-payments'
),
type: TITLE_BADGE_POSITIVE,
}
: undefined,
} }
/>
) ) }
/>
<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'
) }
contentItems={ [
<FeatureSettingsBlock
key="documentation"
title={ __(
'Documentation',
'woocommerce-paypal-payments'
) }
description={ __(
'Find detailed guides and resources to help you set up, manage, and optimize your PayPal integration.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttons: [
{
type: 'tertiary',
text: __(
'View full documentation',
'woocommerce-paypal-payments'
),
url: '#',
},
],
} }
/>,
<FeatureSettingsBlock
key="support"
title={ __( 'Support', 'woocommerce-paypal-payments' ) }
description={ __(
'Need help? Access troubleshooting tips or contact our support team for personalized assistance.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttons: [
{
type: 'tertiary',
text: __(
'View support options',
'woocommerce-paypal-payments'
),
url: '#',
},
],
} }
/>,
] }
/>
</div>
);
};
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
const todosDataDefault = [
{
value: 'paypal_later_messaging',
@ -106,6 +219,7 @@ const todosDataDefault = [
},
];
// 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',
@ -117,6 +231,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -133,7 +248,6 @@ const featuresDefault = [
'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments'
),
featureStatus: true,
description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments'
@ -141,6 +255,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -164,6 +279,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Apply', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -181,10 +297,10 @@ const featuresDefault = [
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
featureStatus: true,
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -194,9 +310,6 @@ const featuresDefault = [
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
{
id: 'apple_pay',
@ -208,6 +321,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
@ -231,6 +345,7 @@ const featuresDefault = [
buttons: [
{
type: 'secondary',
class: 'small-button',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
url: '#',
},
@ -240,6 +355,9 @@ const featuresDefault = [
url: '#',
},
],
notes: [
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
],
},
];

View file

@ -9,55 +9,40 @@ import {
const OrderIntent = ( { updateFormValue, settings } ) => {
return (
<SettingsBlock
components={ [
() => (
<>
<Header>
<Title>
{ __(
'Order Intent',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
'Choose between immediate capture or authorization-only, with manual capture in the Order section.',
'woocommerce-paypal-payments'
) }
</Description>
</Header>
</>
),
() => (
<>
<ToggleSettingsBlock
title={ __(
'Authorize Only',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'authorizeOnly',
value: settings.authorizeOnly,
} }
/>
<SettingsBlock>
<Header>
<Title>
{ __( 'Order Intent', 'woocommerce-paypal-payments' ) }
</Title>
<Description>
{ __(
'Choose between immediate capture or authorization-only, with manual capture in the Order section.',
'woocommerce-paypal-payments'
) }
</Description>
</Header>
<ToggleSettingsBlock
title={ __(
'Capture Virtual-Only Orders',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'captureVirtualOnlyOrders',
value: settings.captureVirtualOnlyOrders,
} }
/>
</>
),
] }
/>
<ToggleSettingsBlock
title={ __( 'Authorize Only', 'woocommerce-paypal-payments' ) }
actionProps={ {
callback: updateFormValue,
key: 'authorizeOnly',
value: settings.authorizeOnly,
} }
/>
<ToggleSettingsBlock
title={ __(
'Capture Virtual-Only Orders',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'captureVirtualOnlyOrders',
value: settings.captureVirtualOnlyOrders,
} }
/>
</SettingsBlock>
);
};

View file

@ -1,82 +1,73 @@
import { __, sprintf } from '@wordpress/i18n';
import {
Header,
SettingsBlock,
ToggleSettingsBlock,
Title,
Description,
} from '../../../../ReusableComponents/SettingsBlocks';
import { Header } from '../../../../ReusableComponents/SettingsBlocks/SettingsBlockElements';
const SavePaymentMethods = ( { updateFormValue, settings } ) => {
return (
<SettingsBlock
className="ppcp-r-settings-block--save-payment-methods"
components={ [
() => (
<>
<Header>
<Title>
{ __(
'Save payment methods',
<SettingsBlock className="ppcp-r-settings-block--save-payment-methods">
<Header>
<Title>
{ __(
'Save payment methods',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
"Securely store customers' payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.",
'woocommerce-paypal-payments'
) }
</Description>
</Header>
<ToggleSettingsBlock
title={ __(
'Save PayPal and Venmo',
'woocommerce-paypal-payments'
) }
description={
<div
dangerouslySetInnerHTML={ {
__html: sprintf(
/* translators: 1: URL to Pay Later documentation, 2: URL to Alternative Payment Methods documentation */
__(
'Securely store your customers\' PayPal accounts for a seamless checkout experience. <br />This will disable all <a target="_blank" rel="noreferrer" href="%1$s">Pay Later</a> features and <a target="_blank" rel="noreferrer" href="%2$s">Alternative Payment Methods</a> on your site.',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
'Securely store customers payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.',
'woocommerce-paypal-payments'
) }
</Description>
</Header>
</>
),
() => (
<ToggleSettingsBlock
title={ __(
'Save PayPal and Venmo',
'woocommerce-paypal-payments'
) }
description={
<div
dangerouslySetInnerHTML={ {
__html: sprintf(
/* translators: 1: URL to Pay Later documentation, 2: URL to Alternative Payment Methods documentation */
__(
'Securely store your customers\' PayPal accounts for a seamless checkout experience. <br />This will disable all <a target="_blank" rel="noreferrer" href="%1$s">Pay Later</a> features and <a target="_blank" rel="noreferrer" href="%2$s">Alternative Payment Methods</a> on your site.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later',
'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
),
} }
/>
}
actionProps={ {
value: settings.savePaypalAndVenmo,
callback: updateFormValue,
key: 'savePaypalAndVenmo',
),
'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later',
'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
),
} }
/>
),
() => (
<ToggleSettingsBlock
title={ __(
'Save Credit and Debit Cards',
'woocommerce-paypal-payments'
) }
description={ __(
"Securely store your customer's credit card.",
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'saveCreditCardAndDebitCard',
value: settings.saveCreditCardAndDebitCard,
} }
/>
),
] }
/>
}
actionProps={ {
value: settings.savePaypalAndVenmo,
callback: updateFormValue,
key: 'savePaypalAndVenmo',
} }
/>
<ToggleSettingsBlock
title={ __(
'Save Credit and Debit Cards',
'woocommerce-paypal-payments'
) }
description={ __(
"Securely store your customer's credit card.",
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'saveCreditCardAndDebitCard',
value: settings.saveCreditCardAndDebitCard,
} }
/>
</SettingsBlock>
);
};

View file

@ -1,174 +0,0 @@
import { __ } from '@wordpress/i18n';
import {
Header,
Title,
Description,
AccordionSettingsBlock,
ToggleSettingsBlock,
ButtonSettingsBlock,
} from '../../../../ReusableComponents/SettingsBlocks';
import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
const Troubleshooting = ( { updateFormValue, settings } ) => {
return (
<AccordionSettingsBlock
className="ppcp-r-settings-block--troubleshooting"
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
description={ __(
'Access tools to help debug and resolve issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
} }
>
<ToggleSettingsBlock
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
description={ __(
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'logging',
value: settings.logging,
} }
/>
<SettingsBlock
components={ [
() => (
<>
<Header>
<Title>
{ __(
'Subscribed PayPal webhooks',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
'woocommerce-paypal-payments'
) }{ ' ' }
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
{ __(
'Webhook Status documentation',
'woocommerce-paypal-payments'
) }
</a>
.
</Description>
</Header>
<HooksTable data={ hooksExampleData() } />
</>
),
] }
/>
<ButtonSettingsBlock
title={ __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
) }
description={ __(
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
callback: () =>
console.log(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
),
value: __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
),
} }
/>
<ButtonSettingsBlock
title={ __(
'Simulate webhooks',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
callback: () =>
console.log(
'Simulate webhooks',
'woocommerce-paypal-payments'
),
value: __(
'Simulate webhooks',
'woocommerce-paypal-payments'
),
} }
/>
</AccordionSettingsBlock>
);
};
const hooksExampleData = () => {
return {
url: 'https://www.rt3.tech/wordpress/paypal-ux-testin/index.php?rest_route=/paypal/v1/incoming',
hooks: [
'billing plan pricing-change activated',
'billing plan updated',
'billing subscription cancelled',
'catalog product updated',
'checkout order approved',
'checkout order completed',
'checkout payment-approval reversed',
'payment authorization voided',
'payment capture completed',
'payment capture denied',
'payment capture pending',
'payment capture refunded',
'payment capture reversed',
'payment order cancelled',
'payment sale completed',
'payment sale refunded',
'vault payment-token created',
'vault payment-token deleted',
],
};
};
const HooksTable = ( { data } ) => {
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">{ data?.url }</td>
<td className="ppcp-r-table__hooks-events">
{ data.hooks.map( ( hook, index ) => (
<span key={ hook }>
{ hook }{ ' ' }
{ index !== data.hooks.length - 1 && ',' }
</span>
) ) }
</td>
</tr>
</tbody>
</table>
);
};
export default Troubleshooting;

View file

@ -0,0 +1,37 @@
import { __ } from '@wordpress/i18n';
import { CommonHooks } from '../../../../../../data';
const HooksTableBlock = () => {
const { webhooks } = CommonHooks.useWebhooks();
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>
);
};
export default HooksTableBlock;

View file

@ -0,0 +1,72 @@
import { useState } from '@wordpress/element';
import { STORE_NAME } from '../../../../../../data/common';
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import { __ } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../../../ReusableComponents/Icons';
const ResubscribeBlock = () => {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const [ resubscribing, setResubscribing ] = useState( false );
const { resubscribeWebhooks } = useDispatch( STORE_NAME );
const startResubscribingWebhooks = async () => {
setResubscribing( true );
try {
await resubscribeWebhooks();
} catch ( error ) {
setResubscribing( false );
createErrorNotice(
__(
'Operation failed. Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_ERROR,
}
);
return;
}
setResubscribing( false );
createSuccessNotice(
__(
'Webhooks were successfully re-subscribed.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
};
return (
<ButtonSettingsBlock
title={ __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
) }
description={ __(
'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
isBusy: resubscribing,
callback: () => startResubscribingWebhooks(),
value: __(
'Resubscribe webhooks',
'woocommerce-paypal-payments'
),
} }
/>
);
};
export default ResubscribeBlock;

View file

@ -0,0 +1,129 @@
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { ButtonSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks } from '../../../../../../data';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../../../ReusableComponents/Icons';
const SimulationBlock = () => {
const {
createSuccessNotice,
createInfoNotice,
createErrorNotice,
removeNotice,
} = useDispatch( noticesStore );
const { startWebhookSimulation, checkWebhookSimulationState } =
CommonHooks.useWebhooks();
const [ simulating, setSimulating ] = useState( false );
const sleep = ( ms ) => {
return new Promise( ( resolve ) => setTimeout( resolve, ms ) );
};
const startSimulation = async ( maxRetries ) => {
const webhookInfoNoticeId = 'paypal-webhook-simulation-info-notice';
const triggerWebhookInfoNotice = () => {
createInfoNotice(
__(
'Waiting for the webhook to arrive…',
'woocommerce-paypal-payments'
),
{
id: webhookInfoNoticeId,
}
);
};
const stopSimulation = () => {
removeNotice( webhookInfoNoticeId );
setSimulating( false );
};
setSimulating( true );
triggerWebhookInfoNotice();
try {
await startWebhookSimulation();
} catch ( error ) {
console.error( error );
setSimulating( false );
createErrorNotice(
__(
'Operation failed. Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_ERROR,
}
);
return;
}
for ( let i = 0; i < maxRetries; i++ ) {
await sleep( 2000 );
const simulationStateResponse = await checkWebhookSimulationState();
try {
if ( ! simulationStateResponse.success ) {
console.error(
'Simulation state query failed: ' +
simulationStateResponse?.data
);
continue;
}
if ( simulationStateResponse?.data?.state === 'received' ) {
createSuccessNotice(
__(
'The webhook was received successfully.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
stopSimulation();
return;
}
removeNotice( webhookInfoNoticeId );
triggerWebhookInfoNotice();
} catch ( error ) {
console.error( error );
}
}
stopSimulation();
createErrorNotice(
__(
'Looks like the webhook cannot be received. Check that your website is accessible from the internet.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_ERROR,
}
);
};
return (
<>
<ButtonSettingsBlock
title={ __(
'Simulate webhooks',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttonType: 'secondary',
isBusy: simulating,
callback: () => startSimulation( 30 ),
value: __(
'Simulate webhooks',
'woocommerce-paypal-payments'
),
} }
/>
</>
);
};
export default SimulationBlock;

View file

@ -0,0 +1,72 @@
import { __ } from '@wordpress/i18n';
import {
AccordionSettingsBlock,
Description,
Header,
Title,
ToggleSettingsBlock,
} from '../../../../../ReusableComponents/SettingsBlocks';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
import SimulationBlock from './SimulationBlock';
import ResubscribeBlock from './ResubscribeBlock';
import HooksTableBlock from './HooksTableBlock';
const Troubleshooting = ( { updateFormValue, settings } ) => {
return (
<AccordionSettingsBlock
className="ppcp-r-settings-block--troubleshooting"
title={ __( 'Troubleshooting', 'woocommerce-paypal-payments' ) }
description={ __(
'Access tools to help debug and resolve issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'payNowExperience',
value: settings.payNowExperience,
} }
>
<ToggleSettingsBlock
title={ __( 'Logging', 'woocommerce-paypal-payments' ) }
description={ __(
'Log additional debugging information in the WooCommerce logs that can assist technical staff to determine issues.',
'woocommerce-paypal-payments'
) }
actionProps={ {
callback: updateFormValue,
key: 'logging',
value: settings.logging,
} }
/>
<SettingsBlock>
<Header>
<Title>
{ __(
'Subscribed PayPal webhooks',
'woocommerce-paypal-payments'
) }
</Title>
<Description>
{ __(
'The following PayPal webhooks are subscribed. More information about the webhooks is available in the',
'woocommerce-paypal-payments'
) }{ ' ' }
<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#webhook-status">
{ __(
'Webhook Status documentation',
'woocommerce-paypal-payments'
) }
</a>
.
</Description>
</Header>
<HooksTableBlock />
<ResubscribeBlock />
<SimulationBlock />
</SettingsBlock>
</AccordionSettingsBlock>
);
};
export default Troubleshooting;

View file

@ -5,7 +5,7 @@ import {
ContentWrapper,
} from '../../../ReusableComponents/SettingsBlocks';
import Sandbox from './Blocks/Sandbox';
import Troubleshooting from './Blocks/Troubleshooting';
import Troubleshooting from './Blocks/Troubleshooting/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';

View file

@ -90,7 +90,7 @@ const TabStyling = () => {
return (
<div className="ppcp-r-styling">
<div className="ppcp-r-styling__settings">
<SectionIntro />
<SectionIntro location={ location } />
<SectionLocations
locationOptions={ locationOptions }
location={ location }
@ -157,20 +157,17 @@ const TabStylingSection = ( props ) => {
);
};
const SectionIntro = () => {
const buttonStyleDescription = sprintf(
// translators: %s: Link to Classic checkout page
__(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a>. Checkout Buttons must be enabled to display the PayPal gateway on the Checkout page.'
),
'#'
);
const SectionIntro = ( { location } ) => {
const { description, descriptionLink } =
defaultLocationSettings[ location ];
const buttonStyleDescription = sprintf( description, descriptionLink );
return (
<TabStylingSection
className="ppcp-r-styling__section--rc ppcp-r-styling__section--empty"
title={ __( 'Button Styling', 'wooocommerce-paypal-payments' ) }
description={ buttonStyleDescription }
></TabStylingSection>
/>
);
};
@ -321,6 +318,7 @@ const SectionButtonPreview = ( { locationSettings } ) => {
clientId: 'test',
merchantId: 'QTQX5NP6N9WZU',
components: 'buttons,googlepay',
'disable-funding': 'card',
'buyer-country': 'US',
currency: 'USD',
} }

View file

@ -1,20 +1,51 @@
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { OnboardingHooks } from '../../data';
import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay';
import Onboarding from './Onboarding/Onboarding';
import SettingsScreen from './SettingsScreen';
const Settings = () => {
const onboardingProgress = OnboardingHooks.useSteps();
if ( ! onboardingProgress.isReady ) {
// TODO: Use better loading state indicator.
return <div>Loading...</div>;
}
// Disable the "Changes you made might not be saved" browser warning.
useEffect( () => {
const suppressBeforeUnload = ( event ) => {
event.stopImmediatePropagation();
return undefined;
};
if ( ! onboardingProgress.completed ) {
return <Onboarding />;
}
window.addEventListener( 'beforeunload', suppressBeforeUnload );
return <SettingsScreen />;
return () => {
window.removeEventListener( 'beforeunload', suppressBeforeUnload );
};
}, [] );
const wrapperClass = classNames( 'ppcp-r-app', {
loading: ! onboardingProgress.isReady,
} );
const Content = useMemo( () => {
if ( ! onboardingProgress.isReady ) {
return (
<SpinnerOverlay
message={ __( 'Loading…', 'woocommerce-paypal-payments' ) }
/>
);
}
if ( ! onboardingProgress.completed ) {
return <Onboarding />;
}
return <SettingsScreen />;
}, [ onboardingProgress ] );
return <div className={ wrapperClass }>{ Content }</div>;
};
export default Settings;