Merge pull request #3059 from woocommerce/PCP-4124-dynamic-logic-for-things-to-do-next-ver4

Settings UI: Add functionality to mark todos as complete on click
This commit is contained in:
Danny Dudzic 2025-02-05 12:07:09 +01:00 committed by GitHub
commit 2d24240d02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 205 additions and 29 deletions

View file

@ -1,6 +1,6 @@
import { selectTab, TAB_IDS } from '../../../utils/tabSelector'; import { selectTab, TAB_IDS } from '../../../utils/tabSelector';
import { useEffect, useState } from '@wordpress/element'; import { useEffect, useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME as TODOS_STORE_NAME } from '../../../data/todos'; import { STORE_NAME as TODOS_STORE_NAME } from '../../../data/todos';
const TodoSettingsBlock = ( { const TodoSettingsBlock = ( {
@ -21,6 +21,8 @@ const TodoSettingsBlock = ( {
[] []
); );
const { completeOnClick } = useDispatch( TODOS_STORE_NAME );
useEffect( () => { useEffect( () => {
if ( dismissedTodos.length === 0 ) { if ( dismissedTodos.length === 0 ) {
setDismissingIds( new Set() ); setDismissingIds( new Set() );
@ -41,6 +43,26 @@ const TodoSettingsBlock = ( {
}, 300 ); }, 300 );
}; };
const handleClick = async ( todo ) => {
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' );
// If it has completeOnClick flag, trigger the action
if ( todo.action.completeOnClick === true ) {
await completeOnClick( todo.id );
}
}
if ( todo.action.modal ) {
setActiveModal( todo.action.modal );
}
if ( todo.action.highlight ) {
setActiveHighlight( todo.action.highlight );
}
};
// Filter out dismissed todos for display // Filter out dismissed todos for display
const visibleTodos = todosData.filter( const visibleTodos = todosData.filter(
( todo ) => ! dismissedTodos.includes( todo.id ) ( todo ) => ! dismissedTodos.includes( todo.id )
@ -59,22 +81,7 @@ const TodoSettingsBlock = ( {
isCompleted={ completedTodos.includes( todo.id ) } isCompleted={ completedTodos.includes( todo.id ) }
isDismissing={ dismissingIds.has( todo.id ) } isDismissing={ dismissingIds.has( todo.id ) }
onDismiss={ ( e ) => handleDismiss( todo.id, e ) } onDismiss={ ( e ) => handleDismiss( todo.id, e ) }
onClick={ async () => { onClick={ () => handleClick( todo ) }
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' );
}
if ( todo.action.modal ) {
setActiveModal( todo.action.modal );
}
if ( todo.action.highlight ) {
setActiveHighlight( todo.action.highlight );
}
} }
/> />
) ) } ) ) }
</div> </div>

View file

@ -17,4 +17,5 @@ export default {
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS', DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
DO_PERSIST_DATA: 'TODOS:DO_PERSIST_DATA', DO_PERSIST_DATA: 'TODOS:DO_PERSIST_DATA',
DO_RESET_DISMISSED_TODOS: 'TODOS:DO_RESET_DISMISSED_TODOS', DO_RESET_DISMISSED_TODOS: 'TODOS:DO_RESET_DISMISSED_TODOS',
DO_COMPLETE_ONCLICK: 'TODOS:DO_COMPLETE_ONCLICK',
}; };

View file

@ -39,8 +39,7 @@ export const resetDismissedTodos = function* () {
const result = yield { type: ACTION_TYPES.DO_RESET_DISMISSED_TODOS }; const result = yield { type: ACTION_TYPES.DO_RESET_DISMISSED_TODOS };
if ( result && result.success ) { if ( result && result.success ) {
// After successful reset, fetch fresh todos yield setDismissedTodos( [] );
yield fetchTodos();
} }
return result; return result;
@ -50,3 +49,19 @@ export const setCompletedTodos = ( completedTodos ) => ( {
type: ACTION_TYPES.SET_COMPLETED_TODOS, type: ACTION_TYPES.SET_COMPLETED_TODOS,
payload: completedTodos, payload: completedTodos,
} ); } );
export const completeOnClick = function* ( todoId ) {
const result = yield {
type: ACTION_TYPES.DO_COMPLETE_ONCLICK,
todoId,
};
if ( result && result.success ) {
// Set transient completed state for visual feedback
const currentTransientCompleted =
yield select( STORE_NAME ).getCompletedTodos();
yield setCompletedTodos( [ ...currentTransientCompleted, todoId ] );
}
return result;
};

View file

@ -9,3 +9,4 @@ export const REST_PATH = '/wc/v3/wc_paypal/todos';
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/todos'; export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/todos';
export const REST_RESET_DISMISSED_TODOS_PATH = export const REST_RESET_DISMISSED_TODOS_PATH =
'/wc/v3/wc_paypal/reset-dismissed-todos'; '/wc/v3/wc_paypal/reset-dismissed-todos';
export const REST_COMPLETE_ONCLICK_PATH = '/wc/v3/wc_paypal/complete-onclick';

View file

@ -12,6 +12,7 @@ import {
REST_PATH, REST_PATH,
REST_PERSIST_PATH, REST_PERSIST_PATH,
REST_RESET_DISMISSED_TODOS_PATH, REST_RESET_DISMISSED_TODOS_PATH,
REST_COMPLETE_ONCLICK_PATH,
} from './constants'; } from './constants';
import ACTION_TYPES from './action-types'; import ACTION_TYPES from './action-types';
@ -44,4 +45,21 @@ export const controls = {
}; };
} }
}, },
async [ ACTION_TYPES.DO_COMPLETE_ONCLICK ]( { todoId } ) {
try {
const response = await apiFetch( {
path: REST_COMPLETE_ONCLICK_PATH,
method: 'POST',
data: { todoId },
} );
return response;
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
},
}; };

View file

@ -28,6 +28,7 @@ const defaultTransient = Object.freeze( {
const defaultPersistent = Object.freeze( { const defaultPersistent = Object.freeze( {
todos: [], todos: [],
dismissedTodos: [], dismissedTodos: [],
completedOnClickTodos: [],
} ); } );
// Reducer logic. // Reducer logic.

View file

@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition; use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CompleteOnClickEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
@ -265,7 +266,8 @@ return array(
}, },
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition { 'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
return new TodosDefinition( return new TodosDefinition(
$container->get( 'settings.service.todos_eligibilities' ) $container->get( 'settings.service.todos_eligibilities' ),
$container->get( 'settings.data.general' )
); );
}, },
'settings.service.todos_eligibilities' => static function( ContainerInterface $container ): TodosEligibilityService { 'settings.service.todos_eligibilities' => static function( ContainerInterface $container ): TodosEligibilityService {
@ -314,4 +316,7 @@ return array(
'settings.rest.reset_dismissed_todos' => static function( ContainerInterface $container ): ResetDismissedTodosEndpoint { 'settings.rest.reset_dismissed_todos' => static function( ContainerInterface $container ): ResetDismissedTodosEndpoint {
return new ResetDismissedTodosEndpoint(); return new ResetDismissedTodosEndpoint();
}, },
'settings.rest.complete_onclick' => static function( ContainerInterface $container ): CompleteOnClickEndpoint {
return new CompleteOnClickEndpoint();
},
); );

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition; namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService; use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
/** /**
* Class TodosDefinition * Class TodosDefinition
@ -26,13 +27,25 @@ class TodosDefinition {
*/ */
protected TodosEligibilityService $eligibilities; protected TodosEligibilityService $eligibilities;
/**
* The general settings service.
*
* @var GeneralSettings
*/
protected GeneralSettings $settings;
/** /**
* Constructor. * Constructor.
* *
* @param TodosEligibilityService $eligibilities The todos eligibility service. * @param TodosEligibilityService $eligibilities The todos eligibility service.
* @param GeneralSettings $settings The general settings service.
*/ */
public function __construct( TodosEligibilityService $eligibilities ) { public function __construct(
TodosEligibilityService $eligibilities,
GeneralSettings $settings
) {
$this->eligibilities = $eligibilities; $this->eligibilities = $eligibilities;
$this->settings = $settings;
} }
/** /**
@ -110,9 +123,11 @@ class TodosDefinition {
'description' => __( 'To enable Apple Pay, you must register your domain with PayPal', 'woocommerce-paypal-payments' ), 'description' => __( 'To enable Apple Pay, you must register your domain with PayPal', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['register_domain_apple_pay'], 'isEligible' => $eligibility_checks['register_domain_apple_pay'],
'action' => array( 'action' => array(
'type' => 'tab', 'type' => 'external',
'tab' => 'overview', 'url' => $this->settings->is_sandbox_merchant()
'section' => 'apple_pay', ? 'https://www.sandbox.paypal.com/uccservicing/apm/applepay'
: 'https://www.paypal.com/uccservicing/apm/applepay',
'completeOnClick' => true,
), ),
), ),
'add_digital_wallets' => array( 'add_digital_wallets' => array(

View file

@ -0,0 +1,96 @@
<?php
/**
* CompleteOnClickEndpoint class file.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare(strict_types=1);
/**
* Handles the REST endpoint for marking todos as completed on click.
*
* This file is part of the WooCommerce PayPal Commerce Settings module and provides
* functionality for tracking which todos have been completed via click actions.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
/**
* Class CompleteOnClickEndpoint
*
* Handles REST API endpoints for marking todos as completed when clicked.
* Extends the base RestEndpoint class to provide specific todo completion functionality.
*/
class CompleteOnClickEndpoint extends RestEndpoint {
/**
* The base URL for the REST endpoint.
*
* @var string
*/
protected $rest_base = 'complete-onclick';
/**
* Registers the routes for the complete-onclick endpoint.
*
* Sets up the REST API route for handling todo completion via POST requests.
*
* @return void
*/
public function register_routes(): void {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'complete_onclick' ),
'permission_callback' => array( $this, 'check_permission' ),
)
);
}
/**
* Handles the completion of a todo item via click.
*
* Processes the POST request to mark a specific todo as completed,
* updating the stored settings accordingly.
*
* @param WP_REST_Request $request The incoming REST request object.
* @return WP_REST_Response The REST response indicating success or failure.
*/
public function complete_onclick( WP_REST_Request $request ): WP_REST_Response {
$todo_id = $request->get_param( 'todoId' );
if ( ! $todo_id ) {
return $this->return_error( __( 'Todo ID is required.', 'woocommerce-paypal-payments' ) );
}
$settings = get_option( 'ppcp-settings', array() );
if ( ! isset( $settings['completedOnClickTodos'] ) ) {
$settings['completedOnClickTodos'] = array();
}
if ( ! in_array( $todo_id, $settings['completedOnClickTodos'], true ) ) {
$settings['completedOnClickTodos'][] = $todo_id;
$update_result = update_option( 'ppcp-settings', $settings );
if ( ! $update_result ) {
return $this->return_error( __( 'Failed to mark todo as completed on click.', 'woocommerce-paypal-payments' ) );
}
}
return $this->return_success(
array(
'message' => __( 'Todo marked as completed on click successfully.', 'woocommerce-paypal-payments' ),
'todoId' => $todo_id,
)
);
}
}

View file

@ -57,7 +57,11 @@ class ResetDismissedTodosEndpoint extends RestEndpoint {
$settings = get_option( 'ppcp-settings', array() ); $settings = get_option( 'ppcp-settings', array() );
$settings['dismissedTodos'] = array(); $settings['dismissedTodos'] = array();
$update_result = update_option( 'ppcp-settings', $settings );
// Clear the completedOnClickTodos for testing purposes.
// $settings['completedOnClickTodos'] = array();.
$update_result = update_option( 'ppcp-settings', $settings );
if ( ! $update_result ) { if ( ! $update_result ) {
return $this->return_error( __( 'Failed to reset dismissed todos.', 'woocommerce-paypal-payments' ) ); return $this->return_error( __( 'Failed to reset dismissed todos.', 'woocommerce-paypal-payments' ) );

View file

@ -103,11 +103,22 @@ class TodosRestEndpoint extends RestEndpoint {
* @return WP_REST_Response The response containing todos data. * @return WP_REST_Response The response containing todos data.
*/ */
public function get_todos(): WP_REST_Response { public function get_todos(): WP_REST_Response {
$settings = get_option( 'ppcp-settings', array() ); $settings = get_option( 'ppcp-settings', array() );
$dismissed_ids = $settings['dismissedTodos'] ?? array(); $dismissed_ids = $settings['dismissedTodos'] ?? array();
$completed_onclick_ids = $settings['completedOnClickTodos'] ?? array();
$todos = array(); $todos = array();
foreach ( $this->todos_definition->get() as $id => $todo ) { foreach ( $this->todos_definition->get() as $id => $todo ) {
// Skip if todo has completeOnClick flag and is in completed list.
if (
in_array( $id, $completed_onclick_ids, true ) &&
isset( $todo['action']['completeOnClick'] ) &&
$todo['action']['completeOnClick'] === true
) {
continue;
}
// Check eligibility and add to todos if eligible.
if ( $todo['isEligible']() ) { if ( $todo['isEligible']() ) {
$todos[] = array_merge( $todos[] = array_merge(
array( 'id' => $id ), array( 'id' => $id ),
@ -118,8 +129,9 @@ class TodosRestEndpoint extends RestEndpoint {
return $this->return_success( return $this->return_success(
array( array(
'todos' => $todos, 'todos' => $todos,
'dismissedTodos' => $dismissed_ids, 'dismissedTodos' => $dismissed_ids,
'completedOnClickTodos' => $completed_onclick_ids,
) )
); );
} }

View file

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