mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 12:25:15 +08:00
🔀 Merge branch 'trunk'
# Conflicts: # modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AcdcFlow.js # modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AcdcOptionalPaymentMethods.js
This commit is contained in:
commit
0894d9e691
34 changed files with 1130 additions and 429 deletions
|
@ -15,12 +15,44 @@
|
|||
}
|
||||
|
||||
// Todo List and Feature Items
|
||||
.ppcp-r-todo-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ppcp-r-todo-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $color-gray-300;
|
||||
padding-bottom: 16px;
|
||||
padding-top: 16px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:first-child, &.is-dismissing:first-child + .ppcp-r-todo-item {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&:last-child,
|
||||
&:not(.is-dismissing):has(+ .is-dismissing:last-child) {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&.is-dismissing {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
.ppcp-r-todo-item__inner {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
@ -32,15 +64,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $color-gray-400;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
&.is-completed {
|
||||
.ppcp-r-todo-item__icon {
|
||||
border-style: solid;
|
||||
|
@ -68,17 +91,46 @@
|
|||
|
||||
&__inner {
|
||||
position: relative;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
width: 100%;
|
||||
padding-right: 36px;
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&__close {
|
||||
margin-left: auto;
|
||||
&__dismiss {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: $color-gray-400;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $color-blueberry;
|
||||
background-color: $color-gray-100;
|
||||
color: $color-gray-700;
|
||||
}
|
||||
|
||||
.dashicons {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,6 +156,7 @@
|
|||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,3 +233,23 @@
|
|||
gap: 48px;
|
||||
}
|
||||
|
||||
.ppcp-highlight {
|
||||
animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid $color-blueberry;
|
||||
border-radius: var(--container-border-radius);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes ppcp-highlight-fade {
|
||||
0%, 20% {
|
||||
background-color: rgba($color-blueberry, 0.08);
|
||||
border-color: $color-blueberry;
|
||||
border-width: 1px;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
border-color: $color-gray-300;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import classNames from 'classnames';
|
|||
import { Description, Header, Title, TitleExtra, Content } from './Elements';
|
||||
|
||||
const SettingsBlock = ( {
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
title,
|
||||
|
@ -15,14 +16,18 @@ const SettingsBlock = ( {
|
|||
'ppcp--horizontal': horizontalLayout,
|
||||
} );
|
||||
|
||||
const props = {
|
||||
className: blockClassName,
|
||||
...( id && { id } ),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ blockClassName } id={ className }>
|
||||
<div { ...props }>
|
||||
<BlockTitle
|
||||
blockTitle={ title }
|
||||
blockSuffix={ titleSuffix }
|
||||
blockDescription={ description }
|
||||
/>
|
||||
|
||||
<Content asCard={ false }>{ children }</Content>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { ToggleControl, Icon, Button } from '@wordpress/components';
|
||||
import { cog } from '@wordpress/icons';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useActiveHighlight } from '../../../data/common/hooks';
|
||||
|
||||
import SettingsBlock from '../SettingsBlock';
|
||||
import PaymentMethodIcon from '../PaymentMethodIcon';
|
||||
|
@ -10,8 +12,28 @@ const PaymentMethodItemBlock = ( {
|
|||
onSelect,
|
||||
isSelected,
|
||||
} ) => {
|
||||
const { activeHighlight, setActiveHighlight } = useActiveHighlight();
|
||||
const isHighlighted = activeHighlight === paymentMethod.id;
|
||||
|
||||
// Reset the active highlight after 2 seconds
|
||||
useEffect( () => {
|
||||
if ( isHighlighted ) {
|
||||
const timer = setTimeout( () => {
|
||||
setActiveHighlight( null );
|
||||
}, 2000 );
|
||||
|
||||
return () => clearTimeout( timer );
|
||||
}
|
||||
}, [ isHighlighted, setActiveHighlight ] );
|
||||
|
||||
return (
|
||||
<SettingsBlock className="ppcp--method-item" separatorAndGap={ false }>
|
||||
<SettingsBlock
|
||||
id={ paymentMethod.id }
|
||||
className={ `ppcp--method-item ${
|
||||
isHighlighted ? 'ppcp-highlight' : ''
|
||||
}` }
|
||||
separatorAndGap={ false }
|
||||
>
|
||||
<div className="ppcp--method-inner">
|
||||
<div className="ppcp--method-title-wrapper">
|
||||
{ paymentMethod?.icon && (
|
||||
|
|
|
@ -1,20 +1,64 @@
|
|||
import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { STORE_NAME as TODOS_STORE_NAME } from '../../../data/todos';
|
||||
|
||||
const TodoSettingsBlock = ( {
|
||||
todosData,
|
||||
className = '',
|
||||
setActiveModal,
|
||||
setActiveHighlight,
|
||||
onDismissTodo,
|
||||
} ) => {
|
||||
const [ dismissingIds, setDismissingIds ] = useState( new Set() );
|
||||
const { completedTodos, dismissedTodos } = useSelect(
|
||||
( select ) => ( {
|
||||
completedTodos:
|
||||
select( TODOS_STORE_NAME ).getCompletedTodos() || [],
|
||||
dismissedTodos:
|
||||
select( TODOS_STORE_NAME ).getDismissedTodos() || [],
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( dismissedTodos.length === 0 ) {
|
||||
setDismissingIds( new Set() );
|
||||
}
|
||||
}, [ dismissedTodos ] );
|
||||
|
||||
const TodoSettingsBlock = ( { todosData, className = '' } ) => {
|
||||
if ( todosData.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDismiss = ( todoId, e ) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDismissingIds( ( prev ) => new Set( [ ...prev, todoId ] ) );
|
||||
|
||||
setTimeout( () => {
|
||||
onDismissTodo( todoId );
|
||||
}, 300 );
|
||||
};
|
||||
|
||||
// Filter out dismissed todos for display
|
||||
const visibleTodos = todosData.filter(
|
||||
( todo ) => ! dismissedTodos.includes( todo.id )
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ `ppcp-r-settings-block__todo ppcp-r-todo-items ${ className }` }
|
||||
>
|
||||
{ todosData.slice( 0, 5 ).map( ( todo ) => (
|
||||
{ visibleTodos.map( ( todo ) => (
|
||||
<TodoItem
|
||||
key={ todo.id }
|
||||
id={ todo.id }
|
||||
title={ todo.title }
|
||||
description={ todo.description }
|
||||
isCompleted={ todo.isCompleted }
|
||||
isCompleted={ completedTodos.includes( todo.id ) }
|
||||
isDismissing={ dismissingIds.has( todo.id ) }
|
||||
onDismiss={ ( e ) => handleDismiss( todo.id, e ) }
|
||||
onClick={ async () => {
|
||||
if ( todo.action.type === 'tab' ) {
|
||||
const tabId =
|
||||
|
@ -23,6 +67,13 @@ const TodoSettingsBlock = ( { todosData, className = '' } ) => {
|
|||
} else if ( todo.action.type === 'external' ) {
|
||||
window.open( todo.action.url, '_blank' );
|
||||
}
|
||||
|
||||
if ( todo.action.modal ) {
|
||||
setActiveModal( todo.action.modal );
|
||||
}
|
||||
if ( todo.action.highlight ) {
|
||||
setActiveHighlight( todo.action.highlight );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
|
@ -30,12 +81,19 @@ const TodoSettingsBlock = ( { todosData, className = '' } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
const TodoItem = ( { title, description, isCompleted, onClick } ) => {
|
||||
const TodoItem = ( {
|
||||
title,
|
||||
description,
|
||||
isCompleted,
|
||||
isDismissing,
|
||||
onClick,
|
||||
onDismiss,
|
||||
} ) => {
|
||||
return (
|
||||
<div
|
||||
className={ `ppcp-r-todo-item ${
|
||||
isCompleted ? 'is-completed' : ''
|
||||
}` }
|
||||
} ${ isDismissing ? 'is-dismissing' : '' }` }
|
||||
onClick={ onClick }
|
||||
>
|
||||
<div className="ppcp-r-todo-item__inner">
|
||||
|
@ -54,6 +112,13 @@ const TodoItem = ( { title, description, isCompleted, onClick } ) => {
|
|||
</div>
|
||||
) }
|
||||
</div>
|
||||
<button
|
||||
className="ppcp-r-todo-item__dismiss"
|
||||
onClick={ onDismiss }
|
||||
aria-label="Dismiss todo item"
|
||||
>
|
||||
<span className="dashicons dashicons-no-alt"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,9 @@ 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 { STORE_NAME as COMMON_STORE_NAME } from '../../../../data/common';
|
||||
import { STORE_NAME as TODOS_STORE_NAME } from '../../../../data/todos';
|
||||
|
||||
import { getFeatures } from '../Components/Overview/features-config';
|
||||
|
||||
import {
|
||||
|
@ -23,29 +25,9 @@ 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">
|
||||
{ showTodos && (
|
||||
<SettingsCard
|
||||
className="ppcp-r-tab-overview-todo"
|
||||
title={ __(
|
||||
'Things to do next',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Complete these tasks to keep your store updated with the latest products and services.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
>
|
||||
<TodoSettingsBlock todosData={ todos } />
|
||||
</SettingsCard>
|
||||
) }
|
||||
|
||||
<OverviewTodos />
|
||||
<OverviewFeatures />
|
||||
<OverviewHelp />
|
||||
</div>
|
||||
|
@ -54,11 +36,82 @@ const TabOverview = () => {
|
|||
|
||||
export default TabOverview;
|
||||
|
||||
const OverviewTodos = () => {
|
||||
const [ isResetting, setIsResetting ] = useState( false );
|
||||
const { todos, isReady: areTodosReady, dismissTodo } = useTodos();
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const OverviewFeatures = () => {
|
||||
const [ isRefreshing, setIsRefreshing ] = useState( false );
|
||||
const { merchant, features: merchantFeatures } = useMerchantInfo();
|
||||
const { refreshFeatureStatuses, setActiveModal } =
|
||||
useDispatch( STORE_NAME );
|
||||
useDispatch( COMMON_STORE_NAME );
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( noticesStore );
|
||||
|
||||
|
@ -68,7 +121,7 @@ const OverviewFeatures = () => {
|
|||
[ setActiveModal ]
|
||||
);
|
||||
|
||||
// Map merchant features status to our config
|
||||
// Map merchant features status to the config
|
||||
const features = useMemo( () => {
|
||||
return featuresData.map( ( feature ) => {
|
||||
const merchantFeature = merchantFeatures?.[ feature.id ];
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
|
||||
|
||||
export const todosData = [
|
||||
{
|
||||
id: 'enable_fastlane',
|
||||
title: __( 'Enable Fastlane', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Accelerate your guest checkout with Fastlane by PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () =>
|
||||
selectTab( TAB_IDS.PAYMENT_METHODS, 'ppcp-card-payments-card' ),
|
||||
},
|
||||
{
|
||||
id: 'enable_credit_debit_cards',
|
||||
title: __(
|
||||
'Enable Credit and Debit Cards on your checkout',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Credit and Debit Cards is now available for Blocks checkout pages.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () =>
|
||||
selectTab( TAB_IDS.PAYMENT_METHODS, 'ppcp-card-payments-card' ),
|
||||
},
|
||||
{
|
||||
id: 'enable_pay_later_messaging',
|
||||
title: __(
|
||||
'Enable Pay Later messaging',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Show Pay Later messaging to boost conversion rate and increase cart size.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => selectTab( TAB_IDS.OVERVIEW, 'pay_later_messaging' ),
|
||||
},
|
||||
{
|
||||
id: 'configure_paypal_subscription',
|
||||
title: __(
|
||||
'Configure a PayPal Subscription',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Connect a subscriptions-type product from WooCommerce with PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log(
|
||||
'Take merchant to product list, filtered with subscription-type products'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'register_domain_apple_pay',
|
||||
title: __(
|
||||
'Register Domain for Apple Pay',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'To enable Apple Pay, you must register your domain with PayPal.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => selectTab( TAB_IDS.OVERVIEW, 'apple_pay' ),
|
||||
},
|
||||
{
|
||||
id: 'add_digital_wallets_to_account',
|
||||
title: __(
|
||||
'Add digital wallets to your account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Add the ability to accept Apple Pay & Google Pay to your PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log(
|
||||
'Take merchant to PayPal to enable Apple Pay & Google Pay'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add_apple_pay_to_account',
|
||||
title: __(
|
||||
'Add Apple Pay to your account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Add the ability to accept Apple Pay to your PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log( 'Take merchant to PayPal to enable Apple Pay' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add_google_pay_to_account',
|
||||
title: __(
|
||||
'Add Google Pay to your account',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
description: __(
|
||||
'Add the ability to accept Google Pay to your PayPal account.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => {
|
||||
console.log( 'Take merchant to PayPal to enable Google Pay' );
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enable_apple_pay',
|
||||
title: __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Allow your buyers to check out via Apple Pay.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => selectTab( TAB_IDS.OVERVIEW, 'apple_pay' ),
|
||||
},
|
||||
{
|
||||
id: 'enable_google_pay',
|
||||
title: __( 'Enable Google Pay', 'woocommerce-paypal-payments' ),
|
||||
description: __(
|
||||
'Allow your buyers to check out via Google Pay.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
isCompleted: () => {
|
||||
return false;
|
||||
},
|
||||
onClick: () => selectTab( TAB_IDS.OVERVIEW, 'google_pay' ),
|
||||
},
|
||||
];
|
|
@ -77,6 +77,15 @@ export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
|
|||
export const setActiveModal = ( activeModal ) =>
|
||||
setTransient( 'activeModal', activeModal );
|
||||
|
||||
/**
|
||||
* Transient. Sets the active settings highlight.
|
||||
*
|
||||
* @param {string} activeHighlight
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setActiveHighlight = ( activeHighlight ) =>
|
||||
setTransient( 'activeHighlight', activeHighlight );
|
||||
|
||||
/**
|
||||
* Transient (Activity): Marks the start of an async activity
|
||||
* Think of it as "setIsBusy(true)"
|
||||
|
|
|
@ -28,6 +28,8 @@ const useHooks = () => {
|
|||
// Transient accessors.
|
||||
const [ isReady ] = useTransient( 'isReady' );
|
||||
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
|
||||
const [ activeHighlight, setActiveHighlight ] =
|
||||
useTransient( 'activeHighlight' );
|
||||
|
||||
// Persistent accessors.
|
||||
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
|
||||
|
@ -62,6 +64,8 @@ const useHooks = () => {
|
|||
isReady,
|
||||
activeModal,
|
||||
setActiveModal,
|
||||
activeHighlight,
|
||||
setActiveHighlight,
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
return savePersistent( setSandboxMode, state );
|
||||
|
@ -162,6 +166,11 @@ export const useActiveModal = () => {
|
|||
return { activeModal, setActiveModal };
|
||||
};
|
||||
|
||||
export const useActiveHighlight = () => {
|
||||
const { activeHighlight, setActiveHighlight } = useHooks();
|
||||
return { activeHighlight, setActiveHighlight };
|
||||
};
|
||||
|
||||
// -- Not using the `useHooks()` data provider --
|
||||
|
||||
export const useBusyState = () => {
|
||||
|
|
|
@ -16,6 +16,7 @@ const defaultTransient = Object.freeze( {
|
|||
isReady: false,
|
||||
activities: new Map(),
|
||||
activeModal: '',
|
||||
activeHighlight: '',
|
||||
|
||||
// Read only values, provided by the server via hydrate.
|
||||
merchant: Object.freeze( {
|
||||
|
|
|
@ -23,14 +23,19 @@ export const initTodoSync = () => {
|
|||
|
||||
try {
|
||||
const paymentState = select( 'wc/paypal/payment' ).persistentData();
|
||||
const todosState = select( 'wc/paypal/todos' ).getTodos();
|
||||
const completedTodos =
|
||||
select( 'wc/paypal/todos' ).getCompletedTodos();
|
||||
|
||||
// Skip if states haven't been initialized yet
|
||||
if ( ! paymentState || ! todosState || ! previousPaymentState ) {
|
||||
if ( ! paymentState || ! previousPaymentState ) {
|
||||
previousPaymentState = paymentState;
|
||||
isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Track which todos should be marked as completed
|
||||
let newCompletedTodos = [ ...( completedTodos || [] ) ];
|
||||
|
||||
Object.entries( TODO_TRIGGERS ).forEach(
|
||||
( [ paymentMethod, todoId ] ) => {
|
||||
const wasEnabled =
|
||||
|
@ -38,26 +43,34 @@ export const initTodoSync = () => {
|
|||
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
|
||||
if ( isEnabled ) {
|
||||
// Add to completed todos if not already there
|
||||
if ( ! newCompletedTodos.includes( todoId ) ) {
|
||||
newCompletedTodos.push( todoId );
|
||||
}
|
||||
} else {
|
||||
// Remove from completed todos
|
||||
newCompletedTodos = newCompletedTodos.filter(
|
||||
( id ) => id !== todoId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
previousPaymentState = paymentState;
|
||||
// Only dispatch if there are changes
|
||||
if (
|
||||
newCompletedTodos.length !== completedTodos.length ||
|
||||
newCompletedTodos.some(
|
||||
( id ) => ! completedTodos.includes( id )
|
||||
)
|
||||
) {
|
||||
dispatch( 'wc/paypal/todos' ).setCompletedTodos(
|
||||
newCompletedTodos
|
||||
);
|
||||
}
|
||||
|
||||
previousPaymentState = { ...paymentState };
|
||||
} catch ( error ) {
|
||||
console.error( 'Error in todo sync:', error );
|
||||
} finally {
|
||||
|
|
|
@ -7,10 +7,14 @@
|
|||
export default {
|
||||
// Transient data
|
||||
SET_TRANSIENT: 'TODOS:SET_TRANSIENT',
|
||||
SET_COMPLETED_TODOS: 'TODOS:SET_COMPLETED_TODOS',
|
||||
|
||||
// Persistent data
|
||||
SET_TODOS: 'TODOS:SET_TODOS',
|
||||
SET_DISMISSED_TODOS: 'TODOS:SET_DISMISSED_TODOS',
|
||||
|
||||
// Controls
|
||||
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
|
||||
DO_PERSIST_DATA: 'TODOS:DO_PERSIST_DATA',
|
||||
DO_RESET_DISMISSED_TODOS: 'TODOS:DO_RESET_DISMISSED_TODOS',
|
||||
};
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
import { select } from '@wordpress/data';
|
||||
import ACTION_TYPES from './action-types';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
export const setIsReady = ( isReady ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
|
@ -19,6 +21,32 @@ export const setTodos = ( todos ) => ( {
|
|||
payload: todos,
|
||||
} );
|
||||
|
||||
export const setDismissedTodos = ( dismissedTodos ) => ( {
|
||||
type: ACTION_TYPES.SET_DISMISSED_TODOS,
|
||||
payload: dismissedTodos,
|
||||
} );
|
||||
|
||||
export const fetchTodos = function* () {
|
||||
yield { type: ACTION_TYPES.DO_FETCH_TODOS };
|
||||
};
|
||||
|
||||
export const persist = function* () {
|
||||
const data = yield select( STORE_NAME ).persistentData();
|
||||
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
|
||||
};
|
||||
|
||||
export const resetDismissedTodos = function* () {
|
||||
const result = yield { type: ACTION_TYPES.DO_RESET_DISMISSED_TODOS };
|
||||
|
||||
if ( result && result.success ) {
|
||||
// After successful reset, fetch fresh todos
|
||||
yield fetchTodos();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const setCompletedTodos = ( completedTodos ) => ( {
|
||||
type: ACTION_TYPES.SET_COMPLETED_TODOS,
|
||||
payload: completedTodos,
|
||||
} );
|
||||
|
|
|
@ -6,3 +6,6 @@
|
|||
|
||||
export const STORE_NAME = 'wc/paypal/todos';
|
||||
export const REST_PATH = '/wc/v3/wc_paypal/todos';
|
||||
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/todos';
|
||||
export const REST_RESET_DISMISSED_TODOS_PATH =
|
||||
'/wc/v3/wc_paypal/reset-dismissed-todos';
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
*/
|
||||
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { REST_PATH } from './constants';
|
||||
import {
|
||||
REST_PATH,
|
||||
REST_PERSIST_PATH,
|
||||
REST_RESET_DISMISSED_TODOS_PATH,
|
||||
} from './constants';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
export const controls = {
|
||||
|
@ -19,4 +23,25 @@ export const controls = {
|
|||
} );
|
||||
return response?.data || [];
|
||||
},
|
||||
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
|
||||
return await apiFetch( {
|
||||
path: REST_PERSIST_PATH,
|
||||
method: 'POST',
|
||||
data,
|
||||
} );
|
||||
},
|
||||
async [ ACTION_TYPES.DO_RESET_DISMISSED_TODOS ]() {
|
||||
try {
|
||||
return await apiFetch( {
|
||||
path: REST_RESET_DISMISSED_TODOS_PATH,
|
||||
method: 'POST',
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,25 +9,87 @@
|
|||
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { STORE_NAME } from './constants';
|
||||
import { createHooksForStore } from '../utils';
|
||||
|
||||
const useTransient = ( key ) =>
|
||||
useSelect(
|
||||
( select ) => select( STORE_NAME ).transientData()?.[ key ],
|
||||
[ key ]
|
||||
const ensureArray = ( value ) => {
|
||||
if ( ! value ) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray( value ) ? value : Object.values( value );
|
||||
};
|
||||
|
||||
const useHooks = () => {
|
||||
const { useTransient } = createHooksForStore( STORE_NAME );
|
||||
const { fetchTodos, setDismissedTodos, setCompletedTodos, persist } =
|
||||
useDispatch( STORE_NAME );
|
||||
|
||||
// Read-only flags and derived state.
|
||||
const [ isReady ] = useTransient( 'isReady' );
|
||||
|
||||
// Get todos data from store
|
||||
const { todos, dismissedTodos, completedTodos } = useSelect( ( select ) => {
|
||||
const store = select( STORE_NAME );
|
||||
return {
|
||||
todos: ensureArray( store.getTodos() ),
|
||||
dismissedTodos: ensureArray( store.getDismissedTodos() ),
|
||||
completedTodos: ensureArray( store.getCompletedTodos() ),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const dismissedSet = new Set( dismissedTodos );
|
||||
|
||||
const dismissTodo = async ( todoId ) => {
|
||||
if ( ! dismissedSet.has( todoId ) ) {
|
||||
const newDismissedTodos = [ ...dismissedTodos, todoId ];
|
||||
await setDismissedTodos( newDismissedTodos );
|
||||
}
|
||||
};
|
||||
|
||||
const setTodoCompleted = async ( todoId, isCompleted ) => {
|
||||
let newCompletedTodos;
|
||||
if ( isCompleted ) {
|
||||
newCompletedTodos = [ ...completedTodos, todoId ];
|
||||
} else {
|
||||
newCompletedTodos = completedTodos.filter(
|
||||
( id ) => id !== todoId
|
||||
);
|
||||
}
|
||||
await setCompletedTodos( newCompletedTodos );
|
||||
};
|
||||
|
||||
const filteredTodos = todos.filter(
|
||||
( todo ) => ! dismissedSet.has( todo.id )
|
||||
);
|
||||
|
||||
export const useTodos = () => {
|
||||
const todos = useSelect(
|
||||
( select ) => select( STORE_NAME ).getTodos(),
|
||||
[]
|
||||
);
|
||||
const isReady = useTransient( 'isReady' );
|
||||
|
||||
const { fetchTodos } = useDispatch( STORE_NAME );
|
||||
|
||||
return {
|
||||
todos,
|
||||
persist,
|
||||
isReady,
|
||||
todos: filteredTodos,
|
||||
dismissedTodos,
|
||||
completedTodos,
|
||||
fetchTodos,
|
||||
dismissTodo,
|
||||
setTodoCompleted,
|
||||
};
|
||||
};
|
||||
|
||||
export const useStore = () => {
|
||||
const { persist, isReady } = useHooks();
|
||||
return { persist, isReady };
|
||||
};
|
||||
|
||||
export const useTodos = () => {
|
||||
const { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady } =
|
||||
useHooks();
|
||||
return { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady };
|
||||
};
|
||||
|
||||
export const useDismissedTodos = () => {
|
||||
const { dismissedTodos } = useHooks();
|
||||
return { dismissedTodos };
|
||||
};
|
||||
|
||||
export const useCompletedTodos = () => {
|
||||
const { completedTodos } = useHooks();
|
||||
return { completedTodos };
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import ACTION_TYPES from './action-types';
|
|||
*/
|
||||
const defaultTransient = Object.freeze( {
|
||||
isReady: false,
|
||||
completedTodos: [],
|
||||
} );
|
||||
|
||||
/**
|
||||
|
@ -26,6 +27,7 @@ const defaultTransient = Object.freeze( {
|
|||
*/
|
||||
const defaultPersistent = Object.freeze( {
|
||||
todos: [],
|
||||
dismissedTodos: [],
|
||||
} );
|
||||
|
||||
// Reducer logic.
|
||||
|
@ -60,6 +62,41 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
return changePersistent( state, { todos: payload } );
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates dismissed todos list while preserving existing entries
|
||||
*
|
||||
* @param {Object} state Current state
|
||||
* @param {Array} payload Array of todo IDs to mark as dismissed
|
||||
* @return {Object} Updated state
|
||||
*/
|
||||
[ ACTION_TYPES.SET_DISMISSED_TODOS ]: ( state, payload ) => {
|
||||
return changePersistent( state, {
|
||||
dismissedTodos: Array.isArray( payload ) ? payload : [],
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates completed todos list while preserving existing entries
|
||||
*
|
||||
* @param {Object} state Current state
|
||||
* @param {Array} payload Array of todo IDs to mark as completed
|
||||
* @return {Object} Updated state
|
||||
*/
|
||||
[ ACTION_TYPES.SET_COMPLETED_TODOS ]: ( state, payload ) => {
|
||||
return changeTransient( state, {
|
||||
completedTodos: Array.isArray( payload ) ? payload : [],
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets dismissed todos list to an empty array
|
||||
* @param {Object} state Current state
|
||||
* @return {Object} Updated state
|
||||
*/
|
||||
[ ACTION_TYPES.DO_RESET_DISMISSED_TODOS ]: ( state ) => {
|
||||
return changePersistent( state, { dismissedTodos: [] } );
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets state to defaults while maintaining initialization status
|
||||
*
|
||||
|
|
|
@ -18,12 +18,11 @@ export const resolvers = {
|
|||
try {
|
||||
const response = yield apiFetch( { path: REST_PATH } );
|
||||
|
||||
// Make sure we're accessing the correct part of the response
|
||||
const todos = response?.data || [];
|
||||
const { todos = [], dismissedTodos = [] } = response?.data || {};
|
||||
|
||||
yield dispatch( STORE_NAME ).setTodos( todos );
|
||||
yield dispatch( STORE_NAME ).setDismissedTodos( dismissedTodos );
|
||||
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||
|
||||
} catch ( e ) {
|
||||
console.error( 'Resolver error:', e );
|
||||
yield dispatch( STORE_NAME ).setIsReady( false );
|
||||
|
|
|
@ -22,7 +22,16 @@ export const transientData = ( state ) => {
|
|||
};
|
||||
|
||||
export const getTodos = ( state ) => {
|
||||
// Access todos directly from state first
|
||||
const todos = state?.todos || persistentData( state ).todos || EMPTY_ARR;
|
||||
return todos;
|
||||
const todos = state?.todos || persistentData( state ).todos;
|
||||
return todos || EMPTY_ARR;
|
||||
};
|
||||
|
||||
export const getDismissedTodos = ( state ) => {
|
||||
const dismissed =
|
||||
state?.dismissedTodos || persistentData( state ).dismissedTodos;
|
||||
return dismissed || EMPTY_ARR;
|
||||
};
|
||||
|
||||
export const getCompletedTodos = ( state ) => {
|
||||
return state?.completedTodos || EMPTY_ARR; // Only look at root state, not persistent data
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
PaymentHooks,
|
||||
SettingsHooks,
|
||||
StylingHooks,
|
||||
TodosHooks,
|
||||
} from '../data';
|
||||
|
||||
export const useSaveSettings = () => {
|
||||
|
@ -14,6 +15,7 @@ export const useSaveSettings = () => {
|
|||
const { persist: persistPayment } = PaymentHooks.useStore();
|
||||
const { persist: persistSettings } = SettingsHooks.useStore();
|
||||
const { persist: persistStyling } = StylingHooks.useStore();
|
||||
const { persist: persistTodos } = TodosHooks.useStore();
|
||||
const { persist: persistPayLaterMessaging } =
|
||||
PayLaterMessagingHooks.useStore();
|
||||
|
||||
|
@ -36,6 +38,7 @@ export const useSaveSettings = () => {
|
|||
'Save styling details',
|
||||
persistStyling
|
||||
);
|
||||
withActivity( 'persist-todos', 'Save todos state', persistTodos );
|
||||
withActivity(
|
||||
'persist-pay-later-messaging',
|
||||
'Save pay later messaging details',
|
||||
|
@ -45,6 +48,7 @@ export const useSaveSettings = () => {
|
|||
persistPayment,
|
||||
persistSettings,
|
||||
persistStyling,
|
||||
persistTodos,
|
||||
persistPayLaterMessaging,
|
||||
withActivity,
|
||||
] );
|
||||
|
|
|
@ -50,10 +50,18 @@ export const selectTab = ( tabId, scrollToId ) => {
|
|||
// Resolve after scroll animation
|
||||
setTimeout( resolve, 300 );
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to scroll: Element with ID "${
|
||||
scrollToId || 'ppcp-settings-container'
|
||||
}" not found`
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
}, 100 );
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to select tab: Tab with ID "${ tabId }" not found`
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
} );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue