Consolidate payment method modals into unified config-driven system

This commit is contained in:
Daniel Dudzic 2025-01-09 00:16:12 +01:00
parent 4bf4b777df
commit 02baae890f
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
9 changed files with 353 additions and 263 deletions

View file

@ -2,7 +2,7 @@ import { ToggleControl } from '@wordpress/components';
import SettingsBlock from './SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon';
import data from '../../../utils/data';
import { MODAL_CONFIG } from '../../Screens/Overview/Modals/Modal';
import { hasSettings } from '../../Screens/Overview/TabSettingsElements/Blocks/PaymentMethods';
const PaymentMethodItemBlock = ( {
id,
@ -13,8 +13,8 @@ const PaymentMethodItemBlock = ( {
onSelect,
isSelected,
} ) => {
// Only show settings icon if this method has a modal configured
const hasModal = Boolean( MODAL_CONFIG[ id ] );
// Only show settings icon if this method has fields configured
const hasModal = hasSettings( id );
return (
<SettingsBlock className="ppcp-r-settings-block__payment-methods__item">

View file

@ -7,13 +7,16 @@ const PaymentMethodsBlock = ( {
className = '',
onTriggerModal,
} ) => {
const [ selectedMethod, setSelectedMethod ] = useState( null );
const [ selectedMethods, setSelectedMethods ] = useState( {} );
const handleSelect = useCallback( ( methodId, isSelected ) => {
setSelectedMethod( isSelected ? methodId : null );
setSelectedMethods( ( prev ) => ( {
...prev,
[ methodId ]: isSelected,
} ) );
}, [] );
if ( paymentMethods.length === 0 ) {
if ( ! paymentMethods?.length ) {
return null;
}
@ -25,7 +28,9 @@ const PaymentMethodsBlock = ( {
<PaymentMethodItemBlock
key={ paymentMethod.id }
{ ...paymentMethod }
isSelected={ selectedMethod === paymentMethod.id }
isSelected={ Boolean(
selectedMethods[ paymentMethod.id ]
) }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}

View file

@ -1,51 +0,0 @@
import { __ } from '@wordpress/i18n';
import ModalPayPal from './ModalPayPal';
import ModalFastlane from './ModalFastlane';
import ModalAcdc from './ModalAcdc';
import { useActiveModal } from '../../../../data/common/hooks';
export const MODAL_CONFIG = {
paypal: {
component: ModalPayPal,
icon: 'payment-method-paypal-big',
title: __( 'PayPal', 'woocommerce-paypal-payments' ),
},
fastlane: {
component: ModalFastlane,
icon: 'payment-method-fastlane-big',
title: __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ),
size: 'small',
},
advanced_credit_and_debit_card_payments: {
component: ModalAcdc,
icon: 'payment-method-cards-big',
title: __(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
),
},
};
const Modal = () => {
const { activeModal, setActiveModal } = useActiveModal();
const handleCloseModal = () => {
setActiveModal( '' );
};
if ( ! activeModal || ! MODAL_CONFIG[ activeModal ] ) {
return null;
}
const { component: ModalComponent, ...modalProps } =
MODAL_CONFIG[ activeModal ];
return (
<ModalComponent
setModalIsVisible={ handleCloseModal }
{ ...modalProps }
/>
);
};
export default Modal;

View file

@ -1,62 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { RadioControl } from '@wordpress/components';
const ModalAcdc = ( { setModalIsVisible } ) => {
const [ threeDSecure, setThreeDSecure ] = useState( 'no-3d-secure' );
const acdcOptions = [
{
label: __( 'No 3D Secure', 'woocommerce-paypal-payments' ),
value: 'no-3d-secure',
},
{
label: __( 'Only when required', 'woocommerce-paypal-payments' ),
value: 'only-required-3d-secure',
},
{
label: __(
'Always require 3D Secure',
'woocommerce-paypal-payments'
),
value: 'always-3d-secure',
},
];
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-cards-big"
title={ __(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
) }
>
<strong className="ppcp-r-modal__content-title">
{ __( '3D Secure', 'woocommerce-paypal-payments' ) }
</strong>
<p className="ppcp-r-modal__description">
{ __(
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
'woocommerce-paypal-payments'
) }
</p>
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--acdc">
<RadioControl
onChange={ setThreeDSecure }
selected={ threeDSecure }
options={ acdcOptions }
/>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalAcdc;

View file

@ -1,63 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { Button, ToggleControl } from '@wordpress/components';
import { PayPalRdb } from '../../../ReusableComponents/Fields';
import { useState } from '@wordpress/element';
const ModalFastlane = ( { setModalIsVisible } ) => {
const [ fastlaneSettings, setFastlaneSettings ] = useState( {
cardholderName: false,
displayWatermark: false,
} );
const updateFormValue = ( key, value ) => {
setFastlaneSettings( { ...fastlaneSettings, [ key ]: value } );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-fastlane-big"
title={ __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ) }
size="small"
>
<div className="ppcp-r-modal__field-rows ppcp-r-modal__field-rows--fastlane">
<div className="ppcp-r-modal__field-row">
<ToggleControl
className="ppcp-r-modal__inverted-toggle-control"
checked={ fastlaneSettings.cardholderName }
onChange={ ( newValue ) =>
updateFormValue( 'cardholderName', newValue )
}
label={ __(
'Display cardholder name',
'woocommerce-paypal-payments'
) }
id="ppcp-r-fastlane-settings-cardholder"
/>
</div>
<div className="ppcp-r-modal__field-row">
<ToggleControl
className="ppcp-r-modal__inverted-toggle-control"
checked={ fastlaneSettings.displayWatermark }
onChange={ ( newValue ) =>
updateFormValue( 'displayWatermark', newValue )
}
label={ __(
'Display Fastlane Watermark',
'woocommerce-paypal-payments'
) }
id="ppcp-r-fastlane-settings-watermark"
/>
</div>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalFastlane;

View file

@ -1,76 +0,0 @@
import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal';
import { __ } from '@wordpress/i18n';
import { ToggleControl, Button, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
const ModalPayPal = ( { setModalIsVisible } ) => {
const [ paypalSettings, setPaypalSettings ] = useState( {
checkoutPageTitle: 'PayPal',
checkoutPageDescription: 'Pay via PayPal',
showLogo: false,
} );
const updateFormValue = ( key, value ) => {
setPaypalSettings( { ...paypalSettings, [ key ]: value } );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon="payment-method-paypal-big"
title={ __( 'PayPal', 'woocommerce-paypal-payments' ) }
>
<div className="ppcp-r-modal__field-rows">
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ __(
'Checkout page title',
'woocommerce-paypal-payments'
) }
value={ paypalSettings.checkoutPageTitle }
onChange={ ( newValue ) =>
updateFormValue( 'checkoutPageTitle', newValue )
}
/>
</div>
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ __(
'Checkout page description',
'woocommerce-paypal-payments'
) }
value={ paypalSettings.checkoutPageDescription }
onChange={ ( newValue ) =>
updateFormValue(
'checkoutPageDescription',
newValue
)
}
/>
</div>
<div className="ppcp-r-modal__field-row">
<ToggleControl
label={ __(
'Show logo',
'woocommerce-paypal-payments'
) }
id="ppcp-r-paypal-settings-show-logo"
checked={ paypalSettings.showLogo }
onChange={ ( newValue ) => {
updateFormValue( 'showLogo', newValue );
} }
/>
</div>
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary">
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default ModalPayPal;

View file

@ -5,11 +5,11 @@ import SettingsCard from '../../ReusableComponents/SettingsCard';
import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock';
import { CommonHooks } from '../../../data';
import { useActiveModal } from '../../../data/common/hooks';
import Modal from './Modals/Modal';
import Modal from './TabSettingsElements/Blocks/Modal';
const TabPaymentMethods = () => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const { setActiveModal } = useActiveModal();
const { activeModal, setActiveModal } = useActiveModal();
const filteredPaymentMethods = useMemo( () => {
const contextProps = { storeCountry, storeCurrency };
@ -30,6 +30,20 @@ const TabPaymentMethods = () => {
};
}, [ storeCountry, storeCurrency ] );
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
}
const allMethods = [
...filteredPaymentMethods.payPalCheckout,
...filteredPaymentMethods.onlineCardPayments,
...filteredPaymentMethods.alternative,
];
return allMethods.find( ( method ) => method.id === activeModal );
};
return (
<div className="ppcp-r-payment-methods">
<SettingsCard
@ -84,7 +98,20 @@ const TabPaymentMethods = () => {
/>
</SettingsCard>
<Modal />
{ activeModal && (
<Modal
method={ getActiveMethod() }
setModalIsVisible={ () => setActiveModal( null ) }
onSave={ ( methodId, settings ) => {
console.log(
'Saving settings for:',
methodId,
settings
);
setActiveModal( null );
} }
/>
) }
</div>
);
};
@ -162,7 +189,7 @@ const paymentMethodsOnlineCardPayments = [
icon: 'payment-method-fastlane',
},
{
id: 'apply_pay',
id: 'apple_pay',
title: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
description: __(
'Allow customers to pay via their Apple Pay digital wallet.',

View file

@ -0,0 +1,131 @@
import { __ } from '@wordpress/i18n';
import {
Button,
TextControl,
ToggleControl,
RadioControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import PaymentMethodModal from '../../../../ReusableComponents/PaymentMethodModal';
import { getPaymentMethods } from './PaymentMethods';
const Modal = ( { method, setModalIsVisible, onSave } ) => {
const [ settings, setSettings ] = useState( () => {
if ( ! method?.id ) {
return {};
}
const methodConfig = getPaymentMethods( method );
if ( ! methodConfig?.fields ) {
return {};
}
const initialSettings = {};
Object.entries( methodConfig.fields ).forEach( ( [ key, field ] ) => {
initialSettings[ key ] = field.default;
} );
return initialSettings;
} );
if ( ! method?.id ) {
return null;
}
const methodConfig = getPaymentMethods( method );
if ( ! methodConfig?.fields ) {
return null;
}
const renderField = ( key, field ) => {
switch ( field.type ) {
case 'text':
return (
<div className="ppcp-r-modal__field-row">
<TextControl
className="ppcp-r-vertical-text-control"
label={ field.label }
value={ settings[ key ] }
onChange={ ( value ) =>
setSettings( ( prev ) => ( {
...prev,
[ key ]: value,
} ) )
}
/>
</div>
);
case 'toggle':
return (
<div className="ppcp-r-modal__field-row">
<ToggleControl
label={ field.label }
checked={ settings[ key ] }
onChange={ ( value ) =>
setSettings( ( prev ) => ( {
...prev,
[ key ]: value,
} ) )
}
/>
</div>
);
case 'radio':
return (
<>
<strong className="ppcp-r-modal__content-title">
{ field.label }
</strong>
{ field.description && (
<p className="ppcp-r-modal__description">
{ field.description }
</p>
) }
<div className="ppcp-r-modal__field-row">
<RadioControl
selected={ settings[ key ] }
options={ field.options }
onChange={ ( value ) =>
setSettings( ( prev ) => ( {
...prev,
[ key ]: value,
} ) )
}
/>
</div>
</>
);
default:
return null;
}
};
const handleSave = () => {
onSave?.( method.id, settings );
setModalIsVisible( false );
};
return (
<PaymentMethodModal
setModalIsVisible={ setModalIsVisible }
icon={ methodConfig.icon }
title={ method.title }
>
<div className="ppcp-r-modal__field-rows">
{ Object.entries( methodConfig.fields ).map(
( [ key, field ] ) => renderField( key, field )
) }
<div className="ppcp-r-modal__field-row ppcp-r-modal__field-row--save">
<Button variant="primary" onClick={ handleSave }>
{ __( 'Save changes', 'woocommerce-paypal-payments' ) }
</Button>
</div>
</div>
</PaymentMethodModal>
);
};
export default Modal;

View file

@ -0,0 +1,179 @@
import { __, sprintf } from '@wordpress/i18n';
const createStandardFields = ( methodId, defaultTitle ) => ( {
checkoutPageTitle: {
type: 'text',
default: defaultTitle,
label: __( 'Checkout page title', 'woocommerce-paypal-payments' ),
},
checkoutPageDescription: {
type: 'text',
default: sprintf(
/* translators: %s: payment method title */
__( 'Pay with %s', 'woocommerce-paypal-payments' ),
defaultTitle
),
label: __( 'Checkout page description', 'woocommerce-paypal-payments' ),
},
} );
const paymentMethods = {
// PayPal Checkout methods
paypal: {
fields: {
...createStandardFields( 'paypal', 'PayPal' ),
showLogo: {
type: 'toggle',
default: false,
label: __( 'Show logo', 'woocommerce-paypal-payments' ),
},
},
},
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',
__(
'Credit and debit card payments',
'woocommerce-paypal-payments'
)
),
},
// Online Card Payments
advanced_credit_and_debit_card_payments: {
fields: {
...createStandardFields(
'advanced_credit_and_debit_card_payments',
__(
'Advanced Credit and Debit Card Payments',
'woocommerce-paypal-payments'
)
),
threeDSecure: {
type: 'radio',
default: 'no-3d-secure',
label: __( '3D Secure', 'woocommerce-paypal-payments' ),
description: __(
'Authenticate cardholders through their card issuers to reduce fraud and improve transaction security. Successful 3D Secure authentication can shift liability for fraudulent chargebacks to the card issuer.',
'woocommerce-paypal-payments'
),
options: [
{
label: __(
'No 3D Secure',
'woocommerce-paypal-payments'
),
value: 'no-3d-secure',
},
{
label: __(
'Only when required',
'woocommerce-paypal-payments'
),
value: 'only-required-3d-secure',
},
{
label: __(
'Always require 3D Secure',
'woocommerce-paypal-payments'
),
value: 'always-3d-secure',
},
],
},
},
},
fastlane: {
fields: {
...createStandardFields( 'fastlane', 'Fastlane by PayPal' ),
cardholderName: {
type: 'toggle',
default: false,
label: __(
'Display cardholder name',
'woocommerce-paypal-payments'
),
},
displayWatermark: {
type: 'toggle',
default: false,
label: __(
'Display Fastlane Watermark',
'woocommerce-paypal-payments'
),
},
},
},
// Digital Wallets
apple_pay: {
fields: createStandardFields( 'apple_pay', 'Apple Pay' ),
},
google_pay: {
fields: createStandardFields( 'google_pay', 'Google Pay' ),
},
// Alternative Payment Methods
bancontact: {
fields: createStandardFields( 'bancontact', 'Bancontact' ),
},
ideal: {
fields: createStandardFields( 'ideal', 'iDEAL' ),
},
eps: {
fields: createStandardFields( 'eps', 'eps' ),
},
blik: {
fields: createStandardFields( 'blik', 'BLIK' ),
},
mybank: {
fields: createStandardFields( 'mybank', 'MyBank' ),
},
przelewy24: {
fields: createStandardFields( 'przelewy24', 'Przelewy24' ),
},
trustly: {
fields: createStandardFields( 'trustly', 'Trustly' ),
},
multibanco: {
fields: createStandardFields( 'multibanco', 'Multibanco' ),
},
pui: {
fields: createStandardFields( 'pui', 'Pay upon Invoice' ),
},
oxxo: {
fields: createStandardFields( 'oxxo', 'OXXO' ),
},
};
// Function to get configuration for a payment method
export const getPaymentMethods = ( method ) => {
if ( ! method?.id ) {
return null;
}
// If method has specific config, return it
if ( paymentMethods[ method.id ] ) {
return {
...paymentMethods[ method.id ],
icon: method.icon,
};
}
// Return standard config for new payment methods
return {
fields: createStandardFields( method.id, method.title ),
icon: method.icon,
};
};
// Function to check if a method has settings defined
export const hasSettings = ( methodId ) => {
return Boolean( methodId && paymentMethods[ methodId ] );
};