Merge branch 'trunk' into PCP-4242-non-acdc-country-onboarding

This commit is contained in:
Emili Castells Guasch 2025-02-24 16:37:01 +01:00
commit a540c80b3d
26 changed files with 1365 additions and 648 deletions

View file

@ -42,10 +42,24 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
public function run( ContainerInterface $c ) : bool {
add_action( 'after_setup_theme', fn() => $this->run_with_translations( $c ) );
return true;
}
/**
* Set up WP hooks that depend on translation features.
* Runs after the theme setup, when translations are available, which is fired
* before the `init` hook, which usually contains most of the logic.
*
* @param ContainerInterface $c The DI container.
* @return void
*/
private function run_with_translations( ContainerInterface $c ) : void {
// When Local APMs are disabled, none of the following hooks are needed.
if ( ! $this->should_add_local_apm_gateways( $c ) ) {
return true;
return;
}
/**
@ -193,8 +207,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
10,
2
);
return true;
}
/**
@ -221,7 +233,7 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @return bool
*/
private function should_add_local_apm_gateways( ContainerInterface $container ) : bool {
// Merchant onboarding must be completed.
// APMs are only available after merchant onboarding is completed.
$is_connected = $container->get( 'settings.flag.is-connected' );
if ( ! $is_connected ) {
/**
@ -231,13 +243,8 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* During the authentication process (which happens via a REST call)
* the gateways need to be present, so they can be correctly
* pre-configured for new merchants.
*
* TODO is there a cleaner solution for this?
*/
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' );
return str_contains( $request_uri, '/wp-json/wc/' );
return $this->is_rest_request();
}
// The general plugin functionality must be enabled.
@ -251,4 +258,16 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
return $settings->has( 'allow_local_apm_gateways' )
&& $settings->get( 'allow_local_apm_gateways' ) === true;
}
/**
* Checks, whether the current request is trying to access a WooCommerce REST endpoint.
*
* @return bool True, if the request path matches the WC-Rest namespace.
*/
private function is_rest_request(): bool {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' );
return str_contains( $request_uri, '/wp-json/wc/' );
}
}

View file

@ -0,0 +1,35 @@
# Glossary
This document provides definitions and explanations of key terms used in the plugin.
---
## Eligibility
**Eligibility** determines whether a merchant can access a specific feature within the plugin. It is a boolean value (`true` or `false`) that depends on certain criteria, such as:
- **Country**: The merchant's location or the country where their business operates.
- **Other Factors**: Additional conditions, such as subscription plans, business type, or compliance requirements.
If a merchant is **eligible** (`true`) for a feature, the feature will be visible and accessible in the plugin. If they are **not eligible** (`false`), the feature will be hidden or unavailable.
---
## Capability
**Capability** refers to the activation status of a feature for an eligible merchant. Even if a merchant is eligible for a feature, they may need to activate it in their PayPal dashboard to use it. Capability has two states:
- **Active**: The feature is enabled, and the merchant can configure and use it.
- **Inactive**: The feature is not enabled, and the merchant will be guided on how to activate it (e.g., through instructions or prompts).
Capability ensures that eligible merchants have control over which features they want to use and configure within the plugin.
---
### Example Workflow
1. A merchant is **eligible** for a feature based on their country and other factors.
2. If the feature is **active** (capability is enabled), the merchant can configure and use it.
3. If the feature is **inactive**, the plugin will provide instructions on how to activate it.
---

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { PayLaterMessagingHooks } from '../../../data';
import { useEffect } from '@wordpress/element';
const TabPayLaterMessaging = () => {
const {

View file

@ -0,0 +1,36 @@
import { __ } from '@wordpress/i18n';
import { Button, Icon } from '@wordpress/components';
import { reusableBlock } from '@wordpress/icons';
const FeatureDescription = ( { refreshHandler, isRefreshing } ) => {
const buttonLabel = isRefreshing
? __( 'Refreshing…', 'woocommerce-paypal-payments' )
: __( 'Refresh', 'woocommerce-paypal-payments' );
return (
<>
<p>
{ __(
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ buttonLabel }
</Button>
</>
);
};
export default FeatureDescription;

View file

@ -0,0 +1,70 @@
import { __ } from '@wordpress/i18n';
import { FeatureSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import { Content } from '../../../../../ReusableComponents/Elements';
import { TITLE_BADGE_POSITIVE } from '../../../../../ReusableComponents/TitleBadge';
import { selectTab, TAB_IDS } from '../../../../../../utils/tabSelector';
import { setActiveModal } from '../../../../../../data/common/actions';
const FeatureItem = ( {
isBusy,
isSandbox,
title,
description,
buttons,
enabled,
notes,
} ) => {
const getButtonUrl = ( button ) => {
if ( button.urls ) {
return isSandbox ? button.urls.sandbox : button.urls.live;
}
return button.url;
};
const visibleButtons = buttons.filter(
( button ) =>
! button.showWhen || // Learn more buttons
( enabled && button.showWhen === 'enabled' ) ||
( ! enabled && button.showWhen === 'disabled' )
);
const handleClick = async ( feature ) => {
if ( feature.action?.type === 'tab' ) {
const tabId = TAB_IDS[ feature.action.tab.toUpperCase() ];
await selectTab( tabId, feature.action.section );
}
if ( feature.action?.modal ) {
setActiveModal( feature.action.modal );
}
};
const actionProps = {
isBusy,
enabled,
notes,
buttons: visibleButtons.map( ( button ) => ( {
...button,
url: getButtonUrl( button ),
onClick: () => handleClick( button ),
} ) ),
};
if ( enabled ) {
actionProps.badge = {
text: __( 'Active', 'woocommerce-paypal-payments' ),
type: TITLE_BADGE_POSITIVE,
};
}
return (
<Content>
<FeatureSettingsBlock
title={ title }
description={ description }
actionProps={ actionProps }
/>
</Content>
);
};
export default FeatureItem;

View file

@ -0,0 +1,95 @@
import { __, sprintf } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import FeatureItem from './FeatureItem';
import FeatureDescription from './FeatureDescription';
import { ContentWrapper } from '../../../../../ReusableComponents/Elements';
import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
import { useMerchantInfo } from '../../../../../../data/common/hooks';
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../../../ReusableComponents/Icons';
import { useFeatures } from '../../../../../../data/features/hooks';
const Features = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo();
const { features, fetchFeatures } = useFeatures();
const { refreshFeatureStatuses } = useDispatch( COMMON_STORE_NAME );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
if ( ! features || features.length === 0 ) {
return null;
}
const refreshHandler = async () => {
setIsRefreshing( true );
try {
const statusResult = await refreshFeatureStatuses();
if ( ! statusResult?.success ) {
throw new Error(
statusResult?.message || 'Failed to refresh status'
);
}
const featuresResult = await fetchFeatures();
if ( featuresResult.success ) {
createSuccessNotice(
__(
'Features refreshed successfully.',
'woocommerce-paypal-payments'
),
{ icon: NOTIFICATION_SUCCESS }
);
} else {
throw new Error(
featuresResult?.message || 'Failed to fetch features'
);
}
} catch ( error ) {
createErrorNotice(
sprintf(
/* translators: %s: error message */
__( 'Operation failed: %s', 'woocommerce-paypal-payments' ),
error.message ||
__( 'Unknown error', 'woocommerce-paypal-payments' )
),
{ icon: NOTIFICATION_ERROR }
);
} finally {
setIsRefreshing( false );
}
};
return (
<SettingsCard
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<FeatureDescription
refreshHandler={ refreshHandler }
isRefreshing={ isRefreshing }
/>
}
contentContainer={ false }
>
<ContentWrapper>
{ features.map( ( { id, enabled, ...feature } ) => (
<FeatureItem
key={ id }
isBusy={ isRefreshing }
isSandbox={ merchant.isSandbox }
enabled={ enabled }
{ ...feature }
/>
) ) }
</ContentWrapper>
</SettingsCard>
);
};
export default Features;

View file

@ -0,0 +1,72 @@
import { __ } from '@wordpress/i18n';
import { FeatureSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import {
Content,
ContentWrapper,
} from '../../../../../ReusableComponents/Elements';
import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
const Help = () => {
return (
<SettingsCard
className="ppcp-r-tab-overview-help"
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
description={ __(
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
'woocommerce-paypal-payments'
) }
contentContainer={ false }
>
<ContentWrapper>
<Content>
<FeatureSettingsBlock
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: 'https://woocommerce.com/document/woocommerce-paypal-payments/',
},
],
} }
/>
</Content>
<Content>
<FeatureSettingsBlock
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: 'https://woocommerce.com/document/woocommerce-paypal-payments/#get-help ',
},
],
} }
/>
</Content>
</ContentWrapper>
</SettingsCard>
);
};
export default Help;

View file

@ -0,0 +1,86 @@
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { reusableBlock } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { TodoSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
import { useTodos } from '../../../../../../data/todos/hooks';
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
import { STORE_NAME as TODOS_STORE_NAME } from '../../../../../../data/todos';
import { NOTIFICATION_SUCCESS } from '../../../../../ReusableComponents/Icons';
const Todos = () => {
const [ isResetting, setIsResetting ] = useState( false );
const { todos, isReady: areTodosReady, dismissTodo } = useTodos();
// eslint-disable-next-line no-shadow
const { setActiveModal, setActiveHighlight } =
useDispatch( COMMON_STORE_NAME );
const { resetDismissedTodos, setDismissedTodos } =
useDispatch( TODOS_STORE_NAME );
const { createSuccessNotice } = useDispatch( noticesStore );
const showTodos = areTodosReady && todos.length > 0;
const resetHandler = async () => {
setIsResetting( true );
try {
await setDismissedTodos( [] );
await resetDismissedTodos();
createSuccessNotice(
__(
'Dismissed items restored successfully.',
'woocommerce-paypal-payments'
),
{ icon: NOTIFICATION_SUCCESS }
);
} finally {
setIsResetting( false );
}
};
if ( ! showTodos ) {
return null;
}
return (
<SettingsCard
className="ppcp-r-tab-overview-todo"
title={ __( 'Things to do next', 'woocommerce-paypal-payments' ) }
description={
<>
<p>
{ __(
'Complete these tasks to keep your store updated with the latest products and services.',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ resetHandler }
disabled={ isResetting }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ isResetting
? __( 'Restoring…', 'woocommerce-paypal-payments' )
: __(
'Restore dismissed Things To Do',
'woocommerce-paypal-payments'
) }
</Button>
</>
}
>
<TodoSettingsBlock
todosData={ todos }
setActiveModal={ setActiveModal }
setActiveHighlight={ setActiveHighlight }
onDismissTodo={ dismissTodo }
/>
</SettingsCard>
);
};
export default Todos;

View file

@ -1,278 +0,0 @@
import { __ } from '@wordpress/i18n';
import { TAB_IDS, selectTab } from '../../../../../utils/tabSelector';
import { payLaterMessaging } from './pay-later-messaging';
export const getFeatures = ( setActiveModal ) => {
const storeCountry = ppcpSettings?.storeCountry;
const features = [
{
id: 'save_paypal_and_venmo',
title: __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
description: __(
'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.SETTINGS,
'ppcp--save-payment-methods'
);
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Sign up', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
live: 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: 'https://www.paypal.com/us/enterprise/payment-processing/accept-venmo',
class: 'small-button',
},
],
},
{
id: 'advanced_credit_and_debit_cards',
title: __(
'Advanced Credit and Debit Cards',
'woocommerce-paypal-payments'
),
description: __(
'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'ppcp-credit-card-gateway' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Sign up', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp',
live: 'https://www.paypal.com/bizsignup/entry?product=ppcp',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: 'https://developer.paypal.com/studio/checkout/advanced',
class: 'small-button',
},
],
},
{
id: 'alternative_payment_methods',
title: __(
'Alternative Payment Methods',
'woocommerce-paypal-payments'
),
description: __(
'Offer global, country-specific payment options for your customers.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-alternative-payments-card'
);
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Sign up', 'woocommerce-paypal-payments' ),
url: 'https://developer.paypal.com/docs/checkout/apm/',
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: 'https://developer.paypal.com/docs/checkout/apm/',
class: 'small-button',
},
],
},
{
id: 'google_pay',
title: __( 'Google Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Google Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'ppcp-googlepay' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Sign up', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: 'https://developer.paypal.com/docs/checkout/apm/google-pay/',
class: 'small-button',
},
],
notes: [
__(
'¹PayPal Q2 Earnings-2021.',
'woocommerce-paypal-payments'
),
],
},
{
id: 'apple_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Let customers pay using their Apple Pay wallet.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'ppcp-applepay' );
} );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __(
'Domain registration',
'woocommerce-paypal-payments'
),
urls: {
sandbox:
'https://www.sandbox.paypal.com/uccservicing/apm/applepay',
live: 'https://www.paypal.com/uccservicing/apm/applepay',
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'secondary',
text: __( 'Sign up', 'woocommerce-paypal-payments' ),
urls: {
sandbox:
'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
live: 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
},
showWhen: 'disabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: 'https://developer.paypal.com/docs/checkout/apm/apple-pay/',
class: 'small-button',
},
],
},
];
const countryData = payLaterMessaging[ storeCountry ] || {};
if (
!! window.ppcpSettings?.isPayLaterConfiguratorAvailable &&
countryData
) {
const countryLocation = [
'UK',
'ES',
'IT',
'FR',
'US',
'DE',
'AU',
].includes( storeCountry )
? storeCountry.toLowerCase()
: 'us';
features.push( {
id: 'pay_later_messaging',
title: __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
description: __(
'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
'woocommerce-paypal-payments'
),
buttons: [
{
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab( TAB_IDS.PAY_LATER_MESSAGING );
},
showWhen: 'enabled',
class: 'small-button',
},
{
type: 'tertiary',
text: __( 'Learn more', 'woocommerce-paypal-payments' ),
url: `https://www.paypal.com/${ countryLocation }/business/accept-payments/checkout/installments`,
class: 'small-button',
},
],
} );
}
return features;
};

View file

@ -1,371 +1,25 @@
import { __, sprintf } from '@wordpress/i18n';
import { useState, useMemo } from '@wordpress/element';
import { Button, Icon } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { reusableBlock } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import {
TodoSettingsBlock,
FeatureSettingsBlock,
} from '../../../ReusableComponents/SettingsBlocks';
import {
Content,
ContentWrapper,
CardActions,
} from '../../../ReusableComponents/Elements';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge';
import {
CommonStoreName,
TodosStoreName,
CommonHooks,
TodosHooks,
} from '../../../../data';
import { getFeatures } from '../Components/Overview/features-config';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../ReusableComponents/Icons';
import Todos from '../Components/Overview/Todos/Todos';
import Features from '../Components/Overview/Features/Features';
import Help from '../Components/Overview/Help/Help';
import { TodosHooks, CommonHooks, FeaturesHooks } from '../../../../data';
import SpinnerOverlay from '../../../ReusableComponents/SpinnerOverlay';
const TabOverview = () => {
const { isReady: areTodosReady } = TodosHooks.useStore();
const { isReady: merchantIsReady } = CommonHooks.useStore();
const { isReady: areTodosReady } = TodosHooks.useTodos();
const { isReady: merchantIsReady } = CommonHooks.useMerchantInfo();
const { isReady: featuresIsReady } = FeaturesHooks.useFeatures();
if ( ! areTodosReady || ! merchantIsReady ) {
if ( ! areTodosReady || ! merchantIsReady || ! featuresIsReady ) {
return <SpinnerOverlay asModal={ true } />;
}
return (
<div className="ppcp-r-tab-overview">
<OverviewTodos />
<OverviewFeatures />
<OverviewHelp />
<Todos />
<Features />
<Help />
</div>
);
};
export default TabOverview;
const OverviewTodos = () => {
const [ isResetting, setIsResetting ] = useState( false );
const { todos, dismissTodo } = TodosHooks.useTodos();
const { isReady: areTodosReady } = TodosHooks.useStore();
const { setActiveModal, setActiveHighlight } =
useDispatch( CommonStoreName );
const { resetDismissedTodos, setDismissedTodos } =
useDispatch( TodosStoreName );
const { createSuccessNotice } = useDispatch( noticesStore );
const showTodos = areTodosReady && todos.length > 0;
const resetHandler = async () => {
setIsResetting( true );
try {
await setDismissedTodos( [] );
await resetDismissedTodos();
createSuccessNotice(
__(
'Dismissed items restored successfully.',
'woocommerce-paypal-payments'
),
{ icon: NOTIFICATION_SUCCESS }
);
} finally {
setIsResetting( false );
}
};
if ( ! showTodos ) {
return null;
}
return (
<SettingsCard
className="ppcp-r-tab-overview-todo"
title={ __( 'Things to do next', 'woocommerce-paypal-payments' ) }
description={
<>
<p>
{ __(
'Complete these tasks to keep your store updated with the latest products and services.',
'woocommerce-paypal-payments'
) }
</p>
<CardActions>
<Button
variant="tertiary"
onClick={ resetHandler }
disabled={ isResetting }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ isResetting
? __(
'Restoring…',
'woocommerce-paypal-payments'
)
: __(
'Restore dismissed Things To Do',
'woocommerce-paypal-payments'
) }
</Button>
</CardActions>
</>
}
>
<TodoSettingsBlock
todosData={ todos }
setActiveModal={ setActiveModal }
setActiveHighlight={ setActiveHighlight }
onDismissTodo={ dismissTodo }
/>
</SettingsCard>
);
};
const OverviewFeatures = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant, features: merchantFeatures } =
CommonHooks.useMerchantInfo();
const { refreshFeatureStatuses, setActiveModal } =
useDispatch( CommonStoreName );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
// Get the features data with access to setActiveModal
const featuresData = useMemo(
() => getFeatures( setActiveModal ),
[ setActiveModal ]
);
// Map merchant features status to the config
const features = useMemo( () => {
return featuresData.map( ( feature ) => {
const merchantFeature = merchantFeatures?.[ feature.id ];
return {
...feature,
enabled: merchantFeature?.enabled ?? false,
};
} );
}, [ featuresData, merchantFeatures ] );
const refreshHandler = async () => {
setIsRefreshing( true );
try {
const result = await refreshFeatureStatuses();
if ( result && ! result.success ) {
const errorMessage = sprintf(
/* translators: %s: error message */
__(
'Operation failed: %s Check WooCommerce logs for more details.',
'woocommerce-paypal-payments'
),
result.message ||
__( 'Unknown error', 'woocommerce-paypal-payments' )
);
createErrorNotice( errorMessage, {
icon: NOTIFICATION_ERROR,
} );
console.error(
'Failed to refresh features:',
result.message || 'Unknown error'
);
} else {
createSuccessNotice(
__(
'Features refreshed successfully.',
'woocommerce-paypal-payments'
),
{
icon: NOTIFICATION_SUCCESS,
}
);
}
} finally {
setIsRefreshing( false );
}
};
return (
<SettingsCard
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<OverviewFeatureDescription
refreshHandler={ refreshHandler }
isRefreshing={ isRefreshing }
/>
}
contentContainer={ false }
>
<ContentWrapper>
{ features.map( ( { id, ...feature } ) => (
<OverviewFeatureItem
key={ id }
isBusy={ isRefreshing }
isSandbox={ merchant.isSandbox }
title={ feature.title }
description={ feature.description }
buttons={ feature.buttons }
enabled={ feature.enabled }
notes={ feature.notes }
/>
) ) }
</ContentWrapper>
</SettingsCard>
);
};
const OverviewFeatureItem = ( {
isBusy,
isSandbox,
title,
description,
buttons,
enabled,
notes,
} ) => {
const getButtonUrl = ( button ) => {
if ( button.urls ) {
return isSandbox ? button.urls.sandbox : button.urls.live;
}
return button.url;
};
const visibleButtons = buttons.filter(
( button ) =>
! button.showWhen || // Learn more buttons
( enabled && button.showWhen === 'enabled' ) ||
( ! enabled && button.showWhen === 'disabled' )
);
const actionProps = {
isBusy,
enabled,
notes,
buttons: visibleButtons.map( ( button ) => ( {
...button,
url: getButtonUrl( button ),
} ) ),
};
if ( enabled ) {
actionProps.badge = {
text: __( 'Active', 'woocommerce-paypal-payments' ),
type: TITLE_BADGE_POSITIVE,
};
}
return (
<Content>
<FeatureSettingsBlock
title={ title }
description={ description }
actionProps={ actionProps }
/>
</Content>
);
};
const OverviewFeatureDescription = ( { refreshHandler, isRefreshing } ) => {
const buttonLabel = isRefreshing
? __( 'Refreshing…', 'woocommerce-paypal-payments' )
: __( 'Refresh', 'woocommerce-paypal-payments' );
return (
<>
<p>
{ __(
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
<CardActions>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ buttonLabel }
</Button>
</CardActions>
</>
);
};
const OverviewHelp = () => {
return (
<SettingsCard
className="ppcp-r-tab-overview-help"
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
description={ __(
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
'woocommerce-paypal-payments'
) }
contentContainer={ false }
>
<ContentWrapper>
<Content>
<FeatureSettingsBlock
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: 'https://woocommerce.com/document/woocommerce-paypal-payments/',
},
],
} }
/>
</Content>
<Content>
<FeatureSettingsBlock
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: 'https://woocommerce.com/document/woocommerce-paypal-payments/#get-help ',
},
],
} }
/>
</Content>
</ContentWrapper>
</SettingsCard>
);
};

View file

@ -168,6 +168,7 @@ export const useMerchantInfo = () => {
const { features } = useHooks();
const merchant = useMerchant();
const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME );
const { isReady } = useStore();
const verifyLoginStatus = useCallback( async () => {
const result = await refreshMerchantData();
@ -193,6 +194,7 @@ export const useMerchantInfo = () => {
merchant, // Merchant details
features, // Eligible merchant features
verifyLoginStatus, // Callback
isReady,
};
};

View file

@ -0,0 +1,14 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data
SET_TRANSIENT: 'ppcp/features/SET_TRANSIENT',
// Persistant data
SET_FEATURES: 'ppcp/features/SET_FEATURES',
HYDRATE: 'ppcp/features/HYDRATE',
};

View file

@ -0,0 +1,87 @@
/**
* Action Creators: Define functions to create action objects.
*
* These functions update state or trigger side effects (e.g., async operations).
* Actions are categorized as Transient or Side effect.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { REST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
* @property {string} type - The action type.
* @property {Object?} payload - Optional payload for the action.
*/
/**
* Set the full store details during app initialization.
*
* @param {{data: {}, flags?: {}}} payload
* @return {Action} The action.
*/
export const hydrate = ( payload ) => ( {
type: ACTION_TYPES.HYDRATE,
payload,
} );
/**
* Generic transient-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Action} The action.
*/
export const setTransient = ( prop, value ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { [ prop ]: value },
} );
/**
* Transient. Marks the store as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Sets the features in the store.
*
* @param {Array} features The features to set.
* @return {Action} The action.
*/
export const setFeatures = ( features ) => ( {
type: ACTION_TYPES.SET_FEATURES,
payload: features,
} );
/**
* Fetches features from the server.
*
* @return {Promise<Array>} The features data.
*/
export const fetchFeatures = async () => {
try {
const response = await apiFetch( { path: REST_PATH } );
if ( response?.data ) {
return {
success: true,
features: response.data.features,
};
}
return {
success: false,
features: [],
};
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
};

View file

@ -0,0 +1,8 @@
/**
* Constants: Define store configuration values.
*
* @file
*/
export const STORE_NAME = 'wc/paypal/features';
export const REST_PATH = '/wc/v3/wc_paypal/features';

View file

@ -0,0 +1,67 @@
/**
* Hooks: Provide the main API for components to interact with the features store.
*
* These encapsulate store interactions, offering a consistent interface.
* Hooks simplify data access and manipulation for components.
*
* @file
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_PATH } from './constants';
export const useFeatures = () => {
const { features, isReady } = useSelect( ( select ) => {
const store = select( STORE_NAME );
return {
features: store.getFeatures() || [],
isReady: select( STORE_NAME ).transientData()?.isReady || false,
};
}, [] );
const { setFeatures, setIsReady } = useDispatch( STORE_NAME );
useEffect( () => {
const loadInitialFeatures = async () => {
try {
const response = await apiFetch( { path: REST_PATH } );
if ( response?.data?.features ) {
const featuresData = response.data.features;
if ( featuresData.length > 0 ) {
await setFeatures( featuresData );
await setIsReady( true );
}
}
} catch ( error ) {}
};
if ( ! isReady ) {
loadInitialFeatures();
}
}, [ isReady, setFeatures, setIsReady ] );
return {
features,
isReady,
fetchFeatures: async () => {
try {
const response = await apiFetch( { path: REST_PATH } );
const featuresData = response.data?.features || [];
if ( featuresData.length > 0 ) {
await setFeatures( featuresData );
await setIsReady( true );
return { success: true, features: featuresData };
}
return { success: false, features: [] };
} catch ( error ) {
return { success: false, error, message: error.message };
}
},
};
};

View file

@ -0,0 +1,29 @@
import { createReduxStore, register } from '@wordpress/data';
import { STORE_NAME } from './constants';
import reducer from './reducer';
import * as selectors from './selectors';
import * as actions from './actions';
import * as hooks from './hooks';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
* Combines custom controls with WordPress data controls.
*
* @return {boolean} True if initialization succeeded, false otherwise.
*/
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
actions,
selectors,
resolvers,
} );
register( store );
return Boolean( wp.data.select( STORE_NAME ) );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,75 @@
/**
* Reducer: Defines store structure and state updates for features module.
*
* Manages both transient (temporary) and persistent (saved) state.
* The initial state must define all properties, as dynamic additions are not supported.
*
* @file
*/
import { createReducer, createReducerSetters } from '../utils';
import ACTION_TYPES from './action-types';
// Store structure.
/**
* Transient: Values that are _not_ saved to the DB (like app lifecycle-flags).
* These reset on page reload.
*/
const defaultTransient = Object.freeze( {
isReady: false,
} );
/**
* Persistent: Values that are loaded from and saved to the DB.
* These represent the core features configuration.
*/
const defaultPersistent = Object.freeze( {
features: [],
} );
// Reducer logic.
const [ changeTransient, changePersistent ] = createReducerSetters(
defaultTransient,
defaultPersistent
);
/**
* Reducer implementation mapping actions to state updates.
*/
const reducer = createReducer( defaultTransient, defaultPersistent, {
/**
* Updates temporary state values
*
* @param {Object} state Current state
* @param {Object} payload Update payload
* @return {Object} Updated state
*/
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
/**
* Updates features list
*
* @param {Object} state Current state
* @param {Object} payload Update payload containing features array
* @return {Object} Updated state
*/
[ ACTION_TYPES.SET_FEATURES ]: ( state, payload ) => {
return changePersistent( state, { features: payload } );
},
/**
* Initializes persistent state with data from the server
*
* @param {Object} state Current state
* @param {Object} payload Hydration payload containing server data
* @param {Object} payload.data The features data to hydrate
* @return {Object} Hydrated state
*/
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
} );
export default reducer;

View file

@ -0,0 +1,34 @@
/**
* Resolvers: Handle asynchronous data fetching for the store.
*
* These functions update store state with data from external sources.
* Each resolver corresponds to a specific selector (selector with same name must exist).
* Resolvers are called automatically when selectors request unavailable data.
*
* @file
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { REST_PATH } from './constants';
/**
* Hydrates the features data from the API.
*
* @return {Object} Action to dispatch.
*/
export function getFeatures() {
return async ( { dispatch } ) => {
try {
const response = await apiFetch( { path: REST_PATH } );
if ( response?.features ) {
dispatch.setFeatures( response.features );
dispatch.setIsReady( true );
}
} catch ( error ) {
console.error( 'Error fetching features:', error );
}
};
}

View file

@ -0,0 +1,27 @@
/**
* Selectors: Extract specific pieces of state from the store.
*
* These functions provide a consistent interface for accessing store data.
* They allow components to retrieve data without knowing the store structure.
*
* @file
*/
const EMPTY_OBJ = Object.freeze( {} );
const EMPTY_ARR = Object.freeze( [] );
const getState = ( state ) => state || EMPTY_OBJ;
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
export const transientData = ( state ) => {
const { data, ...transientState } = getState( state );
return transientState || EMPTY_OBJ;
};
export const getFeatures = ( state ) => {
const features = state?.features || persistentData( state ).features;
return features || EMPTY_ARR;
};

View file

@ -6,6 +6,7 @@ import * as Settings from './settings';
import * as Styling from './styling';
import * as Todos from './todos';
import * as PayLaterMessaging from './pay-later-messaging';
import * as Features from './features';
const stores = [
Onboarding,
@ -15,6 +16,7 @@ const stores = [
Styling,
Todos,
PayLaterMessaging,
Features,
];
stores.forEach( ( store ) => {
@ -40,6 +42,7 @@ export const SettingsHooks = Settings.hooks;
export const StylingHooks = Styling.hooks;
export const TodosHooks = Todos.hooks;
export const PayLaterMessagingHooks = PayLaterMessaging.hooks;
export const FeaturesHooks = Features.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME;
@ -48,6 +51,7 @@ export const SettingsStoreName = Settings.STORE_NAME;
export const StylingStoreName = Styling.STORE_NAME;
export const TodosStoreName = Todos.STORE_NAME;
export const PayLaterMessagingStoreName = PayLaterMessaging.STORE_NAME;
export const FeaturesStoreName = Features.STORE_NAME;
export * from './configuration';

View file

@ -95,7 +95,8 @@ export const useStore = () => {
export const useTodos = () => {
const { todos, fetchTodos, dismissTodo, setTodoCompleted } = useHooks();
return { todos, fetchTodos, dismissTodo, setTodoCompleted };
const { isReady } = useStore();
return { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady };
};
export const useDismissedTodos = () => {

View file

@ -10,7 +10,9 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\FeaturesDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
@ -21,6 +23,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\FeaturesRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
@ -33,6 +36,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\TodosRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService;
@ -475,6 +479,67 @@ return array(
$container->get( 'googlepay.eligible' ) && $capabilities['google_pay'] && ! $gateways['google_pay'],
);
},
'settings.rest.features' => static function ( ContainerInterface $container ) : FeaturesRestEndpoint {
return new FeaturesRestEndpoint(
$container->get( 'settings.data.definition.features' ),
$container->get( 'settings.rest.settings' )
);
},
'settings.data.definition.features' => static function ( ContainerInterface $container ) : FeaturesDefinition {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
);
$payment_endpoint = $container->get( 'settings.rest.payment' );
$settings = $payment_endpoint->get_details()->get_data();
// Settings status.
$gateways = array(
'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
);
// Merchant capabilities, serve to show active or inactive badge and buttons.
$capabilities = array(
'apple_pay' => $features['apple_pay']['enabled'] ?? false,
'google_pay' => $features['google_pay']['enabled'] ?? false,
'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false,
'save_paypal' => $features['save_paypal_and_venmo']['enabled'] ?? false,
'apm' => $features['alternative_payment_methods']['enabled'] ?? false,
'paylater' => $features['pay_later_messaging']['enabled'] ?? false,
);
$merchant_capabilities = array(
'save_paypal' => $capabilities['save_paypal'], // Save PayPal and Venmo eligibility.
'acdc' => $capabilities['acdc'] && ! $gateways['card-button'], // Advanced credit and debit cards eligibility.
'apm' => $capabilities['apm'], // Alternative payment methods eligibility.
'google_pay' => $capabilities['acdc'] && $capabilities['google_pay'], // Google Pay eligibility.
'apple_pay' => $capabilities['acdc'] && $capabilities['apple_pay'], // Apple Pay eligibility.
'pay_later' => $capabilities['paylater'],
);
return new FeaturesDefinition(
$container->get( 'settings.service.features_eligibilities' ),
$container->get( 'settings.data.general' ),
$merchant_capabilities
);
},
'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
$messages_apply = $container->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
$pay_later_eligible = $messages_apply->for_country();
$merchant_country = $container->get( 'api.shop.country' );
$ineligible_countries = array( 'RU', 'BR', 'JP' );
$apm_eligible = ! in_array( $merchant_country, $ineligible_countries, true );
return new FeaturesEligibilityService(
$container->get( 'save-payment-methods.eligible' ), // Save PayPal and Venmo eligibility.
$container->get( 'card-fields.eligible' ), // Advanced credit and debit cards eligibility.
$apm_eligible, // Alternative payment methods eligibility.
$container->get( 'googlepay.eligible' ), // Google Pay eligibility.
$container->get( 'applepay.eligible' ), // Apple Pay eligibility.
$pay_later_eligible, // Pay Later eligibility.
);
},
'settings.service.todos_sorting' => static function ( ContainerInterface $container ) : TodosSortingAndFilteringService {
return new TodosSortingAndFilteringService(
$container->get( 'settings.data.todos' )

View file

@ -0,0 +1,312 @@
<?php
/**
* PayPal Commerce Features Definitions
*
* @package WooCommerce\PayPalCommerce\Settings\Data\Definition
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
/**
* Class FeaturesDefinition
*
* Provides the definitions for all available features in the system.
* Each feature has a title, description, eligibility condition, and associated action.
*/
class FeaturesDefinition {
/**
* The features eligibility service.
*
* @var FeaturesEligibilityService
*/
protected FeaturesEligibilityService $eligibilities;
/**
* The general settings service.
*
* @var GeneralSettings
*/
protected GeneralSettings $settings;
/**
* The merchant capabilities.
*
* @var array
*/
protected array $merchant_capabilities;
/**
* Constructor.
*
* @param FeaturesEligibilityService $eligibilities The features eligibility service.
* @param GeneralSettings $settings The general settings service.
* @param array $merchant_capabilities The merchant capabilities.
*/
public function __construct(
FeaturesEligibilityService $eligibilities,
GeneralSettings $settings,
array $merchant_capabilities
) {
$this->eligibilities = $eligibilities;
$this->settings = $settings;
$this->merchant_capabilities = $merchant_capabilities;
}
/**
* Returns the full list of feature definitions with their eligibility conditions.
*
* @return array The array of feature definitions.
*/
public function get(): array {
$all_features = $this->all_available_features();
$eligible_features = array();
$eligibility_checks = $this->eligibilities->get_eligibility_checks();
foreach ( $all_features as $feature_key => $feature ) {
if ( $eligibility_checks[ $feature_key ]() ) {
$eligible_features[ $feature_key ] = $feature;
}
}
return $eligible_features;
}
/**
* Returns all available features.
*
* @return array[] The array of all available features.
*/
public function all_available_features(): array {
$paylater_countries = array(
'UK',
'ES',
'IT',
'FR',
'US',
'DE',
'AU',
);
$store_country = $this->settings->get_woo_settings()['country'];
$country_location = in_array( $store_country, $paylater_countries, true ) ? strtolower( $store_country ) : 'us';
return array(
'save_paypal_and_venmo' => array(
'title' => __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
'description' => __( 'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.', 'woocommerce-paypal-payments' ),
'enabled' => $this->merchant_capabilities['save_paypal'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'settings',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'secondary',
'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
'urls' => array(
'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
'live' => 'https://www.paypal.com/bizsignup/entry?product=ADVANCED_VAULTING',
),
'showWhen' => 'disabled',
'class' => 'small-button',
),
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => 'https://www.paypal.com/us/enterprise/payment-processing/accept-venmo',
'class' => 'small-button',
),
),
),
'advanced_credit_and_debit_cards' => array(
'title' => __( 'Advanced Credit and Debit Cards', 'woocommerce-paypal-payments' ),
'description' => __( 'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.', 'woocommerce-paypal-payments' ),
'enabled' => $this->merchant_capabilities['acdc'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-credit-card-gateway',
'highlight' => 'ppcp-credit-card-gateway',
'modal' => 'ppcp-credit-card-gateway',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'secondary',
'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
'urls' => array(
'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp',
'live' => 'https://www.paypal.com/bizsignup/entry?product=ppcp',
),
'showWhen' => 'disabled',
'class' => 'small-button',
),
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => 'https://developer.paypal.com/studio/checkout/advanced',
'class' => 'small-button',
),
),
),
'alternative_payment_methods' => array(
'title' => __( 'Alternative Payment Methods', 'woocommerce-paypal-payments' ),
'description' => __( 'Offer global, country-specific payment options for your customers.', 'woocommerce-paypal-payments' ),
'enabled' => $this->merchant_capabilities['apm'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-alternative-payments-card',
'highlight' => 'ppcp-alternative-payments-card',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'secondary',
'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
'url' => 'https://developer.paypal.com/docs/checkout/apm/',
'showWhen' => 'disabled',
'class' => 'small-button',
),
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => 'https://developer.paypal.com/docs/checkout/apm/',
'class' => 'small-button',
),
),
),
'google_pay' => array(
'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Let customers pay using their Google Pay wallet.', 'woocommerce-paypal-payments' ),
'enabled' => $this->merchant_capabilities['google_pay'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'highlight' => 'ppcp-googlepay',
'modal' => 'ppcp-googlepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'secondary',
'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
'urls' => array(
'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
'live' => 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY',
),
'showWhen' => 'disabled',
'class' => 'small-button',
),
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => 'https://developer.paypal.com/docs/checkout/apm/google-pay/',
'class' => 'small-button',
),
),
'notes' => array(
__( '¹PayPal Q2 Earnings-2021.', 'woocommerce-paypal-payments' ),
),
),
'apple_pay' => array(
'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Let customers pay using their Apple Pay wallet.', 'woocommerce-paypal-payments' ),
'enabled' => $this->merchant_capabilities['apple_pay'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'highlight' => 'ppcp-applepay',
'modal' => 'ppcp-applepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'secondary',
'text' => __( 'Domain registration', 'woocommerce-paypal-payments' ),
'urls' => array(
'sandbox' => 'https://www.sandbox.paypal.com/uccservicing/apm/applepay',
'live' => 'https://www.paypal.com/uccservicing/apm/applepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'secondary',
'text' => __( 'Sign up', 'woocommerce-paypal-payments' ),
'urls' => array(
'sandbox' => 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
'live' => 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY',
),
'showWhen' => 'disabled',
'class' => 'small-button',
),
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => 'https://developer.paypal.com/docs/checkout/apm/apple-pay/',
'class' => 'small-button',
),
),
),
'pay_later' => array(
'title' => __( 'Pay Later Messaging', 'woocommerce-paypal-payments' ),
'description' => __(
'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
'woocommerce-paypal-payments'
),
'enabled' => $this->merchant_capabilities['pay_later'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'pay_later_messaging',
),
'showWhen' => 'enabled',
'class' => 'small-button',
),
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => "https://www.paypal.com/$country_location/business/accept-payments/checkout/installments",
'class' => 'small-button',
),
),
),
);
}
}

View file

@ -0,0 +1,99 @@
<?php
/**
* REST endpoint to manage features.
*
* Provides endpoints for retrieving features via WP REST API routes.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Server;
use WP_REST_Response;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\FeaturesDefinition;
/**
* REST controller for the features in the Overview tab.
*
* This API acts as the intermediary between the "external world" and our
* internal data model. It's responsible for checking eligibility and
* providing configuration data for features.
*/
class FeaturesRestEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'features';
/**
* The features definition instance.
*
* @var FeaturesDefinition
*/
protected FeaturesDefinition $features_definition;
/**
* The settings endpoint instance.
*
* @var SettingsRestEndpoint
*/
protected SettingsRestEndpoint $settings;
/**
* FeaturesRestEndpoint constructor.
*
* @param FeaturesDefinition $features_definition The features definition instance.
* @param SettingsRestEndpoint $settings The settings endpoint instance.
*/
public function __construct(
FeaturesDefinition $features_definition,
SettingsRestEndpoint $settings
) {
$this->features_definition = $features_definition;
$this->settings = $settings;
}
/**
* Registers the REST API routes for features management.
*/
public function register_routes(): void {
// GET /features - Get features list.
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_features' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Retrieves the current features.
*
* @return WP_REST_Response The response containing features data.
*/
public function get_features(): WP_REST_Response {
$features = array();
foreach ( $this->features_definition->get() as $id => $feature ) {
$features[] = array_merge(
array( 'id' => $id ),
$feature
);
}
return $this->return_success(
array(
'features' => $features,
)
);
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* PayPal Commerce eligibility service for WooCommerce.
*
* This file contains the FeaturesEligibilityService class which manages eligibility checks
* for various PayPal Commerce features including saving PayPal and Venmo, advanced credit and debit cards,
* alternative payment methods, Google Pay, Apple Pay, and Pay Later.
*
* @package WooCommerce\PayPalCommerce\Settings\Service
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Service;
/**
* Manages eligibility checks for various PayPal Commerce features.
*/
class FeaturesEligibilityService {
/**
* Whether saving PayPal and Venmo is eligible.
*
* @var bool
*/
private bool $is_save_paypal_and_venmo_eligible;
/**
* Whether advanced credit and debit cards are eligible.
*
* @var bool
*/
private bool $is_advanced_credit_and_debit_cards_eligible;
/**
* Whether alternative payment methods are eligible.
*
* @var bool
*/
private bool $is_alternative_payment_methods_eligible;
/**
* Whether Google Pay is eligible.
*
* @var bool
*/
private bool $is_google_pay_eligible;
/**
* Whether Apple Pay is eligible.
*
* @var bool
*/
private bool $is_apple_pay_eligible;
/**
* Whether Pay Later is eligible.
*
* @var bool
*/
private bool $is_pay_later_eligible;
/**
* Constructor.
*
* @param bool $is_save_paypal_and_venmo_eligible Whether saving PayPal and Venmo is eligible.
* @param bool $is_advanced_credit_and_debit_cards_eligible Whether advanced credit and debit cards are eligible.
* @param bool $is_alternative_payment_methods_eligible Whether alternative payment methods are eligible.
* @param bool $is_google_pay_eligible Whether Google Pay is eligible.
* @param bool $is_apple_pay_eligible Whether Apple Pay is eligible.
* @param bool $is_pay_later_eligible Whether Pay Later is eligible.
*/
public function __construct(
bool $is_save_paypal_and_venmo_eligible,
bool $is_advanced_credit_and_debit_cards_eligible,
bool $is_alternative_payment_methods_eligible,
bool $is_google_pay_eligible,
bool $is_apple_pay_eligible,
bool $is_pay_later_eligible
) {
$this->is_save_paypal_and_venmo_eligible = $is_save_paypal_and_venmo_eligible;
$this->is_advanced_credit_and_debit_cards_eligible = $is_advanced_credit_and_debit_cards_eligible;
$this->is_alternative_payment_methods_eligible = $is_alternative_payment_methods_eligible;
$this->is_google_pay_eligible = $is_google_pay_eligible;
$this->is_apple_pay_eligible = $is_apple_pay_eligible;
$this->is_pay_later_eligible = $is_pay_later_eligible;
}
/**
* Returns all eligibility checks as callables.
*
* @return array<string, callable>
*/
public function get_eligibility_checks(): array {
return array(
'save_paypal_and_venmo' => fn() => $this->is_save_paypal_and_venmo_eligible,
'advanced_credit_and_debit_cards' => fn() => $this->is_advanced_credit_and_debit_cards_eligible,
'alternative_payment_methods' => fn() => $this->is_alternative_payment_methods_eligible,
'google_pay' => fn() => $this->is_google_pay_eligible,
'apple_pay' => fn() => $this->is_apple_pay_eligible,
'pay_later' => fn() => $this->is_pay_later_eligible,
);
}
}

View file

@ -258,6 +258,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
'styling' => $container->get( 'settings.rest.styling' ),
'todos' => $container->get( 'settings.rest.todos' ),
'pay_later_messaging' => $container->get( 'settings.rest.pay_later_messaging' ),
'features' => $container->get( 'settings.rest.features' ),
);
foreach ( $endpoints as $endpoint ) {