Commit c443f50f32 for woocommerce

commit c443f50f3227fee3e7bb214b5e5c98c78b8fd9cc
Author: Francesco <frosso@users.noreply.github.com>
Date:   Thu Jan 15 13:50:44 2026 +0100

    feat: custom place order button by payment method (blocks) (#62138)

diff --git a/docs/block-development/extensible-blocks/cart-and-checkout-blocks/README.md b/docs/block-development/extensible-blocks/cart-and-checkout-blocks/README.md
index cd75ce635e..45005043ff 100644
--- a/docs/block-development/extensible-blocks/cart-and-checkout-blocks/README.md
+++ b/docs/block-development/extensible-blocks/cart-and-checkout-blocks/README.md
@@ -118,9 +118,11 @@ const MyComponent = () => {
 }
 ```

-### Importing WooCommerce (React) hooks
+### Importing WooCommerce utilities and React hooks

-Currently, none of our hooks are designed to be used externally, so trying to import hooks such as `useStoreCart` is not supported. Instead, getting the data from the [`wc/store/...`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/data-store/) data stores is preferred.
+Some checkout utilities and React hooks are available for external use from `@woocommerce/blocks-checkout`. See the [Checkout Utilities](/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-utilities/) documentation for available utilities.
+
+For accessing store data, using the [`wc/store/...`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/data-store/) data stores is preferred over importing internal hooks like `useStoreCart`.

 ## Back-end extensibility

diff --git a/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md b/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md
index 595b75dc9c..dda84f776a 100644
--- a/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md
+++ b/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md
@@ -62,18 +62,18 @@ const options = {

 #### `ExpressPaymentMethodConfiguration`

-| Option                | Type       | Description                                                                                                                                                                                                                                                                                                                                                     | Required |
-|-----------------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
-| `name`                | String     | Unique identifier for the gateway client side.                                                                                                                                            | Yes      |
-| `title`               | String     | Human readable name of your payment method. Displayed to the merchant in the editor.                                                                                                                                                                                                                        | No       |
-| `description`         | String     | One or two sentences describing your payment gateway. Displayed to the merchant in the editor.                                                                                                                                                                                                                                                                  | No       |
-| `gatewayId`           | String     | ID of the Payment Gateway registered server side. Used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page.                                                                                                                                   | No       |
-| `content`             | ReactNode  | React node output in the express payment method area when the block is rendered in the frontend. Receives props from the checkout payment method interface.                                                                                                                                                                                                     | Yes      |
-| `edit`                | ReactNode  | React node output in the express payment method area when the block is rendered in the editor. Receives props from the payment method interface to checkout (with preview data).                                                                                                                                                                                | Yes      |
-| `canMakePayment`      | Function   | Callback to determine whether the payment method should be available for the shopper.                                                                                                                                                          | Yes      |
-| `paymentMethodId`     | String     | Identifier accompanying the checkout processing request to the server. Used to identify the payment method gateway class for processing the payment.                                                                                                                                                                                                            | No       |
-| `supports:features`   | Array      | Array of payment features supported by the gateway. Used to crosscheck if the payment method can be used for the cart content. Defaults to `['products']` if no value is provided.                                                                                                                                                                              | No       |
-| `supports:style`      | Array      | This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`.                                                                                                                                 | No       |
+| Option | Type | Description | Required |
+| --- | --- | --- | --- |
+| `name` | String | Unique identifier for the gateway client side. | Yes |
+| `title` | String | Human readable name of your payment method. Displayed to the merchant in the editor. | No |
+| `description` | String | One or two sentences describing your payment gateway. Displayed to the merchant in the editor. | No |
+| `gatewayId` | String | ID of the Payment Gateway registered server side. Used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page. | No |
+| `content` | ReactNode | React node output in the express payment method area when the block is rendered in the frontend. Receives props from the checkout payment method interface. | Yes |
+| `edit` | ReactNode | React node output in the express payment method area when the block is rendered in the editor. Receives props from the payment method interface to checkout (with preview data). | Yes |
+| `canMakePayment` | Function | Callback to determine whether the payment method should be available for the shopper. | Yes |
+| `paymentMethodId` | String | Identifier accompanying the checkout processing request to the server. Used to identify the payment method gateway class for processing the payment. | No |
+| `supports:features` | Array | Array of payment features supported by the gateway. Used to crosscheck if the payment method can be used for the cart content. Defaults to `['products']` if no value is provided. | No |
+| `supports:style` | Array | This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`. | No |

 #### The `canMakePayment` option

@@ -149,44 +149,144 @@ registerPaymentMethod( options );
 The options you feed the configuration instance should be an object in this shape (see `PaymentMethodRegistrationOptions` typedef). The options you feed the configuration instance are the same as those for express payment methods with the following additions:

 | Property | Type | Description |
-|----------|------|-------------|
+| --- | --- | --- |
 | `savedTokenComponent` | ReactNode | A React node that contains logic for handling saved payment methods. Rendered when a customer's saved token for this payment method is selected. |
-| `label` | ReactNode | A React node used to output the label for the payment method option. Can be text or images. |
+| `label` | ReactNode | A React node used to output the label for the payment method option. This can be text or images. |
 | `ariaLabel` | string | The label read by screen-readers when the payment method is selected. |
-| `placeOrderButtonLabel` | string | Optional label to change the default "Place Order" button text when this payment method is selected. |
+| `placeOrderButtonLabel` | string | Optional label to change the default "Place Order" button text when this payment method is selected. Mutually exclusive with `placeOrderButton`. |
+| `placeOrderButton` | React Component | Optional React component to replace the default "Place Order" button when this payment method is selected. Mutually exclusive with `placeOrderButtonLabel`. The component receives `PaymentMethodInterface` props. |
 | `supports` | object | Contains information about supported features: |
 | `supports.showSavedCards` | boolean | Determines if saved cards for this payment method are shown to the customer. |
 | `supports.showSaveOption` | boolean | Controls whether to show the checkbox for saving the payment method for future use. |

+### Using Custom Place Order Button
+
+The `placeOrderButton` property allows you to replace the default "Place Order" button with a custom component. This is useful for payment methods that require custom button styling (e.g., Google Pay, Apple Pay) or need to show a payment UI before submitting the order. If this doesn't apply to your payment method, omitting this property and using the default button is recommended.
+
+Your custom button component receives all the same props as the payment method `content` component via the `PaymentMethodInterface`, plus additional button-specific props:
+
+- `waitingForProcessing` - Whether the checkout is processing
+- `waitingForRedirect` - Whether the checkout is waiting to redirect after success
+- `disabled` - Whether the button should be disabled
+- `isEditor` - Whether the button is being rendered in the block editor
+- `isPreview` - Whether the button is being rendered in preview mode
+
+Here's a simple example:
+
+```js
+const CustomButton = ( props ) => {
+	const { validate, onSubmit, disabled, isEditor, isPreview, eventRegistration: { onPaymentSetup }, emitResponse } = props;
+
+	const [
+		isShowingInternalPaymentSheet,
+		setIsShowingInternalPaymentSheet,
+	] = React.useState( false );
+
+  const paymentResultRef = React.useRef( false );
+
+	const handleClick = async () => {
+		// 1. Validate the checkout form
+		const validationResult = await validate();
+
+		if ( validationResult.hasError ) {
+			return; // WooCommerce automatically displays validation errors
+		}
+
+		// 2. Show your payment UI (e.g., Google Pay sheet, Apple Pay sheet)
+		// setIsShowingInternalPaymentSheet( true );
+		// const paymentResult = await showPaymentSheet( billing.cartTotal.value );
+    // paymentResultRef.current = paymentResult.success;
+		// if ( ! paymentResult.success ) {
+		//     setIsShowingInternalPaymentSheet( false );
+		//     return;
+		// }
+
+		// 3. Submit the checkout to the server once all payment information has been collected.
+		onSubmit();
+	};
+
+  React.useEffect(
+    () =>
+      onPaymentSetup( () => {
+        return ({
+          type: paymentResultRef.current ? emitResponse.responseTypes.SUCCESS : emitResponse.responseTypes.ERROR,
+          meta: {
+            paymentMethodData: {
+              payment_method: 'your-payment-method',
+            },
+          },
+        });
+      } ),
+    [ onPaymentSetup, emitResponse.responseTypes.SUCCESS, emitResponse.responseTypes.ERROR ]
+  );
+
+	// In editor/preview mode, show a placeholder or preview version
+	if ( isEditor || isPreview ) {
+		return (
+			<button type="button" disabled>
+				Pay with Custom Method (Preview)
+			</button>
+		);
+	}
+
+	return (
+		<button
+		  type="button"
+			onClick={ handleClick }
+			disabled={ disabled || isShowingInternalPaymentSheet }
+		>
+			{ disabled || isShowingInternalPaymentSheet
+				? 'Processing...'
+				: 'Pay with Custom Method' }
+		</button>
+	);
+};
+
+registerPaymentMethod( {
+	name: 'my-custom-payment',
+	label: <div>My Custom Payment</div>,
+	content: <div>Payment method description</div>,
+	edit: <div>Payment method description</div>,
+	placeOrderButton: CustomButton,
+	canMakePayment: () => true,
+	supports: {
+		features: [ 'products' ],
+	},
+} );
+```
+
+**Note:** The custom button is only shown when the payment method is selected from the list. When a saved payment token is selected, the default "Place Order" button is used instead.
+
 ## Props Fed to Payment Method Nodes

 A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts).

-| Property                 | Type                                                                                                                                                                                                                                                                                                                                                                      | Description                                                                                                                                                                                                                                                                                                        |
-| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `activePaymentMethod`    | String                                                                                                                                                                                                                                                                                                                                                                    | The slug of the current active payment method in the checkout.                                                                                                                                                                                                                                                     |
-| `billing`                | Object with billingAddress, cartTotal, currency, cartTotalItems, displayPricesIncludingTax, appliedCoupons, customerId properties                                                                                                                                                                                                                                             | Contains everything related to billing.                                                                                                                                                                                                                                                                            |
-| `cartData`               | Object with cartItems, cartFees, extensions properties                                                                                                                                                                                                                                                                                                                                 | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application.                                                                                          |
-| `checkoutStatus`         | Object with isCalculating, isComplete, isIdle, isProcessing properties                                                                                                                                                                                                                                                                                                               | The current checkout status exposed as various boolean state.                                                                                                                                                                                                                                                      |
-| `components`             | Object with ValidationInputError, PaymentMethodLabel, PaymentMethodIcons, LoadingMask properties                                                                                                                                                                                                                                                                                      | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods.                                                                                                                                                                          |
-| `emitResponse`           | Object with noticeContexts and responseTypes properties                                                                                                                                                                                                                                                                                                                      | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. |
-| `eventRegistration`      | Object with onCheckoutValidation, onCheckoutSuccess, onCheckoutFail, onPaymentSetup, onShippingRateSuccess, onShippingRateFail, onShippingRateSelectSuccess, onShippingRateSelectFail properties                                                                                                                                                                            | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info).                                                               |
-| `onClick`                | Function                                                                                                                                                                                                                                                                                                                                                                   | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing)                                                                                                                    |
-| `onClose`                | Function                                                                                                                                                                                                                                                                                                                                                                   | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout.                                                                                                                                                                 |
-| `onSubmit`               | Function                                                                                                                                                                                                                                                                                                                                                                   | Submits the checkout and begins processing                                                                                                                                                                                                                                                                         |
-| `buttonAttributes`       | Object with height, borderRadius properties                                                                                                                                                                                                                                                                                                                                              | Styles set by the merchant that should be respected by all express payment buttons                                                                                                                                                                                                                                 |
-| `paymentStatus`          | Object                                                                                                                                                                                                                                                                       | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md).                                    |
-| `paymentStatus.isPristine`             | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is `PRISTINE`.                                                                                                                                                                                                                                                        |
-| `paymentStatus.isStarted`              | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is `EXPRESS_STARTED`.                                                                                                                                                                                                                                                  |
-| `paymentStatus.isProcessing`           | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is `PROCESSING`.                                                                                                                                                                                                                                                      |
-| `paymentStatus.isFinished`             | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is one of `ERROR`, `FAILED`, or `SUCCESS`.                                                                                                                                                                                                                            |
-| `paymentStatus.hasError`               | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is `ERROR`.                                                                                                                                                                                                                                                           |
-| `paymentStatus.hasFailed`              | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is `FAILED`.                                                                                                                                                                                                                                                          |
-| `paymentStatus.isSuccessful`           | Boolean                                                                                                                                                                                                                                                                                                                                                                    | This is true when the current payment status is `SUCCESS`.                                                                                                                                                                                                                                                         |
-| `setExpressPaymentError` | Function                                                                                                                                                                                                                                                                                                                                                                   | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events.                                                                                  |
-| `shippingData`           | Object with shippingRates, shippingRatesLoading, selectedRates, setSelectedRates, isSelectingRate, shippingAddress, setShippingAddress, needsShipping properties                                                                                                                                                                                                             | Contains all shipping related data (outside of the shipping status).                                                                                                                                                                                                                                               |
-| `shippingStatus`         | Object with shippingErrorStatus, shippingErrorTypes properties                                                                                                                                                                                                                                                                                                                            | Various shipping status helpers.                                                                                                                                                                                                                                                                                   |
-| `shouldSavePayment`      | Boolean                                                                                                                                                                                                                                                                                                                                                                    | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false.                                                                                                                    |
+| Property | Type | Description |
+| --- | --- | --- |
+| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. |
+| `billing` | Object with billingAddress, cartTotal, currency, cartTotalItems, displayPricesIncludingTax, appliedCoupons, customerId properties | Contains everything related to billing. |
+| `cartData` | Object with cartItems, cartFees, extensions properties | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. |
+| `checkoutStatus` | Object with isCalculating, isComplete, isIdle, isProcessing properties | The current checkout status exposed as various boolean state. |
+| `components` | Object with ValidationInputError, PaymentMethodLabel, PaymentMethodIcons, LoadingMask properties | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. |
+| `emitResponse` | Object with noticeContexts and responseTypes properties | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. |
+| `eventRegistration` | Object with onCheckoutValidation, onCheckoutSuccess, onCheckoutFail, onPaymentSetup, onShippingRateSuccess, onShippingRateFail, onShippingRateSelectSuccess, onShippingRateSelectFail properties | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). |
+| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) |
+| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. |
+| `onSubmit` | Function | Submits the checkout and begins processing |
+| `validate` | Function | Async function that validates the checkout form without submitting. Returns a promise resolving to `{ hasError: boolean }`. Useful when you need to validate before showing a payment sheet. |
+| `buttonAttributes` | Object with height, borderRadius properties | Styles set by the merchant that should be respected by all express payment buttons |
+| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). |
+| `paymentStatus.isPristine` | Boolean | This is true when the current payment status is `PRISTINE`. |
+| `paymentStatus.isStarted` | Boolean | This is true when the current payment status is `EXPRESS_STARTED`. |
+| `paymentStatus.isProcessing` | Boolean | This is true when the current payment status is `PROCESSING`. |
+| `paymentStatus.isFinished` | Boolean | This is true when the current payment status is one of `ERROR`, `FAILED`, or `SUCCESS`. |
+| `paymentStatus.hasError` | Boolean | This is true when the current payment status is `ERROR`. |
+| `paymentStatus.hasFailed` | Boolean | This is true when the current payment status is `FAILED`. |
+| `paymentStatus.isSuccessful` | Boolean | This is true when the current payment status is `SUCCESS`. |
+| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. |
+| `shippingData` | Object with shippingRates, shippingRatesLoading, selectedRates, setSelectedRates, isSelectingRate, shippingAddress, setShippingAddress, needsShipping properties | Contains all shipping related data (outside of the shipping status). |
+| `shippingStatus` | Object with shippingErrorStatus, shippingErrorTypes properties | Various shipping status helpers. |
+| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. |

 Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security).

diff --git a/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-utilities.md b/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-utilities.md
new file mode 100644
index 0000000000..3cf618768b
--- /dev/null
+++ b/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-utilities.md
@@ -0,0 +1,57 @@
+---
+post_title: Checkout Utilities
+sidebar_label: Checkout Utilities
+---
+
+# Checkout Utilities
+
+Utility functions and React hooks for checkout functionality, available from `@woocommerce/blocks-checkout`.
+
+## `useValidateCheckout`
+
+A hook that validates the checkout form and automatically scrolls to the first validation error if any are found.
+
+This hook is primarily used internally by the `PlaceOrderButton` component to provide the `validate` prop to custom place order button components. However, it can also be used directly if needed.
+
+### Usage
+
+```jsx
+// Aliased import
+import { useValidateCheckout } from '@woocommerce/blocks-checkout';
+
+// Global import
+// const { useValidateCheckout } = wc.blocksCheckout;
+
+const MyComponent = () => {
+    const validateCheckout = useValidateCheckout();
+
+    const handleClick = async () => {
+        const { hasError } = await validateCheckout();
+
+        if ( hasError ) {
+            // Validation failed - errors are automatically shown and
+            // the page scrolls to the first error
+            return;
+        }
+
+        // Validation passed - proceed with your logic
+    };
+
+    return <button onClick={ handleClick }>Validate</button>;
+};
+```
+
+### Return Value
+
+The hook returns a function that, when called:
+
+1. Emits the `CHECKOUT_VALIDATION` event to run all registered validation callbacks
+2. Checks the validation store for any field-level validation errors
+3. If errors are found:
+    - Shows all validation errors
+    - Scrolls to and focuses the first error element
+4. Returns a promise that resolves to `{ hasError: boolean }`
+
+| Property   | Type      | Description                                      |
+|:-----------|:----------|:-------------------------------------------------|
+| `hasError` | `boolean` | `true` if validation failed, `false` if passed.  |
diff --git a/plugins/woocommerce/changelog/62138-feat-wc-blocks-dynamic-place-order-button b/plugins/woocommerce/changelog/62138-feat-wc-blocks-dynamic-place-order-button
new file mode 100644
index 0000000000..51e2f132a0
--- /dev/null
+++ b/plugins/woocommerce/changelog/62138-feat-wc-blocks-dynamic-place-order-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+feat: allow payment methods to render a custom "Place order" button on block-based checkout.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/index.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/index.tsx
index cb9ad31003..eed367eb2b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/index.tsx
@@ -4,6 +4,7 @@
 import clsx from 'clsx';
 import {
 	useCheckoutSubmit,
+	usePaymentMethodInterface,
 	useStoreCart,
 } from '@woocommerce/base-context/hooks';
 import { check } from '@wordpress/icons';
@@ -14,6 +15,9 @@ import {
 	FormattedMonetaryAmount,
 	Spinner,
 } from '@woocommerce/blocks-components';
+import { useValidateCheckout } from '@woocommerce/blocks-checkout';
+import type { CustomPlaceOrderButtonComponent } from '@woocommerce/types';
+import { useEditorContext } from '@woocommerce/base-context';

 /**
  * Internal dependencies
@@ -25,6 +29,8 @@ interface PlaceOrderButtonProps {
 	fullWidth?: boolean;
 	showPrice?: boolean;
 	priceSeparator?: string;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	CustomButtonComponent?: CustomPlaceOrderButtonComponent;
 }

 const PlaceOrderButton = ( {
@@ -32,6 +38,7 @@ const PlaceOrderButton = ( {
 	fullWidth = false,
 	showPrice = false,
 	priceSeparator = '·',
+	CustomButtonComponent,
 }: PlaceOrderButtonProps ): JSX.Element => {
 	const {
 		onSubmit,
@@ -41,8 +48,32 @@ const PlaceOrderButton = ( {
 		waitingForRedirect,
 	} = useCheckoutSubmit();

+	const paymentMethodInterface = usePaymentMethodInterface();
+	const validateCheckout = useValidateCheckout();
+	const { isEditor, isPreview = false } = useEditorContext();
+
 	const { cartTotals, cartIsLoading } = useStoreCart();
-	const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
+
+	// when provided, the `CustomButtonComponent` should take precedence over the default button.
+	if ( CustomButtonComponent ) {
+		return (
+			<CustomButtonComponent
+				waitingForProcessing={ waitingForProcessing }
+				waitingForRedirect={ waitingForRedirect }
+				disabled={
+					isCalculating ||
+					isDisabled ||
+					waitingForProcessing ||
+					waitingForRedirect ||
+					cartIsLoading
+				}
+				isEditor={ isEditor }
+				isPreview={ isPreview }
+				validate={ validateCheckout }
+				{ ...paymentMethodInterface }
+			/>
+		);
+	}

 	return (
 		<Button
@@ -94,7 +125,9 @@ const PlaceOrderButton = ( {
 						<div className="wc-block-components-checkout-place-order-button__price">
 							<FormattedMonetaryAmount
 								value={ cartTotals.total_price }
-								currency={ totalsCurrency }
+								currency={ getCurrencyFromPriceResponse(
+									cartTotals
+								) }
 							/>
 						</div>
 					</>
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/tests/index.js b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/tests/index.js
new file mode 100644
index 0000000000..33ecb1d2a4
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/place-order-button/tests/index.js
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import PlaceOrderButton from '..';
+
+const mockUseCheckoutSubmit = jest.fn();
+jest.mock( '@woocommerce/base-context/hooks', () => ( {
+	useCheckoutSubmit: () => mockUseCheckoutSubmit(),
+	usePaymentMethodInterface: () => ( {
+		onSubmit: jest.fn(),
+		validate: jest.fn(),
+		activePaymentMethod: 'test-payment',
+	} ),
+	useStoreCart: () => ( {
+		cartIsLoading: false,
+	} ),
+} ) );
+
+jest.mock( '@woocommerce/blocks-components', () => ( {
+	FormattedMonetaryAmount: () => <span>$10.00</span>,
+	Spinner: () => <span>Loading...</span>,
+} ) );
+
+const CustomButtonMock = jest.fn( () => <button>Custom Button</button> );
+
+describe( 'PlaceOrderButton', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockUseCheckoutSubmit.mockReturnValue( {
+			onSubmit: jest.fn(),
+			isCalculating: false,
+			isDisabled: false,
+			waitingForProcessing: false,
+			waitingForRedirect: false,
+		} );
+	} );
+
+	it( 'renders default button', () => {
+		render( <PlaceOrderButton label="Place Order" /> );
+
+		expect( screen.queryByText( 'Place Order' ) ).toBeInTheDocument();
+		expect( screen.queryByText( 'Custom Button' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'displays the provided label', () => {
+		render( <PlaceOrderButton label="Confirm Purchase" /> );
+
+		expect( screen.getByText( 'Confirm Purchase' ) ).toBeInTheDocument();
+	} );
+
+	it( 'renders CustomButtonComponent when provided', () => {
+		render(
+			<PlaceOrderButton
+				label="Place Order"
+				CustomButtonComponent={ CustomButtonMock }
+			/>
+		);
+
+		expect( screen.queryByText( 'Custom Button' ) ).toBeInTheDocument();
+		expect( screen.queryByText( 'Place Order' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'spreads paymentMethodInterface props to the custom component', () => {
+		render(
+			<PlaceOrderButton
+				label="Place Order"
+				CustomButtonComponent={ CustomButtonMock }
+			/>
+		);
+
+		expect( CustomButtonMock ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				onSubmit: expect.any( Function ),
+				validate: expect.any( Function ),
+				activePaymentMethod: 'test-payment',
+			} ),
+			expect.anything()
+		);
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts
index 6ff2092bab..89dcbf7af6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts
@@ -32,7 +32,7 @@ import { prepareTotalItems } from './utils';
 import { useShippingData } from '../shipping/use-shipping-data';

 /**
- * Returns am interface to use as payment method props.
+ * Returns an interface to use as payment method props.
  */
 export const usePaymentMethodInterface = (): PaymentMethodInterface => {
 	const {
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/use-checkout-submit.js b/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/use-checkout-submit.js
index 2ac4766012..b8701969a5 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/use-checkout-submit.js
+++ b/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/use-checkout-submit.js
@@ -53,9 +53,11 @@ export const useCheckoutSubmit = () => {
 		isProcessing || isAfterProcessing || isBeforeProcessing;
 	const waitingForRedirect = isComplete && ! hasError;
 	const paymentMethodButtonLabel = paymentMethod.placeOrderButtonLabel;
+	const paymentMethodPlaceOrderButton = paymentMethod.placeOrderButton;

 	return {
 		paymentMethodButtonLabel,
+		paymentMethodPlaceOrderButton,
 		onSubmit,
 		isCalculating,
 		isDisabled: isProcessing || isExpressPaymentMethodActive,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/payment-method-config.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/payment-method-config.tsx
index 9e99d01be5..9ec09ebb68 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/payment-method-config.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/payment-method-config.tsx
@@ -9,6 +9,7 @@ import type {
 	CanMakePaymentCallback,
 	PaymentMethodConfigInstance,
 	PaymentMethodIcons,
+	CustomPlaceOrderButtonComponent,
 } from '@woocommerce/types';

 /**
@@ -37,6 +38,7 @@ export default class PaymentMethodConfig
 	public label: ReactNode;
 	public ariaLabel: string;
 	public placeOrderButtonLabel?: string;
+	public placeOrderButton?: CustomPlaceOrderButtonComponent;
 	public savedTokenComponent?: ReactNode | null;
 	public canMakePaymentFromConfig: CanMakePaymentCallback;

@@ -46,6 +48,7 @@ export default class PaymentMethodConfig
 		this.name = config.name;
 		this.label = config.label;
 		this.placeOrderButtonLabel = config.placeOrderButtonLabel;
+		this.placeOrderButton = config.placeOrderButton;
 		this.ariaLabel = config.ariaLabel;
 		this.content = config.content;
 		this.savedTokenComponent = config.savedTokenComponent;
@@ -115,6 +118,20 @@ export default class PaymentMethodConfig
 				'The placeOrderButtonLabel property for the payment method must be a string'
 			);
 		}
+		if (
+			config.placeOrderButton &&
+			typeof config.placeOrderButton !== 'function'
+		) {
+			throw new TypeError(
+				'The placeOrderButton property for the payment method must be a React component (function)'
+			);
+		}
+		if ( config.placeOrderButton && config.placeOrderButtonLabel ) {
+			// eslint-disable-next-line no-console
+			console.warn(
+				`Payment method "${ config.name }" provided both placeOrderButton and placeOrderButtonLabel. Using placeOrderButton.`
+			);
+		}
 		assertValidElementOrString( config.label, 'label' );
 		assertValidElement( config.content, 'content' );
 		assertValidElement( config.edit, 'edit' );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/test/payment-method-config.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/test/payment-method-config.tsx
new file mode 100644
index 0000000000..7f05e9e598
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks-registry/payment-methods/test/payment-method-config.tsx
@@ -0,0 +1,112 @@
+/**
+ * Internal dependencies
+ */
+import PaymentMethodConfig from '../payment-method-config';
+
+const MockComponent = () => null;
+const mockReactElement = <MockComponent />;
+
+const baseConfig = {
+	name: 'test-payment-method',
+	label: mockReactElement,
+	ariaLabel: 'Test Payment Method',
+	content: mockReactElement,
+	edit: mockReactElement,
+	canMakePayment: () => true,
+};
+
+describe( 'PaymentMethodConfig', () => {
+	it( 'accepts a valid string for placeOrderButtonLabel', () => {
+		let config = {};
+		expect( () => {
+			config = new PaymentMethodConfig( {
+				...baseConfig,
+				placeOrderButtonLabel: 'Custom Label',
+			} );
+		} ).not.toThrow();
+		expect( config.placeOrderButtonLabel ).toBe( 'Custom Label' );
+	} );
+
+	it( 'accepts undefined for placeOrderButtonLabel', () => {
+		let config = {};
+		expect( () => {
+			config = new PaymentMethodConfig( {
+				...baseConfig,
+				placeOrderButtonLabel: undefined,
+			} );
+		} ).not.toThrow();
+		expect( config.placeOrderButtonLabel ).toBe( undefined );
+	} );
+
+	it( 'throws TypeError when placeOrderButtonLabel is not a string', () => {
+		expect( () => {
+			new PaymentMethodConfig( {
+				...baseConfig,
+				// @ts-expect-error Testing runtime validation of invalid type
+				placeOrderButtonLabel: 123,
+			} );
+		} ).toThrow(
+			'The placeOrderButtonLabel property for the payment method must be a string'
+		);
+	} );
+
+	it( 'accepts a valid function for placeOrderButton', () => {
+		const CustomButton = () => null;
+		let config = {};
+		expect( () => {
+			config = new PaymentMethodConfig( {
+				...baseConfig,
+				placeOrderButton: CustomButton,
+			} );
+		} ).not.toThrow();
+		expect( config.placeOrderButton ).toBe( CustomButton );
+	} );
+
+	it( 'accepts undefined for placeOrderButton', () => {
+		expect( () => {
+			new PaymentMethodConfig( {
+				...baseConfig,
+				placeOrderButton: undefined,
+			} );
+		} ).not.toThrow();
+	} );
+
+	it( 'throws TypeError when placeOrderButton is not a function', () => {
+		expect( () => {
+			new PaymentMethodConfig( {
+				...baseConfig,
+				// @ts-expect-error Testing runtime validation of invalid type
+				placeOrderButton: 'not-a-function',
+			} );
+		} ).toThrow(
+			'The placeOrderButton property for the payment method must be a React component (function)'
+		);
+		expect( () => {
+			new PaymentMethodConfig( {
+				...baseConfig,
+				// @ts-expect-error Testing runtime validation of invalid type
+				placeOrderButton: 123,
+			} );
+		} ).toThrow( TypeError );
+		expect( () => {
+			new PaymentMethodConfig( {
+				...baseConfig,
+				// @ts-expect-error Testing runtime validation of invalid type
+				placeOrderButton: { render: () => null },
+			} );
+		} ).toThrow( TypeError );
+	} );
+
+	it( 'logs a warning when both placeOrderButton and placeOrderButtonLabel are provided', () => {
+		const CustomButton = () => null;
+		new PaymentMethodConfig( {
+			...baseConfig,
+			placeOrderButton: CustomButton,
+			placeOrderButtonLabel: 'Custom Label',
+		} );
+
+		expect( console ).toHaveWarnedWith(
+			'Payment method "test-payment-method" provided both placeOrderButton and placeOrderButtonLabel. Using placeOrderButton.'
+		);
+	} );
+} );
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 df8f3b6534..4c7f2080a2 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
@@ -12,6 +12,8 @@ import { noticeContexts } from '@woocommerce/base-context';
 import { StoreNoticesContainer } from '@woocommerce/blocks-components';
 import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
 import { CART_URL } from '@woocommerce/block-settings';
+import { useSelect } from '@wordpress/data';
+import { paymentStore } from '@woocommerce/block-data';

 /**
  * Internal dependencies
@@ -37,7 +39,18 @@ const Block = ( {
 	returnToCartButtonLabel,
 	priceSeparator,
 }: BlockAttributes ) => {
-	const { paymentMethodButtonLabel } = useCheckoutSubmit();
+	const { paymentMethodButtonLabel, paymentMethodPlaceOrderButton } =
+		useCheckoutSubmit();
+
+	const activeSavedToken = useSelect(
+		( select ) => select( paymentStore ).getActiveSavedToken(),
+		[]
+	);
+
+	// not showing the custom button when a saved token is selected - only when the payment method is selected from the list.
+	const CustomButtonComponent = activeSavedToken
+		? undefined
+		: paymentMethodPlaceOrderButton;

 	const label = applyCheckoutFilter( {
 		filterName: 'placeOrderButtonLabel',
@@ -71,6 +84,7 @@ const Block = ( {
 					</ReturnToCartButton>
 				) }
 				<PlaceOrderButton
+					CustomButtonComponent={ CustomButtonComponent }
 					label={ label }
 					fullWidth={ ! shouldShowReturnToCart }
 					showPrice={ showPrice }
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
new file mode 100644
index 0000000000..0ce8f8e30a
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/tests/block.js
@@ -0,0 +1,151 @@
+/**
+ * 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/types/type-defs/payment-method-interface.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payment-method-interface.ts
index 9a3620ed75..c5ffabc9de 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payment-method-interface.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payment-method-interface.ts
@@ -6,6 +6,7 @@ import type PaymentMethodLabel from '@woocommerce/base-components/cart-checkout/
 import type PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons';
 import type LoadingMask from '@woocommerce/base-components/loading-mask';
 import type { ValidationInputError } from '@woocommerce/blocks-components';
+import type { ComponentType } from 'react';

 /**
  * Internal dependencies
@@ -189,3 +190,29 @@ export type PaymentMethodInterface = {
 	// A boolean which indicates whether the shopper has checked the save payment method checkbox.
 	shouldSavePayment: boolean;
 };
+
+/**
+ * Complete props interface for custom place order button components.
+ * Includes the standard PaymentMethodInterface plus additional button-specific props.
+ */
+export type CustomPlaceOrderButtonProps = PaymentMethodInterface & {
+	// Validates the checkout form without starting processing. Returns a promise with validation results.
+	// If validation fails, automatically scrolls to the first error.
+	validate: () => Promise< { hasError: boolean } >;
+	// Whether checkout is waiting for server processing.
+	waitingForProcessing: boolean;
+	// Whether checkout is waiting to redirect after success.
+	waitingForRedirect: boolean;
+	// Whether the button should act "disabled" because of some processing in the background.
+	disabled: boolean;
+	// Whether the button is being rendered in the block editor.
+	isEditor: boolean;
+	// Whether the button is being rendered in preview mode.
+	isPreview: boolean;
+};
+
+/**
+ * Type for custom place order button components.
+ */
+export type CustomPlaceOrderButtonComponent =
+	ComponentType< CustomPlaceOrderButtonProps >;
diff --git a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payments.ts b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payments.ts
index 2a11bf7e7f..531fd9d146 100644
--- a/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payments.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/types/type-defs/payments.ts
@@ -15,6 +15,7 @@ import type {
 } from './cart-response';
 import type { EmptyObjectType } from './objects';
 import type { CheckoutResponseSuccess } from './checkout';
+import type { CustomPlaceOrderButtonComponent } from './payment-method-interface';

 /**
  * The shape of objects on the `globalPaymentMethods` object from `allSettings`.
@@ -116,6 +117,9 @@ export interface PaymentMethodConfiguration {
 	ariaLabel: string;
 	// Optionally customize the label text for the checkout submit (`Place Order`) button.
 	placeOrderButtonLabel?: string;
+	// Optionally provide a custom React component to replace the Place Order button.
+	// Receives the full payment method interface plus additional button-specific props.
+	placeOrderButton?: CustomPlaceOrderButtonComponent;
 	// A React node that contains logic handling any processing your payment method has to do with saved payment methods if your payment method supports them
 	savedTokenComponent?: ReactNode | null;
 }
@@ -180,6 +184,7 @@ export interface PaymentMethodConfigInstance {
 	label: ReactNode;
 	ariaLabel: string;
 	placeOrderButtonLabel?: string;
+	placeOrderButton?: CustomPlaceOrderButtonComponent;
 	savedTokenComponent?: ReactNode | null;
 	canMakePaymentFromConfig: CanMakePaymentCallback;
 	canMakePayment: CanMakePaymentCallback;
@@ -194,6 +199,7 @@ export interface ExpressPaymentMethodConfigInstance {
 	edit: ReactNode;
 	paymentMethodId?: string;
 	placeOrderButtonLabel?: string;
+	placeOrderButton?: CustomPlaceOrderButtonComponent;
 	supports: Supports;
 	canMakePaymentFromConfig: CanMakePaymentCallback;
 	canMakePayment: CanMakePaymentCallback;
diff --git a/plugins/woocommerce/client/blocks/packages/checkout/README.md b/plugins/woocommerce/client/blocks/packages/checkout/README.md
index ca0ec70fd9..0cfe10fd6c 100644
--- a/plugins/woocommerce/client/blocks/packages/checkout/README.md
+++ b/plugins/woocommerce/client/blocks/packages/checkout/README.md
@@ -50,6 +50,7 @@ This package contains the following directories. Navigate to a directory for mor
 | [`blocks-registry/`](./blocks-registry) | Used to **register new Inner Blocks** that can be inserted either automatically or optionally within the Checkout Block. _**Example use case:** Creating a newsletter subscription block on the Checkout._                                                                                                             |
 | [`components/`](./components)           | Components available for use by Checkout Blocks.                                                                                                                                                                                                                                                                       |
 | [`filter-registry/`](./filter-registry) | Used to **manipulate content** where filters are available. _**Example use case:** Changing how prices are displayed._ ([Documentation](./filter-registry))                                                                                                                                                            |
+| [`hooks/`](./hooks)                     | React hooks for checkout functionality. _**Example use case:** Validating checkout before processing with `useValidateCheckout`._                                                                                                                                                                                      |
 | [`slot/`](./slot)                       | Slot and Fill are a pair of components which enable developers to render in a React element tree. In this context, they are used to **insert content within Blocks** where slot fills are available. _**Example use case:** Adding a custom component after the shipping options._ ([Documentation](./slot/README.md)) |
 | [`utils/`](./utils)                     | Miscellaneous utility functions for dealing with checkout functionality.                                                                                                                                                                                                                                               |

diff --git a/plugins/woocommerce/client/blocks/packages/checkout/hooks/README.md b/plugins/woocommerce/client/blocks/packages/checkout/hooks/README.md
new file mode 100644
index 0000000000..42a100b3ee
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/checkout/hooks/README.md
@@ -0,0 +1,68 @@
+# Checkout Hooks <!-- omit in toc -->
+
+## Table of Contents <!-- omit in toc -->
+
+-   [`useValidateCheckout`](#usevalidatecheckout)
+    -   [Usage](#usage)
+    -   [Return Value](#return-value)
+
+React hooks for checkout functionality.
+
+## `useValidateCheckout`
+
+A hook that validates the checkout form and automatically scrolls to the first validation error if any are found.
+
+This hook is primarily used internally by the `PlaceOrderButton` component to provide the `validate` prop to custom place order button components. However, it can also be used directly if needed.
+
+### Usage
+
+```jsx
+// Aliased import
+import { useValidateCheckout } from '@woocommerce/blocks-checkout';
+
+// Global import
+// const { useValidateCheckout } = wc.blocksCheckout;
+
+const MyComponent = () => {
+	const validateCheckout = useValidateCheckout();
+
+	const handleClick = async () => {
+		const { hasError } = await validateCheckout();
+
+		if ( hasError ) {
+			// Validation failed - errors are automatically shown and
+			// the page scrolls to the first error
+			return;
+		}
+
+		// Validation passed - proceed with your logic
+	};
+
+	return <button onClick={ handleClick }>Validate</button>;
+};
+```
+
+### Return Value
+
+The hook returns a function that, when called:
+
+1. Emits the `CHECKOUT_VALIDATION` event to run all registered validation callbacks
+2. Checks the validation store for any field-level validation errors
+3. If errors are found:
+    - Shows all validation errors
+    - Scrolls to and focuses the first error element
+4. Returns a promise that resolves to `{ hasError: boolean }`
+
+| Property   | Type      | Description                                     |
+| :--------- | :-------- | :---------------------------------------------- |
+| `hasError` | `boolean` | `true` if validation failed, `false` if passed. |
+
+<!-- FEEDBACK -->
+
+---
+
+[We're hiring!](https://woocommerce.com/careers/) Come work with us!
+
+🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=type%3A+documentation&template=suggestion-for-documentation-improvement-correction.md&title=Feedback%20on%20./packages/checkout/hooks/README.md)
+
+<!-- /FEEDBACK -->
diff --git a/plugins/woocommerce/client/blocks/packages/checkout/hooks/index.ts b/plugins/woocommerce/client/blocks/packages/checkout/hooks/index.ts
new file mode 100644
index 0000000000..c39f2befc3
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/checkout/hooks/index.ts
@@ -0,0 +1 @@
+export { useValidateCheckout } from './use-validate-checkout';
diff --git a/plugins/woocommerce/client/blocks/packages/checkout/hooks/test/use-validate-checkout.ts b/plugins/woocommerce/client/blocks/packages/checkout/hooks/test/use-validate-checkout.ts
new file mode 100644
index 0000000000..8a2b71cf75
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/checkout/hooks/test/use-validate-checkout.ts
@@ -0,0 +1,133 @@
+/**
+ * External dependencies
+ */
+import { renderHook, act } from '@testing-library/react';
+import { responseTypes } from '@woocommerce/types';
+
+/**
+ * Internal dependencies
+ */
+import { useValidateCheckout } from '../use-validate-checkout';
+
+type ValidationResult = Awaited<
+	ReturnType< ReturnType< typeof useValidateCheckout > >
+>;
+
+const mockEmit = jest.fn();
+jest.mock( '@woocommerce/blocks-checkout-events', () => ( {
+	checkoutEventsEmitter: {
+		emit: ( ...args: unknown[] ) => mockEmit( ...args ),
+	},
+	CHECKOUT_EVENTS: {
+		CHECKOUT_VALIDATION: 'checkout_validation',
+	},
+} ) );
+
+const mockShowAllValidationErrors = jest.fn();
+const mockSetValidationErrors = jest.fn();
+const mockHasValidationErrors = jest.fn();
+
+jest.mock( '@woocommerce/block-data', () => ( {
+	validationStore: 'wc/store/validation',
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+	useDispatch: () => ( {
+		showAllValidationErrors: mockShowAllValidationErrors,
+		setValidationErrors: mockSetValidationErrors,
+	} ),
+	select: () => ( {
+		hasValidationErrors: mockHasValidationErrors,
+	} ),
+} ) );
+
+const mockScrollIntoView = jest.fn();
+const mockFocus = jest.fn();
+
+describe( 'useValidateCheckout', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		jest.useFakeTimers();
+		mockEmit.mockResolvedValue( [] );
+		mockHasValidationErrors.mockReturnValue( false );
+		document.body.innerHTML = '';
+	} );
+
+	afterEach( () => {
+		jest.useRealTimers();
+	} );
+
+	it( 'returns hasError: false when validation passes', async () => {
+		mockEmit.mockResolvedValue( [ { type: responseTypes.SUCCESS } ] );
+
+		const { result } = renderHook( () => useValidateCheckout() );
+
+		let validationResult: ValidationResult | undefined;
+		await act( async () => {
+			validationResult = await result.current();
+		} );
+
+		expect( mockEmit ).toHaveBeenCalledWith( 'checkout_validation' );
+		expect( validationResult ).toEqual( { hasError: false } );
+		expect( mockShowAllValidationErrors ).not.toHaveBeenCalled();
+	} );
+
+	it.each( [
+		[ 'callback returns error response', responseTypes.ERROR, false ],
+		[ 'callback returns fail response', responseTypes.FAIL, false ],
+		[ 'callback returns non-success response', 'unknown', false ],
+		[ 'validation store has errors', responseTypes.SUCCESS, true ],
+	] )(
+		'returns hasError: true when %s',
+		async ( _, responseType, storeHasErrors ) => {
+			mockEmit.mockResolvedValue( [ { type: responseType } ] );
+			mockHasValidationErrors.mockReturnValue( storeHasErrors );
+
+			const { result } = renderHook( () => useValidateCheckout() );
+
+			let validationResult: ValidationResult | undefined;
+			await act( async () => {
+				validationResult = await result.current();
+			} );
+
+			expect( validationResult ).toEqual( { hasError: true } );
+			expect( mockShowAllValidationErrors ).toHaveBeenCalledTimes( 1 );
+		}
+	);
+
+	it( 'propagates validation errors and scrolls to first error element on failure', async () => {
+		const container = document.createElement( 'div' );
+		container.className = 'has-error';
+		const input = document.createElement( 'input' );
+		input.scrollIntoView = mockScrollIntoView;
+		input.focus = mockFocus;
+		container.appendChild( input );
+		document.body.appendChild( container );
+
+		const validationErrors = {
+			billing_email: { message: 'Email is required', hidden: false },
+		};
+		mockEmit.mockResolvedValue( [
+			{ type: responseTypes.ERROR, validationErrors },
+		] );
+
+		const { result } = renderHook( () => useValidateCheckout() );
+
+		await act( async () => {
+			await result.current();
+		} );
+
+		expect( mockSetValidationErrors ).toHaveBeenCalledWith(
+			validationErrors
+		);
+
+		act( () => {
+			jest.advanceTimersByTime( 50 );
+		} );
+
+		expect( mockScrollIntoView ).toHaveBeenCalledWith( {
+			block: 'center',
+		} );
+		expect( mockFocus ).toHaveBeenCalled();
+	} );
+} );
diff --git a/plugins/woocommerce/client/blocks/packages/checkout/hooks/use-validate-checkout.ts b/plugins/woocommerce/client/blocks/packages/checkout/hooks/use-validate-checkout.ts
new file mode 100644
index 0000000000..e9471f77f0
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/packages/checkout/hooks/use-validate-checkout.ts
@@ -0,0 +1,91 @@
+/**
+ * External dependencies
+ */
+import { useCallback } from '@wordpress/element';
+import { useDispatch, select } from '@wordpress/data';
+import { validationStore } from '@woocommerce/block-data';
+import {
+	checkoutEventsEmitter,
+	CHECKOUT_EVENTS,
+} from '@woocommerce/blocks-checkout-events';
+import {
+	isErrorResponse,
+	isFailResponse,
+	isSuccessResponse,
+} from '@woocommerce/types';
+
+/**
+ * Scrolls to and focuses the first validation error element.
+ */
+const scrollToFirstValidationError = (): void => {
+	const errorSelector = 'input:invalid, .has-error input, .has-error select';
+	const firstErrorElement =
+		document.querySelector< HTMLElement >( errorSelector );
+	if ( firstErrorElement ) {
+		firstErrorElement.scrollIntoView( { block: 'center' } );
+		firstErrorElement.focus();
+	}
+};
+
+/**
+ * Hook that provides checkout validation with automatic scroll-to-error behavior.
+ *
+ * This hook validates the checkout form by emitting the CHECKOUT_VALIDATION event
+ * and checking for validation errors in the validation store. If errors are found,
+ * it automatically shows all validation errors and scrolls to the first error element.
+ *
+ * @return A function that validates checkout and returns a promise with the validation result.
+ */
+export const useValidateCheckout = (): ( () => Promise< {
+	hasError: boolean;
+} > ) => {
+	const { showAllValidationErrors, setValidationErrors } =
+		useDispatch( validationStore );
+
+	return useCallback( async () => {
+		// Emit validation event and collect responses from registered callbacks
+		const responses = await checkoutEventsEmitter.emit(
+			CHECKOUT_EVENTS.CHECKOUT_VALIDATION
+		);
+
+		// Check if any callback returned an error/fail response
+		const hasCallbackError = responses.some(
+			( response ) =>
+				isErrorResponse( response ) || isFailResponse( response )
+		);
+
+		// Check if any callback returned a non-success response
+		// (similar to __internalEmitValidateEvent behavior)
+		const hasNonSuccessResponse =
+			responses.length > 0 && ! responses.every( isSuccessResponse );
+
+		// Check the validation store for field-level validation errors
+		const hasValidationStoreErrors =
+			select( validationStore ).hasValidationErrors();
+
+		const hasError =
+			hasCallbackError ||
+			hasNonSuccessResponse ||
+			hasValidationStoreErrors;
+
+		if ( hasError ) {
+			// Set any validation errors from callbacks
+			responses.forEach( ( response ) => {
+				if (
+					isErrorResponse( response ) ||
+					isFailResponse( response )
+				) {
+					if ( response.validationErrors ) {
+						setValidationErrors( response.validationErrors );
+					}
+				}
+			} );
+
+			// Show all validation errors and scroll to the first one
+			showAllValidationErrors();
+			window.setTimeout( scrollToFirstValidationError, 50 );
+		}
+
+		return { hasError };
+	}, [ showAllValidationErrors, setValidationErrors ] );
+};
diff --git a/plugins/woocommerce/client/blocks/packages/checkout/index.js b/plugins/woocommerce/client/blocks/packages/checkout/index.js
index f80118f275..3030ca91ea 100644
--- a/plugins/woocommerce/client/blocks/packages/checkout/index.js
+++ b/plugins/woocommerce/client/blocks/packages/checkout/index.js
@@ -1,4 +1,5 @@
 export * from './components';
+export * from './hooks';
 export * from './utils';
 export * from './slot';
 export * from './filter-registry';
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/plugins/custom-place-order-button-test.php b/plugins/woocommerce/client/blocks/tests/e2e/plugins/custom-place-order-button-test.php
new file mode 100644
index 0000000000..7ec2490f14
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/tests/e2e/plugins/custom-place-order-button-test.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * Plugin Name: WooCommerce Blocks Test Custom Place Order Button
+ * Description: Registers a test payment method with a custom place order button for e2e testing.
+ * Plugin URI: https://github.com/woocommerce/woocommerce
+ * Author: WooCommerce
+ *
+ * @package woocommerce-blocks-test-custom-place-order-button
+ */
+
+declare(strict_types=1);
+
+add_action(
+	'plugins_loaded',
+	function () {
+		if ( ! class_exists( 'WC_Payment_Gateway' ) ) {
+			return;
+		}
+
+		/**
+		 * Test payment gateway with custom place order button.
+		 */
+		class WC_Gateway_Test_Custom_Button extends WC_Payment_Gateway {
+			/**
+			 * Constructor.
+			 */
+			public function __construct() {
+				$this->id                 = 'test-custom-button';
+				$this->method_title       = 'Test Custom Button';
+				$this->method_description = 'Test payment method with custom place order button';
+				$this->title              = 'Test Custom Button Payment';
+				$this->description        = 'Test payment method for e2e testing custom place order button';
+				$this->has_fields         = false;
+				$this->supports           = array( 'products' );
+				$this->enabled            = 'yes';
+			}
+
+			/**
+			 * Process the payment.
+			 *
+			 * @param int $order_id Order ID.
+			 * @return array
+			 */
+			public function process_payment( $order_id ) {
+				$order = wc_get_order( $order_id );
+				$order->payment_complete();
+
+				return array(
+					'result'   => 'success',
+					'redirect' => $this->get_return_url( $order ),
+				);
+			}
+		}
+
+		add_filter(
+			'woocommerce_payment_gateways',
+			function ( $gateways ) {
+				$gateways[] = 'WC_Gateway_Test_Custom_Button';
+				return $gateways;
+			}
+		);
+	}
+);
+
+add_action(
+	'woocommerce_blocks_payment_method_type_registration',
+	function ( $registry ) {
+		$registry->register(
+			new class() extends \Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType {
+				/**
+				 * Payment method name.
+				 *
+				 * @var string
+				 */
+				protected $name = 'test-custom-button';
+
+				/**
+				 * Initialize.
+				 */
+				public function initialize() {}
+
+				/**
+				 * Check if payment method is active.
+				 *
+				 * @return bool
+				 */
+				public function is_active() {
+					return true;
+				}
+
+				/**
+				 * Get payment method script handles.
+				 *
+				 * @return array
+				 */
+				public function get_payment_method_script_handles() {
+					wp_register_script( 'test-custom-button', '', array( 'wc-blocks-registry' ), '1.0.0', true );
+					wp_add_inline_script( 'test-custom-button', $this->get_inline_script() );
+					return array( 'test-custom-button' );
+				}
+
+				/**
+				 * Get payment method data.
+				 *
+				 * @return array
+				 */
+				public function get_payment_method_data() {
+					return array(
+						'title'       => 'Test Custom Button Payment',
+						'description' => 'Test payment method for e2e testing',
+					);
+				}
+
+				/**
+				 * Get inline script for registering the payment method.
+				 *
+				 * @return string
+				 */
+				private function get_inline_script() {
+					return <<<'JS'
+(
+	function() {
+		const { registerPaymentMethod } = wc.wcBlocksRegistry;
+		const { createElement } = wp.element;
+
+		const CustomButton = function(props) {
+			const handleClick = async function() {
+				const result = await props.validate();
+				if (result.hasError) {
+					return;
+				}
+
+				props.onSubmit();
+			};
+
+			return createElement('button', {
+				type: 'button',
+				'data-testid': 'custom-place-order-button',
+				onClick: handleClick,
+				disabled: props.disabled,
+				className: 'wc-block-components-button wp-element-button',
+			}, 'Custom Payment Button');
+		};
+
+		const NoContent = function() {
+			return null;
+		};
+
+		registerPaymentMethod({
+			name: 'test-custom-button',
+			label: 'Test Custom Button Payment',
+			ariaLabel: 'Test Custom Button Payment',
+			content: createElement(NoContent, null, null),
+			edit: createElement(NoContent, null, null),
+			canMakePayment: function() { return true; },
+			placeOrderButton: CustomButton,
+			supports: { features: ['products'] },
+		});
+	}
+)();
+JS;
+				}
+			}
+		);
+	}
+);
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/checkout/checkout-block-custom-place-order-button.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/checkout/checkout-block-custom-place-order-button.block_theme.spec.ts
new file mode 100644
index 0000000000..1629b6dfd9
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/checkout/checkout-block-custom-place-order-button.block_theme.spec.ts
@@ -0,0 +1,116 @@
+/**
+ * External dependencies
+ */
+import { expect, test as base, guestFile, wpCLI } from '@woocommerce/e2e-utils';
+
+/**
+ * Internal dependencies
+ */
+import { SIMPLE_PHYSICAL_PRODUCT_NAME } from './constants';
+import { CheckoutPage } from './checkout.page';
+
+const test = base.extend< { checkoutPageObject: CheckoutPage } >( {
+	checkoutPageObject: async ( { page, requestUtils }, use ) => {
+		const pageObject = new CheckoutPage( {
+			page,
+			requestUtils,
+		} );
+		await use( pageObject );
+	},
+} );
+
+test.describe( 'Custom Place Order Button', () => {
+	test.use( { storageState: guestFile } );
+
+	test.beforeEach( async ( { requestUtils } ) => {
+		await requestUtils.activatePlugin(
+			'woocommerce-blocks-test-custom-place-order-button'
+		);
+		await wpCLI(
+			'option set woocommerce_test-custom-button_settings --format=json \'{"enabled":"yes"}\''
+		);
+	} );
+
+	test( 'clicking custom button triggers validation when form is invalid', async ( {
+		page,
+		frontendUtils,
+		checkoutPageObject,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
+		await frontendUtils.goToCheckout();
+
+		// Verify the default place order button is visible.
+		await expect(
+			page.locator( '.wc-block-components-checkout-place-order-button' )
+		).toBeVisible();
+
+		// Select the test payment method.
+		await page
+			.getByRole( 'radio', { name: 'Test Custom Button Payment' } )
+			.click();
+
+		// Verify the default place order button is no longer visible.
+		await expect(
+			page.locator( '.wc-block-components-checkout-place-order-button' )
+		).toBeHidden();
+
+		// Fill in valid checkout data.
+		await checkoutPageObject.fillInCheckoutWithTestData();
+
+		// Clear any pre-filled fields to ensure validation fails.
+		await page.getByLabel( 'Email address' ).clear();
+
+		// Wait for the custom button to be enabled - in some cases the tests are so fast that shipping options haven't loaded yet.
+		await expect(
+			page.getByTestId( 'custom-place-order-button' )
+		).toBeEnabled();
+
+		// Click the custom button without filling required fields.
+		await page.getByTestId( 'custom-place-order-button' ).click();
+
+		// Verify validation errors are shown.
+		await expect(
+			page.locator( '.wc-block-components-validation-error' )
+		).toBeVisible();
+	} );
+
+	test( 'clicking custom button submits order when form is valid', async ( {
+		page,
+		frontendUtils,
+		checkoutPageObject,
+	} ) => {
+		await frontendUtils.goToShop();
+		await frontendUtils.addToCart( SIMPLE_PHYSICAL_PRODUCT_NAME );
+		await frontendUtils.goToCheckout();
+
+		// Fill in valid checkout data.
+		await checkoutPageObject.fillInCheckoutWithTestData();
+
+		// Verify the default place order button is visible.
+		await expect(
+			page.locator( '.wc-block-components-checkout-place-order-button' )
+		).toBeVisible();
+
+		// Select the test payment method.
+		await page
+			.getByRole( 'radio', { name: 'Test Custom Button Payment' } )
+			.click();
+
+		// Verify the default place order button is not visible.
+		await expect(
+			page.locator( '.wc-block-components-checkout-place-order-button' )
+		).toBeHidden();
+
+		// Wait for the custom button to be enabled - in some cases the tests are so fast that shipping options haven't loaded yet.
+		const customButton = page.getByTestId( 'custom-place-order-button' );
+		await expect( customButton ).toBeEnabled();
+
+		// Focus then click the custom button to avoid misses due to page content shifting.
+		await customButton.focus();
+		await customButton.click();
+
+		// Verify order was placed successfully by checking for order confirmation.
+		await expect( page ).toHaveURL( /order-received/ );
+	} );
+} );