🔀 Merge branch 'trunk'

# Conflicts:
#	modules/ppcp-settings/resources/js/data/common/hooks.js
This commit is contained in:
Philipp Stracker 2025-02-12 15:45:52 +01:00
commit ff81617b22
No known key found for this signature in database
57 changed files with 817 additions and 725 deletions

View file

@ -9,7 +9,13 @@ const OptionSelector = ( {
} ) => (
<div className="ppcp-r-select-box-wrapper">
{ options.map(
( { value: itemValue, title, description, contents } ) => {
( {
value: itemValue,
title,
description,
contents,
isDisabled = false,
} ) => {
let isSelected;
if ( Array.isArray( value ) ) {
@ -27,6 +33,7 @@ const OptionSelector = ( {
onChange={ onChange }
isMulti={ multiSelect }
isSelected={ isSelected }
isDisabled={ isDisabled }
>
{ contents }
</OptionItem>
@ -46,13 +53,13 @@ const OptionItem = ( {
isMulti,
isSelected,
children,
isDisabled = false,
} ) => {
const boxClassName = classNames( 'ppcp-r-select-box', {
'ppcp--selected': isSelected,
'ppcp--multiselect': isMulti,
'ppcp--no-title': ! itemTitle,
} );
return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control -- label has a nested input control.
<label className={ boxClassName }>
@ -61,6 +68,7 @@ const OptionItem = ( {
isRadio={ ! isMulti }
onChange={ onChange }
isSelected={ isSelected }
isDisabled={ isDisabled }
/>
<div className="ppcp--box-content">
@ -80,7 +88,7 @@ const OptionItem = ( {
);
};
const InputField = ( { value, onChange, isRadio, isSelected } ) => {
const InputField = ( { value, onChange, isRadio, isSelected, isDisabled } ) => {
if ( isRadio ) {
return (
<PayPalRdb
@ -96,6 +104,7 @@ const InputField = ( { value, onChange, isRadio, isSelected } ) => {
value={ value }
onChange={ onChange }
checked={ isSelected }
disabled={ isDisabled }
/>
);
};

View file

@ -49,10 +49,10 @@ const TodoSettingsBlock = ( {
await selectTab( tabId, todo.action.section );
} else if ( todo.action.type === 'external' ) {
window.open( todo.action.url, '_blank' );
// If it has completeOnClick flag, trigger the action
if ( todo.action.completeOnClick === true ) {
await completeOnClick( todo.id );
}
}
if ( todo.action.completeOnClick === true ) {
await completeOnClick( todo.id );
}
if ( todo.action.modal ) {
@ -63,10 +63,10 @@ const TodoSettingsBlock = ( {
}
};
// Filter out dismissed todos for display
const visibleTodos = todosData.filter(
( todo ) => ! dismissedTodos.includes( todo.id )
);
// Filter out dismissed todos for display and limit to 5.
const visibleTodos = todosData
.filter( ( todo ) => ! dismissedTodos.includes( todo.id ) )
.slice( 0, 5 );
return (
<div

View file

@ -24,6 +24,26 @@ const StepBusiness = ( {} ) => {
useEffect( () => {
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === businessChoice );
}, [ businessChoice, setIsCasualSeller ] );
const { canUseSubscriptions } = OnboardingHooks.useFlags();
const businessChoices = [
{
value: BUSINESS_TYPES.BUSINESS,
title: __( 'Business', 'woocommerce-paypal-payments' ),
description: __(
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
'woocommerce-paypal-payments'
),
},
{
value: BUSINESS_TYPES.CASUAL_SELLER,
title: __( 'Personal Account', 'woocommerce-paypal-payments' ),
description: __(
'Ideal for those who primarily make purchases or send personal transactions to family and friends.',
'woocommerce-paypal-payments'
),
contents: canUseSubscriptions ? <DetailsAccountType /> : null,
},
];
return (
<div className="ppcp-r-page-business">
@ -45,23 +65,13 @@ const StepBusiness = ( {} ) => {
);
};
const businessChoices = [
{
value: BUSINESS_TYPES.BUSINESS,
title: __( 'Business', 'woocommerce-paypal-payments' ),
description: __(
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
const DetailsAccountType = () => (
<p>
{ __(
'* Business account is required for subscriptions.',
'woocommerce-paypal-payments'
),
},
{
value: BUSINESS_TYPES.CASUAL_SELLER,
title: __( 'Personal Account', 'woocommerce-paypal-payments' ),
description: __(
'Ideal for those who primarily make purchases or send personal transactions to family and friends.',
'woocommerce-paypal-payments'
),
},
];
) }
</p>
);
export default StepBusiness;

View file

@ -10,6 +10,7 @@ const StepProducts = () => {
const { canUseSubscriptions } = OnboardingHooks.useFlags();
const [ optionState, setOptionState ] = useState( null );
const [ productChoices, setProductChoices ] = useState( [] );
const { isCasualSeller } = OnboardingHooks.useBusiness();
useEffect( () => {
const initChoices = () => {
@ -48,7 +49,36 @@ const StepProducts = () => {
setProducts( getNewValue() );
};
const productChoicesFull = [
{
value: PRODUCT_TYPES.VIRTUAL,
title: __( 'Virtual', 'woocommerce-paypal-payments' ),
description: __(
'Items do not require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsVirtual />,
},
{
value: PRODUCT_TYPES.PHYSICAL,
title: __( 'Physical Goods', 'woocommerce-paypal-payments' ),
description: __(
'Items require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsPhysical />,
},
{
value: PRODUCT_TYPES.SUBSCRIPTIONS,
title: __( 'Subscriptions', 'woocommerce-paypal-payments' ),
description: __(
'Recurring payments for either physical goods or services.',
'woocommerce-paypal-payments'
),
isDisabled: isCasualSeller,
contents: <DetailsSubscriptions showNotice={ isCasualSeller } />,
},
];
return (
<div className="ppcp-r-page-products">
<OnboardingHeader
@ -87,41 +117,21 @@ const DetailsPhysical = () => (
</ul>
);
const DetailsSubscriptions = () => (
<a
target="__blank"
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq"
>
{ __( 'WooCommerce Subscriptions', 'woocommerce-paypal-payments' ) }
</a>
const DetailsSubscriptions = ( { showNotice } ) => (
<>
<a
target="__blank"
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq"
>
{ __( 'WooCommerce Subscriptions', 'woocommerce-paypal-payments' ) }
</a>
{ showNotice && (
<p>
{ __(
'* Business account is required for subscriptions.',
'woocommerce-paypal-payments'
) }
</p>
) }
</>
);
const productChoicesFull = [
{
value: PRODUCT_TYPES.VIRTUAL,
title: __( 'Virtual', 'woocommerce-paypal-payments' ),
description: __(
'Items do not require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsVirtual />,
},
{
value: PRODUCT_TYPES.PHYSICAL,
title: __( 'Physical Goods', 'woocommerce-paypal-payments' ),
description: __(
'Items require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsPhysical />,
},
{
value: PRODUCT_TYPES.SUBSCRIPTIONS,
title: __( 'Subscriptions', 'woocommerce-paypal-payments' ),
description: __(
'Recurring payments for either physical goods or services.',
'woocommerce-paypal-payments'
),
contents: <DetailsSubscriptions />,
},
];

View file

@ -1,4 +1,5 @@
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import SettingsCard from '../../../../ReusableComponents/SettingsCard';
import { CommonHooks } from '../../../../../data';
@ -8,11 +9,15 @@ import SettingsBlock from '../../../../ReusableComponents/SettingsBlock';
import { ControlStaticValue } from '../../../../ReusableComponents/Controls';
const ConnectionStatus = () => {
const { merchant } = CommonHooks.useMerchantInfo();
const merchant = CommonHooks.useMerchant();
const className = classNames( 'ppcp-connection-details ppcp--value-list', {
'ppcp--type-business': merchant.isBusinessSeller,
'ppcp--type-casual': merchant.isCasualSeller,
} );
return (
<SettingsCard
className="ppcp-connection-details ppcp--value-list"
className={ className }
title={ __( 'Connection status', 'woocommerce-paypal-payments' ) }
description={ <ConnectionDescription /> }
>

View file

@ -16,6 +16,7 @@ import { useTodos } from '../../../../data/todos/hooks';
import { useMerchantInfo } from '../../../../data/common/hooks';
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../data/common';
import { STORE_NAME as TODOS_STORE_NAME } from '../../../../data/todos';
import { CommonHooks, TodosHooks } from '../../../../data';
import { getFeatures } from '../Components/Overview/features-config';
@ -23,8 +24,16 @@ import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../ReusableComponents/Icons';
import SpinnerOverlay from '../../../ReusableComponents/SpinnerOverlay';
const TabOverview = () => {
const { isReady: areTodosReady } = TodosHooks.useTodos();
const { isReady: merchantIsReady } = CommonHooks.useMerchantInfo();
if ( ! areTodosReady || ! merchantIsReady ) {
return <SpinnerOverlay asModal={ true } />;
}
return (
<div className="ppcp-r-tab-overview">
<OverviewTodos />

View file

@ -3,7 +3,7 @@ import { useCallback } from '@wordpress/element';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { PaymentMethodsBlock } from '../../../ReusableComponents/SettingsBlocks';
import { PaymentHooks } from '../../../../data';
import { CommonHooks, OnboardingHooks, PaymentHooks } from '../../../../data';
import { useActiveModal } from '../../../../data/common/hooks';
import Modal from '../Components/Payment/Modal';
@ -45,6 +45,9 @@ const TabPaymentMethods = () => {
[ changePaymentSettings, setActiveModal, setPersistent ]
);
const merchant = CommonHooks.useMerchant();
const { canUseCardPayments } = OnboardingHooks.useFlags();
return (
<div className="ppcp-r-payment-methods">
<PaymentMethodCard
@ -58,20 +61,22 @@ const TabPaymentMethods = () => {
methods={ methods.paypal }
onTriggerModal={ setActiveModal }
/>
<PaymentMethodCard
id="ppcp-card-payments-card"
title={ __(
'Online Card Payments',
'woocommerce-paypal-payments'
) }
description={ __(
'Select your preferred card payment options for efficient payment processing.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-online-methods.svg"
methods={ methods.cardPayment }
onTriggerModal={ setActiveModal }
/>
{ merchant.isBusinessSeller && canUseCardPayments && (
<PaymentMethodCard
id="ppcp-card-payments-card"
title={ __(
'Online Card Payments',
'woocommerce-paypal-payments'
) }
description={ __(
'Select your preferred card payment options for efficient payment processing.',
'woocommerce-paypal-payments'
) }
icon="icon-checkout-online-methods.svg"
methods={ methods.cardPayment }
onTriggerModal={ setActiveModal }
/>
) }
<PaymentMethodCard
id="ppcp-alternative-payments-card"
title={ __(

View file

@ -5,6 +5,7 @@ import { getSettingsTabs } from './Tabs';
const SettingsScreen = ( { activePanel, setActivePanel } ) => {
const tabs = getSettingsTabs();
const { Component } = tabs.find( ( tab ) => tab.name === activePanel );
return (
<>
<SettingsNavigation

View file

@ -8,7 +8,7 @@
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
@ -36,10 +36,6 @@ const useHooks = () => {
const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
'useManualConnection'
);
const merchant = useSelect(
( select ) => select( STORE_NAME ).merchant(),
[]
);
// Read-only properties.
const wooSettings = useSelect(
@ -78,7 +74,6 @@ const useHooks = () => {
productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
merchant,
wooSettings,
features,
webhooks,
@ -144,7 +139,8 @@ export const useWebhooks = () => {
};
export const useMerchantInfo = () => {
const { isReady, merchant, features } = useHooks();
const { isReady, features } = useHooks();
const merchant = useMerchant();
const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME );
const verifyLoginStatus = useCallback( async () => {
@ -175,6 +171,29 @@ export const useMerchantInfo = () => {
};
};
// Read-only access to the sanitized merchant details.
export const useMerchant = () => {
const merchant = useSelect(
( select ) => select( STORE_NAME ).merchant(),
[]
);
return useMemo(
() => ( {
isConnected: merchant.isConnected ?? false,
isSandbox: merchant.isSandbox ?? true,
id: merchant.id ?? '',
email: merchant.email ?? '',
clientId: merchant.clientId ?? '',
clientSecret: merchant.clientSecret ?? '',
isBusinessSeller: 'business' === merchant.sellerType,
isCasualSeller: 'personal' === merchant.sellerType,
} ),
// the merchant object is stable, so a new memo is only generated when a merchant prop changes.
[ merchant ]
);
};
export const useActiveModal = () => {
const { activeModal, setActiveModal } = useHooks();
return { activeModal, setActiveModal };

View file

@ -26,6 +26,7 @@ const defaultTransient = Object.freeze( {
email: '',
clientId: '',
clientSecret: '',
sellerType: 'unknown',
} ),
wooSettings: Object.freeze( {