Commit 325bc63f8a4 for woocommerce

commit 325bc63f8a457b57995c972258d14e858c6b5f24
Author: Ryan Tang <ryan.diginomad@gmail.com>
Date:   Thu Jun 18 21:27:31 2026 +0800

    Update offline payment method settings forms to use DataForm (#65731)

    * refactor(settings-payments): migrate cheque settings form to DataForm

    Replace the hand-rolled CheckboxControl/TextControl/TextareaControl field
    rendering with @wordpress/dataviews DataForm, using declarative field
    definitions and shared custom edit controls (checkbox and textarea are
    not built into DataForm 4.x).

    Behavior is unchanged and locked by characterization tests written
    against the previous implementation: field rendering, loading
    placeholders, save payload shape, and hasChanges gating.

    * refactor(settings-payments): migrate cash on delivery settings form to DataForm

    Same approach as the cheque migration: declarative field definitions
    with shared edit controls, plus a COD-specific edit control for the
    shipping methods TreeSelectControl (its options come from the gateway
    settings, so it stays local to this form).

    Behavior locked by characterization tests written against the previous
    implementation, including the enable_for_methods value passthrough.

    * refactor(settings-payments): migrate direct bank transfer settings form to DataForm

    Same approach as the cheque migration for the four settings fields.
    The bank accounts section (BankAccountsList) and the parallel accounts
    save flow are intentionally unchanged - only the hand-rolled field
    rendering moves to DataForm.

    Behavior locked by characterization tests, including the dual-payload
    save (updatePaymentGateway + updateOptions for accounts).

    * Add changelog

    * Add keyboard navigation and save-failure tests to offline payment forms

    Addresses CodeRabbit review feedback: per the admin client testing
    guidelines, component tests should cover keyboard accessibility
    (userEvent.tab() focus order through the form fields and the save
    button) and the save error path (error notice on a rejected
    updatePaymentGateway call).

    * refactor: move @wordpress/dataviews stylesheet import to settings-embed entry

    DataForm depends on the @wordpress/dataviews stylesheet. It was imported
    indirectly via settings-email/style.scss. Since this PR adds DataForm to the
    payment settings UI (also rendered under the settings-embed entry), move the
    import to the shared settings-embed entry stylesheet (settings-ui.scss) and
    drop the settings-email-specific import to avoid the accidental coupling.

    Both settings-email and settings-payments render under settings-embed/index.tsx,
    so settings-email still receives the styles from the shared entry bundle.

    * Update plugins/woocommerce/changelog/61900-update-offline-payment-forms-dataform

    Update changelog

    ---------

    Co-authored-by: Ryan Tang <24728770+ryandiginomad@users.noreply.github.com>
    Co-authored-by: Daniel Mallory <daniel.mallory@automattic.com>

diff --git a/plugins/woocommerce/changelog/61900-update-offline-payment-forms-dataform b/plugins/woocommerce/changelog/61900-update-offline-payment-forms-dataform
new file mode 100644
index 00000000000..c5b835db4e8
--- /dev/null
+++ b/plugins/woocommerce/changelog/61900-update-offline-payment-forms-dataform
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Migrate offline payment settings forms to DataForm.
diff --git a/plugins/woocommerce/client/admin/client/settings-email/style.scss b/plugins/woocommerce/client/admin/client/settings-email/style.scss
index 6bf88c67b84..6cf0bdea479 100644
--- a/plugins/woocommerce/client/admin/client/settings-email/style.scss
+++ b/plugins/woocommerce/client/admin/client/settings-email/style.scss
@@ -1,5 +1,3 @@
-@import "@wordpress/dataviews/build-style/style.css";
-
 .wc-settings-row-disabled {
 	opacity: 0.5;
 	pointer-events: none;
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/dataform-controls.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/dataform-controls.tsx
new file mode 100644
index 00000000000..d7c0b26e99e
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/dataform-controls.tsx
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+import {
+	CheckboxControl,
+	TextControl,
+	TextareaControl,
+} from '@wordpress/components';
+import type { DataFormControlProps } from '@wordpress/dataviews';
+
+/**
+ * The shape of the form values used by the offline payment method settings forms.
+ */
+export type OfflineFormValues = Record< string, string | boolean | string[] >;
+
+/**
+ * A DataForm edit control that renders a `CheckboxControl`, preserving the
+ * markup of the previous hand-rolled offline payment method forms.
+ */
+export const CheckboxEdit = ( {
+	data,
+	field,
+	onChange,
+}: DataFormControlProps< OfflineFormValues > ) => (
+	<CheckboxControl
+		label={ field.label }
+		help={ field.description }
+		checked={ Boolean( field.getValue( { item: data } ) ) }
+		onChange={ ( checked ) => onChange( { [ field.id ]: checked } ) }
+	/>
+);
+
+/**
+ * A DataForm edit control that renders a `TextControl`, preserving the
+ * markup of the previous hand-rolled offline payment method forms.
+ */
+export const TextEdit = ( {
+	data,
+	field,
+	onChange,
+}: DataFormControlProps< OfflineFormValues > ) => (
+	<TextControl
+		label={ field.label }
+		help={ field.description }
+		placeholder={ field.placeholder }
+		value={ String( field.getValue( { item: data } ) ?? '' ) }
+		onChange={ ( value ) => onChange( { [ field.id ]: value } ) }
+	/>
+);
+
+/**
+ * A DataForm edit control that renders a `TextareaControl`, preserving the
+ * markup of the previous hand-rolled offline payment method forms.
+ */
+export const TextareaEdit = ( {
+	data,
+	field,
+	onChange,
+}: DataFormControlProps< OfflineFormValues > ) => (
+	<TextareaControl
+		label={ field.label }
+		help={ field.description }
+		value={ String( field.getValue( { item: data } ) ?? '' ) }
+		onChange={ ( value ) => onChange( { [ field.id ]: value } ) }
+	/>
+);
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx
index 0a246a747c4..c780447e4b2 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx
@@ -3,18 +3,15 @@
  */
 import { __ } from '@wordpress/i18n';
 import { useDispatch, useSelect } from '@wordpress/data';
-import {
-	CheckboxControl,
-	TextControl,
-	TextareaControl,
-	Button,
-} from '@wordpress/components';
-import { useState, useEffect } from '@wordpress/element';
+import { Button } from '@wordpress/components';
+import { useState, useEffect, useMemo } from '@wordpress/element';
 import {
 	paymentGatewaysStore,
 	optionsStore,
 	paymentSettingsStore,
 } from '@woocommerce/data';
+import { DataForm } from '@wordpress/dataviews';
+import type { Field } from '@wordpress/dataviews';

 /**
  * Internal dependencies
@@ -24,6 +21,12 @@ import { Settings } from '~/settings-payments/components/settings';
 import { FieldPlaceholder } from '~/settings-payments/components/field-placeholder';
 import { BankAccountsList } from '~/settings-payments/components/bank-accounts-list';
 import { BankAccount } from '~/settings-payments/components/bank-accounts-list/types';
+import {
+	CheckboxEdit,
+	TextEdit,
+	TextareaEdit,
+	type OfflineFormValues,
+} from './dataform-controls';

 /**
  * This page is used to manage the settings for the BACS (Direct bank transfer) payment gateway.
@@ -64,9 +67,7 @@ export const SettingsPaymentsBacs = () => {
 		};
 	}, [] );

-	const [ formValues, setFormValues ] = useState<
-		Record< string, string | boolean | string[] >
-	>( {} );
+	const [ formValues, setFormValues ] = useState< OfflineFormValues >( {} );

 	const [ isSaving, setIsSaving ] = useState( false );
 	const [ hasChanges, setHasChanges ] = useState( false );
@@ -94,6 +95,48 @@ export const SettingsPaymentsBacs = () => {
 	const { updateOptions } = useDispatch( optionsStore );
 	const { updatePaymentGateway } = useDispatch( paymentGatewaysStore );

+	const fields: Field< OfflineFormValues >[] = useMemo(
+		() => [
+			{
+				id: 'enabled',
+				label: __( 'Enable direct bank transfers', 'woocommerce' ),
+				Edit: CheckboxEdit,
+			},
+			{
+				id: 'title',
+				label: __( 'Title', 'woocommerce' ),
+				description: __(
+					'Payment method name that the customer will see during checkout.',
+					'woocommerce'
+				),
+				placeholder: __(
+					'Direct bank transfer payments',
+					'woocommerce'
+				),
+				Edit: TextEdit,
+			},
+			{
+				id: 'description',
+				label: __( 'Description', 'woocommerce' ),
+				description: __(
+					'Payment method description that the customer will see during checkout.',
+					'woocommerce'
+				),
+				Edit: TextareaEdit,
+			},
+			{
+				id: 'instructions',
+				label: __( 'Instructions', 'woocommerce' ),
+				description: __(
+					'Instructions that will be added to the thank you page and emails.',
+					'woocommerce'
+				),
+				Edit: TextareaEdit,
+			},
+		],
+		[]
+	);
+
 	const saveSettings = async () => {
 		if ( ! bacsSettings ) {
 			return;
@@ -166,80 +209,30 @@ export const SettingsPaymentsBacs = () => {
 						) }
 					>
 						{ isLoading ? (
-							<FieldPlaceholder size="small" />
-						) : (
-							<CheckboxControl
-								label={ __(
-									'Enable direct bank transfers',
-									'woocommerce'
-								) }
-								checked={ Boolean( formValues.enabled ) }
-								onChange={ ( checked ) => {
-									setFormValues( {
-										...formValues,
-										enabled: checked,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="medium" />
-						) : (
-							<TextControl
-								label={ __( 'Title', 'woocommerce' ) }
-								help={ __(
-									'Payment method name that the customer will see during checkout.',
-									'woocommerce'
-								) }
-								placeholder={ __(
-									'Direct bank transfer payments',
-									'woocommerce'
-								) }
-								value={ String( formValues.title ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										title: value,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="large" />
+							<>
+								<FieldPlaceholder size="small" />
+								<FieldPlaceholder size="medium" />
+								<FieldPlaceholder size="large" />
+								<FieldPlaceholder size="large" />
+							</>
 						) : (
-							<TextareaControl
-								label={ __( 'Description', 'woocommerce' ) }
-								help={ __(
-									'Payment method description that the customer will see during checkout.',
-									'woocommerce'
-								) }
-								value={ String( formValues.description ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										description: value,
-									} );
-									setHasChanges( true );
+							<DataForm
+								data={ formValues }
+								fields={ fields }
+								form={ {
+									type: 'regular',
+									fields: [
+										'enabled',
+										'title',
+										'description',
+										'instructions',
+									],
 								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="large" />
-						) : (
-							<TextareaControl
-								label={ __( 'Instructions', 'woocommerce' ) }
-								help={ __(
-									'Instructions that will be added to the thank you page and emails.',
-									'woocommerce'
-								) }
-								value={ String( formValues.instructions ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										instructions: value,
-									} );
+								onChange={ ( edits: OfflineFormValues ) => {
+									setFormValues( ( values ) => ( {
+										...values,
+										...edits,
+									} ) );
 									setHasChanges( true );
 								} }
 							/>
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cheque.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cheque.tsx
index c86d4be20f4..9289b9bb1ee 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cheque.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cheque.tsx
@@ -1,16 +1,13 @@
 /**
  * External dependencies
  */
-import {
-	Button,
-	CheckboxControl,
-	TextControl,
-	TextareaControl,
-} from '@wordpress/components';
+import { Button } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
 import { useDispatch, useSelect } from '@wordpress/data';
 import { paymentGatewaysStore, paymentSettingsStore } from '@woocommerce/data';
-import { useState, useEffect } from '@wordpress/element';
+import { useState, useEffect, useMemo } from '@wordpress/element';
+import { DataForm } from '@wordpress/dataviews';
+import type { Field } from '@wordpress/dataviews';

 /**
  * Internal dependencies
@@ -18,6 +15,12 @@ import { useState, useEffect } from '@wordpress/element';
 import '../settings-payments-body.scss';
 import { Settings } from '~/settings-payments/components/settings';
 import { FieldPlaceholder } from '~/settings-payments/components/field-placeholder';
+import {
+	CheckboxEdit,
+	TextEdit,
+	TextareaEdit,
+	type OfflineFormValues,
+} from './dataform-controls';

 /**
  * This page is used to manage the settings for the Cheque payment gateway.
@@ -48,9 +51,7 @@ export const SettingsPaymentsCheque = () => {
 			invalidateResolutionForPaymentSettings,
 	} = useDispatch( paymentSettingsStore );

-	const [ formValues, setFormValues ] = useState<
-		Record< string, string | boolean | string[] >
-	>( {} );
+	const [ formValues, setFormValues ] = useState< OfflineFormValues >( {} );
 	const [ isSaving, setIsSaving ] = useState( false );
 	const [ hasChanges, setHasChanges ] = useState( false );

@@ -65,6 +66,45 @@ export const SettingsPaymentsCheque = () => {
 		}
 	}, [ chequeSettings ] );

+	const fields: Field< OfflineFormValues >[] = useMemo(
+		() => [
+			{
+				id: 'enabled',
+				label: __( 'Enable check payments', 'woocommerce' ),
+				Edit: CheckboxEdit,
+			},
+			{
+				id: 'title',
+				label: __( 'Title', 'woocommerce' ),
+				description: __(
+					'Payment method name that the customer will see during checkout.',
+					'woocommerce'
+				),
+				placeholder: __( 'Check payments', 'woocommerce' ),
+				Edit: TextEdit,
+			},
+			{
+				id: 'description',
+				label: __( 'Description', 'woocommerce' ),
+				description: __(
+					'Payment method description that the customer will see during checkout.',
+					'woocommerce'
+				),
+				Edit: TextareaEdit,
+			},
+			{
+				id: 'instructions',
+				label: __( 'Instructions', 'woocommerce' ),
+				description: __(
+					'Instructions that will be added to the thank you page and emails.',
+					'woocommerce'
+				),
+				Edit: TextareaEdit,
+			},
+		],
+		[]
+	);
+
 	const saveSettings = () => {
 		if ( ! chequeSettings ) {
 			return;
@@ -120,80 +160,30 @@ export const SettingsPaymentsCheque = () => {
 						) }
 					>
 						{ isLoading ? (
-							<FieldPlaceholder size="small" />
-						) : (
-							<CheckboxControl
-								label={ __(
-									'Enable check payments',
-									'woocommerce'
-								) }
-								checked={ Boolean( formValues.enabled ) }
-								onChange={ ( checked ) => {
-									setFormValues( {
-										...formValues,
-										enabled: checked,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="medium" />
+							<>
+								<FieldPlaceholder size="small" />
+								<FieldPlaceholder size="medium" />
+								<FieldPlaceholder size="large" />
+								<FieldPlaceholder size="large" />
+							</>
 						) : (
-							<TextControl
-								label={ __( 'Title', 'woocommerce' ) }
-								help={ __(
-									'Payment method name that the customer will see during checkout.',
-									'woocommerce'
-								) }
-								placeholder={ __(
-									'Check payments',
-									'woocommerce'
-								) }
-								value={ String( formValues.title ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										title: value,
-									} );
-									setHasChanges( true );
+							<DataForm
+								data={ formValues }
+								fields={ fields }
+								form={ {
+									type: 'regular',
+									fields: [
+										'enabled',
+										'title',
+										'description',
+										'instructions',
+									],
 								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="large" />
-						) : (
-							<TextareaControl
-								label={ __( 'Description', 'woocommerce' ) }
-								help={ __(
-									'Payment method description that the customer will see during checkout.',
-									'woocommerce'
-								) }
-								value={ String( formValues.description ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										description: value,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="large" />
-						) : (
-							<TextareaControl
-								label={ __( 'Instructions', 'woocommerce' ) }
-								help={ __(
-									'Instructions that will be added to the thank you page and emails.',
-									'woocommerce'
-								) }
-								value={ String( formValues.instructions ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										instructions: value,
-									} );
+								onChange={ ( edits: OfflineFormValues ) => {
+									setFormValues( ( values ) => ( {
+										...values,
+										...edits,
+									} ) );
 									setHasChanges( true );
 								} }
 							/>
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cod.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cod.tsx
index a2d1621f37d..88c122ac6a8 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cod.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-cod.tsx
@@ -1,17 +1,14 @@
 /**
  * External dependencies
  */
-import {
-	Button,
-	CheckboxControl,
-	TextControl,
-	TextareaControl,
-} from '@wordpress/components';
+import { Button } from '@wordpress/components';
 import { TreeSelectControl } from '@woocommerce/components';
 import { __ } from '@wordpress/i18n';
 import { useDispatch, useSelect } from '@wordpress/data';
 import { paymentGatewaysStore, paymentSettingsStore } from '@woocommerce/data';
-import { useState, useEffect } from '@wordpress/element';
+import { useState, useEffect, useMemo } from '@wordpress/element';
+import { DataForm } from '@wordpress/dataviews';
+import type { Field } from '@wordpress/dataviews';

 /**
  * Internal dependencies
@@ -20,6 +17,12 @@ import '../settings-payments-body.scss';
 import { mapShippingMethodsOptions } from '~/settings-payments/offline/utils';
 import { Settings } from '~/settings-payments/components/settings';
 import { FieldPlaceholder } from '~/settings-payments/components/field-placeholder';
+import {
+	CheckboxEdit,
+	TextEdit,
+	TextareaEdit,
+	type OfflineFormValues,
+} from './dataform-controls';

 /**
  * This page is used to manage the settings for the Cash on delivery payment gateway.
@@ -48,9 +51,7 @@ export const SettingsPaymentsCod = () => {
 			invalidateResolutionForPaymentSettings,
 	} = useDispatch( paymentSettingsStore );

-	const [ formValues, setFormValues ] = useState<
-		Record< string, string | boolean | string[] >
-	>( {} );
+	const [ formValues, setFormValues ] = useState< OfflineFormValues >( {} );
 	const [ isSaving, setIsSaving ] = useState( false );
 	const [ hasChanges, setHasChanges ] = useState( false );

@@ -73,6 +74,89 @@ export const SettingsPaymentsCod = () => {
 		}
 	}, [ codSettings ] );

+	const shippingMethodsOptions = useMemo(
+		() =>
+			codSettings?.settings.enable_for_methods?.options
+				? mapShippingMethodsOptions(
+						codSettings.settings.enable_for_methods.options
+				  )
+				: [],
+		[ codSettings ]
+	);
+
+	const fields: Field< OfflineFormValues >[] = useMemo(
+		() => [
+			{
+				id: 'enabled',
+				label: __( 'Enable cash on delivery payments', 'woocommerce' ),
+				Edit: CheckboxEdit,
+			},
+			{
+				id: 'title',
+				label: __( 'Title', 'woocommerce' ),
+				description: __(
+					'Payment method name that the customer will see during checkout.',
+					'woocommerce'
+				),
+				placeholder: __( 'Cash on delivery payments', 'woocommerce' ),
+				Edit: TextEdit,
+			},
+			{
+				id: 'description',
+				label: __( 'Description', 'woocommerce' ),
+				description: __(
+					'Payment method description that the customer will see during checkout.',
+					'woocommerce'
+				),
+				Edit: TextareaEdit,
+			},
+			{
+				id: 'instructions',
+				label: __( 'Instructions', 'woocommerce' ),
+				description: __(
+					'Instructions that will be added to the thank you page and emails.',
+					'woocommerce'
+				),
+				Edit: TextareaEdit,
+			},
+			{
+				id: 'enable_for_methods',
+				label: __( 'Enable for shipping methods', 'woocommerce' ),
+				description: __(
+					'Select shipping methods for which this payment method is enabled.',
+					'woocommerce'
+				),
+				// COD-specific edit control: renders the shipping methods
+				// multi-select using the options that ship with the gateway.
+				Edit: ( { data, field, onChange } ) => {
+					const value = field.getValue( { item: data } );
+					return (
+						<TreeSelectControl
+							label={ field.label }
+							help={ field.description }
+							options={ shippingMethodsOptions }
+							value={ Array.isArray( value ) ? value : [] }
+							onChange={ ( newValue: string[] ) =>
+								onChange( { [ field.id ]: newValue } )
+							}
+							selectAllLabel={ false }
+						/>
+					);
+				},
+			},
+			{
+				id: 'enable_for_virtual',
+				label: __( 'Accept for virtual orders', 'woocommerce' ),
+				description: __(
+					'Accept cash on delivery if the order is virtual',
+					'woocommerce'
+				),
+				Edit: CheckboxEdit,
+			},
+		],
+		[ shippingMethodsOptions ]
+	);
+
 	const saveSettings = () => {
 		if ( ! codSettings ) {
 			return;
@@ -132,142 +216,34 @@ export const SettingsPaymentsCod = () => {
 						) }
 					>
 						{ isLoading ? (
-							<FieldPlaceholder size="small" />
-						) : (
-							<CheckboxControl
-								label={ __(
-									'Enable cash on delivery payments',
-									'woocommerce'
-								) }
-								checked={ Boolean( formValues.enabled ) }
-								onChange={ ( checked ) => {
-									setFormValues( {
-										...formValues,
-										enabled: checked,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="medium" />
+							<>
+								<FieldPlaceholder size="small" />
+								<FieldPlaceholder size="medium" />
+								<FieldPlaceholder size="large" />
+								<FieldPlaceholder size="large" />
+								<FieldPlaceholder size="medium" />
+								<FieldPlaceholder size="small" />
+							</>
 						) : (
-							<TextControl
-								label={ __( 'Title', 'woocommerce' ) }
-								help={ __(
-									'Payment method name that the customer will see during checkout.',
-									'woocommerce'
-								) }
-								placeholder={ __(
-									'Cash on delivery payments',
-									'woocommerce'
-								) }
-								value={ String( formValues.title ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										title: value,
-									} );
-									setHasChanges( true );
+							<DataForm
+								data={ formValues }
+								fields={ fields }
+								form={ {
+									type: 'regular',
+									fields: [
+										'enabled',
+										'title',
+										'description',
+										'instructions',
+										'enable_for_methods',
+										'enable_for_virtual',
+									],
 								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="large" />
-						) : (
-							<TextareaControl
-								label={ __( 'Description', 'woocommerce' ) }
-								help={ __(
-									'Payment method description that the customer will see during checkout.',
-									'woocommerce'
-								) }
-								value={ String( formValues.description ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										description: value,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="large" />
-						) : (
-							<TextareaControl
-								label={ __( 'Instructions', 'woocommerce' ) }
-								help={ __(
-									'Instructions that will be added to the thank you page and emails.',
-									'woocommerce'
-								) }
-								value={ String( formValues.instructions ) }
-								onChange={ ( value ) => {
-									setFormValues( {
-										...formValues,
-										instructions: value,
-									} );
-									setHasChanges( true );
-								} }
-							/>
-						) }
-						{ isLoading || ! codSettings ? (
-							<FieldPlaceholder size="medium" />
-						) : (
-							<TreeSelectControl
-								label={ __(
-									'Enable for shipping methods',
-									'woocommerce'
-								) }
-								help={ __(
-									'Select shipping methods for which this payment method is enabled.',
-									'woocommerce'
-								) }
-								options={
-									codSettings.settings.enable_for_methods
-										?.options
-										? mapShippingMethodsOptions(
-												codSettings.settings
-													.enable_for_methods.options
-										  )
-										: []
-								}
-								value={
-									Array.isArray(
-										formValues.enable_for_methods
-									)
-										? formValues.enable_for_methods
-										: []
-								}
-								onChange={ ( value: string[] ) => {
-									setFormValues( {
-										...formValues,
-										enable_for_methods: value,
-									} );
-									setHasChanges( true );
-								} }
-								selectAllLabel={ false }
-							/>
-						) }
-						{ isLoading ? (
-							<FieldPlaceholder size="small" />
-						) : (
-							<CheckboxControl
-								label={ __(
-									'Accept for virtual orders',
-									'woocommerce'
-								) }
-								help={ __(
-									'Accept cash on delivery if the order is virtual',
-									'woocommerce'
-								) }
-								checked={ Boolean(
-									formValues.enable_for_virtual
-								) }
-								onChange={ ( checked ) => {
-									setFormValues( {
-										...formValues,
-										enable_for_virtual: checked,
-									} );
+								onChange={ ( edits: OfflineFormValues ) => {
+									setFormValues( ( values ) => ( {
+										...values,
+										...edits,
+									} ) );
 									setHasChanges( true );
 								} }
 							/>
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx
new file mode 100644
index 00000000000..1cebc50602c
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx
@@ -0,0 +1,239 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { SettingsPaymentsBacs } from '../settings-payments-bacs';
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	useSelect: jest.fn(),
+	useDispatch: jest.fn(),
+} ) );
+
+jest.mock( '~/settings-payments/components/bank-accounts-list', () => ( {
+	BankAccountsList: () => <div data-testid="bank-accounts-list" />,
+} ) );
+
+const bacsSettings = {
+	enabled: true,
+	description: 'Make your payment directly into our bank account.',
+	settings: {
+		title: { value: 'Direct bank transfer' },
+		instructions: { value: 'Use your order ID as the payment reference.' },
+	},
+};
+
+const accountsOption = [
+	{
+		id: 'extra-field-that-should-not-be-saved',
+		account_name: 'Main account',
+		account_number: '12345678',
+		bank_name: 'Test Bank',
+		sort_code: '12-34-56',
+		iban: 'GB29NWBK60161331926819',
+		bic: 'NWBKGB2L',
+		country_code: 'GB',
+	},
+];
+
+describe( 'SettingsPaymentsBacs', () => {
+	let updatePaymentGateway: jest.Mock;
+	let updateOptions: jest.Mock;
+
+	beforeEach( () => {
+		updatePaymentGateway = jest.fn().mockResolvedValue( {} );
+		updateOptions = jest.fn().mockResolvedValue( {} );
+		( useDispatch as jest.Mock ).mockReturnValue( {
+			createSuccessNotice: jest.fn(),
+			createErrorNotice: jest.fn(),
+			updatePaymentGateway,
+			updateOptions,
+			invalidateResolution: jest.fn(),
+			invalidateResolutionForStoreSelector: jest.fn(),
+		} );
+		( useSelect as jest.Mock ).mockReturnValue( {
+			bacsSettings,
+			isLoading: false,
+			accountsOption,
+			isLoadingAccounts: false,
+		} );
+	} );
+
+	it( 'renders all settings fields with stored values', () => {
+		render( <SettingsPaymentsBacs /> );
+
+		expect(
+			screen.getByLabelText( 'Enable direct bank transfers' )
+		).toBeChecked();
+		expect( screen.getByLabelText( 'Title' ) ).toHaveValue(
+			'Direct bank transfer'
+		);
+		expect( screen.getByLabelText( 'Description' ) ).toHaveValue(
+			'Make your payment directly into our bank account.'
+		);
+		expect( screen.getByLabelText( 'Instructions' ) ).toHaveValue(
+			'Use your order ID as the payment reference.'
+		);
+	} );
+
+	it( 'renders the bank accounts section', () => {
+		render( <SettingsPaymentsBacs /> );
+
+		expect(
+			screen.getByTestId( 'bank-accounts-list' )
+		).toBeInTheDocument();
+	} );
+
+	it( 'renders placeholders while loading', () => {
+		( useSelect as jest.Mock ).mockReturnValue( {
+			bacsSettings: null,
+			isLoading: true,
+			accountsOption: undefined,
+			isLoadingAccounts: true,
+		} );
+
+		const { container } = render( <SettingsPaymentsBacs /> );
+
+		expect(
+			container.querySelectorAll( '.woocommerce-field-placeholder' )
+				.length
+		).toBeGreaterThan( 0 );
+		expect( screen.queryByLabelText( 'Title' ) ).not.toBeInTheDocument();
+		expect(
+			screen.queryByTestId( 'bank-accounts-list' )
+		).not.toBeInTheDocument();
+	} );
+
+	it( 'disables save until a change is made', () => {
+		render( <SettingsPaymentsBacs /> );
+
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toBeDisabled();
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Bank transfer payments' },
+		} );
+
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toBeEnabled();
+	} );
+
+	it( 'saves the edited values with the expected payload shape', async () => {
+		render( <SettingsPaymentsBacs /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Bank transfer payments' },
+		} );
+		fireEvent.click(
+			screen.getByLabelText( 'Enable direct bank transfers' )
+		);
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect( updatePaymentGateway ).toHaveBeenCalledWith( 'bacs', {
+				enabled: false,
+				description:
+					'Make your payment directly into our bank account.',
+				settings: {
+					title: 'Bank transfer payments',
+					instructions: 'Use your order ID as the payment reference.',
+				},
+			} );
+		} );
+
+		expect( updateOptions ).toHaveBeenCalledWith( {
+			woocommerce_bacs_accounts: [
+				{
+					account_name: 'Main account',
+					account_number: '12345678',
+					bank_name: 'Test Bank',
+					sort_code: '12-34-56',
+					iban: 'GB29NWBK60161331926819',
+					bic: 'NWBKGB2L',
+					country_code: 'GB',
+				},
+			],
+		} );
+	} );
+
+	it( 'disables save again after a successful save', async () => {
+		render( <SettingsPaymentsBacs /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Bank transfer payments' },
+		} );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect(
+				screen.getByRole( 'button', { name: 'Save changes' } )
+			).toBeDisabled();
+		} );
+	} );
+
+	it( 'supports keyboard navigation through the form fields', () => {
+		render( <SettingsPaymentsBacs /> );
+
+		// Make a change first so the Save button is enabled (and tabbable).
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Edited title' },
+		} );
+
+		userEvent.tab();
+		expect(
+			screen.getByLabelText( 'Enable direct bank transfers' )
+		).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Title' ) ).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Description' ) ).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Instructions' ) ).toHaveFocus();
+		userEvent.tab();
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toHaveFocus();
+	} );
+
+	it( 'shows an error notice when saving fails', async () => {
+		const createErrorNotice = jest.fn();
+		updatePaymentGateway.mockRejectedValueOnce(
+			new Error( 'save failed' )
+		);
+		( useDispatch as jest.Mock ).mockReturnValue( {
+			createSuccessNotice: jest.fn(),
+			createErrorNotice,
+			updatePaymentGateway,
+			updateOptions: jest.fn().mockResolvedValue( {} ),
+			invalidateResolution: jest.fn(),
+			invalidateResolutionForStoreSelector: jest.fn(),
+		} );
+
+		render( <SettingsPaymentsBacs /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Edited title' },
+		} );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect( createErrorNotice ).toHaveBeenCalledWith(
+				'Failed to update settings'
+			);
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-cheque.test.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-cheque.test.tsx
new file mode 100644
index 00000000000..0a0dbde35b7
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-cheque.test.tsx
@@ -0,0 +1,186 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { SettingsPaymentsCheque } from '../settings-payments-cheque';
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	useSelect: jest.fn(),
+	useDispatch: jest.fn(),
+} ) );
+
+const chequeSettings = {
+	enabled: true,
+	description: 'Take payments in person via checks.',
+	settings: {
+		title: { value: 'Check payments' },
+		instructions: { value: 'Send the check to our address.' },
+	},
+};
+
+describe( 'SettingsPaymentsCheque', () => {
+	let updatePaymentGateway: jest.Mock;
+
+	beforeEach( () => {
+		updatePaymentGateway = jest.fn().mockResolvedValue( {} );
+		( useDispatch as jest.Mock ).mockReturnValue( {
+			createSuccessNotice: jest.fn(),
+			createErrorNotice: jest.fn(),
+			updatePaymentGateway,
+			invalidateResolution: jest.fn(),
+			invalidateResolutionForStoreSelector: jest.fn(),
+		} );
+		( useSelect as jest.Mock ).mockReturnValue( {
+			chequeSettings,
+			isLoading: false,
+		} );
+	} );
+
+	it( 'renders all settings fields with stored values', () => {
+		render( <SettingsPaymentsCheque /> );
+
+		expect(
+			screen.getByLabelText( 'Enable check payments' )
+		).toBeChecked();
+		expect( screen.getByLabelText( 'Title' ) ).toHaveValue(
+			'Check payments'
+		);
+		expect( screen.getByLabelText( 'Description' ) ).toHaveValue(
+			'Take payments in person via checks.'
+		);
+		expect( screen.getByLabelText( 'Instructions' ) ).toHaveValue(
+			'Send the check to our address.'
+		);
+	} );
+
+	it( 'renders placeholders while loading', () => {
+		( useSelect as jest.Mock ).mockReturnValue( {
+			chequeSettings: null,
+			isLoading: true,
+		} );
+
+		const { container } = render( <SettingsPaymentsCheque /> );
+
+		expect(
+			container.querySelectorAll( '.woocommerce-field-placeholder' )
+				.length
+		).toBeGreaterThan( 0 );
+		expect( screen.queryByLabelText( 'Title' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'disables save until a change is made', () => {
+		render( <SettingsPaymentsCheque /> );
+
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toBeDisabled();
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Cheque payments' },
+		} );
+
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toBeEnabled();
+	} );
+
+	it( 'saves the edited values with the expected payload shape', async () => {
+		render( <SettingsPaymentsCheque /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Cheque payments' },
+		} );
+		fireEvent.click( screen.getByLabelText( 'Enable check payments' ) );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect( updatePaymentGateway ).toHaveBeenCalledWith( 'cheque', {
+				enabled: false,
+				description: 'Take payments in person via checks.',
+				settings: {
+					title: 'Cheque payments',
+					instructions: 'Send the check to our address.',
+				},
+			} );
+		} );
+	} );
+
+	it( 'disables save again after a successful save', async () => {
+		render( <SettingsPaymentsCheque /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Cheque payments' },
+		} );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect(
+				screen.getByRole( 'button', { name: 'Save changes' } )
+			).toBeDisabled();
+		} );
+	} );
+
+	it( 'supports keyboard navigation through the form fields', () => {
+		render( <SettingsPaymentsCheque /> );
+
+		// Make a change first so the Save button is enabled (and tabbable).
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Edited title' },
+		} );
+
+		userEvent.tab();
+		expect(
+			screen.getByLabelText( 'Enable check payments' )
+		).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Title' ) ).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Description' ) ).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Instructions' ) ).toHaveFocus();
+		userEvent.tab();
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toHaveFocus();
+	} );
+
+	it( 'shows an error notice when saving fails', async () => {
+		const createErrorNotice = jest.fn();
+		updatePaymentGateway.mockRejectedValueOnce(
+			new Error( 'save failed' )
+		);
+		( useDispatch as jest.Mock ).mockReturnValue( {
+			createSuccessNotice: jest.fn(),
+			createErrorNotice,
+			updatePaymentGateway,
+			invalidateResolution: jest.fn(),
+			invalidateResolutionForStoreSelector: jest.fn(),
+		} );
+
+		render( <SettingsPaymentsCheque /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Edited title' },
+		} );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect( createErrorNotice ).toHaveBeenCalledWith(
+				'Failed to update settings'
+			);
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-cod.test.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-cod.test.tsx
new file mode 100644
index 00000000000..bde2c458f4b
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-cod.test.tsx
@@ -0,0 +1,217 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { SettingsPaymentsCod } from '../settings-payments-cod';
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	useSelect: jest.fn(),
+	useDispatch: jest.fn(),
+} ) );
+
+const codSettings = {
+	enabled: true,
+	description: 'Pay with cash upon delivery.',
+	settings: {
+		title: { value: 'Cash on delivery' },
+		instructions: { value: 'Pay with cash upon delivery.' },
+		enable_for_methods: {
+			value: [ 'flat_rate:1' ],
+			options: {
+				'Flat rate': {
+					'flat_rate:1': 'Flat rate (#1)',
+				},
+				'Free shipping': {
+					'free_shipping:2': 'Free shipping (#2)',
+				},
+			},
+		},
+		enable_for_virtual: { value: 'yes' },
+	},
+};
+
+describe( 'SettingsPaymentsCod', () => {
+	let updatePaymentGateway: jest.Mock;
+
+	beforeEach( () => {
+		updatePaymentGateway = jest.fn().mockResolvedValue( {} );
+		( useDispatch as jest.Mock ).mockReturnValue( {
+			createSuccessNotice: jest.fn(),
+			createErrorNotice: jest.fn(),
+			updatePaymentGateway,
+			invalidateResolution: jest.fn(),
+			invalidateResolutionForStoreSelector: jest.fn(),
+		} );
+		( useSelect as jest.Mock ).mockReturnValue( {
+			codSettings,
+			isLoading: false,
+		} );
+	} );
+
+	it( 'renders all settings fields with stored values', () => {
+		render( <SettingsPaymentsCod /> );
+
+		expect(
+			screen.getByLabelText( 'Enable cash on delivery payments' )
+		).toBeChecked();
+		expect( screen.getByLabelText( 'Title' ) ).toHaveValue(
+			'Cash on delivery'
+		);
+		expect( screen.getByLabelText( 'Description' ) ).toHaveValue(
+			'Pay with cash upon delivery.'
+		);
+		expect( screen.getByLabelText( 'Instructions' ) ).toHaveValue(
+			'Pay with cash upon delivery.'
+		);
+		expect(
+			screen.getByLabelText( 'Enable for shipping methods' )
+		).toBeInTheDocument();
+		// The stored shipping method selection is rendered as a tag.
+		expect( screen.getByText( 'Flat rate (#1)' ) ).toBeInTheDocument();
+		expect(
+			screen.getByLabelText( 'Accept for virtual orders' )
+		).toBeChecked();
+	} );
+
+	it( 'renders placeholders while loading', () => {
+		( useSelect as jest.Mock ).mockReturnValue( {
+			codSettings: null,
+			isLoading: true,
+		} );
+
+		const { container } = render( <SettingsPaymentsCod /> );
+
+		expect(
+			container.querySelectorAll( '.woocommerce-field-placeholder' )
+				.length
+		).toBeGreaterThan( 0 );
+		expect( screen.queryByLabelText( 'Title' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'disables save until a change is made', () => {
+		render( <SettingsPaymentsCod /> );
+
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toBeDisabled();
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'COD payments' },
+		} );
+
+		expect(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		).toBeEnabled();
+	} );
+
+	it( 'saves the edited values with the expected payload shape', async () => {
+		render( <SettingsPaymentsCod /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'COD payments' },
+		} );
+		fireEvent.click( screen.getByLabelText( 'Accept for virtual orders' ) );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect( updatePaymentGateway ).toHaveBeenCalledWith( 'cod', {
+				enabled: true,
+				description: 'Pay with cash upon delivery.',
+				settings: {
+					title: 'COD payments',
+					instructions: 'Pay with cash upon delivery.',
+					enable_for_methods: [ 'flat_rate:1' ],
+					enable_for_virtual: 'no',
+				},
+			} );
+		} );
+	} );
+
+	it( 'disables save again after a successful save', async () => {
+		render( <SettingsPaymentsCod /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'COD payments' },
+		} );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect(
+				screen.getByRole( 'button', { name: 'Save changes' } )
+			).toBeDisabled();
+		} );
+	} );
+
+	it( 'supports keyboard navigation through the form fields', () => {
+		render( <SettingsPaymentsCod /> );
+
+		// Make a change first so the Save button is enabled (and tabbable).
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Edited title' },
+		} );
+
+		userEvent.tab();
+		expect(
+			screen.getByLabelText( 'Enable cash on delivery payments' )
+		).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Title' ) ).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Description' ) ).toHaveFocus();
+		userEvent.tab();
+		expect( screen.getByLabelText( 'Instructions' ) ).toHaveFocus();
+		// The shipping methods tree select and the virtual orders checkbox
+		// sit between Instructions and Save; tab until Save receives focus.
+		const saveButton = screen.getByRole( 'button', {
+			name: 'Save changes',
+		} );
+		for (
+			let i = 0;
+			i < 6 && saveButton.ownerDocument.activeElement !== saveButton;
+			i++
+		) {
+			userEvent.tab();
+		}
+		expect( saveButton ).toHaveFocus();
+	} );
+
+	it( 'shows an error notice when saving fails', async () => {
+		const createErrorNotice = jest.fn();
+		updatePaymentGateway.mockRejectedValueOnce(
+			new Error( 'save failed' )
+		);
+		( useDispatch as jest.Mock ).mockReturnValue( {
+			createSuccessNotice: jest.fn(),
+			createErrorNotice,
+			updatePaymentGateway,
+			invalidateResolution: jest.fn(),
+			invalidateResolutionForStoreSelector: jest.fn(),
+		} );
+
+		render( <SettingsPaymentsCod /> );
+
+		fireEvent.change( screen.getByLabelText( 'Title' ), {
+			target: { value: 'Edited title' },
+		} );
+		fireEvent.click(
+			screen.getByRole( 'button', { name: 'Save changes' } )
+		);
+
+		await waitFor( () => {
+			expect( createErrorNotice ).toHaveBeenCalledWith(
+				'Failed to update settings'
+			);
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss b/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
index 460fb7c13bb..d7b57058491 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
@@ -1,4 +1,5 @@
 @import "@wordpress/admin-ui/build-style/style.css";
+@import "@wordpress/dataviews/build-style/style.css";

 body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
 	background-color: #fff;