Merge remote-tracking branch 'origin/trunk' into PCP-3930-Make-the-webhook-resubscribe/simulate-logic-usable-in-React-application

# Conflicts:
#	modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ButtonSettingsBlock.js
#	modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js
#	modules/ppcp-settings/resources/js/data/common/action-types.js
#	modules/ppcp-settings/resources/js/data/common/constants.js
#	modules/ppcp-settings/src/SettingsModule.php
This commit is contained in:
inpsyde-maticluznar 2024-12-18 07:05:09 +01:00
commit 40b1b0d280
No known key found for this signature in database
GPG key ID: D005973F231309F6
31 changed files with 747 additions and 1386 deletions

View file

@ -182,6 +182,26 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
2
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$product_status = $c->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus );
$apple_pay_enabled = $product_status->is_active();
$merchant_data['features']['apple_pay'] = array(
'enabled' => $apple_pay_enabled,
);
return $merchant_data;
}
);
return true;
}

View file

@ -232,6 +232,26 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
2
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function ( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$product_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus );
$google_pay_enabled = $product_status->is_active();
$merchant_data['features']['google_pay'] = array(
'enabled' => $google_pay_enabled,
);
return $merchant_data;
}
);
return true;
}
}

View file

@ -7,14 +7,13 @@
"build": "wp-scripts build --webpack-src-dir=resources/js --output-path=assets"
},
"devDependencies": {
"@woocommerce/navigation": "~8.1.0",
"@wordpress/data": "^10.10.0",
"@wordpress/data-controls": "^4.10.0",
"@wordpress/scripts": "^30.3.0"
"@wordpress/scripts": "^30.3.0",
"classnames": "^2.5.1"
},
"dependencies": {
"@paypal/react-paypal-js": "^8.7.0",
"@woocommerce/settings": "^1.0.0",
"react-select": "^5.8.3"
}
}

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

@ -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

@ -3,33 +3,25 @@ import SettingsBlock from './SettingsBlock';
import { Header, Title, Action, Description } 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
isBusy={ props.actionProps?.isBusy }
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,42 @@ 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
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,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,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">
@ -39,30 +73,54 @@ const TabOverview = () => {
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…',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh…',
'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,
} }
/>
) ) }
@ -71,6 +129,7 @@ const TabOverview = () => {
);
};
// TODO: This list should be refactored into a separate module, maybe utils/thingsToDoNext.js
const todosDataDefault = [
{
value: 'paypal_later_messaging',
@ -106,6 +165,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',
@ -133,7 +193,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'
@ -181,7 +240,6 @@ const featuresDefault = [
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
featureStatus: true,
buttons: [
{
type: 'secondary',

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

@ -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

@ -23,5 +23,6 @@ export default {
DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES',
DO_WEBHOOKS_DATA: 'COMMON:DO_WEBHOOKS_DATA',
};

View file

@ -7,7 +7,7 @@
* @file
*/
import { select } from '@wordpress/data';
import { dispatch, select } from '@wordpress/data';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
@ -192,5 +192,30 @@ export const connectViaIdAndSecret = function* () {
* @return {Action} The action.
*/
export const refreshMerchantData = function* () {
return yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
if ( result.success && result.merchant ) {
yield hydrate( result );
}
return result;
};
/**
* Side effect.
* Purges all feature status data via a REST request.
* Refreshes the merchant data via a REST request.
*
* @return {Action} The action.
*/
export const refreshFeatureStatuses = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES };
if ( result && result.success ) {
// TODO: Review if we can get the updated feature details in the result.data instead of
// doing a second refreshMerchantData() request.
yield refreshMerchantData();
}
return result;
};

View file

@ -55,3 +55,14 @@ export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link';
export const REST_WEBHOOKS = '/wc/v3/wc_paypal/webhook_settings';
export const REST_WEBHOOKS_SIMULATE = '/wc/v3/wc_paypal/webhooks_simulate';
/**
* REST path to refresh the feature status.
*
* Used by: Controls
* See: RefreshFeatureStatusEndpoint.php
*
* @type {string}
*/
export const REST_REFRESH_FEATURES_PATH =
'/wc/v3/wc_paypal/refresh-feature-status';

View file

@ -7,22 +7,21 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import {
STORE_NAME,
REST_PERSIST_PATH,
REST_MANUAL_CONNECTION_PATH,
REST_CONNECTION_URL_PATH,
REST_HYDRATE_MERCHANT_PATH,
REST_REFRESH_FEATURES_PATH,
} from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
try {
return await apiFetch( {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
@ -33,10 +32,8 @@ export const controls = {
},
async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
let result = null;
try {
result = await apiFetch( {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
@ -45,20 +42,16 @@ export const controls = {
},
} );
} catch ( e ) {
result = {
return {
success: false,
error: e,
};
}
return result;
},
async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) {
let result = null;
try {
result = await apiFetch( {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
@ -67,13 +60,11 @@ export const controls = {
},
} );
} catch ( e ) {
result = {
return {
success: false,
error: e,
};
}
return result;
},
async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
@ -81,10 +72,8 @@ export const controls = {
clientSecret,
useSandbox,
} ) {
let result = null;
try {
result = await apiFetch( {
return await apiFetch( {
path: REST_MANUAL_CONNECTION_PATH,
method: 'POST',
data: {
@ -94,31 +83,36 @@ export const controls = {
},
} );
} catch ( e ) {
result = {
return {
success: false,
error: e,
};
}
return result;
},
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
let result = null;
try {
result = await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );
if ( result.success && result.merchant ) {
await dispatch( STORE_NAME ).hydrate( result );
}
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );
} catch ( e ) {
result = {
return {
success: false,
error: e,
};
}
},
return result;
async [ ACTION_TYPES.DO_REFRESH_FEATURES ]() {
try {
return await apiFetch( {
path: REST_REFRESH_FEATURES_PATH,
method: 'POST',
} );
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
},
};

View file

@ -25,26 +25,56 @@ export const defaultLocationSettings = {
value: 'cart',
label: __( 'Cart', 'woocommerce-paypal-payments' ),
settings: { ...cartAndExpressCheckoutSettings },
// translators: %s: Link to Cart page
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Cart page</a> and select which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'classic-checkout': {
value: 'classic-checkout',
label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ),
settings: { ...settings },
// translators: %s: Link to Classic Checkout page
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'express-checkout': {
value: 'express-checkout',
label: __( 'Express Checkout', 'woocommerce-paypal-payments' ),
settings: { ...cartAndExpressCheckoutSettings },
// translators: %s: Link to Express Checkout location
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Express Checkout location</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'mini-cart': {
value: 'mini-cart',
label: __( 'Mini Cart', 'woocommerce-paypel-payements' ),
settings: { ...settings },
// translators: %s: Link to Mini Cart
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Mini Cart</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
'product-page': {
value: 'product-page',
label: __( 'Product Page', 'woocommerce-paypal-payments' ),
settings: { ...settings },
// translators: %s: Link to Product Page
description: __(
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Product Page</a> and choose which additional payment buttons to display in this location.',
'wooocommerce-paypal-payments'
),
descriptionLink: '#',
},
};
@ -57,10 +87,6 @@ export const paymentMethodOptions = [
value: 'paylater',
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
},
{
value: 'card',
label: __( 'Debit or Credit Card', 'woocommerce-paypal-payments' ),
},
{
value: 'googlepay',
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),

View file

@ -0,0 +1,39 @@
import { addQueryArgs } from '@wordpress/url';
const getLocation = () => window.location;
const pushHistory = ( path ) => window.history.pushState( { path }, '', path );
/**
* Get the current path from the browser.
*
* @return {string} Current path.
*/
export const getPath = () => getLocation().pathname;
/**
* Get the current query string, parsed into an object, from history.
*
* @return {Object} Current query object, defaults to empty object.
*/
export const getQuery = () =>
Object.fromEntries( new URLSearchParams( getLocation().search ) );
/**
* Updates the query parameters of the current page.
*
* @param {Object} query Object of params to be updated.
* @throws {TypeError} If the query is not an object.
*/
export const updateQueryString = ( query ) =>
pushHistory( getNewPath( query ) );
/**
* Return a URL with set query parameters.
*
* @param {Object} query Object of params to be updated.
* @param {string} basePath Optional. Define the path for the new URL.
* @return {string} Updated URL merging query params into existing params.
*/
export const getNewPath = ( query, basePath = getPath() ) =>
addQueryArgs( basePath, { ...getQuery(), ...query } );

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
@ -71,6 +72,13 @@ return array(
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
return new CommonRestEndpoint( $container->get( 'settings.data.common' ) );
},
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
return new RefreshFeatureStatusEndpoint(
$container->get( 'wcgateway.settings' ),
new Cache( 'ppcp-timeout' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint {
return new ConnectManualRestEndpoint(
$container->get( 'api.paypal-host-production' ),

View file

@ -209,6 +209,11 @@ class CommonRestEndpoint extends RestEndpoint {
$this->merchant_info_map
);
$extra_data['merchant'] = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_data',
$extra_data['merchant'],
);
return $extra_data;
}

View file

@ -0,0 +1,132 @@
<?php
/**
* REST endpoint to refresh feature status.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* REST controller for refreshing feature status.
*/
class RefreshFeatureStatusEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'refresh-feature-status';
/**
* Cache timeout in seconds.
*
* @var int
*/
private const TIMEOUT = 60;
/**
* Cache key for tracking request timeouts.
*
* @var string
*/
private const CACHE_KEY = 'refresh_feature_status_timeout';
/**
* The settings.
*
* @var ContainerInterface
*/
protected ContainerInterface $settings;
/**
* The cache.
*
* @var Cache
*/
protected Cache $cache;
/**
* The logger.
*
* @var LoggerInterface
*/
protected LoggerInterface $logger;
/**
* Constructor.
*
* @param ContainerInterface $settings The settings.
* @param Cache $cache The cache.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
ContainerInterface $settings,
Cache $cache,
LoggerInterface $logger
) {
$this->settings = $settings;
$this->cache = $cache;
$this->logger = $logger;
}
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'refresh_status' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Handles the refresh status request.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response
*/
public function refresh_status( WP_REST_Request $request ): WP_REST_Response {
$now = time();
$last_request_time = $this->cache->get( self::CACHE_KEY ) ?: 0;
$seconds_missing = $last_request_time + self::TIMEOUT - $now;
if ( $seconds_missing > 0 ) {
return $this->return_error(
sprintf(
// translators: %1$s is the number of seconds remaining.
__( 'Wait %1$s seconds before trying again.', 'woocommerce-paypal-payments' ),
$seconds_missing
)
);
}
$this->cache->set( self::CACHE_KEY, $now, self::TIMEOUT );
do_action( 'woocommerce_paypal_payments_clear_apm_product_status', $this->settings );
$this->logger->info( 'Feature status refreshed successfully' );
return $this->return_success(
array(
'message' => __( 'Feature status refreshed successfully.', 'woocommerce-paypal-payments' ),
)
);
}
}

View file

@ -181,7 +181,8 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$container->get( 'settings.rest.common' ),
$container->get( 'settings.rest.connect_manual' ),
$container->get( 'settings.rest.login_link' ),
$container->get('settings.rest.webhooks')
$container->get('settings.rest.webhooks'),
$container->get( 'settings.rest.refresh_feature_status' ),
);
foreach ( $endpoints as $endpoint ) {

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -547,6 +548,33 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
}
);
add_filter(
'woocommerce_paypal_payments_rest_common_merchant_data',
function( array $merchant_data ) use ( $c ): array {
if ( ! isset( $merchant_data['features'] ) ) {
$merchant_data['features'] = array();
}
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
$reference_transactions_enabled = $billing_agreements_endpoint->reference_transaction_enabled();
$merchant_data['features']['save_paypal_and_venmo'] = array(
'enabled' => $reference_transactions_enabled,
);
$dcc_product_status = $c->get( 'wcgateway.helper.dcc-product-status' );
assert( $dcc_product_status instanceof DCCProductStatus );
$dcc_enabled = $dcc_product_status->dcc_is_active();
$merchant_data['features']['advanced_credit_and_debit_cards'] = array(
'enabled' => $dcc_enabled,
);
return $merchant_data;
}
);
return true;
}