🔀 Merge branch 'trunk'

This commit is contained in:
Philipp Stracker 2025-02-03 16:46:23 +01:00
commit 7cdeab40fe
No known key found for this signature in database
120 changed files with 2741 additions and 1126 deletions

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo } from '@wordpress/element';
import { useEffect, useMemo, useState } from '@wordpress/element';
import classNames from 'classnames';
import { OnboardingHooks, CommonHooks } from '../data';
@ -31,6 +31,8 @@ const SettingsApp = () => {
loading: ! onboardingIsReady,
} );
const [ activePanel, setActivePanel ] = useState( 'overview' );
const Content = useMemo( () => {
if ( ! onboardingIsReady || ! merchantIsReady ) {
return <SpinnerOverlay />;
@ -44,12 +46,18 @@ const SettingsApp = () => {
return <OnboardingScreen />;
}
return <SettingsScreen />;
return (
<SettingsScreen
activePanel={ activePanel }
setActivePanel={ setActivePanel }
/>
);
}, [
isSendOnlyCountry,
merchantIsReady,
onboardingCompleted,
onboardingIsReady,
activePanel,
] );
return <div className={ wrapperClass }>{ Content }</div>;

View file

@ -1,4 +1,4 @@
import data from '../../utils/data';
import { PPIcon } from './Icons';
const ImageBadge = ( { images } ) => {
if ( ! images || ! images.length ) {
@ -8,7 +8,13 @@ const ImageBadge = ( { images } ) => {
return (
<BadgeContent>
<span className="ppcp-r-badge-box__title-image-badge">
{ images.map( ( badge ) => data().getImage( badge ) ) }
{ images.map( ( badge, index ) => (
<PPIcon
key={ `badge-${ index }` }
imageName={ badge }
className="ppcp-r-badge-box__image"
/>
) ) }
</span>
</BadgeContent>
);

View file

@ -5,7 +5,7 @@ const ControlToggleButton = ( { label, description, value, onChange } ) => (
<Action>
<ToggleControl
className="ppcp--control-toggle"
__nextHasNoMarginBottom={ true }
__nextHasNoMarginBottom
checked={ value }
onChange={ onChange }
label={ label }

View file

@ -65,7 +65,9 @@ const OptionItem = ( {
<div className="ppcp--box-content">
<div className="ppcp--box-content-inner">
<span className="ppcp--box-title">{ itemTitle }</span>
<p className="ppcp--box-description">{ itemDescription }</p>
<div className="ppcp--box-description">
{ itemDescription }
</div>
{ children && (
<div className="ppcp--box-details">{ children }</div>
) }

View file

@ -0,0 +1,15 @@
import React from 'react';
const GenericIcon = ( { imageName, className = '', alt = '' } ) => {
const pathToImages = global.ppcpSettings.assets.imagesUrl;
return (
<img
className={ className }
alt={ alt }
src={ `${ pathToImages }${ imageName }` }
/>
);
};
export default GenericIcon;

View file

@ -1,6 +1,6 @@
import { SVG, Path } from '@wordpress/primitives';
const logoPayPal = (
const LogoPayPal = (
<SVG fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 38">
<Path
d="M109.583.683v27.359h-6.225V.683h6.225Zm-8.516 9.234v18.175h-5.534v-1.567c-.7.683-1.5 1.2-2.383 1.567a7.259 7.259 0 0 1-2.892.583c-1.3 0-2.508-.242-3.616-.725a9.216 9.216 0 0 1-2.892-2.067 10.021 10.021 0 0 1-1.958-3.05c-.459-1.183-.684-2.458-.684-3.816 0-1.359.225-2.617.684-3.775.483-1.184 1.133-2.217 1.958-3.092a8.708 8.708 0 0 1 2.892-2.033c1.108-.509 2.316-.767 3.616-.767 1.034 0 2 .192 2.892.583a7.312 7.312 0 0 1 2.383 1.567V9.933h5.534v-.016Zm-9.809 13.225c1.134 0 2.059-.384 2.784-1.167.75-.775 1.125-1.767 1.125-2.975 0-1.208-.375-2.208-1.125-2.975-.725-.775-1.659-1.167-2.784-1.167-1.125 0-2.075.384-2.825 1.167-.725.775-1.083 1.767-1.083 2.975 0 1.208.367 2.208 1.083 2.975.75.775 1.692 1.167 2.825 1.167ZM72.225.683c1.642 0 3.042.234 4.2.692 1.158.458 2.133 1.1 2.933 1.925a9.439 9.439 0 0 1 1.917 2.908c.458 1.092.683 2.267.683 3.525 0 1.259-.225 2.434-.683 3.525a9.293 9.293 0 0 1-1.917 2.909c-.791.825-1.775 1.466-2.933 1.925-1.158.458-2.558.691-4.2.691h-3v9.3h-6.333V.683h9.333Zm-.908 12.467c.85 0 1.491-.083 1.958-.258a3.853 3.853 0 0 0 1.192-.725c.65-.609.975-1.417.975-2.434 0-1.016-.325-1.825-.975-2.433a3.329 3.329 0 0 0-1.192-.692c-.458-.191-1.108-.291-1.958-.291h-2.1v6.833h2.1ZM39.558 9.917h6.875l4.667 8.716h.075l4.158-8.716H61.7l-13.642 27.4h-6.333l6.225-12.534-8.392-14.866Zm-1.225 0v18.175H32.8v-1.567c-.7.683-1.5 1.2-2.383 1.567a7.258 7.258 0 0 1-2.892.583c-1.3 0-2.508-.242-3.617-.725a9.218 9.218 0 0 1-2.891-2.067 10.18 10.18 0 0 1-1.959-3.05c-.458-1.183-.683-2.458-.683-3.816 0-1.359.225-2.617.683-3.775.484-1.184 1.134-2.217 1.959-3.092a8.626 8.626 0 0 1 2.891-2.033c1.109-.509 2.317-.767 3.617-.767 1.033 0 2 .192 2.892.583A7.312 7.312 0 0 1 32.8 11.5V9.933h5.533v-.016Zm-9.808 13.225c1.133 0 2.058-.384 2.792-1.167.75-.775 1.125-1.767 1.125-2.975 0-1.208-.375-2.208-1.125-2.975-.725-.775-1.659-1.167-2.792-1.167-1.133 0-2.075.384-2.825 1.167-.725.775-1.083 1.767-1.083 2.975 0 1.208.366 2.208 1.083 2.975.75.775 1.692 1.167 2.825 1.167ZM9.75.683c1.642 0 3.042.234 4.2.692 1.158.458 2.133 1.1 2.933 1.925A9.439 9.439 0 0 1 18.8 6.208c.458 1.092.683 2.267.683 3.525 0 1.259-.225 2.434-.683 3.525a9.293 9.293 0 0 1-1.917 2.909c-.791.825-1.775 1.466-2.933 1.925-1.158.458-2.558.691-4.2.691h-3v9.3H.417V.683H9.75Zm-.9 12.467c.85 0 1.492-.083 1.958-.258A3.855 3.855 0 0 0 12 12.167c.65-.609.975-1.417.975-2.434 0-1.016-.325-1.825-.975-2.433a3.33 3.33 0 0 0-1.192-.692c-.458-.191-1.108-.291-1.958-.291h-2.1v6.833h2.1Z"
@ -9,4 +9,4 @@ const logoPayPal = (
</SVG>
);
export default logoPayPal;
export default LogoPayPal;

View file

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

View file

@ -1,5 +1,6 @@
export { default as openSignup } from './open-signup';
export { default as logoPayPal } from './logo-paypal';
export { default as PPIcon } from './GenericIcon';
export { default as OpenSignup } from './OpenSignup';
export { default as LogoPayPal } from './LogoPayPal';
export const NOTIFICATION_SUCCESS = '✔️';
export const NOTIFICATION_ERROR = '❌';

View file

@ -16,7 +16,7 @@ const SettingsBlock = ( {
} );
return (
<div className={ blockClassName }>
<div className={ blockClassName } id={ className }>
<BlockTitle
blockTitle={ title }
blockSuffix={ titleSuffix }

View file

@ -14,10 +14,12 @@ const PaymentMethodItemBlock = ( {
<SettingsBlock className="ppcp--method-item" separatorAndGap={ false }>
<div className="ppcp--method-inner">
<div className="ppcp--method-title-wrapper">
<PaymentMethodIcon
icons={ [ paymentMethod.icon ] }
type={ paymentMethod.icon }
/>
{ paymentMethod?.icon && (
<PaymentMethodIcon
icons={ [ paymentMethod.icon ] }
type={ paymentMethod.icon }
/>
) }
<span className="ppcp--method-title">
{ paymentMethod.itemTitle }
</span>
@ -27,7 +29,7 @@ const PaymentMethodItemBlock = ( {
</p>
<div className="ppcp--method-footer">
<ToggleControl
__nextHasNoMarginBottom={ true }
__nextHasNoMarginBottom
checked={ isSelected }
onChange={ onSelect }
/>

View file

@ -1,3 +1,5 @@
import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
const TodoSettingsBlock = ( { todosData, className = '' } ) => {
if ( todosData.length === 0 ) {
return null;
@ -7,29 +9,50 @@ const TodoSettingsBlock = ( { todosData, className = '' } ) => {
<div
className={ `ppcp-r-settings-block__todo ppcp-r-todo-items ${ className }` }
>
{ todosData
.slice( 0, 5 )
.filter( ( todo ) => {
return ! todo.isCompleted();
} )
.map( ( todo ) => (
<TodoItem
key={ todo.id }
title={ todo.title }
onClick={ todo.onClick }
/>
) ) }
{ todosData.slice( 0, 5 ).map( ( todo ) => (
<TodoItem
key={ todo.id }
title={ todo.title }
description={ todo.description }
isCompleted={ todo.isCompleted }
onClick={ async () => {
if ( todo.action.type === 'tab' ) {
const tabId =
TAB_IDS[ todo.action.tab.toUpperCase() ];
await selectTab( tabId, todo.action.section );
} else if ( todo.action.type === 'external' ) {
window.open( todo.action.url, '_blank' );
}
} }
/>
) ) }
</div>
);
};
const TodoItem = ( props ) => {
const TodoItem = ( { title, description, isCompleted, onClick } ) => {
return (
<div className="ppcp-r-todo-item" onClick={ props.onClick }>
<div
className={ `ppcp-r-todo-item ${
isCompleted ? 'is-completed' : ''
}` }
onClick={ onClick }
>
<div className="ppcp-r-todo-item__inner">
<div className="ppcp-r-todo-item__icon"></div>
<div className="ppcp-r-todo-item__description">
{ props.title }
<div className="ppcp-r-todo-item__icon">
{ isCompleted && (
<span className="dashicons dashicons-yes"></span>
) }
</div>
<div className="ppcp-r-todo-item__content">
<div className="ppcp-r-todo-item__description">
{ title }
</div>
{ description && (
<div className="ppcp-r-todo-item__secondary-description">
{ description }
</div>
) }
</div>
</div>
</div>

View file

@ -43,6 +43,7 @@ const SettingsToggleBlock = ( {
</div>
<div className="ppcp-r-toggle-block__switch">
<ToggleControl
__nextHasNoMarginBottom
ref={ toggleRef }
checked={ isToggled }
onChange={ ( newState ) => setToggled( newState ) }

View file

@ -1,26 +1,14 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useCallback, useEffect } from '@wordpress/element';
// TODO: Migrate to Tabs (TabPanel v2) once its API is publicly available, as it provides programmatic tab switching support: https://github.com/WordPress/gutenberg/issues/52997
import { TabPanel } from '@wordpress/components';
import { getQuery, updateQueryString } from '../../utils/navigation';
const TabNavigation = ( { tabs } ) => {
const { panel } = getQuery();
import { updateQueryString } from '../../utils/navigation';
const TabBar = ( { tabs, activePanel, setActivePanel } ) => {
const isValidTab = ( tabsList, checkTab ) => {
return tabsList.some( ( tab ) => tab.name === checkTab );
};
const getValidInitialPanel = () => {
if ( ! panel || ! isValidTab( tabs, panel ) ) {
return tabs[ 0 ].name;
}
return panel;
};
const [ activePanel, setActivePanel ] = useState( getValidInitialPanel );
const updateActivePanel = useCallback(
( tabName ) => {
if ( isValidTab( tabs, tabName ) ) {
@ -29,7 +17,7 @@ const TabNavigation = ( { tabs } ) => {
console.warn( `Invalid tab name: ${ tabName }` );
}
},
[ tabs ]
[ tabs, setActivePanel ]
);
useEffect( () => {
@ -43,9 +31,9 @@ const TabNavigation = ( { tabs } ) => {
onSelect={ updateActivePanel }
tabs={ tabs }
>
{ ( { Component } ) => Component }
{ () => '' }
</TabPanel>
);
};
export default TabNavigation;
export default TabBar;

View file

@ -15,6 +15,7 @@ const TopNavigation = ( {
onTitleClick = null,
showProgressBar = false,
progressBarPercent = 0,
subNavigation = null,
} ) => {
const { goToWooCommercePaymentsTab } = useNavigation();
const { isScrolled } = useIsScrolled();
@ -40,35 +41,43 @@ const TopNavigation = ( {
}, [] );
return (
<div className={ className }>
<div className="ppcp-r-navigation">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! exitOnTitleClick }
>
<Button
variant="link"
onClick={ handleTitleClick }
className="is-title"
<>
<nav className={ className }>
<div className="ppcp-r-navigation">
<BusyStateWrapper
className="ppcp-r-navigation--left"
busySpinner={ false }
enabled={ ! exitOnTitleClick }
>
<Icon icon={ chevronLeft } />
<span className={ titleClassName }>{ title }</span>
</Button>
</BusyStateWrapper>
<Button
variant="link"
onClick={ handleTitleClick }
className="is-title"
>
<Icon icon={ chevronLeft } />
<span className={ titleClassName }>{ title }</span>
</Button>
</BusyStateWrapper>
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
{ children }
</BusyStateWrapper>
<BusyStateWrapper
className="ppcp-r-navigation--right"
busySpinner={ false }
>
{ children }
</BusyStateWrapper>
</div>
{ subNavigation && (
<section className="ppcp--top-sub-navigation">
{ subNavigation }
</section>
) }
{ showProgressBar && (
<ProgressBar percent={ progressBarPercent } />
) }
</div>
</div>
</nav>
</>
);
};

View file

@ -145,7 +145,7 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments'
) }
imageBadge={ [
'icon-button-sepa.svg',
// 'icon-button-sepa.svg',
'icon-button-ideal.svg',
'icon-button-blik.svg',
'icon-button-bancontact.svg',
@ -211,7 +211,7 @@ const AcdcOptionalPaymentMethods = ( {
'woocommerce-paypal-payments'
) }
imageBadge={ [
'icon-button-sepa.svg',
// 'icon-button-sepa.svg',
'icon-button-ideal.svg',
'icon-button-blik.svg',
'icon-button-bancontact.svg',

View file

@ -1,7 +1,7 @@
import { Button } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import classNames from 'classnames';
import { openSignup } from '../../../ReusableComponents/Icons';
import { OpenSignup } from '../../../ReusableComponents/Icons';
import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
@ -27,7 +27,7 @@ const ButtonOrPlaceholder = ( {
const buttonProps = {
className,
variant,
icon: showIcon ? openSignup : null,
icon: showIcon ? OpenSignup : null,
};
if ( href ) {

View file

@ -21,9 +21,37 @@ const OnboardingNavigation = ( { stepDetails, onNext, onPrev } ) => {
showProgressBar={ true }
progressBarPercent={ percentage * 0.9 }
>
<Button variant="link" onClick={ goToWooCommercePaymentsTab }>
<OnboardingNavigationActions
onExit={ goToWooCommercePaymentsTab }
isFirst={ isFirst }
isDisabled={ isDisabled }
showNext={ showNext }
onNext={ onNext }
/>
</TopNavigation>
);
};
export default OnboardingNavigation;
const OnboardingNavigationActions = ( {
isFirst,
showNext,
isDisabled,
onExit,
onNext,
} ) => {
// On first page we don't have any actions.
if ( isFirst ) {
return null;
}
return (
<>
<Button variant="link" onClick={ onExit }>
{ __( 'Save and exit', 'woocommerce-paypal-payments' ) }
</Button>
{ showNext && (
<Button
variant="primary"
@ -33,8 +61,6 @@ const OnboardingNavigation = ( { stepDetails, onNext, onPrev } ) => {
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</Button>
) }
</TopNavigation>
</>
);
};
export default OnboardingNavigation;

View file

@ -1,13 +1,13 @@
import { Icon } from '@wordpress/components';
import { logoPayPal } from '../../../ReusableComponents/Icons';
import { LogoPayPal } from '../../../ReusableComponents/Icons';
const OnboardingHeader = ( props ) => {
return (
<section className="ppcp-r-onboarding-header">
<div className="ppcp-r-onboarding-header__logo">
<div className="ppcp-r-onboarding-header__logo-wrapper">
<Icon icon={ logoPayPal } width="auto" height={ 38 } />
<Icon icon={ LogoPayPal } width={ 110 } height={ 38 } />
</div>
</div>
<div className="ppcp-r-onboarding-header__content">

View file

@ -1,7 +1,16 @@
import React, { useEffect } from 'react';
import { PayLaterMessagingHooks } from '../../../data';
const TabPayLaterMessaging = () => {
const config = {}; // Replace with the appropriate/saved configuration.
const {
config,
setCart,
setCheckout,
setProduct,
setShop,
setHome,
setCustom_placement,
} = PayLaterMessagingHooks.usePayLaterMessaging();
const PcpPayLaterConfigurator =
window.ppcpSettings?.PcpPayLaterConfigurator;
@ -27,17 +36,16 @@ const TabPayLaterMessaging = () => {
subheader: 'ppcp-r-paylater-configurator__subheader',
},
onSave: ( data ) => {
/*
TODO:
- The saving will be handled in a separate PR.
- One option could be:
- When saving the settings, programmatically click on the configurator's
"Save Changes" button and send the request to PHP.
*/
setCart( data.config.cart );
setCheckout( data.config.checkout );
setProduct( data.config.product );
setShop( data.config.shop );
setHome( data.config.home );
setCustom_placement( data.config.custom_placement );
},
} );
}
}, [ PcpPayLaterConfigurator ] );
}, [ PcpPayLaterConfigurator, config ] );
return (
<div

View file

@ -3,14 +3,30 @@ import { __ } from '@wordpress/i18n';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
import { useSaveSettings } from '../../../../hooks/useSaveSettings';
import TabBar from '../../../ReusableComponents/TabBar';
const SettingsNavigation = ( { canSave = true } ) => {
const SettingsNavigation = ( {
canSave = true,
tabs,
activePanel,
setActivePanel,
} ) => {
const { persistAll } = useSaveSettings();
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
return (
<TopNavigation title={ title } exitOnTitleClick={ true }>
<TopNavigation
title={ title }
exitOnTitleClick={ true }
subNavigation={
<TabBar
tabs={ tabs }
activePanel={ activePanel }
setActivePanel={ setActivePanel }
/>
}
>
{ canSave && (
<Button variant="primary" onClick={ persistAll }>
{ __( 'Save', 'woocommerce-paypal-payments' ) }

View file

@ -18,11 +18,9 @@ export const getFeatures = ( setActiveModal ) => {
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-paypal-checkout-card'
).then( () => {
setActiveModal( 'paypal' );
} );
TAB_IDS.SETTINGS,
'ppcp--save-payment-methods'
);
},
showWhen: 'enabled',
class: 'small-button',
@ -68,9 +66,7 @@ export const getFeatures = ( setActiveModal ) => {
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal(
'advanced_credit_and_debit_card_payments'
);
setActiveModal( 'ppcp-credit-card-gateway' );
} );
},
showWhen: 'enabled',
@ -149,7 +145,7 @@ export const getFeatures = ( setActiveModal ) => {
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'google_pay' );
setActiveModal( 'ppcp-googlepay' );
} );
},
showWhen: 'enabled',
@ -196,7 +192,7 @@ export const getFeatures = ( setActiveModal ) => {
TAB_IDS.PAYMENT_METHODS,
'ppcp-card-payments-card'
).then( () => {
setActiveModal( 'apple_pay' );
setActiveModal( 'ppcp-applepay' );
} );
},
showWhen: 'enabled',
@ -239,7 +235,6 @@ export const getFeatures = ( setActiveModal ) => {
const countryData = payLaterMessaging[ storeCountry ] || {};
// Add "Pay Later Messaging" to the feature list, if it's available.
if (
!! window.ppcpSettings?.isPayLaterConfiguratorAvailable &&
countryData
@ -256,12 +251,7 @@ export const getFeatures = ( setActiveModal ) => {
type: 'secondary',
text: __( 'Configure', 'woocommerce-paypal-payments' ),
onClick: () => {
selectTab(
TAB_IDS.PAYMENT_METHODS,
'ppcp-paypal-checkout-card'
).then( () => {
setActiveModal( 'paypal' );
} );
selectTab( TAB_IDS.PAY_LATER_MESSAGING );
},
showWhen: 'enabled',
class: 'small-button',

View file

@ -11,7 +11,7 @@ import PaymentMethodModal from '../../../../ReusableComponents/PaymentMethodModa
import { PaymentHooks } from '../../../../../data';
const Modal = ( { method, setModalIsVisible, onSave } ) => {
const { paymentMethods } = PaymentHooks.usePaymentMethods();
const { all: paymentMethods } = PaymentHooks.usePaymentMethods();
const {
paypalShowLogo,
threeDSecure,
@ -64,9 +64,9 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
switch ( field.type ) {
case 'text':
return (
<div className="ppcp-r-modal__field-row">
<div key={ key } className="ppcp-r-modal__field-row">
<TextControl
__nextHasNoMarginBottom={ true }
__nextHasNoMarginBottom
className="ppcp-r-vertical-text-control"
label={ field.label }
value={ settings[ key ] }
@ -82,8 +82,9 @@ const Modal = ( { method, setModalIsVisible, onSave } ) => {
case 'toggle':
return (
<div className="ppcp-r-modal__field-row">
<div key={ key } className="ppcp-r-modal__field-row">
<ToggleControl
__nextHasNoMarginBottom
label={ field.label }
checked={ settings[ key ] }
onChange={ ( value ) =>

View file

@ -12,10 +12,10 @@ import {
import { Content, ContentWrapper } from '../../../ReusableComponents/Elements';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge';
import { useTodos } from '../../../../data/todos/hooks';
import { useMerchantInfo } from '../../../../data/common/hooks';
import { STORE_NAME } from '../../../../data/common';
import { getFeatures } from '../Components/Overview/features-config';
import { todosData } from '../todo-items';
import {
NOTIFICATION_ERROR,
@ -23,9 +23,14 @@ import {
} from '../../../ReusableComponents/Icons';
const TabOverview = () => {
const { todos, isReady: areTodosReady } = useTodos();
// Don't render todos section until data is ready
const showTodos = areTodosReady && todos.length > 0;
return (
<div className="ppcp-r-tab-overview">
{ todosData.length > 0 && (
{ showTodos && (
<SettingsCard
className="ppcp-r-tab-overview-todo"
title={ __(
@ -37,7 +42,7 @@ const TabOverview = () => {
'woocommerce-paypal-payments'
) }
>
<TodoSettingsBlock todosData={ todosData } />
<TodoSettingsBlock todosData={ todos } />
</SettingsCard>
) }

View file

@ -1,17 +1,18 @@
import Container from '../../ReusableComponents/Container';
import TabNavigation from '../../ReusableComponents/TabNavigation';
import { getSettingsTabs } from './Tabs';
import SettingsNavigation from './Components/Navigation';
import { getSettingsTabs } from './Tabs';
const SettingsScreen = () => {
const SettingsScreen = ( { activePanel, setActivePanel } ) => {
const tabs = getSettingsTabs();
const { Component } = tabs.find( ( tab ) => tab.name === activePanel );
return (
<>
<SettingsNavigation />
<Container page="settings">
<TabNavigation tabs={ tabs }></TabNavigation>
</Container>
<SettingsNavigation
tabs={ tabs }
activePanel={ activePanel }
setActivePanel={ setActivePanel }
/>
<Container page="settings">{ Component }</Container>
</>
);
};

View file

@ -34,7 +34,7 @@ const useHooks = () => {
};
};
export const useState = () => {
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
};

View file

@ -4,16 +4,24 @@ import {
PaymentStoreName,
SettingsStoreName,
StylingStoreName,
TodosStoreName,
} from './index';
import { setCompleted } from './onboarding/actions';
export const addDebugTools = ( context, modules ) => {
if ( ! context || ! context?.debug ) {
if ( ! context ) {
return;
}
/*
// TODO - enable this condition for version 3.0.1
// In version 3.0.0 we want to have the debug tools available on every installation
if ( ! context.debug ) { return }
*/
const debugApi = ( window.ppcpDebugger = window.ppcpDebugger || {} );
// Dump the current state of all our Redux stores.
context.dumpStore = async () => {
debugApi.dumpStore = async () => {
/* eslint-disable no-console */
if ( ! console?.groupCollapsed ) {
console.error( 'console.groupCollapsed is not supported.' );
@ -41,7 +49,7 @@ export const addDebugTools = ( context, modules ) => {
};
// Reset all Redux stores to their initial state.
context.resetStore = () => {
debugApi.resetStore = () => {
const stores = [];
const { isConnected } = wp.data.select( CommonStoreName ).merchant();
@ -56,6 +64,7 @@ export const addDebugTools = ( context, modules ) => {
stores.push( PaymentStoreName );
stores.push( SettingsStoreName );
stores.push( StylingStoreName );
stores.push( TodosStoreName );
} else {
// Only reset the common & onboarding stores to restart the onboarding wizard.
stores.push( CommonStoreName );
@ -68,13 +77,17 @@ export const addDebugTools = ( context, modules ) => {
// eslint-disable-next-line no-console
console.log( `Reset store: ${ storeName }...` );
store.reset();
store.persist();
try {
store.reset();
store.persist();
} catch ( error ) {
console.error( ' ... Reset failed, skipping this store' );
}
} );
};
// Disconnect the merchant and display the onboarding wizard.
context.disconnect = () => {
debugApi.disconnect = () => {
const common = wp.data.dispatch( CommonStoreName );
common.disconnectMerchant();
@ -86,10 +99,13 @@ export const addDebugTools = ( context, modules ) => {
};
// Enters or completes the onboarding wizard without changing anything else.
context.onboardingMode = ( state ) => {
debugApi.onboardingMode = ( state ) => {
const onboarding = wp.data.dispatch( OnboardingStoreName );
onboarding.setCompleted( ! state );
onboarding.persist();
};
// Expose original debug API.
Object.assign( context, debugApi );
};

View file

@ -4,8 +4,18 @@ import * as Common from './common';
import * as Payment from './payment';
import * as Settings from './settings';
import * as Styling from './styling';
import * as Todos from './todos';
import * as PayLaterMessaging from './pay-later-messaging';
const stores = [ Onboarding, Common, Payment, Settings, Styling ];
const stores = [
Onboarding,
Common,
Payment,
Settings,
Styling,
Todos,
PayLaterMessaging,
];
stores.forEach( ( store ) => {
try {
@ -28,12 +38,16 @@ export const CommonHooks = Common.hooks;
export const PaymentHooks = Payment.hooks;
export const SettingsHooks = Settings.hooks;
export const StylingHooks = Styling.hooks;
export const TodosHooks = Todos.hooks;
export const PayLaterMessagingHooks = PayLaterMessaging.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME;
export const PaymentStoreName = Payment.STORE_NAME;
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 * from './configuration';

View file

@ -0,0 +1,18 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data.
SET_TRANSIENT: 'PAY_LATER_MESSAGING:SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'PAY_LATER_MESSAGING:SET_PERSISTENT',
RESET: 'PAY_LATER_MESSAGING:RESET',
HYDRATE: 'PAY_LATER_MESSAGING:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'PAY_LATER_MESSAGING:DO_PERSIST_DATA',
};

View file

@ -0,0 +1,80 @@
/**
* 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, Persistent, or Side effect.
*
* @file
*/
import { select } from '@wordpress/data';
import ACTION_TYPES from './action-types';
import { STORE_NAME } 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.
*/
/**
* Special. Resets all values in the store to initial defaults.
*
* @return {Action} The action.
*/
export const reset = () => ( { type: ACTION_TYPES.RESET } );
/**
* Persistent. 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 },
} );
/**
* Generic persistent-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 setPersistent = ( prop, value ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
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 );
/**
* Side effect. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};

View file

@ -0,0 +1,28 @@
/**
* Name of the Redux store module.
*
* Used by: Reducer, Selector, Index
*
* @type {string}
*/
export const STORE_NAME = 'wc/paypal/pay_later_messaging';
/**
* REST path to hydrate data of this module by loading data from the WP DB.
*
* Used by: Resolvers
* See: PayLaterMessagingEndpoint.php
*
* @type {string}
*/
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/pay_later_messaging';
/**
* REST path to persist data of this module to the WP DB.
*
* Used by: Controls
* See: PayLaterMessagingEndpoint.php
*
* @type {string}
*/
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/pay_later_messaging';

View file

@ -0,0 +1,23 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import { REST_PERSIST_PATH } from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -0,0 +1,89 @@
/**
* Hooks: Provide the main API for components to interact with the store.
*
* These encapsulate store interactions, offering a consistent interface.
* Hooks simplify data access and manipulation for components.
*
* @file
*/
import { useDispatch } from '@wordpress/data';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
const useHooks = () => {
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
// Nothing here yet.
// Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
// Persistent accessors.
const [ cart, setCart ] = usePersistent( 'cart' );
const [ checkout, setCheckout ] = usePersistent( 'checkout' );
const [ product, setProduct ] = usePersistent( 'product' );
const [ shop, setShop ] = usePersistent( 'shop' );
const [ home, setHome ] = usePersistent( 'home' );
const [ custom_placement, setCustom_placement ] =
usePersistent( 'custom_placement' );
return {
persist,
isReady,
cart,
setCart,
checkout,
setCheckout,
product,
setProduct,
shop,
setShop,
home,
setHome,
custom_placement,
setCustom_placement,
};
};
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
};
export const usePayLaterMessaging = () => {
const {
cart,
setCart,
checkout,
setCheckout,
product,
setProduct,
shop,
setShop,
home,
setHome,
custom_placement,
setCustom_placement,
} = useHooks();
return {
config: {
cart,
checkout,
product,
shop,
home,
custom_placement,
},
setCart,
setCheckout,
setProduct,
setShop,
setHome,
setCustom_placement,
};
};

View file

@ -0,0 +1,32 @@
import { createReduxStore, register } from '@wordpress/data';
import { controls as wpControls } from '@wordpress/data-controls';
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 { resolvers } from './resolvers';
import { controls } from './controls';
/**
* 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,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
return Boolean( wp.data.select( STORE_NAME ) );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,60 @@
/**
* Reducer: Defines store structure and state updates for this 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).
const defaultTransient = Object.freeze( {
isReady: false,
} );
// Persistent: Values that are loaded from the DB.
const defaultPersistent = Object.freeze( {
cart: {},
checkout: {},
product: {},
shop: {},
home: {},
custom_placement: [],
} );
// Reducer logic.
const [ changeTransient, changePersistent ] = createReducerSetters(
defaultTransient,
defaultPersistent
);
const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
changePersistent( state, payload ),
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = changeTransient(
changePersistent( state, defaultPersistent ),
defaultTransient
);
// Keep "read-only" details and initialization flags.
cleanState.isReady = true;
return cleanState;
},
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
} );
export default reducer;

View file

@ -0,0 +1,37 @@
/**
* 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 { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
// TODO: Add the module name to the error message.
__(
'Error retrieving Pay Later Messaging config details.',
'woocommerce-paypal-payments'
)
);
}
},
};

View file

@ -0,0 +1,21 @@
/**
* 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 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;
};

View file

@ -8,6 +8,7 @@ import * as actions from './actions';
import * as hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
import { initTodoSync } from '../sync/todo-state-sync';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -26,6 +27,9 @@ export const initStore = () => {
register( store );
// Initialize todo sync after store registration. Potentially should be moved elsewhere.
initTodoSync();
return Boolean( wp.data.select( STORE_NAME ) );
};

View file

@ -21,25 +21,28 @@ const useHooks = () => {
// Persistent accessors.
const [ invoicePrefix, setInvoicePrefix ] =
usePersistent( 'invoicePrefix' );
const [ brandName, setBrandName ] = usePersistent( 'brandName' );
const [ softDescriptor, setSoftDescriptor ] =
usePersistent( 'softDescriptor' );
const [ subtotalAdjustment, setSubtotalAdjustment ] =
usePersistent( 'subtotalAdjustment' );
const [ landingPage, setLandingPage ] = usePersistent( 'landingPage' );
const [ buttonLanguage, setButtonLanguage ] =
usePersistent( 'buttonLanguage' );
const [ authorizeOnly, setAuthorizeOnly ] =
usePersistent( 'authorizeOnly' );
const [ captureVirtualOnlyOrders, setCaptureVirtualOnlyOrders ] =
usePersistent( 'captureVirtualOnlyOrders' );
usePersistent( 'captureVirtualOrders' );
const [ savePaypalAndVenmo, setSavePaypalAndVenmo ] =
usePersistent( 'savePaypalAndVenmo' );
const [ saveCardDetails, setSaveCardDetails ] =
usePersistent( 'saveCardDetails' );
const [ payNowExperience, setPayNowExperience ] =
usePersistent( 'payNowExperience' );
const [ logging, setLogging ] = usePersistent( 'logging' );
const [ subtotalAdjustment, setSubtotalAdjustment ] =
usePersistent( 'subtotalAdjustment' );
const [ brandName, setBrandName ] = usePersistent( 'brandName' );
const [ softDescriptor, setSoftDescriptor ] =
usePersistent( 'softDescriptor' );
const [ landingPage, setLandingPage ] = usePersistent( 'landingPage' );
const [ buttonLanguage, setButtonLanguage ] =
usePersistent( 'buttonLanguage' );
usePersistent( 'enablePayNow' );
const [ logging, setLogging ] = usePersistent( 'enableLogging' );
const [ disabledCards, setDisabledCards ] =
usePersistent( 'disabledCards' );

View file

@ -25,18 +25,25 @@ const defaultTransient = Object.freeze( {
* These represent the core PayPal payment settings configuration.
*/
const defaultPersistent = Object.freeze( {
// String values.
invoicePrefix: '', // Prefix for PayPal invoice IDs
authorizeOnly: false, // Whether to only authorize payments initially
captureVirtualOnlyOrders: false, // Auto-capture virtual-only orders
savePaypalAndVenmo: false, // Enable PayPal & Venmo vaulting
saveCardDetails: false, // Enable card vaulting
payNowExperience: false, // Enable Pay Now experience
logging: false, // Enable debug logging
subtotalAdjustment: 'skip_details', // Handling for subtotal mismatches
brandName: '', // Merchant brand name for PayPal
softDescriptor: '', // Payment descriptor on statements
landingPage: 'any', // PayPal checkout landing page
// Limited value strings.
subtotalAdjustment: 'no_details', // [correction|no_details] Handling for subtotal mismatches
landingPage: 'any', // [any|login|guest_checkout] PayPal checkout landing page
buttonLanguage: '', // Language for PayPal buttons
// Boolean flags.
authorizeOnly: false, // Whether to only authorize payments initially
captureVirtualOrders: false, // Auto-capture virtual-only orders
savePaypalAndVenmo: false, // Enable PayPal & Venmo vaulting
saveCardDetails: false, // Enable card vaulting
enablePayNow: false, // Enable Pay Now experience
enableLogging: false, // Enable debug logging
// String arrays.
disabledCards: [], // Disabled credit card types
} );

View file

@ -0,0 +1,67 @@
import { subscribe, select, dispatch } from '@wordpress/data';
const TODO_TRIGGERS = {
'ppcp-applepay': 'enable_apple_pay',
'ppcp-googlepay': 'enable_google_pay',
'ppcp-axo-gateway': 'enable_fastlane',
'ppcp-card-button-gateway': 'enable_credit_debit_cards',
};
/**
* Initialize todo synchronization
*/
export const initTodoSync = () => {
let previousPaymentState = null;
let isProcessing = false;
subscribe( () => {
if ( isProcessing ) {
return;
}
isProcessing = true;
try {
const paymentState = select( 'wc/paypal/payment' ).persistentData();
const todosState = select( 'wc/paypal/todos' ).getTodos();
// Skip if states haven't been initialized yet
if ( ! paymentState || ! todosState || ! previousPaymentState ) {
previousPaymentState = paymentState;
return;
}
Object.entries( TODO_TRIGGERS ).forEach(
( [ paymentMethod, todoId ] ) => {
const wasEnabled =
previousPaymentState[ paymentMethod ]?.enabled;
const isEnabled = paymentState[ paymentMethod ]?.enabled;
if ( wasEnabled !== isEnabled ) {
const todoToUpdate = todosState.find(
( todo ) => todo.id === todoId
);
if ( todoToUpdate ) {
const updatedTodos = todosState.map( ( todo ) =>
todo.id === todoId
? { ...todo, isCompleted: isEnabled }
: todo
);
dispatch( 'wc/paypal/todos' ).setTodos(
updatedTodos
);
}
}
}
);
previousPaymentState = paymentState;
} catch ( error ) {
console.error( 'Error in todo sync:', error );
} finally {
isProcessing = false;
}
} );
};

View file

@ -0,0 +1,16 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data
SET_TRANSIENT: 'TODOS:SET_TRANSIENT',
// Persistent data
SET_TODOS: 'TODOS:SET_TODOS',
// Controls
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
};

View file

@ -0,0 +1,24 @@
/**
* 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, Persistent, or Side effect.
*
* @file
*/
import ACTION_TYPES from './action-types';
export const setIsReady = ( isReady ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { isReady },
} );
export const setTodos = ( todos ) => ( {
type: ACTION_TYPES.SET_TODOS,
payload: todos,
} );
export const fetchTodos = function* () {
yield { type: ACTION_TYPES.DO_FETCH_TODOS };
};

View file

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

View file

@ -0,0 +1,22 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import { REST_PATH } from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_FETCH_TODOS ]() {
const response = await apiFetch( {
path: REST_PATH,
method: 'GET',
} );
return response?.data || [];
},
};

View file

@ -0,0 +1,33 @@
/**
* Hooks: Provide the main API for components to interact with the 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 { STORE_NAME } from './constants';
const useTransient = ( key ) =>
useSelect(
( select ) => select( STORE_NAME ).transientData()?.[ key ],
[ key ]
);
export const useTodos = () => {
const todos = useSelect(
( select ) => select( STORE_NAME ).getTodos(),
[]
);
const isReady = useTransient( 'isReady' );
const { fetchTodos } = useDispatch( STORE_NAME );
return {
todos,
isReady,
fetchTodos,
};
};

View file

@ -0,0 +1,32 @@
import { createReduxStore, register } from '@wordpress/data';
import { controls as wpControls } from '@wordpress/data-controls';
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 { resolvers } from './resolvers';
import { controls } from './controls';
/**
* Initializes and registers the todos 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,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
return Boolean( wp.data.select( STORE_NAME ) );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,90 @@
/**
* Reducer: Defines store structure and state updates for todos 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 todos configuration.
*/
const defaultPersistent = Object.freeze( {
todos: [],
} );
// 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 todos list
*
* @param {Object} state Current state
* @param {Object} payload Update payload
* @return {Object} Updated state
*/
[ ACTION_TYPES.SET_TODOS ]: ( state, payload ) => {
return changePersistent( state, { todos: payload } );
},
/**
* Resets state to defaults while maintaining initialization status
*
* @param {Object} state Current state
* @return {Object} Reset state
*/
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = changeTransient(
changePersistent( state, defaultPersistent ),
defaultTransient
);
cleanState.isReady = true; // Keep initialization flag
return cleanState;
},
/**
* 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 todos data to hydrate
* @return {Object} Hydrated state
*/
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
} );
export default reducer;

View file

@ -0,0 +1,35 @@
/**
* 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 { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_PATH } from './constants';
export const resolvers = {
*getTodos() {
try {
const response = yield apiFetch( { path: REST_PATH } );
// Make sure we're accessing the correct part of the response
const todos = response?.data || [];
yield dispatch( STORE_NAME ).setTodos( todos );
yield dispatch( STORE_NAME ).setIsReady( true );
} catch ( e ) {
console.error( 'Resolver error:', e );
yield dispatch( STORE_NAME ).setIsReady( false );
yield dispatch( 'core/notices' ).createErrorNotice(
__( 'Error retrieving todos.', 'woocommerce-paypal-payments' )
);
}
},
};

View file

@ -0,0 +1,28 @@
/**
* 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 getTodos = ( state ) => {
// Access todos directly from state first
const todos = state?.todos || persistentData( state ).todos || EMPTY_ARR;
return todos;
};

View file

@ -2,6 +2,7 @@ import { useCallback } from '@wordpress/element';
import {
CommonHooks,
PayLaterMessagingHooks,
PaymentHooks,
SettingsHooks,
StylingHooks,
@ -13,8 +14,13 @@ export const useSaveSettings = () => {
const { persist: persistPayment } = PaymentHooks.useStore();
const { persist: persistSettings } = SettingsHooks.useStore();
const { persist: persistStyling } = StylingHooks.useStore();
const { persist: persistPayLaterMessaging } =
PayLaterMessagingHooks.useStore();
const persistAll = useCallback( () => {
// Executes onSave on TabPayLaterMessaging component.
document.getElementById( 'configurator-publishButton' )?.click();
withActivity(
'persist-methods',
'Save payment methods',
@ -30,7 +36,18 @@ export const useSaveSettings = () => {
'Save styling details',
persistStyling
);
}, [ persistPayment, persistSettings, persistStyling, withActivity ] );
withActivity(
'persist-pay-later-messaging',
'Save pay later messaging details',
persistPayLaterMessaging
);
}, [
persistPayment,
persistSettings,
persistStyling,
persistPayLaterMessaging,
withActivity,
] );
return { persistAll };
};

View file

@ -2,139 +2,139 @@ export const countryPriceInfo = {
US: {
fixedFee: {
USD: 0.49,
GBP: 0.39,
CAD: 0.59,
AUD: 0.59,
EUR: 0.39,
GBP: 0.39,
CAD: 0.59,
AUD: 0.59,
EUR: 0.39,
},
checkout: 3.49,
plater: 4.99,
ccf: {
percentage: 2.59,
fixedFee: 0.29,
},
plater: 4.99,
ccf: {
percentage: 2.59,
fixedFee: 0.29,
},
dw: {
percentage: 2.59,
fixedFee: 0.29,
},
percentage: 2.59,
fixedFee: 0.29,
},
apm: {
percentage: 2.89,
fixedFee: 0.29,
},
fast: {
percentage: 2.59,
fixedFee: 0.29,
},
percentage: 2.89,
fixedFee: 0.29,
},
fast: {
percentage: 2.59,
fixedFee: 0.29,
},
standardCardFields: 2.99,
},
UK: {
GB: {
fixedFee: {
GPB: 0.3,
USD: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
},
checkout: 2.9,
plater: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
CA: {
fixedFee: {
CAD: 0.3,
USD: 0.3,
GBP: 0.2,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.2,
AUD: 0.3,
EUR: 0.35,
},
checkout: 2.9,
ccf: 2.7,
dw: 2.7,
fast: 2.7,
fast: 2.7,
apm: 2.9,
standardCardFields: 2.9,
},
AU: {
fixedFee: {
AUD: 0.3,
USD: 0.3,
GBP: 0.2,
CAD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.2,
CAD: 0.3,
EUR: 0.35,
},
checkout: 2.6,
plater: 2.6,
plater: 2.6,
ccf: 1.75,
dw: 1.75,
fast: 1.75,
fast: 1.75,
apm: 2.6,
standardCardFields: 2.6,
},
FR: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 2.9,
plater: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
IT: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 3.4,
plater: 3.4,
plater: 3.4,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},
DE: {
fixedFee: {
EUR: 0.39,
USD: 0.49,
GBP: 0.29,
CAD: 0.59,
AUD: 0.59,
USD: 0.49,
GBP: 0.29,
CAD: 0.59,
AUD: 0.59,
},
checkout: 2.99,
plater: 2.99,
plater: 2.99,
ccf: 2.99,
dw: 2.99,
fast: 2.99,
fast: 2.99,
apm: 2.99,
standardCardFields: 2.99,
},
ES: {
fixedFee: {
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
EUR: 0.35,
USD: 0.3,
GBP: 0.3,
CAD: 0.3,
AUD: 0.3,
},
checkout: 2.9,
plater: 2.9,
plater: 2.9,
ccf: 1.2,
dw: 1.2,
fast: 1.2,
fast: 1.2,
apm: 1.2,
standardCardFields: 1.2,
},