diff --git a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php index df4e4affa..b1f77de85 100644 --- a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php +++ b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php @@ -94,7 +94,7 @@ public function handle_request(): bool { * * @param array $config The configurator config. */ - private function save_config( array $config ): void { + public function save_config( array $config ): void { $this->settings->set( 'pay_later_enable_styling_per_messaging_location', true ); $this->settings->set( 'pay_later_messaging_enabled', true ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js index a3a3083c9..274ef91c2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPayLaterMessaging.js @@ -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 (
{ }; }; -export const useState = () => { +export const useStore = () => { const { persist, isReady } = useHooks(); return { persist, isReady }; }; diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 0985aa972..227a62226 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -5,8 +5,17 @@ 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, Todos ]; +const stores = [ + Onboarding, + Common, + Payment, + Settings, + Styling, + Todos, + PayLaterMessaging, +]; stores.forEach( ( store ) => { try { @@ -30,6 +39,7 @@ 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; @@ -37,6 +47,7 @@ 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'; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/action-types.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/action-types.js new file mode 100644 index 000000000..70913ddd6 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/action-types.js @@ -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', +}; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js new file mode 100644 index 000000000..59d68d37c --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js @@ -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 }; +}; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/constants.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/constants.js new file mode 100644 index 000000000..09f3bab52 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/constants.js @@ -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'; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/controls.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/controls.js new file mode 100644 index 000000000..9295b62bc --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/controls.js @@ -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, + } ); + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/hooks.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/hooks.js new file mode 100644 index 000000000..0f51051cd --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/hooks.js @@ -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, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/index.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/index.js new file mode 100644 index 000000000..3bd6e4459 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/index.js @@ -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 }; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/reducer.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/reducer.js new file mode 100644 index 000000000..5843ef400 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/reducer.js @@ -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; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/resolvers.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/resolvers.js new file mode 100644 index 000000000..39ff6c343 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/resolvers.js @@ -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' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/selectors.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/selectors.js new file mode 100644 index 000000000..14334fcf3 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/selectors.js @@ -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; +}; diff --git a/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js b/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js index f268d3214..ca594e39d 100644 --- a/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js +++ b/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js @@ -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 }; }; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 1326f4a4a..3ce60b18e 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\PaymentRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint; @@ -125,6 +126,12 @@ $container->get( 'webhook.status.simulation' ) ); }, + 'settings.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint { + return new PayLaterMessagingEndpoint( + $container->get( 'wcgateway.settings' ), + $container->get( 'paylater-configurator.endpoint.save-config' ) + ); + }, 'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint { return new SettingsRestEndpoint( $container->get( 'settings.data.settings' ) diff --git a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php new file mode 100644 index 000000000..5713ce570 --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php @@ -0,0 +1,110 @@ +settings = $settings; + $this->save_config = $save_config; + } + + /** + * Configure REST API routes. + */ + public function register_routes() : void { + /** + * GET wc/v3/wc_paypal/pay_later_messaging + */ + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + + /** + * POST wc/v3/wc_paypal/pay_later_messaging + */ + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + } + + /** + * Returns Pay Later Messaging configuration details. + * + * @return WP_REST_Response The current payment methods details. + */ + public function get_details() : WP_REST_Response { + return $this->return_success( ( new ConfigFactory() )->from_settings( $this->settings ) ); + } + + /** + * Updates Pay Later Messaging configuration details based on the request. + * + * @param WP_REST_Request $request Full data about the request. + * + * @return WP_REST_Response The updated Pay Later Messaging configuration details. + */ + public function update_details( WP_REST_Request $request ) : WP_REST_Response { + $this->save_config->save_config( $request->get_json_params() ); + + return $this->get_details(); + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index b7c334425..c6fea6621 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -238,6 +238,7 @@ static function () use ( $container ) : void { 'settings' => $container->get( 'settings.rest.settings' ), 'styling' => $container->get( 'settings.rest.styling' ), 'todos' => $container->get( 'settings.rest.todos' ), + 'pay_later_messaging' => $container->get( 'settings.rest.pay_later_messaging' ), ); foreach ( $endpoints as $endpoint ) {