Commit 979c8e86e24 for woocommerce
commit 979c8e86e24f5a64c507a21f37b264c34216ed6a
Author: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
Date: Wed Mar 25 13:06:47 2026 +0200
feat: Add notice field to recommended payment methods for requirement warnings (#63776)
* feat: add notice field to recommended payment method standardization
Add support for an optional notice object (badge, message, link_text,
link_url) in recommended payment methods. This allows payment gateways
to surface warnings about requirements or verification needs.
Fields are sanitized: text fields stripped of HTML, messages allow safe
HTML tags, URLs validated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add notice type to RecommendedPaymentMethod
Add optional notice object to RecommendedPaymentMethod type with badge,
message, link_text, and link_url fields. Update test stubs accordingly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add notice field to recommended payment methods REST schema
Add schema definition for the notice object in the recommended payment
methods response, enabling frontends to consume notice data for
displaying payment method warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: render notice badge and warning in onboarding payment method list
When a recommended payment method includes notice data, render:
- A chip badge next to the method title (always visible)
- A warning notice below the item when the method is toggled on
This is generic and works for any payment method with notice data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: add styles for payment method notice badge and warning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: fix test_get_details regression and add changelog entries
Update test_get_details expected arrays to include notice defaults.
Add changelog entries for @woocommerce/plugin-woocommerce and
@woocommerce/data packages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add notice defaults to PaymentsProviders and REST integration tests
Update expected arrays in test_get_payment_gateway_base_details to
include the notice field defaults, fixing test failures from the new
notice field in recommended payment methods.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct notice layout by adding flex-wrap to list item
The .woocommerce-list__item base style uses display:flex with nowrap,
causing the notice to render beside the content instead of below it.
Add flex-wrap:wrap to the list item and flex-basis:100% to the notice
so it wraps to a new row.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: match notice and badge styles to Figma design
- Change notice from warning (yellow) to info (blue) per design
- Change badge from yellow (#f0c33c) to gray (#f6f7f7) per design
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: match notice styling to Figma design
Override WordPress Notice component styles to match the design:
- Light blue background (#f5f9fd) with blue border (#9fbcdd)
- Rounded corners (8px)
- Full-width layout
- Dark blue text (#001758)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace WP Notice with custom info notice matching Figma
Replace the WordPress Notice component with a custom info notice that
matches the Figma design exactly: light blue background (#f5f9fd),
blue border (#9fbcdd), 8px border-radius, info icon, dark blue text
(#001758), and full-width layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: align notice with icon and toggle, add 20px gap
Remove extra horizontal margin so notice aligns left with the payment
method icon and right with the toggle. Add 20px top margin for gap
between the payment method content and the notice.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: merge duplicate @wordpress/components imports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use dark blue fill for notice info icon
Change icon fill from light border color (#9fbcdd) to the foreground
color (#001758) matching the Figma design token fgcon-info.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use correct medium blue for notice icon
Use fgCon-info-weak (#006cd8) for the icon fill, matching the Figma
design token for info icon foreground.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review findings for notice field
- Add notice badge rendering to apple_google layout branch
- Add role="status" and aria-live="polite" to dynamic notice div
- Add is_string() type guard before wc_is_valid_url() call
- Use idiomatic getByText assertion in test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: align notice field with server — embed links in message HTML
The server PR (wpcom#208809) sends notice with only badge and message
fields, with links embedded as <a> tags in the message HTML. Align the
client by:
- Render notice.message as HTML via dangerouslySetInnerHTML + sanitizeHTML
- Remove separate link_text/link_url fields from TS types, PHP
standardization, REST schema, and all tests
- Remove unused ExternalLink import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove link_text/link_url from payment settings test stubs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "fix: remove link_text/link_url from payment settings test stubs"
This reverts commit 87efe8f83b5d61e8850df8e355daa792fe9d5574.
* Revert "fix: align notice field with server — embed links in message HTML"
This reverts commit 6c2966f951af6c2d8789eb19e099b01aed48849a.
* fix: address code review findings for notice field
- Replace wp_kses() with sanitize_text_field() for notice.message to
align the plain-text contract across all notice sub-fields
- Update REST schema description to "Plain text only"
- Use speak() from @wordpress/a11y for screen reader announcements
instead of conditionally mounted live region
- Fix RTL: margin-left → margin-inline-start on badge
- Add transition test covering disabled→enabled notice visibility
- Update sanitization test expectations for sanitize_text_field()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address lower-priority review findings
- Rename data-testid from "payment-method-notice-warning" to
"payment-method-notice-info" to align with CSS class and semantics
- Add beforeEach jest.clearAllMocks() to prevent mock state leaking
- Skip Pill primitive for badge — visual mismatch (28px rounded pill
vs 2px flat chip from Figma)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/packages/js/data/changelog/add-recommended-payment-method-notice b/packages/js/data/changelog/add-recommended-payment-method-notice
new file mode 100644
index 00000000000..e7b2d5fe18a
--- /dev/null
+++ b/packages/js/data/changelog/add-recommended-payment-method-notice
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add notice property to RecommendedPaymentMethod type for payment method warnings.
diff --git a/packages/js/data/src/payment-settings/test/helpers/stub.ts b/packages/js/data/src/payment-settings/test/helpers/stub.ts
index 939fcd30f5f..50fff7c8748 100644
--- a/packages/js/data/src/payment-settings/test/helpers/stub.ts
+++ b/packages/js/data/src/payment-settings/test/helpers/stub.ts
@@ -104,6 +104,12 @@ export const providersStub: PaymentsProvider[] = [
extraDescription: 'Extra description.',
extraIcon:
'http://localhost:8082/wp-content/plugins/woocommerce/assets/images/onboarding/extra-icon.svg',
+ notice: {
+ badge: '',
+ message: '',
+ link_text: '',
+ link_url: '',
+ },
},
{
id: 'woopay',
@@ -116,6 +122,12 @@ export const providersStub: PaymentsProvider[] = [
extraDescription: 'Extra description.',
extraIcon:
'http://localhost:8082/wp-content/plugins/woocommerce/assets/images/onboarding/extra-icon.svg',
+ notice: {
+ badge: '',
+ message: '',
+ link_text: '',
+ link_url: '',
+ },
},
],
type: 'native_in_context',
diff --git a/packages/js/data/src/payment-settings/types.ts b/packages/js/data/src/payment-settings/types.ts
index 229878b6e96..aa7d9dcc034 100644
--- a/packages/js/data/src/payment-settings/types.ts
+++ b/packages/js/data/src/payment-settings/types.ts
@@ -67,6 +67,12 @@ export type RecommendedPaymentMethod = {
extraTitle: string;
extraDescription: string;
extraIcon: string;
+ notice?: {
+ badge: string;
+ message: string;
+ link_text: string;
+ link_url: string;
+ };
};
export type PaymentsProviderOnboardingState = {
diff --git a/plugins/woocommerce/changelog/add-recommended-payment-method-notice b/plugins/woocommerce/changelog/add-recommended-payment-method-notice
new file mode 100644
index 00000000000..5a0ee5f131b
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-recommended-payment-method-notice
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add support for notice data in recommended payment methods, enabling payment gateways to surface requirement warnings during onboarding.
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/list-item.tsx b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/list-item.tsx
index 233965a3157..4a3ad8964c4 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/list-item.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/list-item.tsx
@@ -3,8 +3,10 @@
*/
import { decodeEntities } from '@wordpress/html-entities';
import { type RecommendedPaymentMethod } from '@woocommerce/data';
-import { ToggleControl } from '@wordpress/components';
-import { useRef } from '@wordpress/element';
+import { ExternalLink, Icon, ToggleControl } from '@wordpress/components';
+import { info as infoIcon } from '@wordpress/icons';
+import { useEffect, useRef } from '@wordpress/element';
+import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
@@ -80,6 +82,14 @@ export const PaymentMethodListItem = ( {
const shouldRender = isExpanded || baseVisibility;
+ // Announce notice to screen readers when the method is toggled on.
+ const isEnabled = paymentMethodsState[ method.id ] ?? false;
+ useEffect( () => {
+ if ( isEnabled && method.notice?.message ) {
+ speak( method.notice.message, 'polite' );
+ }
+ }, [ isEnabled, method.notice?.message ] );
+
if ( ! shouldRender ) {
return null;
}
@@ -103,6 +113,14 @@ export const PaymentMethodListItem = ( {
<div className="woocommerce-list__item-text">
<span className="woocommerce-list__item-title">
{ method.title }
+ { method.notice?.badge && (
+ <span
+ className="woocommerce-list__item-notice-badge"
+ data-testid="payment-method-notice-badge"
+ >
+ { method.notice.badge }
+ </span>
+ ) }
</span>
<span
className="woocommerce-list__item-content"
@@ -127,6 +145,14 @@ export const PaymentMethodListItem = ( {
<div className="woocommerce-list__item-text">
<span className="woocommerce-list__item-title">
{ method.title }
+ { method.notice?.badge && (
+ <span
+ className="woocommerce-list__item-notice-badge"
+ data-testid="payment-method-notice-badge"
+ >
+ { method.notice.badge }
+ </span>
+ ) }
</span>
<span
className="woocommerce-list__item-content"
@@ -183,6 +209,29 @@ export const PaymentMethodListItem = ( {
</div>
</div>
</div>
+ { method.notice?.message &&
+ ( paymentMethodsState[ method.id ] ?? false ) && (
+ <div
+ className="woocommerce-list__item-notice-info"
+ data-testid="payment-method-notice-info"
+ >
+ <Icon icon={ infoIcon } size={ 24 } />
+ <p>
+ { method.notice.message }
+ { method.notice.link_url &&
+ method.notice.link_text && (
+ <>
+ { ' ' }
+ <ExternalLink
+ href={ method.notice.link_url }
+ >
+ { method.notice.link_text }
+ </ExternalLink>
+ </>
+ ) }
+ </p>
+ </div>
+ ) }
</div>
);
};
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/style.scss b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/style.scss
index 52aa635710a..4a013a8e83b 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/style.scss
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/style.scss
@@ -28,6 +28,7 @@
&__item {
padding: 2 * $gap-smaller 4 * $gap-smaller;
border-color: $gray-100;
+ flex-wrap: wrap;
&:hover {
background-color: $white;
@@ -242,3 +243,43 @@
}
}
}
+
+.woocommerce-list__item-notice-badge {
+ display: inline-flex;
+ align-items: center;
+ margin-inline-start: 8px;
+ padding: 2px 8px;
+ border-radius: 2px;
+ background-color: #f6f7f7;
+ color: #2c3338;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ vertical-align: middle;
+}
+
+.woocommerce-list__item-notice-info {
+ flex-basis: 100%;
+ display: flex;
+ gap: 4px;
+ align-items: flex-start;
+ margin: 20px 0 0;
+ padding: 12px;
+ background-color: #f5f9fd;
+ border: 1px solid #9fbcdd;
+ border-radius: 8px;
+
+ svg {
+ flex-shrink: 0;
+ fill: #006cd8;
+ }
+
+ p {
+ margin: 0;
+ padding: 2px 0;
+ font-size: 13px;
+ font-weight: 400;
+ line-height: 20px;
+ color: #001758;
+ }
+}
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/test/list-item.test.tsx b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/test/list-item.test.tsx
new file mode 100644
index 00000000000..626b5ac8848
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/payment-methods-selection/test/list-item.test.tsx
@@ -0,0 +1,234 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import { type RecommendedPaymentMethod } from '@woocommerce/data';
+
+const mockSpeak = jest.fn();
+jest.mock( '@wordpress/a11y', () => ( {
+ speak: ( ...args: unknown[] ) => mockSpeak( ...args ),
+} ) );
+
+/**
+ * Internal dependencies
+ */
+import { PaymentMethodListItem } from '../list-item';
+
+const createMethod = (
+ overrides: Partial< RecommendedPaymentMethod > = {}
+): RecommendedPaymentMethod => ( {
+ id: 'test_method',
+ _order: 0,
+ title: 'Test Method',
+ description: 'A test payment method.',
+ icon: 'https://example.com/icon.png',
+ enabled: false,
+ extraTitle: '',
+ extraDescription: '',
+ extraIcon: '',
+ ...overrides,
+} );
+
+const defaultProps = {
+ paymentMethodsState: { test_method: false } as Record< string, boolean >,
+ setPaymentMethodsState: jest.fn(),
+ isExpanded: true,
+ initialVisibilityStatus: true,
+};
+
+describe( 'PaymentMethodListItem', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ describe( 'Notice badge', () => {
+ it( 'renders a badge chip when notice.badge is set', () => {
+ const method = createMethod( {
+ notice: {
+ badge: 'Verification required',
+ message: '',
+ link_text: '',
+ link_url: '',
+ },
+ } );
+
+ render(
+ <PaymentMethodListItem { ...defaultProps } method={ method } />
+ );
+
+ expect(
+ screen.getByText( 'Verification required' )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'does not render a badge chip when notice.badge is empty', () => {
+ const method = createMethod( {
+ notice: {
+ badge: '',
+ message: 'Some warning.',
+ link_text: '',
+ link_url: '',
+ },
+ } );
+
+ render(
+ <PaymentMethodListItem { ...defaultProps } method={ method } />
+ );
+
+ expect(
+ screen.queryByTestId( 'payment-method-notice-badge' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'does not render a badge chip when notice is not provided', () => {
+ const method = createMethod();
+
+ render(
+ <PaymentMethodListItem { ...defaultProps } method={ method } />
+ );
+
+ expect(
+ screen.queryByTestId( 'payment-method-notice-badge' )
+ ).not.toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'Warning notice', () => {
+ it( 'renders a warning notice when method is enabled and notice.message is set', () => {
+ const method = createMethod( {
+ id: 'p24',
+ notice: {
+ badge: 'Verification required',
+ message: 'Strict requirements apply.',
+ link_text: 'Review requirements',
+ link_url: 'https://example.com/docs',
+ },
+ } );
+
+ render(
+ <PaymentMethodListItem
+ { ...defaultProps }
+ method={ method }
+ paymentMethodsState={ { p24: true } }
+ />
+ );
+
+ expect(
+ screen.getByText( 'Strict requirements apply.' )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole( 'link', { name: /review requirements/i } )
+ ).toHaveAttribute( 'href', 'https://example.com/docs' );
+ } );
+
+ it( 'does not render a warning notice when method is disabled', () => {
+ const method = createMethod( {
+ id: 'p24',
+ notice: {
+ badge: 'Verification required',
+ message: 'Strict requirements apply.',
+ link_text: 'Review requirements',
+ link_url: 'https://example.com/docs',
+ },
+ } );
+
+ render(
+ <PaymentMethodListItem
+ { ...defaultProps }
+ method={ method }
+ paymentMethodsState={ { p24: false } }
+ />
+ );
+
+ expect(
+ screen.queryByText( 'Strict requirements apply.' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'does not render a warning notice when notice.message is empty', () => {
+ const method = createMethod( {
+ notice: {
+ badge: 'Verification required',
+ message: '',
+ link_text: '',
+ link_url: '',
+ },
+ } );
+
+ render(
+ <PaymentMethodListItem
+ { ...defaultProps }
+ method={ method }
+ paymentMethodsState={ { test_method: true } }
+ />
+ );
+
+ expect(
+ screen.queryByTestId( 'payment-method-notice-info' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'shows notice after rerender with enabled state', () => {
+ const method = createMethod( {
+ id: 'p24',
+ notice: {
+ badge: 'Verification required',
+ message: 'Strict requirements apply.',
+ link_text: '',
+ link_url: '',
+ },
+ } );
+
+ const { rerender } = render(
+ <PaymentMethodListItem
+ { ...defaultProps }
+ method={ method }
+ paymentMethodsState={ { p24: false } }
+ />
+ );
+
+ expect(
+ screen.queryByTestId( 'payment-method-notice-info' )
+ ).not.toBeInTheDocument();
+
+ rerender(
+ <PaymentMethodListItem
+ { ...defaultProps }
+ method={ method }
+ paymentMethodsState={ { p24: true } }
+ />
+ );
+
+ expect(
+ screen.getByTestId( 'payment-method-notice-info' )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'renders notice without link when link_url is empty', () => {
+ const method = createMethod( {
+ id: 'p24',
+ notice: {
+ badge: '',
+ message: 'Warning message.',
+ link_text: 'Click here',
+ link_url: '',
+ },
+ } );
+
+ render(
+ <PaymentMethodListItem
+ { ...defaultProps }
+ method={ method }
+ paymentMethodsState={ { p24: true } }
+ />
+ );
+
+ expect(
+ screen.getByText( 'Warning message.' )
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole( 'link', { name: /click here/i } )
+ ).not.toBeInTheDocument();
+ } );
+ } );
+} );
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php
index c929e754e09..2e4e9051fc2 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders/PaymentGateway.php
@@ -1075,6 +1075,12 @@ class PaymentGateway {
'description' => '',
'icon' => '',
'category' => self::PAYMENT_METHOD_CATEGORY_PRIMARY, // Default to primary.
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
);
// If the payment method has a description, sanitize it before use.
@@ -1109,6 +1115,27 @@ class PaymentGateway {
$standard_details['category'] = $recommended_pm['category'];
}
+ // If the payment method has a notice, sanitize and use its fields.
+ if ( ! empty( $recommended_pm['notice'] ) && is_array( $recommended_pm['notice'] ) ) {
+ $notice = $recommended_pm['notice'];
+
+ if ( ! empty( $notice['badge'] ) ) {
+ $standard_details['notice']['badge'] = sanitize_text_field( $notice['badge'] );
+ }
+
+ if ( ! empty( $notice['message'] ) ) {
+ $standard_details['notice']['message'] = sanitize_text_field( $notice['message'] );
+ }
+
+ if ( ! empty( $notice['link_text'] ) ) {
+ $standard_details['notice']['link_text'] = sanitize_text_field( $notice['link_text'] );
+ }
+
+ if ( ! empty( $notice['link_url'] ) && is_string( $notice['link_url'] ) && wc_is_valid_url( $notice['link_url'] ) ) {
+ $standard_details['notice']['link_url'] = sanitize_url( $notice['link_url'] );
+ }
+ }
+
return $standard_details;
}
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsRestController.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsRestController.php
index 55c01a6b8ad..0d5ca48f744 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsRestController.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsRestController.php
@@ -1015,6 +1015,38 @@ class PaymentsRestController extends RestApiControllerBase {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
+ 'notice' => array(
+ 'type' => 'object',
+ 'description' => esc_html__( 'An optional notice to display for this payment method (e.g., verification requirements).', 'woocommerce' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ 'properties' => array(
+ 'badge' => array(
+ 'type' => 'string',
+ 'description' => esc_html__( 'Short text for a badge/chip displayed next to the payment method title.', 'woocommerce' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'message' => array(
+ 'type' => 'string',
+ 'description' => esc_html__( 'Warning message displayed when the payment method is enabled. Plain text only.', 'woocommerce' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'link_text' => array(
+ 'type' => 'string',
+ 'description' => esc_html__( 'Text for the call-to-action link in the notice.', 'woocommerce' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'link_url' => array(
+ 'type' => 'string',
+ 'description' => esc_html__( 'URL for the call-to-action link in the notice.', 'woocommerce' ),
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ ),
+ ),
),
),
),
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php
index fb5061e1b58..51e67f22394 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/PaymentGatewayTest.php
@@ -186,6 +186,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'description' => 'WooPay express checkout',
'icon' => '', // The icon with an invalid URL is ignored.
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'card',
@@ -196,6 +202,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'description' => '<strong>Accepts</strong> <b>all major</b><em>credit</em> and <a href="#" target="_blank">debit cards</a>.',
'icon' => 'https://example.com/card-icon.png',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'basic2',
@@ -206,6 +218,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'description' => '',
'icon' => '',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'basic',
@@ -216,6 +234,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'description' => '',
'icon' => '',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_SECONDARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
),
),
@@ -1898,6 +1922,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'description' => 'WooPay express checkout',
'icon' => 'https://example.com/icon.png',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_SECONDARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'card',
@@ -1908,6 +1938,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'description' => 'Accepts all major credit and debit cards.',
'icon' => 'https://example.com/card-icon.png',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
),
$this->sut->get_recommended_payment_methods( $fake_gateway )
@@ -1962,6 +1998,12 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
// No icon.
'icon' => '',
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
),
$this->sut->get_recommended_payment_methods( $fake_gateway )
@@ -1980,6 +2022,102 @@ class PaymentGatewayTest extends WC_Unit_Test_Case {
);
}
+ /**
+ * Test that recommended payment methods with a notice are standardized correctly.
+ */
+ public function test_get_recommended_payment_methods_with_notice() {
+ $fake_gateway = new FakePaymentGateway(
+ 'gateway1',
+ array(
+ 'recommended_payment_methods' => array(
+ array(
+ 'id' => 'p24',
+ 'title' => 'Przelewy24',
+ 'enabled' => false,
+ 'notice' => array(
+ 'badge' => 'Additional verification required',
+ 'message' => 'Przelewy24 has strict requirements.',
+ 'link_text' => 'Review requirements',
+ 'link_url' => 'https://woocommerce.com/document/woopayments/payment-methods/local-payment-methods/#p24-additional-requirements',
+ ),
+ ),
+ ),
+ )
+ );
+
+ $result = $this->sut->get_recommended_payment_methods( $fake_gateway );
+
+ $this->assertCount( 1, $result );
+ $this->assertArrayHasKey( 'notice', $result[0] );
+ $this->assertSame( 'Additional verification required', $result[0]['notice']['badge'] );
+ $this->assertSame( 'Przelewy24 has strict requirements.', $result[0]['notice']['message'] );
+ $this->assertSame( 'Review requirements', $result[0]['notice']['link_text'] );
+ $this->assertSame(
+ 'https://woocommerce.com/document/woopayments/payment-methods/local-payment-methods/#p24-additional-requirements',
+ $result[0]['notice']['link_url']
+ );
+ }
+
+ /**
+ * Test that recommended payment methods without a notice have an empty notice.
+ */
+ public function test_get_recommended_payment_methods_without_notice() {
+ $fake_gateway = new FakePaymentGateway(
+ 'gateway1',
+ array(
+ 'recommended_payment_methods' => array(
+ array(
+ 'id' => 'card',
+ 'title' => 'Card',
+ ),
+ ),
+ )
+ );
+
+ $result = $this->sut->get_recommended_payment_methods( $fake_gateway );
+
+ $this->assertCount( 1, $result );
+ $this->assertArrayHasKey( 'notice', $result[0] );
+ $this->assertSame( '', $result[0]['notice']['badge'] );
+ $this->assertSame( '', $result[0]['notice']['message'] );
+ $this->assertSame( '', $result[0]['notice']['link_text'] );
+ $this->assertSame( '', $result[0]['notice']['link_url'] );
+ }
+
+ /**
+ * Test that notice fields are sanitized (HTML stripped from text, URL sanitized).
+ */
+ public function test_get_recommended_payment_methods_notice_sanitization() {
+ $fake_gateway = new FakePaymentGateway(
+ 'gateway1',
+ array(
+ 'recommended_payment_methods' => array(
+ array(
+ 'id' => 'p24',
+ 'title' => 'Przelewy24',
+ 'notice' => array(
+ 'badge' => '<script>alert("xss")</script>Badge text',
+ 'message' => '<b>Bold</b> message with <script>xss</script>',
+ 'link_text' => '<em>Link</em> text',
+ 'link_url' => 'javascript:alert(1)',
+ ),
+ ),
+ ),
+ )
+ );
+
+ $result = $this->sut->get_recommended_payment_methods( $fake_gateway );
+
+ $this->assertCount( 1, $result );
+ // All text fields use sanitize_text_field() which calls wp_strip_all_tags()
+ // which removes all HTML tags and their content for script tags.
+ $this->assertSame( 'Badge text', $result[0]['notice']['badge'] );
+ $this->assertSame( 'Link text', $result[0]['notice']['link_text'] );
+ $this->assertSame( 'Bold message with', $result[0]['notice']['message'] );
+ // javascript: URLs should be sanitized away by wc_is_valid_url().
+ $this->assertSame( '', $result[0]['notice']['link_url'] );
+ }
+
/**
* Test is_onboarding_supported returns null when gateway doesn't provide the method.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
index 2157a53f5e9..31a68ac68c5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
@@ -778,6 +778,12 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
'description' => 'WooPay express checkout',
'icon' => '', // The icon with an invalid URL is ignored.
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'card',
@@ -788,6 +794,12 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
'description' => '<strong>Accepts</strong> <b>all major</b><em>credit</em> and <a href="#" target="_blank">debit cards</a>.',
'icon' => 'https://example.com/card-icon.png',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'basic2',
@@ -798,6 +810,12 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
'description' => '',
'icon' => '',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_PRIMARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'basic',
@@ -808,6 +826,12 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
'description' => '',
'icon' => '',
'category' => PaymentGateway::PAYMENT_METHOD_CATEGORY_SECONDARY,
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
),
$gateway_details['onboarding']['recommended_payment_methods']
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
index 1c63d4d3b8b..35f769f5c87 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
@@ -792,6 +792,12 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
'title' => 'Credit/debit card (required)',
'description' => 'Accepts all major credit and debit cards',
'icon' => 'https://example.com/card-icon.png',
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'woopay',
@@ -801,6 +807,12 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
'title' => 'WooPay',
'description' => 'WooPay express checkout',
'icon' => 'https://example.com/woopay-icon.png',
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
),
$provider['onboarding']['recommended_payment_methods']
@@ -972,6 +984,12 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
'title' => 'Credit/debit card (required)',
'description' => 'Accepts all major credit and debit cards',
'icon' => 'https://example.com/card-icon.png',
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
array(
'id' => 'woopay',
@@ -981,6 +999,12 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
'title' => 'WooPay',
'description' => 'WooPay express checkout',
'icon' => 'https://example.com/woopay-icon.png',
+ 'notice' => array(
+ 'badge' => '',
+ 'message' => '',
+ 'link_text' => '',
+ 'link_url' => '',
+ ),
),
),
$provider['onboarding']['recommended_payment_methods']