Commit e007adae827 for woocommerce

commit e007adae8271a6a50a4fbf5f7fe5e39e82d0f865
Author: Mike Jolley <mike.jolley@me.com>
Date:   Fri Nov 6 10:43:51 2026 +0000

    Add terms and conditions options to the Checkout Actions block (#70066)

    * Add terms and conditions settings to the Checkout Actions block

    Terms render next to Place order: a required checkbox above it, text-only
    terms below it in the same column. Return to Cart sits beside the button
    on wider checkouts and below the terms on narrow ones.

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Use flexbox for the Checkout actions row and match the fields' spacing

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Space the terms notices in the Actions block settings and keep the link arrow inline

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Suppress the ExternalLink lint rule inline in the Terms block editor

    The branch lint runs from the plugin root and doesn't see the blocks bulk
    suppressions file. @wordpress/ui isn't available to swap to yet.

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Regenerate hook docs after rebase

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Reset the checkoutShowTerms test flag before each Actions block test

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Let merchants switch from the Terms block to the Actions block terms

    While a checkout still has the Terms and Conditions block, the Actions
    block hides its terms settings so both can't be shown. Either block offers
    a button that moves the Terms block settings onto the Actions block and
    removes the old block.

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Start new checkouts with terms on the Actions block

    New Checkout blocks and new store pages no longer include the Terms and
    Conditions block. Existing checkouts are unchanged.

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Deprecate the Checkout Terms and Conditions block

    Hide it from the inserter and recommend switching to the terms settings on
    the Place order button. It stays registered so existing checkouts keep
    working.

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Switch to the Actions block terms in a single undoable change

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Test switching from the Terms block to the Actions block terms

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Update the changelog entry for the Checkout terms changes

    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

    * Keep checkout terms switching in the Terms block inspector

    * Update Terms block switch to follow soft deprecation

    Keep the Terms block unmarked in code while it is hidden from the inserter, show the switch through the shared upgrade notice, and track the switch. Capitalise the Terms and Conditions panel title.

    * Only run the checkout terms filter on the frontend

    Callbacks for woocommerce_checkout_show_terms were written for classic checkout, where the cart is loaded. Running the filter on block editor screens could fatal the editor, so the editor no longer receives the value or shows the hidden-by-extension notice.

    * Apply review wording and make the editor terms checkbox inert

    Wrap the editor preview checkbox in Disabled on the Actions and Terms and Conditions blocks, so it ignores clicks and focus. Register checkoutShowTerms before the incompatible extensions early return, so admins viewing checkout still get the filtered value. Use the Terms and Conditions block name in tests and comments, and match the switch notice text without regular expressions.

    ---------

    Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

diff --git a/docs/block-development/reference/block-references.md b/docs/block-development/reference/block-references.md
index 4c32b0e8ad3..a9e15dd204f 100644
--- a/docs/block-development/reference/block-references.md
+++ b/docs/block-development/reference/block-references.md
@@ -428,13 +428,13 @@ Display a checkout form so your customers can submit orders.

 ## Actions - woocommerce/checkout-actions-block

-Allow customers to place their order.
+Place order button, with optional Terms and Conditions and Return to Cart links.

 - **Name:** woocommerce/checkout-actions-block
 - **Category:** woocommerce
 - **Parent:** woocommerce/checkout-fields-block
 - **Supports:** ~~align~~, ~~html~~, ~~inserter~~, ~~lock~~, ~~multiple~~, ~~reusable~~
-- **Attributes:** cartPageId, className, lock, priceSeparator, showReturnToCart
+- **Attributes:** cartPageId, className, lock, priceSeparator, showReturnToCart, showTerms, termsCheckbox, termsText

 ## Additional information - woocommerce/checkout-additional-information-block

@@ -643,7 +643,7 @@ Ensure that customers agree to your Terms & Conditions and Privacy Policy.
 - **Name:** woocommerce/checkout-terms-block
 - **Category:** woocommerce
 - **Parent:** woocommerce/checkout-fields-block
-- **Supports:** ~~align~~, ~~html~~, ~~multiple~~, ~~reusable~~
+- **Supports:** ~~align~~, ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~
 - **Attributes:** checkbox, className, showSeparator, text

 ## Checkout Totals - woocommerce/checkout-totals-block
diff --git a/plugins/woocommerce/changelog/add-checkout-terms-in-actions-block b/plugins/woocommerce/changelog/add-checkout-terms-in-actions-block
new file mode 100644
index 00000000000..fad6e65b529
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-checkout-terms-in-actions-block
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add Terms and Conditions settings to the Checkout Place order button, and hide the separate Terms and Conditions block from the inserter.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
index e8c798c862b..525a2ab3711 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.json
@@ -1,7 +1,7 @@
 {
 	"name": "woocommerce/checkout-actions-block",
 	"title": "Actions",
-	"description": "Allow customers to place their order.",
+	"description": "Place order button, with optional Terms and Conditions and Return to Cart links.",
 	"category": "woocommerce",
 	"supports": {
 		"align": false,
@@ -34,6 +34,15 @@
 		"priceSeparator": {
 			"type": "string",
 			"default": "·"
+		},
+		"showTerms": {
+			"type": "boolean"
+		},
+		"termsCheckbox": {
+			"type": "boolean"
+		},
+		"termsText": {
+			"type": "string"
 		}
 	},
 	"parent": [ "woocommerce/checkout-fields-block" ],
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx
index 4c7f2080a2d..c9bda23063e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx
@@ -20,6 +20,7 @@ import { paymentStore } from '@woocommerce/block-data';
  */
 import { defaultPlaceOrderButtonLabel } from './constants';
 import { CheckoutOrderSummarySlot } from '../checkout-order-summary-block/slotfills';
+import TermsAndConditions from '../checkout-terms-block/frontend';
 import './style.scss';

 export type BlockAttributes = {
@@ -29,6 +30,9 @@ export type BlockAttributes = {
 	placeOrderButtonLabel: string;
 	priceSeparator: string;
 	returnToCartButtonLabel: string;
+	showTerms?: boolean;
+	termsCheckbox?: boolean;
+	termsText?: string;
 };

 const Block = ( {
@@ -38,6 +42,9 @@ const Block = ( {
 	placeOrderButtonLabel,
 	returnToCartButtonLabel,
 	priceSeparator,
+	showTerms,
+	termsCheckbox,
+	termsText,
 }: BlockAttributes ) => {
 	const { paymentMethodButtonLabel, paymentMethodPlaceOrderButton } =
 		useCheckoutSubmit();
@@ -66,16 +73,32 @@ const Block = ( {

 	const showPrice = className?.includes( 'is-style-with-price' ) || false;

+	// A required checkbox goes above the button; consent text goes below it. Extensions can hide the terms
+	// with the woocommerce_checkout_show_terms filter.
+	const terms =
+		showTerms && getSetting( 'checkoutShowTerms', true ) ? (
+			<TermsAndConditions
+				text={ termsText || '' }
+				checkbox={ !! termsCheckbox }
+				showSeparator={ false }
+				// Matches the Terms block wrapper, which extensions target.
+				className="wp-block-woocommerce-checkout-terms-block"
+			/>
+		) : null;
+
 	return (
 		<div className={ clsx( 'wc-block-checkout__actions', className ) }>
 			<CheckoutOrderSummarySlot />
 			<StoreNoticesContainer
 				context={ noticeContexts.CHECKOUT_ACTIONS }
 			/>
+			{ termsCheckbox && terms }
 			<div
 				className={ clsx( 'wc-block-checkout__actions_row', {
 					'wc-block-checkout__actions_row--justify-flex-end':
 						! shouldShowReturnToCart,
+					'wc-block-checkout__actions_row--with-return-to-cart':
+						shouldShowReturnToCart,
 				} ) }
 			>
 				{ shouldShowReturnToCart && (
@@ -86,10 +109,11 @@ const Block = ( {
 				<PlaceOrderButton
 					CustomButtonComponent={ CustomButtonComponent }
 					label={ label }
-					fullWidth={ ! shouldShowReturnToCart }
+					fullWidth
 					showPrice={ showPrice }
 					priceSeparator={ priceSeparator }
 				/>
+				{ ! termsCheckbox && terms }
 			</div>
 		</div>
 	);
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/edit.tsx
index 598629c9476..8b18e5ad0a1 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/edit.tsx
@@ -9,34 +9,51 @@ import {
 	RichText,
 	useBlockProps,
 } from '@wordpress/block-editor';
+import {
+	Disabled,
+	PanelBody,
+	TextControl,
+	ToggleControl,
+} from '@wordpress/components';
 import PageSelector from '@woocommerce/editor-components/page-selector';
-import { PanelBody, ToggleControl, TextControl } from '@wordpress/components';
 import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
 import {
 	PlaceOrderButton,
 	ReturnToCartButton,
 } from '@woocommerce/base-components/cart-checkout';
 import clsx from 'clsx';
+import { CheckboxControl } from '@woocommerce/blocks-components';

 /**
  * Internal dependencies
  */
 import { BlockAttributes } from './block';
 import { defaultReturnToCartButtonLabel } from './constants';
+import { TermsNotices } from '../checkout-terms-block/edit';
+import { useCheckoutTermsBlocks } from '../checkout-terms-block/switch-to-actions-terms';
+import {
+	termsConsentDefaultText,
+	termsCheckboxDefaultText,
+} from '../checkout-terms-block/constants';

 export const Edit = ( {
+	clientId,
 	attributes,
 	setAttributes,
 }: {
+	clientId: string;
 	attributes: BlockAttributes;
 	setAttributes: ( attributes: Record< string, unknown > ) => void;
-} ): JSX.Element => {
+} ) => {
 	const blockProps = useBlockProps();
 	const {
 		cartPageId = 0,
 		showReturnToCart = false,
 		placeOrderButtonLabel,
 		returnToCartButtonLabel,
+		showTerms = false,
+		termsCheckbox = false,
+		termsText = '',
 	} = attributes;
 	const { current: savedCartPageId } = useRef( cartPageId );
 	const currentPostId = useSelect(
@@ -52,6 +69,36 @@ export const Edit = ( {

 	const showPrice = blockProps.className.includes( 'is-style-with-price' );

+	// While the Terms and Conditions block is in the checkout, it owns the terms.
+	const { termsClientId } = useCheckoutTermsBlocks( clientId );
+
+	// Mirrors the frontend: a required checkbox goes above the button, consent text below it.
+	const terms = showTerms ? (
+		<div className="wc-block-checkout__terms">
+			{ termsCheckbox && (
+				<div className="wc-block-checkout__terms-checkbox-wrapper">
+					<Disabled>
+						<CheckboxControl
+							id="terms-and-conditions"
+							checked={ false }
+							onChange={ () => undefined }
+						/>
+					</Disabled>
+				</div>
+			) }
+			<RichText
+				tagName="span"
+				value={
+					termsText ||
+					( termsCheckbox
+						? termsCheckboxDefaultText
+						: termsConsentDefaultText )
+				}
+				onChange={ ( value ) => setAttributes( { termsText: value } ) }
+			/>
+		</div>
+	) : null;
+
 	return (
 		<div { ...blockProps }>
 			<InspectorControls>
@@ -90,6 +137,48 @@ export const Edit = ( {
 					) }
 				</PanelBody>

+				{ ! termsClientId && (
+					<PanelBody
+						title={ __( 'Terms and Conditions', 'woocommerce' ) }
+					>
+						{ showTerms && (
+							<TermsNotices
+								text={ termsText }
+								checkbox={ termsCheckbox }
+								setText={ ( value ) =>
+									setAttributes( { termsText: value } )
+								}
+							/>
+						) }
+						<ToggleControl
+							__nextHasNoMarginBottom
+							label={ __(
+								'Show terms and conditions',
+								'woocommerce'
+							) }
+							checked={ showTerms }
+							onChange={ () =>
+								setAttributes( { showTerms: ! showTerms } )
+							}
+						/>
+						{ showTerms && (
+							<ToggleControl
+								__nextHasNoMarginBottom
+								label={ __(
+									'Require checkbox',
+									'woocommerce'
+								) }
+								checked={ termsCheckbox }
+								onChange={ () =>
+									setAttributes( {
+										termsCheckbox: ! termsCheckbox,
+									} )
+								}
+							/>
+						) }
+					</PanelBody>
+				) }
+
 				{ showReturnToCart &&
 					! (
 						currentPostId === CHECKOUT_PAGE_ID &&
@@ -114,10 +203,13 @@ export const Edit = ( {
 					) }
 			</InspectorControls>
 			<div className="wc-block-checkout__actions">
+				{ termsCheckbox && terms }
 				<div
 					className={ clsx( 'wc-block-checkout__actions_row', {
 						'wc-block-checkout__actions_row--justify-flex-end':
 							! showReturnToCart,
+						'wc-block-checkout__actions_row--with-return-to-cart':
+							showReturnToCart,
 					} ) }
 				>
 					{ showReturnToCart && (
@@ -148,10 +240,11 @@ export const Edit = ( {
 								} }
 							/>
 						}
-						fullWidth={ ! showReturnToCart }
+						fullWidth
 						showPrice={ showPrice }
 						priceSeparator={ attributes.priceSeparator }
 					/>
+					{ ! termsCheckbox && terms }
 				</div>
 			</div>
 		</div>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/style.scss
index a9e55331a28..8d65fcc4a31 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/style.scss
@@ -1,33 +1,67 @@
+// The Place order button's width when it shares the row with Return to Cart.
+$place-order-column-width: max(50%, 260px);
+
 .wc-block-checkout__actions {
 	margin-bottom: $gap-larger;

+	// Checkbox terms sit above the row.
+	> .wc-block-checkout__terms {
+		margin: 0 0 $gap-small;
+		padding-block: 0;
+	}
+
 	.wc-block-checkout__actions_row {
 		@include font-small-locked;
 		display: flex;
+		flex-wrap: wrap;
 		justify-content: space-between;
 		align-items: center;
-		gap: $gap-smaller;
-
-		&--justify-flex-end {
-			justify-content: flex-end;
-		}
+		gap: $gap-small;

 		.wc-block-components-checkout-place-order-button--full-width {
 			width: 100%;
 		}
-	}
-}

-@include cart-checkout-mobile-container {
-	.wc-block-checkout__actions {
-		.wc-block-checkout__actions_row {
-			flex-direction: column-reverse;
+		// Text-only terms take a whole line, so they wrap below the button.
+		> .wc-block-checkout__terms {
+			box-sizing: border-box;
+			flex: 0 0 100%;
 			justify-content: center;
-			gap: $gap;
+			margin: 0;
+			padding-block: 0;
+			text-align: center;
 		}

+		&--with-return-to-cart {
+			.wc-block-components-checkout-place-order-button {
+				flex: 0 0 $place-order-column-width;
+			}
+
+			// Line the terms up under the button.
+			> .wc-block-checkout__terms {
+				padding-inline-start: calc(100% - #{$place-order-column-width});
+			}
+		}
+	}
+}
+
+// On narrow checkouts everything is full width, with Return to Cart below the terms.
+@include cart-checkout-below-medium-container {
+	.wc-block-checkout__actions
+		.wc-block-checkout__actions_row--with-return-to-cart {
 		.wc-block-components-checkout-place-order-button {
-			width: 100%;
+			flex-basis: 100%;
+			order: 1;
+		}
+
+		> .wc-block-checkout__terms {
+			order: 2;
+			padding-inline-start: 0;
+		}
+
+		.wc-block-components-checkout-return-to-cart-button {
+			order: 3;
+			margin-inline: auto;
 		}
 	}
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/test/block.js b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/test/block.js
new file mode 100644
index 00000000000..47b37c35e67
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/test/block.js
@@ -0,0 +1,318 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import Block from '../block';
+
+const mockPlaceOrderButton = jest.fn( ( { label, CustomButtonComponent } ) => {
+	if ( CustomButtonComponent ) {
+		return <CustomButtonComponent />;
+	}
+	return <button>{ label }</button>;
+} );
+
+jest.mock( '@woocommerce/base-components/cart-checkout', () => ( {
+	PlaceOrderButton: ( props ) => mockPlaceOrderButton( props ),
+	ReturnToCartButton: ( { children, href } ) => (
+		<a href={ href }>{ children }</a>
+	),
+} ) );
+
+const mockUseCheckoutSubmit = jest.fn();
+jest.mock( '@woocommerce/base-context/hooks', () => ( {
+	useCheckoutSubmit: () => mockUseCheckoutSubmit(),
+} ) );
+
+const mockUseSelect = jest.fn();
+jest.mock( '@wordpress/data', () => ( {
+	useSelect: () => mockUseSelect(),
+} ) );
+
+jest.mock( '@woocommerce/block-data', () => ( {
+	paymentStore: 'wc/store/payment',
+} ) );
+
+let mockCheckoutShowTerms = true;
+jest.mock( '@woocommerce/settings', () => ( {
+	getSetting: jest.fn( ( key, fallback ) =>
+		key === 'checkoutShowTerms' ? mockCheckoutShowTerms : fallback
+	),
+} ) );
+
+jest.mock( '@woocommerce/base-context', () => ( {
+	noticeContexts: {
+		CHECKOUT_ACTIONS: 'wc/checkout/checkout-actions',
+	},
+} ) );
+
+jest.mock( '@woocommerce/blocks-components', () => ( {
+	StoreNoticesContainer: () => null,
+} ) );
+
+jest.mock( '@woocommerce/blocks-checkout', () => ( {
+	applyCheckoutFilter: ( { defaultValue } ) => defaultValue,
+} ) );
+
+jest.mock( '@woocommerce/block-settings', () => ( {
+	CART_URL: '/cart',
+} ) );
+
+jest.mock( '../../checkout-order-summary-block/slotfills', () => ( {
+	CheckoutOrderSummarySlot: () => null,
+} ) );
+
+jest.mock(
+	'../../checkout-terms-block/frontend',
+	() =>
+		( { checkbox, text, className } ) => (
+			<div
+				data-testid="terms"
+				data-checkbox={ checkbox ? 'yes' : 'no' }
+				className={ className }
+			>
+				{ text }
+			</div>
+		)
+);
+
+jest.mock( '../../checkout-actions-block/constants', () => ( {
+	defaultPlaceOrderButtonLabel: 'Place Order',
+} ) );
+
+const defaultProps = {
+	cartPageId: 1,
+	showReturnToCart: false,
+	placeOrderButtonLabel: 'Place Order',
+	priceSeparator: '·',
+	returnToCartButtonLabel: 'Return to Cart',
+};
+
+const CustomPlaceOrderButton = () => <button>Custom Button</button>;
+
+describe( 'Checkout Actions Block', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockPlaceOrderButton.mockClear();
+		mockCheckoutShowTerms = true;
+		mockUseCheckoutSubmit.mockReturnValue( {
+			paymentMethodButtonLabel: '',
+			paymentMethodPlaceOrderButton: undefined,
+		} );
+		mockUseSelect.mockReturnValue( null );
+	} );
+
+	it( 'does not pass CustomButtonComponent to PlaceOrderButton when a saved token is active', () => {
+		mockUseCheckoutSubmit.mockReturnValue( {
+			paymentMethodButtonLabel: '',
+			paymentMethodPlaceOrderButton: CustomPlaceOrderButton,
+		} );
+		mockUseSelect.mockReturnValue( 'saved-token-123' );
+
+		render( <Block { ...defaultProps } /> );
+
+		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				CustomButtonComponent: undefined,
+			} )
+		);
+		expect( screen.queryByText( 'Place Order' ) ).toBeInTheDocument();
+		expect( screen.queryByText( 'Custom Button' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'passes CustomButtonComponent to PlaceOrderButton when no saved token is active', () => {
+		mockUseCheckoutSubmit.mockReturnValue( {
+			paymentMethodButtonLabel: '',
+			paymentMethodPlaceOrderButton: CustomPlaceOrderButton,
+		} );
+		mockUseSelect.mockReturnValue( null );
+
+		render( <Block { ...defaultProps } /> );
+
+		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				CustomButtonComponent: CustomPlaceOrderButton,
+			} )
+		);
+		expect( screen.queryByText( 'Place Order' ) ).not.toBeInTheDocument();
+		expect( screen.queryByText( 'Custom Button' ) ).toBeInTheDocument();
+	} );
+
+	it( 'passes undefined CustomButtonComponent when payment method does not provide one', () => {
+		mockUseCheckoutSubmit.mockReturnValue( {
+			paymentMethodButtonLabel: '',
+			paymentMethodPlaceOrderButton: undefined,
+		} );
+		mockUseSelect.mockReturnValue( null );
+
+		render( <Block { ...defaultProps } /> );
+
+		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				CustomButtonComponent: undefined,
+			} )
+		);
+		expect( screen.queryByText( 'Place Order' ) ).toBeInTheDocument();
+	} );
+
+	it( 'uses payment method button label when provided', () => {
+		mockUseCheckoutSubmit.mockReturnValue( {
+			paymentMethodButtonLabel: 'Pay with Card',
+			paymentMethodPlaceOrderButton: undefined,
+		} );
+		mockUseSelect.mockReturnValue( null );
+
+		render( <Block { ...defaultProps } /> );
+
+		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				label: 'Pay with Card',
+			} )
+		);
+		expect( screen.queryByText( 'Pay with Card' ) ).toBeInTheDocument();
+	} );
+
+	describe( 'terms and conditions', () => {
+		const isBefore = ( a, b ) => {
+			const elements = Array.from(
+				document.body.querySelectorAll( '*' )
+			);
+			return elements.indexOf( a ) < elements.indexOf( b );
+		};
+
+		it( 'renders checkbox terms above the Place order button', () => {
+			render(
+				<Block
+					{ ...defaultProps }
+					showTerms={ true }
+					termsCheckbox={ true }
+					termsText="Custom"
+				/>
+			);
+
+			const terms = screen.getByTestId( 'terms' );
+			expect( terms ).toHaveAttribute( 'data-checkbox', 'yes' );
+			expect( terms ).toHaveTextContent( 'Custom' );
+			expect( isBefore( terms, screen.getByRole( 'button' ) ) ).toBe(
+				true
+			);
+		} );
+
+		it( 'keeps the Terms and Conditions block wrapper class that extensions target', () => {
+			render( <Block { ...defaultProps } showTerms={ true } /> );
+
+			expect( screen.getByTestId( 'terms' ) ).toHaveClass(
+				'wp-block-woocommerce-checkout-terms-block'
+			);
+		} );
+
+		it( 'renders text-only terms below the Place order button', () => {
+			render(
+				<Block
+					{ ...defaultProps }
+					showTerms={ true }
+					termsCheckbox={ false }
+				/>
+			);
+
+			const terms = screen.getByTestId( 'terms' );
+			expect( terms ).toHaveAttribute( 'data-checkbox', 'no' );
+			expect( isBefore( screen.getByRole( 'button' ), terms ) ).toBe(
+				true
+			);
+		} );
+
+		it( 'renders no terms when showTerms is false', () => {
+			render( <Block { ...defaultProps } showTerms={ false } /> );
+
+			expect( screen.queryByTestId( 'terms' ) ).not.toBeInTheDocument();
+		} );
+
+		it( 'renders no terms when an extension hides them with woocommerce_checkout_show_terms', () => {
+			mockCheckoutShowTerms = false;
+			render(
+				<Block
+					{ ...defaultProps }
+					showTerms={ true }
+					termsCheckbox={ true }
+				/>
+			);
+
+			expect( screen.queryByTestId( 'terms' ) ).not.toBeInTheDocument();
+		} );
+
+		it( 'renders no terms when showTerms is not set', () => {
+			render( <Block { ...defaultProps } /> );
+
+			expect( screen.queryByTestId( 'terms' ) ).not.toBeInTheDocument();
+		} );
+	} );
+
+	describe( 'Return to Cart', () => {
+		const getRow = () =>
+			document.querySelector( '.wc-block-checkout__actions_row' );
+
+		it( 'renders the Return to Cart link when enabled', () => {
+			render( <Block { ...defaultProps } showReturnToCart={ true } /> );
+
+			expect(
+				screen.getByRole( 'link', { name: 'Return to Cart' } )
+			).toHaveAttribute( 'href', '/cart' );
+			expect( getRow() ).toHaveClass(
+				'wc-block-checkout__actions_row--with-return-to-cart'
+			);
+		} );
+
+		it( 'does not render the Return to Cart link when disabled', () => {
+			render( <Block { ...defaultProps } showReturnToCart={ false } /> );
+
+			expect(
+				screen.queryByRole( 'link', { name: 'Return to Cart' } )
+			).not.toBeInTheDocument();
+			expect( getRow() ).not.toHaveClass(
+				'wc-block-checkout__actions_row--with-return-to-cart'
+			);
+		} );
+
+		it( 'keeps the Place order button full width within its column', () => {
+			render( <Block { ...defaultProps } showReturnToCart={ true } /> );
+
+			expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
+				expect.objectContaining( { fullWidth: true } )
+			);
+		} );
+
+		it( 'puts text-only terms in the same column as the Place order button', () => {
+			render(
+				<Block
+					{ ...defaultProps }
+					showReturnToCart={ true }
+					showTerms={ true }
+					termsCheckbox={ false }
+				/>
+			);
+
+			expect( screen.getByTestId( 'terms' ).parentElement ).toBe(
+				getRow()
+			);
+		} );
+
+		it( 'keeps checkbox terms above the row', () => {
+			render(
+				<Block
+					{ ...defaultProps }
+					showReturnToCart={ true }
+					showTerms={ true }
+					termsCheckbox={ true }
+				/>
+			);
+
+			expect( getRow() ).not.toContainElement(
+				screen.getByTestId( 'terms' )
+			);
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/test/edit.js b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/test/edit.js
new file mode 100644
index 00000000000..7fb71064b84
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/test/edit.js
@@ -0,0 +1,272 @@
+/**
+ * External dependencies
+ */
+import { render, screen, queryByText } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { Edit } from '../edit';
+import {
+	termsCheckboxDefaultText,
+	termsConsentDefaultText,
+} from '../../checkout-terms-block/constants';
+
+const mockReplaceInnerBlocks = jest.fn();
+const mockSelectBlock = jest.fn();
+let mockCheckoutBlocks = [];
+
+jest.mock( '@wordpress/data', () => ( {
+	...jest.requireActual( '@wordpress/data' ),
+	useDispatch: () => ( {
+		replaceInnerBlocks: mockReplaceInnerBlocks,
+		selectBlock: mockSelectBlock,
+	} ),
+	useRegistry: () => ( {
+		select: () => ( {
+			getBlockRootClientId: () => 'fields-1',
+			getBlocks: () => mockCheckoutBlocks,
+		} ),
+	} ),
+	useSelect: jest.fn().mockImplementation( ( fn ) => {
+		const select = () => {
+			return {
+				getSelectionStart: () => ( {
+					clientId: null,
+				} ),
+				getSelectionEnd: () => ( {
+					clientId: null,
+				} ),
+				getFormatTypes: () => [],
+				getCurrentPostId: () => 1,
+				getBlockRootClientId: () => 'fields-1',
+				getBlockOrder: () =>
+					mockCheckoutBlocks.map( ( block ) => block.clientId ),
+				getBlockName: ( clientId ) =>
+					mockCheckoutBlocks.find(
+						( block ) => block.clientId === clientId
+					)?.name,
+				getBlockAttributes: ( clientId ) =>
+					mockCheckoutBlocks.find(
+						( block ) => block.clientId === clientId
+					)?.attributes,
+			};
+		};
+
+		if ( typeof fn === 'function' ) {
+			return fn( select );
+		}
+
+		return {
+			isCaretWithinFormattedText: () => false,
+		};
+	} ),
+} ) );
+
+jest.mock( '@wordpress/block-editor', () => ( {
+	...jest.requireActual( '@wordpress/block-editor' ),
+	useBlockProps: jest.fn( () => ( { className: '' } ) ),
+	InspectorControls: jest.fn( ( { children } ) => <div>{ children }</div> ),
+} ) );
+
+jest.mock( '@woocommerce/base-components/cart-checkout', () => ( {
+	PlaceOrderButton: ( { label } ) => (
+		<div data-testid="place-order">{ label }</div>
+	),
+	ReturnToCartButton: ( { children } ) => <span>{ children }</span>,
+} ) );
+
+jest.mock( '@woocommerce/editor-components/page-selector', () => () => null );
+
+jest.mock( '@woocommerce/block-settings', () => ( {
+	...jest.requireActual( '@woocommerce/block-settings' ),
+	PRIVACY_URL: '/privacy-policy',
+	TERMS_URL: '/terms-and-conditions',
+} ) );
+
+const blockSettingsMock = jest.requireMock( '@woocommerce/block-settings' );
+
+const defaultAttributes = {
+	cartPageId: 0,
+	showReturnToCart: false,
+	placeOrderButtonLabel: 'Place Order',
+	returnToCartButtonLabel: 'Return to Cart',
+	priceSeparator: '·',
+};
+
+const renderEdit = ( attributes = {} ) =>
+	render(
+		<Edit
+			clientId="actions-1"
+			attributes={ { ...defaultAttributes, ...attributes } }
+			setAttributes={ () => void 0 }
+		/>
+	);
+
+const isBefore = ( a, b ) => {
+	const elements = Array.from( document.body.querySelectorAll( '*' ) );
+	return elements.indexOf( a ) < elements.indexOf( b );
+};
+
+describe( 'Checkout Actions Block Edit', () => {
+	describe( 'terms and conditions', () => {
+		it( 'shows a "Show terms and conditions" toggle in a "Terms and Conditions" panel', () => {
+			renderEdit( { showTerms: false } );
+
+			expect(
+				screen.getByRole( 'button', { name: 'Terms and Conditions' } )
+			).toBeInTheDocument();
+			expect(
+				screen.getByRole( 'checkbox', {
+					name: 'Show terms and conditions',
+				} )
+			).not.toBeChecked();
+			expect(
+				screen.queryByRole( 'checkbox', { name: 'Require checkbox' } )
+			).not.toBeInTheDocument();
+		} );
+
+		it( 'shows the "Require checkbox" toggle when terms are shown', () => {
+			renderEdit( { showTerms: true, termsCheckbox: true } );
+
+			expect(
+				screen.getByRole( 'checkbox', { name: 'Require checkbox' } )
+			).toBeChecked();
+		} );
+
+		it( 'renders editable checkbox terms above the Place order button', () => {
+			const { container } = renderEdit( {
+				showTerms: true,
+				termsCheckbox: true,
+				termsText: 'Custom terms',
+			} );
+
+			const terms = queryByText( container, 'Custom terms' );
+			expect( terms ).toHaveAttribute( 'contenteditable', 'true' );
+			expect(
+				isBefore( terms, screen.getByTestId( 'place-order' ) )
+			).toBe( true );
+		} );
+
+		it( 'renders editable text-only terms below the Place order button', () => {
+			const { container } = renderEdit( {
+				showTerms: true,
+				termsCheckbox: false,
+				termsText: 'Custom terms',
+			} );
+
+			const terms = queryByText( container, 'Custom terms' );
+			expect(
+				isBefore( screen.getByTestId( 'place-order' ), terms )
+			).toBe( true );
+		} );
+
+		it( 'renders no terms when showTerms is false', () => {
+			const { container } = renderEdit( {
+				showTerms: false,
+				termsText: 'Custom terms',
+			} );
+
+			expect(
+				queryByText( container, 'Custom terms' )
+			).not.toBeInTheDocument();
+		} );
+
+		it( 'falls back to the default checkbox text when termsText is empty', () => {
+			const { container } = renderEdit( {
+				showTerms: true,
+				termsCheckbox: true,
+				termsText: '',
+			} );
+
+			expect( container.innerHTML ).toContain( termsCheckboxDefaultText );
+		} );
+
+		it( 'falls back to the default consent text when termsText is empty', () => {
+			const { container } = renderEdit( {
+				showTerms: true,
+				termsCheckbox: false,
+				termsText: '',
+			} );
+
+			expect( container.innerHTML ).toContain( termsConsentDefaultText );
+		} );
+
+		it( 'shows a notice when the terms or privacy pages are not set up', () => {
+			blockSettingsMock.PRIVACY_URL = '';
+			blockSettingsMock.TERMS_URL = '';
+			const { container } = renderEdit( {
+				showTerms: true,
+				termsText: 'I agree to the terms and conditions',
+			} );
+
+			expect(
+				queryByText( container, 'Set up a Terms and Conditions page' )
+			).toBeInTheDocument();
+			expect(
+				queryByText( container, 'Set up a Privacy Policy page' )
+			).toBeInTheDocument();
+		} );
+	} );
+
+	describe( 'Return to Cart', () => {
+		it( 'has a Return to Cart toggle', () => {
+			renderEdit( { showReturnToCart: true } );
+
+			expect(
+				screen.getByRole( 'checkbox', {
+					name: 'Show a "Return to Cart" link',
+				} )
+			).toBeChecked();
+		} );
+
+		it( 'renders the Return to Cart link when enabled', () => {
+			const { container } = renderEdit( { showReturnToCart: true } );
+
+			expect(
+				queryByText( container, 'Return to Cart' )
+			).toBeInTheDocument();
+		} );
+	} );
+
+	describe( 'switching from the Terms and Conditions block', () => {
+		beforeEach( () => {
+			mockCheckoutBlocks = [
+				{
+					clientId: 'terms-1',
+					name: 'woocommerce/checkout-terms-block',
+					attributes: { checkbox: true, text: 'Custom terms' },
+				},
+				{
+					clientId: 'actions-1',
+					name: 'woocommerce/checkout-actions-block',
+					attributes: {},
+				},
+			];
+		} );
+
+		afterEach( () => {
+			mockCheckoutBlocks = [];
+		} );
+
+		it( 'hides terms settings and the switch notice while the Terms and Conditions block is in the checkout', () => {
+			renderEdit();
+
+			expect(
+				screen.queryByText(
+					'Show Terms and Conditions next to the Place order button. Your text and checkbox setting move to the Actions block.'
+				)
+			).not.toBeInTheDocument();
+			expect(
+				screen.queryByRole( 'button', {
+					name: 'Switch to the Actions block',
+				} )
+			).not.toBeInTheDocument();
+			expect(
+				screen.queryByRole( 'checkbox', {
+					name: 'Show terms and conditions',
+				} )
+			).not.toBeInTheDocument();
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/block.js b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/block.js
deleted file mode 100644
index 0ce8f8e30a6..00000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/block.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * External dependencies
- */
-import { render, screen } from '@testing-library/react';
-
-/**
- * Internal dependencies
- */
-import Block from '../block';
-
-const mockPlaceOrderButton = jest.fn( ( { label, CustomButtonComponent } ) => {
-	if ( CustomButtonComponent ) {
-		return <CustomButtonComponent />;
-	}
-	return <button>{ label }</button>;
-} );
-
-jest.mock( '@woocommerce/base-components/cart-checkout', () => ( {
-	PlaceOrderButton: ( props ) => mockPlaceOrderButton( props ),
-} ) );
-
-const mockUseCheckoutSubmit = jest.fn();
-jest.mock( '@woocommerce/base-context/hooks', () => ( {
-	useCheckoutSubmit: () => mockUseCheckoutSubmit(),
-} ) );
-
-const mockUseSelect = jest.fn();
-jest.mock( '@wordpress/data', () => ( {
-	useSelect: () => mockUseSelect(),
-} ) );
-
-jest.mock( '@woocommerce/block-data', () => ( {
-	paymentStore: 'wc/store/payment',
-} ) );
-
-jest.mock( '@woocommerce/settings', () => ( {
-	getSetting: jest.fn( () => '' ),
-} ) );
-
-jest.mock( '@woocommerce/base-context', () => ( {
-	noticeContexts: {
-		CHECKOUT_ACTIONS: 'wc/checkout/checkout-actions',
-	},
-} ) );
-
-jest.mock( '@woocommerce/blocks-components', () => ( {
-	StoreNoticesContainer: () => null,
-} ) );
-
-jest.mock( '@woocommerce/blocks-checkout', () => ( {
-	applyCheckoutFilter: ( { defaultValue } ) => defaultValue,
-} ) );
-
-jest.mock( '@woocommerce/block-settings', () => ( {
-	CART_URL: '/cart',
-} ) );
-
-jest.mock( '../../checkout-order-summary-block/slotfills', () => ( {
-	CheckoutOrderSummarySlot: () => null,
-} ) );
-
-jest.mock( '../../checkout-actions-block/constants', () => ( {
-	defaultPlaceOrderButtonLabel: 'Place Order',
-} ) );
-
-const defaultProps = {
-	cartPageId: 1,
-	showReturnToCart: false,
-	placeOrderButtonLabel: 'Place Order',
-	priceSeparator: '·',
-	returnToCartButtonLabel: 'Return to Cart',
-};
-
-const CustomPlaceOrderButton = () => <button>Custom Button</button>;
-
-describe( 'Checkout Actions Block', () => {
-	beforeEach( () => {
-		jest.clearAllMocks();
-		mockPlaceOrderButton.mockClear();
-	} );
-
-	it( 'does not pass CustomButtonComponent to PlaceOrderButton when a saved token is active', () => {
-		mockUseCheckoutSubmit.mockReturnValue( {
-			paymentMethodButtonLabel: '',
-			paymentMethodPlaceOrderButton: CustomPlaceOrderButton,
-		} );
-		mockUseSelect.mockReturnValue( 'saved-token-123' );
-
-		render( <Block { ...defaultProps } /> );
-
-		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
-			expect.objectContaining( {
-				CustomButtonComponent: undefined,
-			} )
-		);
-		expect( screen.queryByText( 'Place Order' ) ).toBeInTheDocument();
-		expect( screen.queryByText( 'Custom Button' ) ).not.toBeInTheDocument();
-	} );
-
-	it( 'passes CustomButtonComponent to PlaceOrderButton when no saved token is active', () => {
-		mockUseCheckoutSubmit.mockReturnValue( {
-			paymentMethodButtonLabel: '',
-			paymentMethodPlaceOrderButton: CustomPlaceOrderButton,
-		} );
-		mockUseSelect.mockReturnValue( null );
-
-		render( <Block { ...defaultProps } /> );
-
-		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
-			expect.objectContaining( {
-				CustomButtonComponent: CustomPlaceOrderButton,
-			} )
-		);
-		expect( screen.queryByText( 'Place Order' ) ).not.toBeInTheDocument();
-		expect( screen.queryByText( 'Custom Button' ) ).toBeInTheDocument();
-	} );
-
-	it( 'passes undefined CustomButtonComponent when payment method does not provide one', () => {
-		mockUseCheckoutSubmit.mockReturnValue( {
-			paymentMethodButtonLabel: '',
-			paymentMethodPlaceOrderButton: undefined,
-		} );
-		mockUseSelect.mockReturnValue( null );
-
-		render( <Block { ...defaultProps } /> );
-
-		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
-			expect.objectContaining( {
-				CustomButtonComponent: undefined,
-			} )
-		);
-		expect( screen.queryByText( 'Place Order' ) ).toBeInTheDocument();
-	} );
-
-	it( 'uses payment method button label when provided', () => {
-		mockUseCheckoutSubmit.mockReturnValue( {
-			paymentMethodButtonLabel: 'Pay with Card',
-			paymentMethodPlaceOrderButton: undefined,
-		} );
-		mockUseSelect.mockReturnValue( null );
-
-		render( <Block { ...defaultProps } /> );
-
-		expect( mockPlaceOrderButton ).toHaveBeenCalledWith(
-			expect.objectContaining( {
-				label: 'Pay with Card',
-			} )
-		);
-		expect( screen.queryByText( 'Pay with Card' ) ).toBeInTheDocument();
-	} );
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/edit.tsx
index 2c132e08438..07562bb0467 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/edit.tsx
@@ -45,8 +45,7 @@ export const Edit = ( {
 		[ 'woocommerce/checkout-payment-block', {}, [] ],
 		[ 'woocommerce/checkout-additional-information-block', {}, [] ],
 		[ 'woocommerce/checkout-order-note-block', {}, [] ],
-		[ 'woocommerce/checkout-terms-block', {}, [] ],
-		[ 'woocommerce/checkout-actions-block', {}, [] ],
+		[ 'woocommerce/checkout-actions-block', { showTerms: true }, [] ],
 	].filter( Boolean ) as unknown as TemplateArray;

 	useForcedLayout( {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/block.json
index 1aa46010191..ef037812aea 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/block.json
@@ -7,7 +7,8 @@
 		"align": false,
 		"html": false,
 		"multiple": false,
-		"reusable": false
+		"reusable": false,
+		"inserter": false
 	},
 	"attributes": {
 		"className": {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/edit.tsx
index 1e3924db732..2ef47af746e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/edit.tsx
@@ -9,9 +9,12 @@ import {
 } from '@wordpress/block-editor';
 import { CheckboxControl } from '@woocommerce/blocks-components';
 import {
+	Disabled,
 	PanelBody,
 	ToggleControl,
 	Notice,
+	// `Link` from `@wordpress/ui` isn't available to this package yet.
+	// eslint-disable-next-line @wordpress/use-recommended-components
 	ExternalLink,
 } from '@wordpress/components';
 import { PRIVACY_URL, TERMS_URL } from '@woocommerce/block-settings';
@@ -23,13 +26,111 @@ import clsx from 'clsx';
  */
 import './editor.scss';
 import { termsConsentDefaultText, termsCheckboxDefaultText } from './constants';
+import { SwitchToActionsTermsNotice } from './switch-to-actions-terms';

 const noop = () => undefined;

+/**
+ * Warns when the policy pages are missing, or when the terms text doesn't link to them. Also used by the Actions block.
+ */
+export const TermsNotices = ( {
+	text,
+	checkbox,
+	setText,
+}: {
+	text: string;
+	checkbox: boolean;
+	setText: ( text: string ) => void;
+} ) => {
+	const currentText =
+		text ||
+		( checkbox ? termsCheckboxDefaultText : termsConsentDefaultText );
+
+	return (
+		<>
+			{ /* Show this notice if a terms page or a privacy page is not setup. */ }
+			{ ( ! TERMS_URL || ! PRIVACY_URL ) && (
+				<Notice
+					className="wc-block-checkout__terms_notice"
+					status="warning"
+					isDismissible={ false }
+				>
+					{ __(
+						"Link to your store's Terms and Conditions and Privacy Policy pages by creating pages for them.",
+						'woocommerce'
+					) }
+					<br />
+					{ ! TERMS_URL && (
+						<>
+							<br />
+							<ExternalLink
+								href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=advanced` }
+							>
+								{ __(
+									'Set up a Terms and Conditions page',
+									'woocommerce'
+								) }
+							</ExternalLink>
+						</>
+					) }
+					{ ! PRIVACY_URL && (
+						<>
+							<br />
+							<ExternalLink
+								href={ `${ ADMIN_URL }options-privacy.php` }
+							>
+								{ __(
+									'Set up a Privacy Policy page',
+									'woocommerce'
+								) }
+							</ExternalLink>
+						</>
+					) }
+				</Notice>
+			) }
+			{ /* Show this notice if we have both a terms and privacy pages, but they're not present in the text. */ }
+			{ TERMS_URL &&
+				PRIVACY_URL &&
+				! (
+					currentText.includes( TERMS_URL ) &&
+					currentText.includes( PRIVACY_URL )
+				) && (
+					<Notice
+						className="wc-block-checkout__terms_notice"
+						status="warning"
+						isDismissible={ false }
+						actions={
+							termsConsentDefaultText !== text
+								? [
+										{
+											label: __(
+												'Restore default text',
+												'woocommerce'
+											),
+											onClick: () => setText( '' ),
+										},
+									]
+								: []
+						}
+					>
+						<p>
+							{ __(
+								'Ensure you add links to your policy pages in this section.',
+								'woocommerce'
+							) }
+						</p>
+					</Notice>
+				) }
+		</>
+	);
+};
+
 export const Edit = ( {
+	clientId,
 	attributes: { checkbox, text, showSeparator },
 	setAttributes,
 }: {
+	clientId: string;
 	attributes: { text: string; checkbox: boolean; showSeparator: boolean };
 	setAttributes: ( attributes: Record< string, unknown > ) => void;
 } ) => {
@@ -43,82 +144,12 @@ export const Edit = ( {
 	return (
 		<div { ...blockProps }>
 			<InspectorControls>
-				{ /* Show this notice if a terms page or a privacy page is not setup. */ }
-				{ ( ! TERMS_URL || ! PRIVACY_URL ) && (
-					<Notice
-						className="wc-block-checkout__terms_notice"
-						status="warning"
-						isDismissible={ false }
-					>
-						{ __(
-							"Link to your store's Terms and Conditions and Privacy Policy pages by creating pages for them.",
-							'woocommerce'
-						) }
-						<br />
-						{ ! TERMS_URL && (
-							<>
-								<br />
-								<ExternalLink
-									href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=advanced` }
-								>
-									{ __(
-										'Set up a Terms and Conditions page',
-										'woocommerce'
-									) }
-								</ExternalLink>
-							</>
-						) }
-						{ ! PRIVACY_URL && (
-							<>
-								<br />
-								<ExternalLink
-									href={ `${ ADMIN_URL }options-privacy.php` }
-								>
-									{ __(
-										'Set up a Privacy Policy page',
-										'woocommerce'
-									) }
-								</ExternalLink>
-							</>
-						) }
-					</Notice>
-				) }
-				{ /* Show this notice if we have both a terms and privacy pages, but they're not present in the text. */ }
-				{ TERMS_URL &&
-					PRIVACY_URL &&
-					! (
-						currentText.includes( TERMS_URL ) &&
-						currentText.includes( PRIVACY_URL )
-					) && (
-						<Notice
-							className="wc-block-checkout__terms_notice"
-							status="warning"
-							isDismissible={ false }
-							actions={
-								termsConsentDefaultText !== text
-									? [
-											{
-												label: __(
-													'Restore default text',
-													'woocommerce'
-												),
-												onClick: () =>
-													setAttributes( {
-														text: '',
-													} ),
-											},
-										]
-									: []
-							}
-						>
-							<p>
-								{ __(
-									'Ensure you add links to your policy pages in this section.',
-									'woocommerce'
-								) }
-							</p>
-						</Notice>
-					) }
+				<SwitchToActionsTermsNotice clientId={ clientId } />
+				<TermsNotices
+					text={ text }
+					checkbox={ checkbox }
+					setText={ ( value ) => setAttributes( { text: value } ) }
+				/>
 				<PanelBody title={ __( 'Display options', 'woocommerce' ) }>
 					<ToggleControl
 						__nextHasNoMarginBottom
@@ -150,11 +181,13 @@ export const Edit = ( {
 				{ checkbox ? (
 					<>
 						<div className="wc-block-checkout__terms-checkbox-wrapper">
-							<CheckboxControl
-								id="terms-condition"
-								checked={ false }
-								onChange={ noop }
-							/>
+							<Disabled>
+								<CheckboxControl
+									id="terms-condition"
+									checked={ false }
+									onChange={ noop }
+								/>
+							</Disabled>
 						</div>
 						<RichText
 							value={ currentText }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss
index ba402aa125f..4e57bde1f06 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/editor.scss
@@ -25,10 +25,18 @@
 }

 .wc-block-checkout__terms_notice {
-	margin: 0;
+	margin: $gap;
 	.components-notice__content {
 		margin: 4px 0;
 	}
+	// Core makes the arrow inline-block, which lets it wrap onto its own line.
+	.components-external-link__icon {
+		display: inline;
+	}
+	// The Actions block shows these notices inside its settings panel.
+	.components-panel__body & {
+		margin: 0 0 $gap;
+	}
 	.components-notice__actions {
 		button {
 			margin-left: 0;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/index.tsx
index ddc69b417a6..454b635960c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/index.tsx
@@ -11,6 +11,10 @@ import { Edit, Save } from './edit';
 import metadata from './block.json';
 import './style.scss';

+/**
+ * Hidden from the inserter: new checkouts use the Terms and Conditions settings on the Actions block. Existing checkouts keep this
+ * block, and its settings offer a switch to the Actions block terms.
+ */
 registerBlockType( metadata, {
 	icon: {
 		src: (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/switch-to-actions-terms.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/switch-to-actions-terms.tsx
new file mode 100644
index 00000000000..21202e861d8
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/switch-to-actions-terms.tsx
@@ -0,0 +1,100 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { recordEvent } from '@woocommerce/tracks';
+import { UpgradeDowngradeNotice } from '@woocommerce/editor-components/upgrade-downgrade-notice';
+import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
+import type { BlockInstance } from '@wordpress/blocks';
+import { store as blockEditorStore } from '@wordpress/block-editor';
+
+const TERMS_BLOCK = 'woocommerce/checkout-terms-block';
+const ACTIONS_BLOCK = 'woocommerce/checkout-actions-block';
+
+/**
+ * Finds the Terms and Actions blocks that sit alongside the given block in Checkout Fields.
+ */
+export const useCheckoutTermsBlocks = ( clientId: string ) =>
+	useSelect(
+		( select ) => {
+			const { getBlockRootClientId, getBlockOrder, getBlockName } =
+				select( blockEditorStore );
+			const siblings: string[] = getBlockOrder(
+				getBlockRootClientId( clientId )
+			);
+
+			return {
+				termsClientId: siblings.find(
+					( id ) => getBlockName( id ) === TERMS_BLOCK
+				),
+				actionsClientId: siblings.find(
+					( id ) => getBlockName( id ) === ACTIONS_BLOCK
+				),
+			};
+		},
+		[ clientId ]
+	);
+
+/**
+ * Recommends moving the Terms and Conditions block's settings onto the Place order button, and does it in one click.
+ */
+export const SwitchToActionsTermsNotice = ( {
+	clientId,
+}: {
+	clientId: string;
+} ) => {
+	const { termsClientId, actionsClientId } =
+		useCheckoutTermsBlocks( clientId );
+	const registry = useRegistry();
+	const { replaceInnerBlocks, selectBlock } = useDispatch( blockEditorStore );
+
+	if ( ! termsClientId || ! actionsClientId ) {
+		return null;
+	}
+
+	// Replace the inner blocks in one change, so a single undo restores the Terms and Conditions block.
+	const switchTerms = () => {
+		const { getBlockRootClientId, getBlocks } =
+			registry.select( blockEditorStore );
+		const rootClientId = getBlockRootClientId( termsClientId );
+		const blocks = getBlocks( rootClientId ) as BlockInstance[];
+		const termsAttributes = blocks.find(
+			( block ) => block.clientId === termsClientId
+		)?.attributes;
+		const innerBlocks = blocks
+			.filter( ( block ) => block.clientId !== termsClientId )
+			.map( ( block ) =>
+				block.clientId === actionsClientId
+					? {
+							...block,
+							attributes: {
+								...block.attributes,
+								showTerms: true,
+								termsCheckbox: !! termsAttributes?.checkbox,
+								termsText: termsAttributes?.text || '',
+							},
+						}
+					: block
+			);
+
+		replaceInnerBlocks( rootClientId, innerBlocks, false );
+		selectBlock( actionsClientId );
+		recordEvent( 'blocks_checkout_terms_migration', {
+			transform_to: 'actions_block',
+		} );
+	};
+
+	return (
+		<UpgradeDowngradeNotice
+			className="wc-block-checkout__terms_notice"
+			isDismissible={ false }
+			actionLabel={ __( 'Switch to the Actions block', 'woocommerce' ) }
+			onActionClick={ switchTerms }
+		>
+			{ __(
+				'Show Terms and Conditions next to the Place order button. Your text and checkbox setting move to the Actions block.',
+				'woocommerce'
+			) }
+		</UpgradeDowngradeNotice>
+	);
+};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js
index 9cbfb0b8c07..da6a07a2a34 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-terms-block/test/edit.js
@@ -1,15 +1,32 @@
 /**
  * External dependencies
  */
-import { render, queryByText } from '@testing-library/react';
+import { render, queryByText, screen, fireEvent } from '@testing-library/react';
+import { recordEvent } from '@woocommerce/tracks';

 /**
  * Internal dependencies
  */
 import { Edit } from '../edit';

+const mockReplaceInnerBlocks = jest.fn();
+const mockSelectBlock = jest.fn();
+let mockCheckoutBlocks = [];
+
+jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
+
 jest.mock( '@wordpress/data', () => ( {
 	...jest.requireActual( '@wordpress/data' ),
+	useDispatch: () => ( {
+		replaceInnerBlocks: mockReplaceInnerBlocks,
+		selectBlock: mockSelectBlock,
+	} ),
+	useRegistry: () => ( {
+		select: () => ( {
+			getBlockRootClientId: () => 'fields-1',
+			getBlocks: () => mockCheckoutBlocks,
+		} ),
+	} ),
 	useSelect: jest.fn().mockImplementation( ( fn ) => {
 		const select = () => {
 			return {
@@ -20,6 +37,17 @@ jest.mock( '@wordpress/data', () => ( {
 					clientId: null,
 				} ),
 				getFormatTypes: () => [],
+				getBlockRootClientId: () => 'fields-1',
+				getBlockOrder: () =>
+					mockCheckoutBlocks.map( ( block ) => block.clientId ),
+				getBlockName: ( clientId ) =>
+					mockCheckoutBlocks.find(
+						( block ) => block.clientId === clientId
+					)?.name,
+				getBlockAttributes: ( clientId ) =>
+					mockCheckoutBlocks.find(
+						( block ) => block.clientId === clientId
+					)?.attributes,
 			};
 		};

@@ -48,6 +76,11 @@ jest.mock( '@woocommerce/block-settings', () => ( {
 const blockSettingsMock = jest.requireMock( '@woocommerce/block-settings' );

 describe( 'Edit', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockCheckoutBlocks = [];
+	} );
+
 	it( 'Renders a checkbox if the checkbox attribute is true', async () => {
 		const { container } = render(
 			<Edit
@@ -134,4 +167,79 @@ describe( 'Edit', () => {
 			)
 		).not.toBeInTheDocument();
 	} );
+
+	it.each( [ true, false ] )(
+		'only moves terms after clicking the switch button (checkbox: %s)',
+		( checkbox ) => {
+			mockCheckoutBlocks = [
+				{
+					clientId: 'terms-1',
+					name: 'woocommerce/checkout-terms-block',
+					attributes: {
+						checkbox,
+						text: 'Custom terms',
+						showSeparator: true,
+					},
+				},
+				{
+					clientId: 'payment-1',
+					name: 'woocommerce/checkout-payment-block',
+					attributes: {},
+				},
+				{
+					clientId: 'actions-1',
+					name: 'woocommerce/checkout-actions-block',
+					attributes: { showReturnToCart: true, cartPageId: 123 },
+				},
+			];
+			const { container } = render(
+				<Edit
+					clientId="terms-1"
+					attributes={ mockCheckoutBlocks[ 0 ].attributes }
+					setAttributes={ () => void 0 }
+				/>
+			);
+			expect( mockReplaceInnerBlocks ).not.toHaveBeenCalled();
+			expect( mockSelectBlock ).not.toHaveBeenCalled();
+
+			expect(
+				queryByText(
+					container,
+					'Show Terms and Conditions next to the Place order button. Your text and checkbox setting move to the Actions block.'
+				)
+			).toBeInTheDocument();
+			expect(
+				queryByText( container, 'Switch to the Actions block' )
+			).toBeInTheDocument();
+
+			fireEvent.click(
+				screen.getByRole( 'button', {
+					name: 'Switch to the Actions block',
+				} )
+			);
+
+			expect( mockReplaceInnerBlocks ).toHaveBeenCalledTimes( 1 );
+			expect( mockReplaceInnerBlocks ).toHaveBeenCalledWith(
+				'fields-1',
+				[
+					mockCheckoutBlocks[ 1 ],
+					{
+						...mockCheckoutBlocks[ 2 ],
+						attributes: {
+							...mockCheckoutBlocks[ 2 ].attributes,
+							showTerms: true,
+							termsCheckbox: checkbox,
+							termsText: 'Custom terms',
+						},
+					},
+				],
+				false
+			);
+			expect( mockSelectBlock ).toHaveBeenCalledWith( 'actions-1' );
+			expect( recordEvent ).toHaveBeenCalledWith(
+				'blocks_checkout_terms_migration',
+				{ transform_to: 'actions_block' }
+			);
+		}
+	);
 } );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/deprecated.js b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/deprecated.js
new file mode 100644
index 00000000000..9797cb7f09c
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/test/deprecated.js
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+jest.mock( '@wordpress/blocks', () => ( {
+	registerBlockType: jest.fn(),
+	createBlock: ( name, attributes, innerBlocks = [] ) => ( {
+		name,
+		attributes,
+		innerBlocks,
+	} ),
+} ) );
+jest.mock( '../edit', () => ( { Edit: () => null, Save: () => null } ) );
+jest.mock( '../inner-blocks', () => ( {} ) );
+
+/**
+ * Internal dependencies
+ */
+import '../index';
+
+const migrateV1 = ( attributes ) => {
+	const [ , settings ] = registerBlockType.mock.calls[ 0 ];
+	const [ , innerBlocks ] = settings.deprecated[ 0 ].migrate( {
+		showOrderNotes: true,
+		showReturnToCart: false,
+		cartPageId: 0,
+		...attributes,
+	} );
+	return innerBlocks[ 0 ].innerBlocks;
+};
+
+describe( 'Checkout v1 migration', () => {
+	it.each( [ true, false ] )(
+		'preserves the separate Terms block when showPolicyLinks is %s',
+		( showPolicyLinks ) => {
+			const fields = migrateV1( { showPolicyLinks } );
+
+			expect(
+				fields.some(
+					( block ) =>
+						block.name === 'woocommerce/checkout-terms-block'
+				)
+			).toBe( showPolicyLinks );
+			expect(
+				fields.find(
+					( block ) =>
+						block.name === 'woocommerce/checkout-actions-block'
+				).attributes.showTerms
+			).toBeUndefined();
+		}
+	);
+} );
diff --git a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
index d1e6e08b2ca..23e585fc7de 100644
--- a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
+++ b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
@@ -36,6 +36,7 @@
 - [woocommerce_cart_contents_changed](#woocommerce_cart_contents_changed)
 - [woocommerce_cart_item_permalink](#woocommerce_cart_item_permalink)
 - [woocommerce_checkout_must_be_logged_in_message](#woocommerce_checkout_must_be_logged_in_message)
+- [woocommerce_checkout_show_terms](#woocommerce_checkout_show_terms)
 - [woocommerce_default_payment_method](#woocommerce_default_payment_method)
 - [woocommerce_delete_expired_draft_orders_batch_size](#woocommerce_delete_expired_draft_orders_batch_size)
 - [woocommerce_disable_compatibility_layer](#woocommerce_disable_compatibility_layer)
@@ -882,6 +883,31 @@ apply_filters( 'woocommerce_checkout_must_be_logged_in_message', string $message

 ---

+## woocommerce_checkout_show_terms
+
+
+Filters whether checkout shows the terms and conditions.
+
+```php
+apply_filters( 'woocommerce_checkout_show_terms', bool $show_terms )
+```
+
+### Description
+
+Returning false hides the terms and conditions on classic checkout and on the Checkout block's Actions block, for example when an extension shows its own legal checkboxes. Like classic checkout, this only runs on the frontend, where callbacks can rely on the cart being loaded.
+
+### Parameters
+
+| Argument | Type | Description |
+| -------- | ---- | ----------- |
+| $show_terms | bool | Whether to show the terms and conditions. |
+
+### Source
+
+- [Blocks/BlockTypes/Checkout.php](../../../../../../src/Blocks/BlockTypes/Checkout.php)
+
+---
+
 ## woocommerce_default_payment_method


diff --git a/plugins/woocommerce/client/blocks/eslint-suppressions.json b/plugins/woocommerce/client/blocks/eslint-suppressions.json
index 9b516a814f9..978514f4da5 100644
--- a/plugins/woocommerce/client/blocks/eslint-suppressions.json
+++ b/plugins/woocommerce/client/blocks/eslint-suppressions.json
@@ -24,11 +24,6 @@
       "count": 1
     }
   },
-  "assets/js/blocks/checkout/inner-blocks/checkout-terms-block/edit.tsx": {
-    "@wordpress/use-recommended-components": {
-      "count": 1
-    }
-  },
   "assets/js/blocks/classic-shortcode/index.tsx": {
     "@wordpress/use-recommended-components": {
       "count": 1
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 71471c545c4..d3625299757 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -3333,11 +3333,7 @@ EOT;
 <div class="wp-block-woocommerce-checkout-order-note-block"></div>
 <!-- /wp:woocommerce/checkout-order-note-block -->

-<!-- wp:woocommerce/checkout-terms-block -->
-<div class="wp-block-woocommerce-checkout-terms-block"></div>
-<!-- /wp:woocommerce/checkout-terms-block -->
-
-<!-- wp:woocommerce/checkout-actions-block -->
+<!-- wp:woocommerce/checkout-actions-block {"showTerms":true} -->
 <div class="wp-block-woocommerce-checkout-actions-block"></div>
 <!-- /wp:woocommerce/checkout-actions-block --></div>
 <!-- /wp:woocommerce/checkout-fields-block -->
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
index 5d48876aaf4..e16583e1665 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
@@ -551,6 +551,21 @@ class Checkout extends AbstractBlock {
 			$this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods );
 		}

+		if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
+			/**
+			 * Filters whether checkout shows the terms and conditions.
+			 *
+			 * Returning false hides the terms and conditions on classic checkout and on the Checkout block's Actions block, for example
+			 * when an extension shows its own legal checkboxes. Like classic checkout, this only runs on the frontend,
+			 * where callbacks can rely on the cart being loaded.
+			 *
+			 * @since 3.4.0
+			 * @since 11.4.0 Also applies to the Checkout block.
+			 * @param bool $show_terms Whether to show the terms and conditions.
+			 */
+			$this->asset_data_registry->add( 'checkoutShowTerms', (bool) apply_filters( 'woocommerce_checkout_show_terms', true ) );
+		}
+
 		// Check `current_user_can` so we can show notices about incompatible extensions in the front-end to admins too.
 		if ( ( $is_block_editor || current_user_can( 'manage_woocommerce' ) ) && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) {
 			if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) || ! function_exists( 'get_plugins' ) ) {
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutTermsBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutTermsBlock.php
index 2a1588a804d..3ea2d5c76a8 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutTermsBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CheckoutTermsBlock.php
@@ -1,8 +1,13 @@
 <?php
+declare( strict_types = 1 );
+
 namespace Automattic\WooCommerce\Blocks\BlockTypes;

 /**
  * CheckoutTermsBlock class.
+ *
+ * Terms and conditions are now settings on the Checkout's Actions block, so this block is hidden from the inserter. Existing checkouts
+ * keep this block, and merchants can switch from it in the editor.
  */
 class CheckoutTermsBlock extends AbstractInnerBlock {
 	/**
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts
index dcb7aaee331..cbe734472a9 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/checkout/checkout-block.merchant.block_theme.spec.ts
@@ -537,6 +537,54 @@ test.describe( 'Merchant → Checkout', () => {
 			await editor.selectBlocks( blockSelectorInEditor );
 		} );

+		test( 'Merchant can switch from the Terms and Conditions block to the Actions block terms', async ( {
+			editor,
+		} ) => {
+			await editor.selectBlocks(
+				`${ blockSelectorInEditor } .wp-block-woocommerce-checkout-actions-block`
+			);
+
+			const showTermsToggle = editor.page.getByLabel(
+				'Show terms and conditions',
+				{ exact: true }
+			);
+			const actionsBlock = await editor.getBlockByName(
+				'woocommerce/checkout-actions-block'
+			);
+			const terms = actionsBlock.locator( '.wc-block-checkout__terms' );
+
+			// While the Terms and Conditions block is in the checkout, it owns the terms.
+			await expect( showTermsToggle ).toHaveCount( 0 );
+			await expect(
+				editor.page.getByRole( 'button', {
+					name: 'Switch to the Actions block',
+					exact: true,
+				} )
+			).toHaveCount( 0 );
+
+			await editor.selectBlocks(
+				`${ blockSelectorInEditor } .wp-block-woocommerce-checkout-terms-block`
+			);
+
+			await editor.page
+				.getByRole( 'button', {
+					name: 'Switch to the Actions block',
+					exact: true,
+				} )
+				.click();
+
+			await expect(
+				editor.canvas.locator(
+					'[data-type="woocommerce/checkout-terms-block"]'
+				)
+			).toHaveCount( 0 );
+			await expect( showTermsToggle ).toBeChecked();
+			await expect( terms ).toBeVisible();
+
+			await showTermsToggle.uncheck();
+			await expect( terms ).toBeHidden();
+		} );
+
 		test( 'Return to cart link is visible and can be toggled', async ( {
 			editor,
 		} ) => {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php
index cf6754d0f5c..af3c94628ed 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/Checkout.php
@@ -68,6 +68,9 @@ class Checkout extends \WC_Unit_Test_Case {
 		if ( \WP_Block_Type_Registry::get_instance()->is_registered( 'woocommerce/checkout-mock' ) ) {
 			\WP_Block_Type_Registry::get_instance()->unregister( 'woocommerce/checkout-mock' );
 		}
+
+		// A test sets an admin screen so is_admin() is true.
+		unset( $GLOBALS['current_screen'] );
 	}

 	/**
@@ -138,6 +141,60 @@ class Checkout extends \WC_Unit_Test_Case {
 		$this->assertEquals( 'Changed pickup', $data_from_registry['localPickupText'] );
 	}

+	/**
+	 * @testdox Checkout is told whether woocommerce_checkout_show_terms hides the terms.
+	 */
+	public function test_enqueue_data_exposes_checkout_show_terms(): void {
+		$checkout = new CheckoutMock( $this->asset_api, $this->registry, $this->integration_registry, 'checkout-mock' );
+
+		$checkout->mock_enqueue_data();
+		$this->assertTrue( $this->registry->get()['checkoutShowTerms'], 'Terms are shown by default' );
+
+		add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
+		\WP_Block_Type_Registry::get_instance()->unregister( 'woocommerce/checkout-mock' );
+		$registry = new AssetDataRegistryMock( $this->asset_api );
+		$checkout = new CheckoutMock( $this->asset_api, $registry, $this->integration_registry, 'checkout-mock' );
+		$checkout->mock_enqueue_data();
+		$this->assertFalse( $registry->get()['checkoutShowTerms'], 'The filter should hide terms' );
+	}
+
+	/**
+	 * @testdox The editor does not run woocommerce_checkout_show_terms, because callbacks may expect a loaded cart.
+	 */
+	public function test_enqueue_data_skips_checkout_show_terms_in_admin(): void {
+		set_current_screen( 'page' );
+		$filter = function () {
+			// Mirrors a callback written for the frontend, where the cart is loaded.
+			return WC()->cart->get_total() > 0;
+		};
+		add_filter( 'woocommerce_checkout_show_terms', $filter );
+		$cart      = WC()->cart;
+		WC()->cart = null;
+
+		try {
+			$checkout = new CheckoutMock( $this->asset_api, $this->registry, $this->integration_registry, 'checkout-mock' );
+			$checkout->mock_enqueue_data();
+		} finally {
+			WC()->cart = $cart;
+			remove_filter( 'woocommerce_checkout_show_terms', $filter );
+		}
+
+		$this->assertArrayNotHasKey( 'checkoutShowTerms', $this->registry->get(), 'The editor should not run the filter' );
+	}
+
+	/**
+	 * @testdox New Checkout pages put terms on the Actions block instead of a Terms and Conditions block.
+	 */
+	public function test_default_checkout_page_content_uses_actions_terms(): void {
+		$method = new \ReflectionMethod( \WC_Install::class, 'get_checkout_block_content' );
+		$method->setAccessible( true );
+		$fields = parse_blocks( $method->invoke( null ) )[0]['innerBlocks'][0];
+		$attrs  = array_column( $fields['innerBlocks'], 'attrs', 'blockName' );
+
+		$this->assertArrayNotHasKey( 'woocommerce/checkout-terms-block', $attrs, 'No Terms and Conditions block' );
+		$this->assertTrue( $attrs['woocommerce/checkout-actions-block']['showTerms'] ?? null, 'Actions block shows terms' );
+	}
+
 	/**
 	 * Overrides the WC logger.
 	 *