Merge pull request #3154 from woocommerce/PCP-4249-fastlane-add-incompatible-setup-notice

Settings UI: Add support for incompatible message for payment method items (4249)
This commit is contained in:
Emili Castells 2025-02-26 14:32:53 +01:00 committed by GitHub
commit 63b3c8a113
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 281 additions and 56 deletions

View file

@ -206,6 +206,13 @@ return array(
return $settings_notice_generator->generate_checkout_notice();
},
'axo.checkout-config-notice.raw' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
return $settings_notice_generator->generate_checkout_notice( true );
},
'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
@ -213,6 +220,14 @@ return array(
return $settings_notice_generator->generate_incompatible_plugins_notice();
},
'axo.incompatible-plugins-notice.raw' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = new SettingsNoticeGenerator(
$container->get( 'axo.fastlane-incompatible-plugin-names' )
);
return $settings_notice_generator->generate_incompatible_plugins_notice( true );
},
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );

View file

@ -23,7 +23,7 @@ class SettingsNoticeGenerator {
*
* @var string[]
*/
protected $incompatible_plugin_names;
protected array $incompatible_plugin_names;
/**
* SettingsNoticeGenerator constructor.
@ -37,16 +37,21 @@ class SettingsNoticeGenerator {
/**
* Generates the full HTML of the notification.
*
* @param string $message HTML of the inner message contents.
* @param bool $is_error Whether the provided message is an error. Affects the notice color.
* @param string $message HTML of the inner message contents.
* @param bool $is_error Whether the provided message is an error. Affects the notice color.
* @param bool $raw_message Whether to return raw message without HTML wrappers.
*
* @return string The full HTML code of the notification, or an empty string.
* @return string The full HTML code of the notification, or an empty string, or raw message.
*/
private function render_notice( string $message, bool $is_error = false ) : string {
private function render_notice( string $message, bool $is_error = false, bool $raw_message = false ) : string {
if ( ! $message ) {
return '';
}
if ( $raw_message ) {
return $message;
}
return sprintf(
'<div class="ppcp-notice %1$s"><p>%2$s</p></div>',
$is_error ? 'ppcp-notice-error' : '',
@ -57,9 +62,10 @@ class SettingsNoticeGenerator {
/**
* Generates the checkout notice.
*
* @param bool $raw_message Whether to return raw message without HTML wrappers.
* @return string
*/
public function generate_checkout_notice(): string {
public function generate_checkout_notice( bool $raw_message = false ): string {
$checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
$block_checkout_docs_link = __(
'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/#using-the-cart-and-checkout-blocks',
@ -90,15 +96,16 @@ class SettingsNoticeGenerator {
);
}
return $notice_content ? '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>' : '';
return $this->render_notice( $notice_content, true, $raw_message );
}
/**
* Generates the incompatible plugins notice.
*
* @param bool $raw_message Whether to return raw message without HTML wrappers.
* @return string
*/
public function generate_incompatible_plugins_notice(): string {
public function generate_incompatible_plugins_notice( bool $raw_message = false ): string {
if ( empty( $this->incompatible_plugin_names ) ) {
return '';
}
@ -114,17 +121,17 @@ class SettingsNoticeGenerator {
implode( '', $this->incompatible_plugin_names )
);
return '<div class="ppcp-notice"><p>' . $notice_content . '</p></div>';
return $this->render_notice( $notice_content, false, $raw_message );
}
/**
* Generates a warning notice with instructions on conflicting plugin-internal settings.
*
* @param Settings $settings The plugin settings container, which is checked for conflicting
* values.
* @param Settings $settings The plugin settings container, which is checked for conflicting values.
* @param bool $raw_message Whether to return raw message without HTML wrappers.
* @return string
*/
public function generate_settings_conflict_notice( Settings $settings ) : string {
public function generate_settings_conflict_notice( Settings $settings, bool $raw_message = false ) : string {
$notice_content = '';
$is_dcc_enabled = false;
@ -142,6 +149,6 @@ class SettingsNoticeGenerator {
);
}
return $this->render_notice( $notice_content, true );
return $this->render_notice( $notice_content, true, $raw_message );
}
}

View file

@ -17,6 +17,7 @@ $color-text-text: #070707;
$color-border: #AEAEAE;
$color-divider: #F0F0F0;
$color-error-red: #cc1818;
$color-warning: #e2a030;
$shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15);
$color-gradient-dark: #001435;
@ -66,6 +67,7 @@ $card-vertical-gap: 48px;
--color-text-teriary: #{$color-text-tertiary};
--color-text-description: #{$color-gray-700};
--color-error: #{$color-error-red};
--color-warning: #{$color-warning};
// Default settings-block theme.
--block-item-gap: 16px;

View file

@ -85,6 +85,11 @@
margin-top: auto;
min-height: 24px;
}
.ppcp--method-toggle-wrapper {
display: flex;
align-items: center;
}
}
}
@ -145,7 +150,7 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
z-index: 8;
border-radius: var(--container-border-radius);
pointer-events: auto;
opacity: 0;
@ -162,10 +167,127 @@
@include font(13, 20, 500);
color: $color-text-tertiary;
position: relative;
z-index: 51;
z-index: 9;
border: none;
a {
text-decoration: none;
}
}
/* Warning message */
.ppcp--method-warning {
position: relative;
display: inline-flex;
cursor: help;
svg {
fill: currentColor;
color: $color-warning;
}
/* Add invisible bridge to prevent gap between icon and popover */
&:before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 15px;
background-color: transparent;
}
// Popover bubble
.ppcp--method-warning-message {
position: absolute;
bottom: calc(100% + 15px);
display: flex;
flex-direction: column;
gap: 10px;
left: 50%;
transform: translateX(-50%);
width: 250px;
padding: 16px;
background-color: $color-white;
border: 1px solid $color-gray-200;
border-radius: 4px;
z-index: 9;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s;
pointer-events: none;
&:after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -6px;
width: 12px;
height: 12px;
background: $color-white;
border-right: 1px solid $color-gray-200;
border-bottom: 1px solid $color-gray-200;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.01);
transform: rotate(45deg);
margin-top: -6px;
}
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.ppcp--method-notice-list {
margin-bottom: 0;
}
.highlight {
font-weight: 700;
background: inherit;
color: inherit;
}
code {
font-size: 12px;
}
ul {
list-style: inside;
}
}
&:hover .ppcp--method-warning-message,
& .ppcp--method-warning-message:hover {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
}
// For RTL support
html[dir="rtl"] .ppcp--method-warning {
&:before {
left: auto;
right: 50%;
transform: translateX(50%);
}
.ppcp--method-warning-message {
left: auto;
right: 50%;
transform: translateX(50%);
&:after {
left: auto;
right: 50%;
margin-right: -6px;
margin-left: 0;
}
}
}

View file

@ -5,6 +5,7 @@ import { useActiveHighlight } from '../../../data/common/hooks';
import SettingsBlock from '../SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon';
import WarningMessages from '../../../Components/Screens/Settings/Components/Payment/WarningMessages';
const PaymentMethodItemBlock = ( {
paymentMethod,
@ -13,9 +14,12 @@ const PaymentMethodItemBlock = ( {
isSelected,
isDisabled,
disabledMessage,
warningMessages,
} ) => {
const { activeHighlight, setActiveHighlight } = useActiveHighlight();
const isHighlighted = activeHighlight === paymentMethod.id;
const hasWarning =
warningMessages && Object.keys( warningMessages ).length > 0;
// Reset the active highlight after 2 seconds
useEffect( () => {
@ -28,12 +32,20 @@ const PaymentMethodItemBlock = ( {
}
}, [ isHighlighted, setActiveHighlight ] );
// Determine class names based on states
const methodItemClasses = [
'ppcp--method-item',
isHighlighted ? 'ppcp-highlight' : '',
isDisabled ? 'ppcp--method-item--disabled' : '',
hasWarning && ! isDisabled ? 'ppcp--method-item--warning' : '',
]
.filter( Boolean )
.join( ' ' );
return (
<SettingsBlock
id={ paymentMethod.id }
className={ `ppcp--method-item ${
isHighlighted ? 'ppcp-highlight' : ''
} ${ isDisabled ? 'ppcp--method-item--disabled' : '' }` }
className={ methodItemClasses }
separatorAndGap={ false }
>
{ isDisabled && (
@ -59,11 +71,18 @@ const PaymentMethodItemBlock = ( {
{ paymentMethod.itemDescription }
</p>
<div className="ppcp--method-footer">
<ToggleControl
__nextHasNoMarginBottom
checked={ isSelected }
onChange={ onSelect }
/>
<div className="ppcp--method-toggle-wrapper">
<ToggleControl
__nextHasNoMarginBottom
checked={ isSelected }
onChange={ onSelect }
/>
{ hasWarning && ! isDisabled && isSelected && (
<WarningMessages
warningMessages={ warningMessages }
/>
) }
</div>
{ paymentMethod?.fields && onTriggerModal && (
<Button
className="ppcp--method-settings"

View file

@ -20,21 +20,24 @@ const PaymentMethodsBlock = ( { paymentMethods = [], onTriggerModal } ) => {
{ paymentMethods
// Remove empty/invalid payment method entries.
.filter( ( m ) => m && m.id )
.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
paymentMethod={ paymentMethod }
isSelected={ paymentMethod.enabled }
isDisabled={ paymentMethod.isDisabled }
disabledMessage={ paymentMethod.disabledMessage }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}
onTriggerModal={ () =>
onTriggerModal?.( paymentMethod.id )
}
/>
) ) }
.map( ( paymentMethod ) => {
return (
<PaymentMethodItemBlock
key={ paymentMethod.id }
paymentMethod={ paymentMethod }
isSelected={ paymentMethod.enabled }
isDisabled={ paymentMethod.isDisabled }
disabledMessage={ paymentMethod.disabledMessage }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}
onTriggerModal={ () =>
onTriggerModal?.( paymentMethod.id )
}
warningMessages={ paymentMethod.warningMessages }
/>
);
} ) }
</SettingsBlock>
);
};

View file

@ -0,0 +1,34 @@
import { Icon } from '@wordpress/components';
import { warning } from '@wordpress/icons';
/**
* Component to display warning messages for payment methods
*
* @param {Object} props - Component props
* @param {Object} props.warningMessages - The warning messages to display
* @return {JSX.Element|null} The formatted warning messages or null
*/
const WarningMessages = ( { warningMessages } ) => {
const messages = Object.values( warningMessages || {} );
if ( messages.length === 0 ) {
return null;
}
return (
<span className="ppcp--method-warning">
<Icon icon={ warning } />
<div className="ppcp--method-warning-message">
{ messages.map( ( message, index ) => (
<div
key={ index }
className="ppcp--method-warning__item"
dangerouslySetInnerHTML={ { __html: message } }
/>
) ) }
</div>
</span>
);
};
export default WarningMessages;

View file

@ -354,9 +354,21 @@ return array(
);
},
'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
$axo_checkout_config_notice = $container->get( 'axo.checkout-config-notice.raw' );
$axo_incompatible_plugins_notice = $container->get( 'axo.incompatible-plugins-notice.raw' );
// Combine the notices - only include non-empty ones.
$axo_notices = array_filter(
array(
$axo_checkout_config_notice,
$axo_incompatible_plugins_notice,
)
);
return new PaymentMethodsDefinition(
$container->get( 'settings.data.payment' ),
$container->get( 'settings.data.definition.method_dependencies' )
$container->get( 'settings.data.definition.method_dependencies' ),
$axo_notices
);
},
'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition {

View file

@ -47,6 +47,13 @@ class PaymentMethodsDefinition {
*/
private PaymentMethodsDependenciesDefinition $dependencies_definition;
/**
* Conflict notices for Axo gateway.
*
* @var array
*/
private array $axo_conflicts_notices;
/**
* List of WooCommerce payment gateways.
*
@ -59,13 +66,16 @@ class PaymentMethodsDefinition {
*
* @param PaymentSettings $settings Payment methods data model.
* @param PaymentMethodsDependenciesDefinition $dependencies_definition Payment dependencies definition.
* @param array $axo_conflicts_notices Conflicts notices for Axo.
*/
public function __construct(
PaymentSettings $settings,
PaymentMethodsDependenciesDefinition $dependencies_definition
PaymentMethodsDependenciesDefinition $dependencies_definition,
array $axo_conflicts_notices = array()
) {
$this->settings = $settings;
$this->dependencies_definition = $dependencies_definition;
$this->axo_conflicts_notices = $axo_conflicts_notices;
}
/**
@ -76,39 +86,34 @@ class PaymentMethodsDefinition {
public function get_definitions() : array {
// Refresh the WooCommerce gateway details before we build the definitions.
$this->wc_gateways = WC()->payment_gateways()->payment_gateways();
$all_methods = array_merge(
$all_methods = array_merge(
$this->group_paypal_methods(),
$this->group_card_methods(),
$this->group_apms(),
);
$result = array();
$result = array();
foreach ( $all_methods as $method ) {
$method_id = $method['id'];
// Add dependency info if applicable.
$depends_on = $this->dependencies_definition->get_parent_methods( $method_id );
if ( ! empty( $depends_on ) ) {
$method['depends_on'] = $depends_on;
}
$result[ $method_id ] = $this->build_method_definition(
$method_id,
$method['title'],
$method['description'],
$method['icon'],
$method['fields'] ?? array(),
$depends_on
$depends_on,
$method['warningMessages'] ?? array()
);
}
// Add dependency maps to metadata.
$result['__meta'] = array(
'dependencies' => $this->dependencies_definition->get_dependencies(),
'dependents' => $this->dependencies_definition->get_dependents_map(),
);
return $result;
}
@ -123,14 +128,17 @@ class PaymentMethodsDefinition {
* @param array|false $fields Optional. Additional fields to display in the edit modal.
* Setting this to false omits all fields.
* @param array $depends_on Optional. IDs of payment methods that this depends on.
* @param array $warning_messages Optional. Warning messages to display in the UI.
* @return array Payment method definition.
*/
private function build_method_definition(
string $gateway_id,
string $title,
string $description,
string $icon, $fields = array(),
array $depends_on = array()
string $icon,
$fields = array(),
array $depends_on = array(),
array $warning_messages = array()
) : array {
$gateway = $this->wc_gateways[ $gateway_id ] ?? null;
@ -145,6 +153,7 @@ class PaymentMethodsDefinition {
'icon' => $icon,
'itemTitle' => $title,
'itemDescription' => $description,
'warningMessages' => $warning_messages,
);
// Add dependency information if provided - ensure it's included directly in the config.
@ -274,11 +283,11 @@ class PaymentMethodsDefinition {
),
),
array(
'id' => AxoGateway::ID,
'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',
'fields' => array(
'id' => AxoGateway::ID,
'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',
'fields' => array(
'fastlaneCardholderName' => array(
'type' => 'toggle',
'default' => $this->settings->get_fastlane_cardholder_name(),
@ -296,6 +305,7 @@ class PaymentMethodsDefinition {
),
),
),
'warningMessages' => $this->axo_conflicts_notices,
),
array(
'id' => ApplePayGateway::ID,

View file

@ -169,6 +169,7 @@ class PaymentRestEndpoint extends RestEndpoint {
'icon' => $method['icon'],
'itemTitle' => $method['itemTitle'],
'itemDescription' => $method['itemDescription'],
'warningMessages' => $method['warningMessages'],
);
if ( isset( $method['fields'] ) ) {