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 ) {