diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js index 17f610660..1e1e9ee30 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodsBlock.js @@ -1,25 +1,25 @@ -import { useState, useCallback } from '@wordpress/element'; import SettingsBlock from './SettingsBlock'; import PaymentMethodItemBlock from './PaymentMethodItemBlock'; +import { usePaymentMethods } from '../../../data/payment/hooks'; const PaymentMethodsBlock = ( { paymentMethods, className = '', onTriggerModal, } ) => { - const [ selectedMethods, setSelectedMethods ] = useState( {} ); - - const handleSelect = useCallback( ( methodId, isSelected ) => { - setSelectedMethods( ( prev ) => ( { - ...prev, - [ methodId ]: isSelected, - } ) ); - }, [] ); + const { setPersistent } = usePaymentMethods(); if ( ! paymentMethods?.length ) { return null; } + const handleSelect = ( paymentMethod, isSelected ) => { + setPersistent( paymentMethod.id, { + ...paymentMethod, + enabled: isSelected, + } ); + }; + return ( - handleSelect( paymentMethod.id, checked ) + handleSelect( paymentMethod, checked ) } onTriggerModal={ () => onTriggerModal?.( paymentMethod.id ) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js index 63d627692..3216faf40 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js @@ -1,44 +1,30 @@ import { __ } from '@wordpress/i18n'; -import { useMemo } from '@wordpress/element'; import SettingsCard from '../../ReusableComponents/SettingsCard'; import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock'; -import { CommonHooks } from '../../../data'; +import { PaymentHooks } from '../../../data'; import { useActiveModal } from '../../../data/common/hooks'; import Modal from './TabSettingsElements/Blocks/Modal'; const TabPaymentMethods = () => { - const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + const { paymentMethodsPayPalCheckout } = + PaymentHooks.usePaymentMethodsPayPalCheckout(); + const { paymentMethodsOnlineCardPayments } = + PaymentHooks.usePaymentMethodsOnlineCardPayments(); + const { paymentMethodsAlternative } = + PaymentHooks.usePaymentMethodsAlternative(); + const { activeModal, setActiveModal } = useActiveModal(); - const filteredPaymentMethods = useMemo( () => { - const contextProps = { storeCountry, storeCurrency }; - - return { - payPalCheckout: filterPaymentMethods( - paymentMethodsPayPalCheckout, - contextProps - ), - onlineCardPayments: filterPaymentMethods( - paymentMethodsOnlineCardPayments, - contextProps - ), - alternative: filterPaymentMethods( - paymentMethodsAlternative, - contextProps - ), - }; - }, [ storeCountry, storeCurrency ] ); - const getActiveMethod = () => { if ( ! activeModal ) { return null; } const allMethods = [ - ...filteredPaymentMethods.payPalCheckout, - ...filteredPaymentMethods.onlineCardPayments, - ...filteredPaymentMethods.alternative, + ...paymentMethodsPayPalCheckout, + ...paymentMethodsOnlineCardPayments, + ...paymentMethodsAlternative, ]; return allMethods.find( ( method ) => method.id === activeModal ); @@ -57,7 +43,7 @@ const TabPaymentMethods = () => { contentContainer={ false } > @@ -75,7 +61,7 @@ const TabPaymentMethods = () => { contentContainer={ false } > @@ -93,7 +79,7 @@ const TabPaymentMethods = () => { contentContainer={ false } > @@ -116,193 +102,4 @@ const TabPaymentMethods = () => { ); }; -function filterPaymentMethods( paymentMethods, contextProps ) { - return paymentMethods.filter( ( method ) => - typeof method.condition === 'function' - ? method.condition( contextProps ) - : true - ); -} - -const paymentMethodsPayPalCheckout = [ - { - id: 'paypal', - title: __( 'PayPal', 'woocommerce-paypal-payments' ), - description: __( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-paypal', - }, - { - id: 'venmo', - title: __( 'Venmo', 'woocommerce-paypal-payments' ), - description: __( - 'Offer Venmo at checkout to millions of active users.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-venmo', - }, - { - id: 'paypal_credit', - title: __( 'Pay Later', 'woocommerce-paypal-payments' ), - description: __( - 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-paypal', - }, - { - id: 'credit_and_debit_card_payments', - title: __( - 'Credit and debit card payments', - 'woocommerce-paypal-payments' - ), - description: __( - "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-cards', - }, -]; - -const paymentMethodsOnlineCardPayments = [ - { - id: 'advanced_credit_and_debit_card_payments', - title: __( - 'Advanced Credit and Debit Card Payments', - 'woocommerce-paypal-payments' - ), - description: __( - "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-advanced-cards', - }, - { - id: 'fastlane', - title: __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ), - description: __( - "Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.", - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-fastlane', - }, - { - id: 'apple_pay', - title: __( 'Apple Pay', 'woocommerce-paypal-payments' ), - description: __( - 'Allow customers to pay via their Apple Pay digital wallet.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-apple-pay', - }, - { - id: 'google_pay', - title: __( 'Google Pay', 'woocommerce-paypal-payments' ), - description: __( - 'Allow customers to pay via their Google Pay digital wallet.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-google-pay', - }, -]; - -const paymentMethodsAlternative = [ - { - id: 'bancontact', - title: __( 'Bancontact', 'woocommerce-paypal-payments' ), - description: __( - 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-bancontact', - }, - { - id: 'ideal', - title: __( 'iDEAL', 'woocommerce-paypal-payments' ), - description: __( - 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-ideal', - }, - { - id: 'eps', - title: __( 'eps', 'woocommerce-paypal-payments' ), - description: __( - 'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-eps', - }, - { - id: 'blik', - title: __( 'BLIK', 'woocommerce-paypal-payments' ), - description: __( - 'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-blik', - }, - { - id: 'mybank', - title: __( 'MyBank', 'woocommerce-paypal-payments' ), - description: __( - 'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-mybank', - }, - { - id: 'przelewy24', - title: __( 'Przelewy24', 'woocommerce-paypal-payments' ), - description: __( - 'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-przelewy24', - }, - { - id: 'trustly', - title: __( 'Trustly', 'woocommerce-paypal-payments' ), - description: __( - 'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-trustly', - }, - { - id: 'multibanco', - title: __( 'Multibanco', 'woocommerce-paypal-payments' ), - description: __( - 'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-multibanco', - }, - { - id: 'pui', - title: __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), - description: __( - 'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-ratepay', - condition: ( { storeCountry, storeCurrency } ) => - storeCountry === 'DE' && storeCurrency === 'EUR', - }, - { - id: 'oxxo', - title: __( 'OXXO', 'woocommerce-paypal-payments' ), - description: __( - 'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 800–925–0304', - 'woocommerce-paypal-payments' - ), - icon: 'payment-method-oxxo', - condition: ( { storeCountry, storeCurrency } ) => - storeCountry === 'MX' && storeCurrency === 'MXN', - }, -]; - export default TabPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js index 21ae3f028..658ec17d1 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaymentMethods.js @@ -19,7 +19,7 @@ const createStandardFields = ( methodId, defaultTitle ) => ( { const paymentMethods = { // PayPal Checkout methods - paypal: { + 'ppcp-gateway': { fields: { ...createStandardFields( 'paypal', 'PayPal' ), showLogo: { @@ -29,12 +29,6 @@ const paymentMethods = { }, }, }, - venmo: { - fields: createStandardFields( 'venmo', 'Venmo' ), - }, - paypal_credit: { - fields: createStandardFields( 'paypal_credit', 'PayPal Credit' ), - }, credit_and_debit_card_payments: { fields: createStandardFields( 'credit_and_debit_card_payments', diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index 861af8de9..49360be6d 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -1,4 +1,8 @@ -import { OnboardingStoreName, CommonStoreName } from './index'; +import { + OnboardingStoreName, + CommonStoreName, + PaymentStoreName, +} from './index'; export const addDebugTools = ( context, modules ) => { if ( ! context || ! context?.debug ) { @@ -47,7 +51,8 @@ export const addDebugTools = ( context, modules ) => { // Reset all stores, except for the onboarding store. stores.push( CommonStoreName ); // TODO: Add other stores here once they are available. - } else { + stores.push( PaymentStoreName ); + } else { // Only reset the common & onboarding stores to restart the onboarding wizard. stores.push( CommonStoreName ); stores.push( OnboardingStoreName ); diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 959c5f187..393c09db2 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -2,19 +2,23 @@ import { addDebugTools } from './debug'; import * as Onboarding from './onboarding'; import * as Common from './common'; import * as Styling from './styling'; +import * as Payment from './payment'; Onboarding.initStore(); Common.initStore(); +Payment.initStore(); Styling.initStore(); export const OnboardingHooks = Onboarding.hooks; export const CommonHooks = Common.hooks; +export const PaymentHooks = Payment.hooks; export const StylingHooks = Styling.hooks; export const OnboardingStoreName = Onboarding.STORE_NAME; export const CommonStoreName = Common.STORE_NAME; +export const PaymentStoreName = Payment.STORE_NAME; export const StylingStoreName = Styling.STORE_NAME; export * from './configuration'; -addDebugTools( window.ppcpSettings, [ Onboarding, Common, Styling ] ); +addDebugTools( window.ppcpSettings, [ Onboarding, Common, Payment, Styling ] ); diff --git a/modules/ppcp-settings/resources/js/data/payment/README.md b/modules/ppcp-settings/resources/js/data/payment/README.md new file mode 100644 index 000000000..b97f6ca4c --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/README.md @@ -0,0 +1,45 @@ +# Store template + +This template contains all files for a Redux store. + +## New Store: Redux integration + +1. Copy this folder, give it a correct name. +2. Check each file for `` placeholders and `TODO` remarks. +3. Edit the main store-index file and add the relevant store integration there. +4. Check the debug-module, and add relevant debug code. + - Register the store in the `reset()` method. + +--- + +Main store-index: +`modules/ppcp-settings/resources/js/data/index.js` + +Sample store integration: +```js +import * as YourStore from './yourStore'; +// ... +YourStore.initStore(); +// ... +export const YourStoreHooks = YourStore.hooks; +// ... +export const YourStoreName = YourStore.STORE_NAME; +// ... +addDebugTools( window.ppcpSettings, [ ..., YourStoreName ] ); +``` + +--- + +### New Store: PHP integration + +1. Create the **REST endpoint** for hydrating and persisting data. + - `modules/ppcp-settings/src/Endpoint/YourStoreRestEndpoint.php` + - Extend from base class `RestEndpoint` +2. Create the **data model** class to manage the DB interaction. + - `modules/ppcp-settings/src/Data/YourStoreSettings.php` + - Extend from base class `AbstractDataModel` +3. Create relevant **DI services** for both files. + - `modules/ppcp-settings/services.php` +4. Register the REST endpoint in the **service module**. + - `modules/ppcp-settings/src/SettingsModule.php` + - Find the action `rest_api_init` diff --git a/modules/ppcp-settings/resources/js/data/payment/action-types.js b/modules/ppcp-settings/resources/js/data/payment/action-types.js new file mode 100644 index 000000000..e68253c5d --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/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: 'PAYMENT:SET_TRANSIENT', + + // Persistent data. + SET_PERSISTENT: 'PAYMENT:SET_PERSISTENT', + RESET: 'PAYMENT:RESET', + HYDRATE: 'PAYMENT:HYDRATE', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: 'PAYMENT:DO_PERSIST_DATA', +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/actions.js b/modules/ppcp-settings/resources/js/data/payment/actions.js new file mode 100644 index 000000000..95c0235b1 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/actions.js @@ -0,0 +1,71 @@ +/** + * 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, +} ); + +/** + * Transient. Marks the store as "ready", i.e., fully initialized. + * + * @param {boolean} isReady + * @return {Action} The action. + */ +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { 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 }; +}; + +/** + * 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 }, +} ); diff --git a/modules/ppcp-settings/resources/js/data/payment/constants.js b/modules/ppcp-settings/resources/js/data/payment/constants.js new file mode 100644 index 000000000..82c428074 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/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/payment'; + +/** + * REST path to hydrate data of this module by loading data from the WP DB. + * + * Used by: Resolvers + * See: payment.php + * + * @type {string} + */ +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/payment'; + +/** + * REST path to persist data of this module to the WP DB. + * + * Used by: Controls + * See: payment.php + * + * @type {string} + */ +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/payment'; diff --git a/modules/ppcp-settings/resources/js/data/payment/controls.js b/modules/ppcp-settings/resources/js/data/payment/controls.js new file mode 100644 index 000000000..9295b62bc --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/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/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js new file mode 100644 index 000000000..c10f0969e --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js @@ -0,0 +1,155 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * + * @file + */ + +import { useSelect, useDispatch } from '@wordpress/data'; + +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); + +const useHooks = () => { + const { persist, setPersistent } = useDispatch( STORE_NAME ); + + // Read-only flags and derived state. + // Nothing here yet. + + // Transient accessors. + const isReady = useTransient( 'isReady' ); + + // PayPal checkout. + const paypal = usePersistent( 'ppcp-gateway' ); + const venmo = usePersistent( 'venmo' ); + const payLater = usePersistent( 'pay-later' ); + const creditCard = usePersistent( 'ppcp-card-button-gateway' ); + + // Online card Payments. + const advancedCreditCard = usePersistent( 'ppcp-credit-card-gateway' ); + const fastlane = usePersistent( 'ppcp-axo-gateway' ); + const applePay = usePersistent( 'ppcp-applepay' ); + const googlePay = usePersistent( 'ppcp-googlepay' ); + + // Alternative payment methods. + const bancontact = usePersistent( 'ppcp-bancontact' ); + const blik = usePersistent( 'ppcp-blik' ); + const eps = usePersistent( 'ppcp-eps' ); + const ideal = usePersistent( 'ppcp-ideal' ); + const mybank = usePersistent( 'ppcp-mybank' ); + const p24 = usePersistent( 'ppcp-p24' ); + const trustly = usePersistent( 'ppcp-trustly' ); + const multibanco = usePersistent( 'ppcp-multibanco' ); + const pui = usePersistent( 'ppcp-pay-upon-invoice-gateway' ); + const oxxo = usePersistent( 'ppcp-oxxo-gateway' ); + + return { + persist, + isReady, + setPersistent, + paypal, + venmo, + payLater, + creditCard, + advancedCreditCard, + fastlane, + applePay, + googlePay, + bancontact, + blik, + eps, + ideal, + mybank, + p24, + trustly, + multibanco, + pui, + oxxo, + }; +}; + +export const useState = () => { + const { persist, isReady } = useHooks(); + return { persist, isReady }; +}; + +export const usePaymentMethods = () => { + const { setPersistent } = useHooks(); + + return { + setPersistent, + }; +}; + +export const usePaymentMethodsPayPalCheckout = () => { + const { paypal, venmo, payLater, creditCard } = useHooks(); + const paymentMethodsPayPalCheckout = [ + paypal, + venmo, + payLater, + creditCard, + ]; + + return { + paymentMethodsPayPalCheckout, + }; +}; + +export const usePaymentMethodsOnlineCardPayments = () => { + const { advancedCreditCard, fastlane, applePay, googlePay } = useHooks(); + const paymentMethodsOnlineCardPayments = [ + advancedCreditCard, + fastlane, + applePay, + googlePay, + ]; + + return { + paymentMethodsOnlineCardPayments, + }; +}; + +export const usePaymentMethodsAlternative = () => { + const { + bancontact, + blik, + eps, + ideal, + mybank, + p24, + trustly, + multibanco, + pui, + oxxo, + } = useHooks(); + + const paymentMethodsAlternative = [ + bancontact, + blik, + eps, + ideal, + mybank, + p24, + trustly, + multibanco, + pui, + oxxo, + ]; + + return { + paymentMethodsAlternative, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/index.js b/modules/ppcp-settings/resources/js/data/payment/index.js new file mode 100644 index 000000000..28c162f98 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/index.js @@ -0,0 +1,24 @@ +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'; + +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/payment/reducer.js b/modules/ppcp-settings/resources/js/data/payment/reducer.js new file mode 100644 index 000000000..4894c9996 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/reducer.js @@ -0,0 +1,72 @@ +/** + * 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, createSetters } 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( { + 'ppcp-gateway': {}, + venmo: {}, + 'pay-later': {}, + 'ppcp-card-button-gateway': {}, + 'ppcp-credit-card-gateway': {}, + 'ppcp-axo-gateway': {}, + 'ppcp-applepay': {}, + 'ppcp-googlepay': {}, + 'ppcp-bancontact': {}, + 'ppcp-blik': {}, + 'ppcp-eps': {}, + 'ppcp-ideal': {}, + 'ppcp-mybank': {}, + 'ppcp-p24': {}, + 'ppcp-trustly': {}, + 'ppcp-multibanco': {}, + 'ppcp-pay-upon-invoice-gateway': {}, + 'ppcp-oxxo-gateway': {}, +} ); + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +const reducer = createReducer( defaultTransient, defaultPersistent, { + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => + setTransient( state, payload ), + + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => + setPersistent( state, payload ), + + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.isReady = true; + + return cleanState; + }, + + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => + setPersistent( state, payload.data ), +} ); + +export default reducer; diff --git a/modules/ppcp-settings/resources/js/data/payment/resolvers.js b/modules/ppcp-settings/resources/js/data/payment/resolvers.js new file mode 100644 index 000000000..ebc6832bb --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/resolvers.js @@ -0,0 +1,36 @@ +/** + * 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( + __( + 'Error retrieving payment details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/payment/selectors.js b/modules/ppcp-settings/resources/js/data/payment/selectors.js new file mode 100644 index 000000000..14334fcf3 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/payment/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/services.php b/modules/ppcp-settings/services.php index a0d564fca..2d362e978 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -13,11 +13,13 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\PaymentRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SettingsRestEndpoint; @@ -80,6 +82,9 @@ return array( 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); }, + 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { + return new PaymentRestEndpoint(); + }, 'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint { return new StylingRestEndpoint( $container->get( 'settings.data.styling' ), diff --git a/modules/ppcp-settings/src/Data/PaymentSettings.php b/modules/ppcp-settings/src/Data/PaymentSettings.php deleted file mode 100644 index 0180150a2..000000000 --- a/modules/ppcp-settings/src/Data/PaymentSettings.php +++ /dev/null @@ -1,34 +0,0 @@ - array( + 'id' => 'ppcp-gateway', + 'title' => __( 'PayPal', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximize conversion.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-paypal', + ), + 'venmo' => array( + 'id' => 'venmo', + 'title' => __( 'Venmo', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Offer Venmo at checkout to millions of active users.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-venmo', + ), + 'pay-later' => array( + 'id' => 'paypal_credit', + 'title' => __( 'Pay Later', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Get paid in full at checkout while giving your customers the flexibility to pay in installments over time with no late fees.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-paypal', + ), + CardButtonGateway::ID => array( + 'id' => 'credit_and_debit_card_payments', + 'title' => __( + 'Credit and debit card payments', + 'woocommerce-paypal-payments' + ), + 'description' => __( + "Accept all major credit and debit cards - even if your customer doesn't have a PayPal account.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-cards', + ), + + // Online card Payments. + CreditCardGateway::ID => array( + 'id' => 'advanced_credit_and_debit_card_payments', + 'title' => __( + 'Advanced Credit and Debit Card Payments', + 'woocommerce-paypal-payments' + ), + 'description' => __( + "Present custom credit and debit card fields to your payers so they can pay with credit and debit cards using your site's branding.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-advanced-cards', + ), + AxoGateway::ID => array( + 'id' => 'fastlane', + 'title' => __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ), + 'description' => __( + "Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.", + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-fastlane', + ), + ApplePayGateway::ID => array( + 'id' => 'apple_pay', + 'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Allow customers to pay via their Apple Pay digital wallet.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-apple-pay', + ), + GooglePayGateway::ID => array( + 'id' => 'google_pay', + 'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Allow customers to pay via their Google Pay digital wallet.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-google-pay', + ), + + // Alternative payment methods. + BancontactGateway::ID => array( + 'id' => 'bancontact', + 'title' => __( 'Bancontact', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Bancontact is the most widely used, accepted and trusted electronic payment method in Belgium. Bancontact makes it possible to pay directly through the online payment systems of all major Belgian banks.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-bancontact', + ), + BlikGateway::ID => array( + 'id' => 'blik', + 'title' => __( 'BLIK', 'woocommerce-paypal-payments' ), + 'description' => __( + 'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-blik', + ), + EPSGateway::ID => array( + 'id' => 'eps', + 'title' => __( 'eps', 'woocommerce-paypal-payments' ), + 'description' => __( + 'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-eps', + ), + IDealGateway::ID => array( + 'id' => 'ideal', + 'title' => __( 'iDEAL', 'woocommerce-paypal-payments' ), + 'description' => __( + 'iDEAL is a payment method in the Netherlands that allows buyers to select their issuing bank from a list of options.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-ideal', + ), + MyBankGateway::ID => array( + 'id' => 'mybank', + 'title' => __( 'MyBank', 'woocommerce-paypal-payments' ), + 'description' => __( + 'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-mybank', + ), + P24Gateway::ID => array( + 'id' => 'przelewy24', + 'title' => __( 'Przelewy24', 'woocommerce-paypal-payments' ), + 'description' => __( + 'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-przelewy24', + ), + TrustlyGateway::ID => array( + 'id' => 'trustly', + 'title' => __( 'Trustly', 'woocommerce-paypal-payments' ), + 'description' => __( + 'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-trustly', + ), + MultibancoGateway::ID => array( + 'id' => 'multibanco', + 'title' => __( 'Multibanco', 'woocommerce-paypal-payments' ), + 'description' => __( + 'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + 'icon' => 'payment-method-multibanco', + ), + PayUponInvoiceGateway::ID => array( + 'id' => 'pui', + 'title' => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), + 'description' => __( + 'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.', + 'woocommerce-paypal-payments' + ), + 'icon' => '', + ), + OXXO::ID => array( + 'id' => 'oxxo', + 'title' => __( 'OXXO', 'woocommerce-paypal-payments' ), + 'description' => __( + 'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 800–925–0304', + 'woocommerce-paypal-payments' + ), + 'icon' => '', + ), + ); + } + + /** + * Configure REST API routes. + */ + public function register_routes() : void { + /** + * GET wc/v3/wc_paypal/payment + */ + 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/payment + * { + * [gateway_id]: { + * enabled + * title + * description + * } + * } + */ + 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 all payment methods details. + * + * @return WP_REST_Response The current payment methods details. + */ + public function get_details() : WP_REST_Response { + $all_gateways = WC()->payment_gateways->payment_gateways(); + + $gateway_settings = array(); + + foreach ( $this->gateways() as $key => $value ) { + if ( ! isset( $all_gateways[ $key ] ) ) { + $gateway_settings[ $key ] = array( + 'id' => $this->gateways()[ $key ]['id'] ?? '', + 'title' => $this->gateways()[ $key ]['title'] ?? '', + 'description' => $this->gateways()[ $key ]['description'] ?? '', + 'enabled' => false, + 'icon' => $this->gateways()[ $key ]['icon'] ?? '', + ); + + continue; + } + + $gateway = $all_gateways[ $key ]; + + $gateway_settings[ $key ] = array( + 'enabled' => 'yes' === $gateway->enabled, + 'title' => $this->gateways()[ $key ]['title'] ?? $gateway->get_title(), + 'description' => $this->gateways()[ $key ]['description'] ?? $gateway->get_description(), + 'method_title' => $gateway->get_method_title(), + 'id' => $this->gateways()[ $key ]['id'] ?? $key, + 'icon' => $this->gateways()[ $key ]['icon'] ?? '', + ); + } + + return $this->return_success( $gateway_settings ); + } + + /** + * Updates payment methods details based on the request. + * + * @param WP_REST_Request $request Full data about the request. + * + * @return WP_REST_Response The updated payment methods details. + */ + public function update_details( WP_REST_Request $request ) : WP_REST_Response { + $all_gateways = WC()->payment_gateways->payment_gateways(); + + $request_data = $request->get_params(); + + foreach ( $this->gateways() as $key => $value ) { + // Check if the REST body contains details for this gateway. + if ( ! isset( $request_data[ $key ] ) || ! isset( $all_gateways[ $key ] ) ) { + continue; + } + + $gateway = $all_gateways[ $key ]; + $new_data = $request_data[ $key ]; + + if ( isset( $new_data['enabled'] ) ) { + $gateway->update_option( 'enabled', $new_data['enabled'] ? 'yes' : 'no' ); + } + if ( isset( $new_data['title'] ) ) { + $gateway->update_option( 'title', sanitize_text_field( $new_data['title'] ) ); + } + if ( isset( $new_data['description'] ) ) { + $gateway->update_option( 'description', wp_kses_post( $new_data['description'] ) ); + } + + $gateway->process_admin_options(); + } + + return $this->get_details(); + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index c9ec7ad15..402379d8c 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -211,6 +211,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { 'login_link' => $container->get( 'settings.rest.login_link' ), 'webhooks' => $container->get( 'settings.rest.webhooks' ), 'refresh_feature_status' => $container->get( 'settings.rest.refresh_feature_status' ), + 'payment' => $container->get( 'settings.rest.payment' ), 'settings' => $container->get( 'settings.rest.settings' ), 'styling' => $container->get( 'settings.rest.styling' ), );