Commit c11d9741099 for woocommerce

commit c11d97410993202fa5e1035963d81d33813d47c7
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Fri Sep 11 20:27:10 2026 +0300

    Prepare My Account stock notifications template data in a view class (#68537)

    * refactor: stop the stock notifications template calling MyAccountEndpoint

    Claude-Session: https://claude.ai/code/session_01EMSPiQgaGEQcoZhQ9eEjE8

    * refactor: keep stock notifications template args scalar and guarded

    Claude-Session: https://claude.ai/code/session_01QCpD8A2WjzJXXqcyWw5eQg

    * chore: make stock notifications changelog a comment, feature unreleased

diff --git a/plugins/woocommerce/changelog/wooplug-7667-bis-my-account-template-no-internals b/plugins/woocommerce/changelog/wooplug-7667-bis-my-account-template-no-internals
new file mode 100644
index 00000000000..b1f76f18ec8
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-7667-bis-my-account-template-no-internals
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Prepare the My Account stock notifications template data in MyAccountView instead of calling MyAccountEndpoint from the template. The feature is unreleased, so there is no user-facing change.
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php b/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php
index 486995286e8..ce30000bd0d 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountEndpoint.php
@@ -127,6 +127,19 @@ class MyAccountEndpoint {
 		return in_array( (string) $notification->get_status(), self::get_cancellable_statuses(), true );
 	}

+	/**
+	 * Check whether the customer can ask for the verification email again.
+	 *
+	 * Only pending sign-ups have anything to verify; the management service
+	 * enforces the same rule when the request comes in.
+	 *
+	 * @param Notification $notification The notification to check.
+	 * @return bool True when the notification is still awaiting confirmation.
+	 */
+	public static function can_resend( Notification $notification ): bool {
+		return NotificationStatus::PENDING === (string) $notification->get_status();
+	}
+
 	/**
 	 * Notification management service, owns the resend-verification domain logic.
 	 *
@@ -134,6 +147,13 @@ class MyAccountEndpoint {
 	 */
 	private NotificationManagementService $notification_management_service;

+	/**
+	 * Builds the template arguments for the endpoint.
+	 *
+	 * @var MyAccountView
+	 */
+	private MyAccountView $view;
+
 	/**
 	 * Constructor.
 	 */
@@ -151,9 +171,11 @@ class MyAccountEndpoint {
 	 * @internal
 	 *
 	 * @param NotificationManagementService $notification_management_service The notification management service.
+	 * @param MyAccountView                 $view                            Builds the template arguments.
 	 */
-	final public function init( NotificationManagementService $notification_management_service ): void {
+	final public function init( NotificationManagementService $notification_management_service, MyAccountView $view ): void {
 		$this->notification_management_service = $notification_management_service;
+		$this->view                            = $view;
 	}

 	/**
@@ -311,15 +333,15 @@ class MyAccountEndpoint {

 		\wc_get_template(
 			'myaccount/stock-notifications.php',
-			array(
-				'notifications'         => $page['notifications'],
-				'pending_notifications' => $pending,
-				'has_pending'           => ! empty( $pending ),
-				'has_items'             => ! empty( $pending ) || ! empty( $page['notifications'] ),
-				'current_page'          => $page['current_page'],
-				'total_pages'           => $page['total_pages'],
-				'total_items'           => $page['total_items'],
-				'per_page'              => $per_page,
+			$this->view->get_template_args(
+				$pending,
+				$page['notifications'],
+				array(
+					'current_page' => $page['current_page'],
+					'total_pages'  => $page['total_pages'],
+					'total_items'  => $page['total_items'],
+					'per_page'     => $per_page,
+				)
 			)
 		);
 	}
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountView.php b/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountView.php
new file mode 100644
index 00000000000..19cd26cb825
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Frontend/MyAccountView.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * MyAccountView class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\StockNotifications\Frontend;
+
+use Automattic\WooCommerce\Internal\StockNotifications\Notification;
+
+/**
+ * Prepares the data the My Account stock notifications template renders.
+ *
+ * Resolves every endpoint constant, URL and label to a plain value so the
+ * template, which themes copy and override, never references this namespace.
+ *
+ * @internal
+ */
+final class MyAccountView {
+
+	/**
+	 * Get template arguments for the stock notifications tables.
+	 *
+	 * @param array<Notification> $pending Pending notifications, newest first.
+	 * @param array<Notification> $active  Active notifications for the current page.
+	 * @param array<string,int>   $page    Pagination state: current_page, total_pages, total_items, per_page.
+	 * @return array<string,mixed>
+	 *
+	 * @since 11.2.0
+	 */
+	public function get_template_args( array $pending, array $active, array $page ): array {
+		$current_page = max( 1, (int) ( $page['current_page'] ?? 1 ) );
+		$total_pages  = max( 1, (int) ( $page['total_pages'] ?? 1 ) );
+
+		return array(
+			'pending_rows'      => $this->get_rows( $pending, $current_page ),
+			'active_rows'       => $this->get_rows( $active, $current_page ),
+			'current_page'      => $current_page,
+			'total_pages'       => $total_pages,
+			'total_items'       => max( 0, (int) ( $page['total_items'] ?? 0 ) ),
+			'per_page'          => max( 1, (int) ( $page['per_page'] ?? MyAccountEndpoint::DEFAULT_PER_PAGE ) ),
+			'previous_page_url' => $current_page > 1 ? MyAccountEndpoint::get_endpoint_url( $current_page - 1 ) : '',
+			'next_page_url'     => $current_page < $total_pages ? MyAccountEndpoint::get_endpoint_url( $current_page + 1 ) : '',
+			'shop_url'          => \wc_get_page_permalink( 'shop' ),
+		);
+	}
+
+	/**
+	 * Flatten notifications into display rows.
+	 *
+	 * Action URLs are empty strings when the action does not apply to the row,
+	 * so the template only has to test for a non-empty value.
+	 *
+	 * @param array<Notification> $notifications Notifications to flatten.
+	 * @param int                 $current_page  1-indexed page the rows render on, carried by the action URLs.
+	 * @return array<int,array<string,mixed>>
+	 */
+	private function get_rows( array $notifications, int $current_page ): array {
+		$rows = array();
+
+		foreach ( $notifications as $notification ) {
+			if ( ! $notification instanceof Notification ) {
+				continue;
+			}
+
+			$id           = (int) $notification->get_id();
+			$product_name = MyAccountEndpoint::get_display_product_name( $notification );
+			$variation    = (string) $notification->get_product_formatted_variation_list( true );
+			$date_created = $notification->get_date_created();
+			$can_resend   = MyAccountEndpoint::can_resend( $notification );
+			$can_cancel   = MyAccountEndpoint::is_cancellable( $notification );
+
+			$label_name = '' !== $product_name ? $product_name : __( 'an unavailable product', 'woocommerce' );
+			if ( '' !== $variation ) {
+				$label_name .= ' ' . $variation;
+			}
+
+			$rows[] = array(
+				'id'           => $id,
+				'status'       => (string) $notification->get_status(),
+				'product_name' => $product_name,
+				'product_url'  => (string) $notification->get_product_permalink(),
+				'variation'    => $variation,
+				'date_iso'     => $date_created ? $date_created->date( 'c' ) : '',
+				'date_display' => $date_created ? \wc_format_datetime( $date_created ) : '',
+				'resend_url'   => $can_resend ? MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_RESEND, $id, $current_page ) : '',
+				/* translators: %s: product name, followed by its variation attributes when the sign-up is for a variation. */
+				'resend_label' => $can_resend ? sprintf( __( 'Resend verification email for %s', 'woocommerce' ), $label_name ) : '',
+				'cancel_url'   => $can_cancel ? MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_CANCEL, $id, $current_page ) : '',
+				/* translators: %s: product name, followed by its variation attributes when the sign-up is for a variation. */
+				'cancel_label' => $can_cancel ? sprintf( __( 'Cancel stock notification for %s', 'woocommerce' ), $label_name ) : '',
+			);
+		}
+
+		return $rows;
+	}
+}
diff --git a/plugins/woocommerce/templates/myaccount/stock-notifications.php b/plugins/woocommerce/templates/myaccount/stock-notifications.php
index f147fabf411..b551e3345cb 100644
--- a/plugins/woocommerce/templates/myaccount/stock-notifications.php
+++ b/plugins/woocommerce/templates/myaccount/stock-notifications.php
@@ -16,22 +16,70 @@
  * @package WooCommerce\Templates
  * @version 11.2.0
  *
- * @var array $notifications         Array of active Notification objects for the current user (one page).
- * @var array $pending_notifications Array of Notification objects awaiting email confirmation (capped, not paginated).
- * @var bool  $has_pending           Whether there are any pending notifications to render.
- * @var bool  $has_items             Whether there are any notifications (pending or active) to render.
- * @var int   $current_page          1-indexed current page number of the active table.
- * @var int   $total_pages           Total number of pages of active notifications.
- * @var int   $total_items           Total number of active notifications across all pages.
- * @var int   $per_page              Active notifications shown per page.
+ * @var array  $pending_rows      Rows awaiting email confirmation (capped, not paginated). See row keys below.
+ * @var array  $active_rows       Active rows for the current page. See row keys below.
+ * @var int    $current_page      1-indexed current page number of the active table.
+ * @var int    $total_pages       Total number of pages of active rows.
+ * @var int    $total_items       Total number of active rows across all pages.
+ * @var int    $per_page          Active rows shown per page.
+ * @var string $previous_page_url URL of the previous page of active rows, or an empty string on the first page.
+ * @var string $next_page_url     URL of the next page of active rows, or an empty string on the last page.
+ * @var string $shop_url          URL the empty-state "Browse products" button points at.
+ *
+ * Each row is an array with these keys:
+ *   - int    id           Notification id.
+ *   - string status       Notification status slug, used as a row class modifier.
+ *   - string product_name Product title, or an empty string when the product is gone.
+ *   - string product_url  Product permalink, or an empty string when the product is gone.
+ *   - string variation    Flat list of variation attributes, or an empty string for simple products.
+ *   - string date_iso     Sign-up date in ISO 8601, or an empty string when unknown.
+ *   - string date_display Sign-up date formatted for display, or an empty string when unknown.
+ *   - string resend_url   Nonce-protected URL that resends the verification email, or an empty string when the row cannot be resent.
+ *   - string resend_label Accessible label for the resend link.
+ *   - string cancel_url   Nonce-protected URL that cancels the notification, or an empty string when the row cannot be cancelled.
+ *   - string cancel_label Accessible label for the cancel link.
  */

-use Automattic\WooCommerce\Internal\StockNotifications\Frontend\MyAccountEndpoint;
-
 defined( 'ABSPATH' ) || exit;

+$pending_rows      = isset( $pending_rows ) && is_array( $pending_rows ) ? $pending_rows : array();
+$active_rows       = isset( $active_rows ) && is_array( $active_rows ) ? $active_rows : array();
+$has_pending       = ! empty( $pending_rows );
+$has_items         = $has_pending || ! empty( $active_rows );
+$previous_page_url = isset( $previous_page_url ) ? (string) $previous_page_url : '';
+$next_page_url     = isset( $next_page_url ) ? (string) $next_page_url : '';
+$shop_url          = isset( $shop_url ) ? (string) $shop_url : wc_get_page_permalink( 'shop' );
+
 $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '';

+$row_defaults = array(
+	'id'           => 0,
+	'status'       => '',
+	'product_name' => '',
+	'product_url'  => '',
+	'variation'    => '',
+	'date_iso'     => '',
+	'date_display' => '',
+	'resend_url'   => '',
+	'resend_label' => '',
+	'cancel_url'   => '',
+	'cancel_label' => '',
+);
+
+$tables = array(
+	'pending' => array(
+		'heading' => __( 'Awaiting confirmation', 'woocommerce' ),
+		'caption' => __( 'Stock notifications awaiting confirmation', 'woocommerce' ),
+		'rows'    => $pending_rows,
+	),
+	'active'  => array(
+		// A store without double opt-in only ever has one table, so the heading is redundant there.
+		'heading' => $has_pending ? __( 'Active', 'woocommerce' ) : '',
+		'caption' => __( 'Active stock notifications', 'woocommerce' ),
+		'rows'    => $active_rows,
+	),
+);
+
 /**
  * Fires before the back in stock notifications table is rendered on My Account.
  *
@@ -42,98 +90,32 @@ $wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_
 do_action( 'woocommerce_before_account_customer_stock_notifications', $has_items );
 ?>

-<?php if ( $has_pending ) : ?>
-
+<?php foreach ( $tables as $table_key => $table ) : ?>
 	<?php
-	/**
-	 * Fires before the pending (awaiting confirmation) stock notifications table is rendered on My Account.
-	 *
-	 * @since 11.2.0
-	 *
-	 * @param bool $has_pending Whether there are any pending notifications to render.
-	 */
-	do_action( 'woocommerce_before_account_customer_stock_notifications_pending', $has_pending );
+	if ( empty( $table['rows'] ) ) {
+		continue;
+	}
 	?>

-	<h2 class="woocommerce-customer-stock-notifications-heading woocommerce-customer-stock-notifications-heading--pending"><?php esc_html_e( 'Awaiting confirmation', 'woocommerce' ); ?></h2>
-
-	<table class="woocommerce-customer-stock-notifications-table woocommerce-customer-stock-notifications-table--pending woocommerce-MyAccount-customerStockNotifications shop_table shop_table_responsive">
-		<caption class="screen-reader-text"><?php esc_html_e( 'Stock notifications awaiting confirmation', 'woocommerce' ); ?></caption>
-		<thead>
-			<tr>
-				<th scope="col" class="woocommerce-customer-stock-notifications-table__header woocommerce-customer-stock-notifications-table__header-product"><span class="nobr"><?php esc_html_e( 'Product', 'woocommerce' ); ?></span></th>
-				<th scope="col" class="woocommerce-customer-stock-notifications-table__header woocommerce-customer-stock-notifications-table__header-date"><span class="nobr"><?php esc_html_e( 'Date', 'woocommerce' ); ?></span></th>
-				<th scope="col" class="woocommerce-customer-stock-notifications-table__header woocommerce-customer-stock-notifications-table__header-actions"><span class="nobr"><?php esc_html_e( 'Actions', 'woocommerce' ); ?></span></th>
-			</tr>
-		</thead>
-		<tbody>
-		<?php foreach ( $pending_notifications as $notification ) : ?>
-			<?php
-			$product_name = MyAccountEndpoint::get_display_product_name( $notification );
-			$permalink    = $notification->get_product_permalink();
-			$variation    = $notification->get_product_formatted_variation_list( true );
-			$date_created = $notification->get_date_created();
-
-			$action_label_name = '' !== $product_name ? $product_name : __( 'an unavailable product', 'woocommerce' );
-			if ( '' !== $variation ) {
-				$action_label_name .= ' ' . $variation;
-			}
-			/* translators: %s: product name, followed by its variation attributes when the sign-up is for a variation. */
-			$resend_label = sprintf( __( 'Resend verification email for %s', 'woocommerce' ), $action_label_name );
-			/* translators: %s: product name, followed by its variation attributes when the sign-up is for a variation. */
-			$cancel_label = sprintf( __( 'Cancel stock notification for %s', 'woocommerce' ), $action_label_name );
-			?>
-			<tr class="woocommerce-customer-stock-notifications-table__row woocommerce-customer-stock-notifications-table__row--status-<?php echo esc_attr( (string) $notification->get_status() ); ?>">
-				<td class="woocommerce-customer-stock-notifications-table__cell woocommerce-customer-stock-notifications-table__cell-product" data-title="<?php esc_attr_e( 'Product', 'woocommerce' ); ?>">
-					<?php if ( '' !== $product_name && '' !== $permalink ) : ?>
-						<a href="<?php echo esc_url( $permalink ); ?>"><?php echo esc_html( $product_name ); ?></a>
-					<?php elseif ( '' !== $product_name ) : ?>
-						<?php echo esc_html( $product_name ); ?>
-					<?php else : ?>
-						<?php esc_html_e( 'Product unavailable', 'woocommerce' ); ?>
-					<?php endif; ?>
-
-					<?php if ( '' !== $variation ) : ?>
-						<div class="description"><?php echo esc_html( $variation ); ?></div>
-					<?php endif; ?>
-				</td>
-				<td class="woocommerce-customer-stock-notifications-table__cell woocommerce-customer-stock-notifications-table__cell-date" data-title="<?php esc_attr_e( 'Date', 'woocommerce' ); ?>">
-					<?php if ( $date_created ) : ?>
-						<time datetime="<?php echo esc_attr( $date_created->date( 'c' ) ); ?>"><?php echo esc_html( wc_format_datetime( $date_created ) ); ?></time>
-					<?php else : ?>
-						&mdash;
-					<?php endif; ?>
-				</td>
-				<td class="woocommerce-customer-stock-notifications-table__cell woocommerce-customer-stock-notifications-table__cell-actions actions" data-title="<?php esc_attr_e( 'Actions', 'woocommerce' ); ?>">
-					<a href="<?php echo esc_url( MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_RESEND, (int) $notification->get_id(), $current_page ) ); ?>" class="woocommerce-button button woocommerce-customer-stock-notifications-action-link woocommerce-customer-stock-notifications-action-link--resend<?php echo esc_attr( $wp_button_class ); ?>" aria-label="<?php echo esc_attr( $resend_label ); ?>"><?php esc_html_e( 'Resend verification', 'woocommerce' ); ?></a>
-					<a href="<?php echo esc_url( MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_CANCEL, (int) $notification->get_id(), $current_page ) ); ?>" class="woocommerce-button button woocommerce-customer-stock-notifications-action-link woocommerce-customer-stock-notifications-action-link--cancel<?php echo esc_attr( $wp_button_class ); ?>" aria-label="<?php echo esc_attr( $cancel_label ); ?>"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
-				</td>
-			</tr>
-		<?php endforeach; ?>
-		</tbody>
-	</table>
-
-	<?php
-	/**
-	 * Fires after the pending (awaiting confirmation) stock notifications table is rendered on My Account.
-	 *
-	 * @since 11.2.0
-	 *
-	 * @param bool $has_pending Whether there were any pending notifications rendered.
-	 */
-	do_action( 'woocommerce_after_account_customer_stock_notifications_pending', $has_pending );
-	?>
-
-<?php endif; ?>
-
-<?php if ( ! empty( $notifications ) ) : ?>
+	<?php if ( 'pending' === $table_key ) : ?>
+		<?php
+		/**
+		 * Fires before the pending (awaiting confirmation) stock notifications table is rendered on My Account.
+		 *
+		 * @since 11.2.0
+		 *
+		 * @param bool $has_pending Whether there are any pending notifications to render.
+		 */
+		do_action( 'woocommerce_before_account_customer_stock_notifications_pending', $has_pending );
+		?>
+	<?php endif; ?>

-	<?php if ( $has_pending ) : ?>
-		<h2 class="woocommerce-customer-stock-notifications-heading woocommerce-customer-stock-notifications-heading--active"><?php esc_html_e( 'Active', 'woocommerce' ); ?></h2>
+	<?php if ( '' !== $table['heading'] ) : ?>
+		<h2 class="woocommerce-customer-stock-notifications-heading woocommerce-customer-stock-notifications-heading--<?php echo esc_attr( $table_key ); ?>"><?php echo esc_html( $table['heading'] ); ?></h2>
 	<?php endif; ?>

-	<table class="woocommerce-customer-stock-notifications-table woocommerce-customer-stock-notifications-table--active woocommerce-MyAccount-customerStockNotifications shop_table shop_table_responsive">
-		<caption class="screen-reader-text"><?php esc_html_e( 'Active stock notifications', 'woocommerce' ); ?></caption>
+	<table class="woocommerce-customer-stock-notifications-table woocommerce-customer-stock-notifications-table--<?php echo esc_attr( $table_key ); ?> woocommerce-MyAccount-customerStockNotifications shop_table shop_table_responsive">
+		<caption class="screen-reader-text"><?php echo esc_html( $table['caption'] ); ?></caption>
 		<thead>
 			<tr>
 				<th scope="col" class="woocommerce-customer-stock-notifications-table__header woocommerce-customer-stock-notifications-table__header-product"><span class="nobr"><?php esc_html_e( 'Product', 'woocommerce' ); ?></span></th>
@@ -142,21 +124,14 @@ do_action( 'woocommerce_before_account_customer_stock_notifications', $has_items
 			</tr>
 		</thead>
 		<tbody>
-		<?php foreach ( $notifications as $notification ) : ?>
+		<?php foreach ( $table['rows'] as $row ) : ?>
 			<?php
-			$product_name = MyAccountEndpoint::get_display_product_name( $notification );
-			$permalink    = $notification->get_product_permalink();
-			$variation    = $notification->get_product_formatted_variation_list( true );
-			$date_created = $notification->get_date_created();
-
-			$cancel_label_name = '' !== $product_name ? $product_name : __( 'an unavailable product', 'woocommerce' );
-			if ( '' !== $variation ) {
-				$cancel_label_name .= ' ' . $variation;
+			if ( ! is_array( $row ) ) {
+				continue;
 			}
-			/* translators: %s: product name, followed by its variation attributes when the sign-up is for a variation. */
-			$cancel_label = sprintf( __( 'Cancel stock notification for %s', 'woocommerce' ), $cancel_label_name );
+			$row = wp_parse_args( $row, $row_defaults );
 			?>
-			<tr class="woocommerce-customer-stock-notifications-table__row woocommerce-customer-stock-notifications-table__row--status-<?php echo esc_attr( (string) $notification->get_status() ); ?>">
+			<tr class="woocommerce-customer-stock-notifications-table__row woocommerce-customer-stock-notifications-table__row--status-<?php echo esc_attr( $row['status'] ); ?>">
 				<td class="woocommerce-customer-stock-notifications-table__cell woocommerce-customer-stock-notifications-table__cell-product" data-title="<?php esc_attr_e( 'Product', 'woocommerce' ); ?>">
 					<?php
 					/*
@@ -164,29 +139,32 @@ do_action( 'woocommerce_before_account_customer_stock_notifications', $has_items
 					 * customer can see the sign-up exists and cancel it.
 					 */
 					?>
-					<?php if ( '' !== $product_name && '' !== $permalink ) : ?>
-						<a href="<?php echo esc_url( $permalink ); ?>"><?php echo esc_html( $product_name ); ?></a>
-					<?php elseif ( '' !== $product_name ) : ?>
-						<?php echo esc_html( $product_name ); ?>
+					<?php if ( '' !== $row['product_name'] && '' !== $row['product_url'] ) : ?>
+						<a href="<?php echo esc_url( $row['product_url'] ); ?>"><?php echo esc_html( $row['product_name'] ); ?></a>
+					<?php elseif ( '' !== $row['product_name'] ) : ?>
+						<?php echo esc_html( $row['product_name'] ); ?>
 					<?php else : ?>
 						<?php esc_html_e( 'Product unavailable', 'woocommerce' ); ?>
 					<?php endif; ?>

-					<?php if ( '' !== $variation ) : ?>
-						<div class="description"><?php echo esc_html( $variation ); ?></div>
+					<?php if ( '' !== $row['variation'] ) : ?>
+						<div class="description"><?php echo esc_html( $row['variation'] ); ?></div>
 					<?php endif; ?>
 				</td>
 				<td class="woocommerce-customer-stock-notifications-table__cell woocommerce-customer-stock-notifications-table__cell-date" data-title="<?php esc_attr_e( 'Date signed up', 'woocommerce' ); ?>">
-					<?php if ( $date_created ) : ?>
-						<time datetime="<?php echo esc_attr( $date_created->date( 'c' ) ); ?>"><?php echo esc_html( wc_format_datetime( $date_created ) ); ?></time>
+					<?php if ( '' !== $row['date_iso'] ) : ?>
+						<time datetime="<?php echo esc_attr( $row['date_iso'] ); ?>"><?php echo esc_html( $row['date_display'] ); ?></time>
 					<?php else : ?>
 						&mdash;
 					<?php endif; ?>
 				</td>
 				<td class="woocommerce-customer-stock-notifications-table__cell woocommerce-customer-stock-notifications-table__cell-actions actions" data-title="<?php esc_attr_e( 'Actions', 'woocommerce' ); ?>">
-					<?php if ( MyAccountEndpoint::is_cancellable( $notification ) ) : ?>
-						<a href="<?php echo esc_url( MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_CANCEL, (int) $notification->get_id(), $current_page ) ); ?>" class="woocommerce-button button woocommerce-customer-stock-notifications-action-link woocommerce-customer-stock-notifications-action-link--cancel<?php echo esc_attr( $wp_button_class ); ?>" aria-label="<?php echo esc_attr( $cancel_label ); ?>"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
-					<?php else : ?>
+					<?php if ( '' !== $row['resend_url'] ) : ?>
+						<a href="<?php echo esc_url( $row['resend_url'] ); ?>" class="woocommerce-button button woocommerce-customer-stock-notifications-action-link woocommerce-customer-stock-notifications-action-link--resend<?php echo esc_attr( $wp_button_class ); ?>" aria-label="<?php echo esc_attr( $row['resend_label'] ); ?>"><?php esc_html_e( 'Resend verification', 'woocommerce' ); ?></a>
+					<?php endif; ?>
+					<?php if ( '' !== $row['cancel_url'] ) : ?>
+						<a href="<?php echo esc_url( $row['cancel_url'] ); ?>" class="woocommerce-button button woocommerce-customer-stock-notifications-action-link woocommerce-customer-stock-notifications-action-link--cancel<?php echo esc_attr( $wp_button_class ); ?>" aria-label="<?php echo esc_attr( $row['cancel_label'] ); ?>"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></a>
+					<?php elseif ( '' === $row['resend_url'] ) : ?>
 						&mdash;
 					<?php endif; ?>
 				</td>
@@ -195,6 +173,22 @@ do_action( 'woocommerce_before_account_customer_stock_notifications', $has_items
 		</tbody>
 	</table>

+	<?php if ( 'pending' === $table_key ) : ?>
+		<?php
+		/**
+		 * Fires after the pending (awaiting confirmation) stock notifications table is rendered on My Account.
+		 *
+		 * @since 11.2.0
+		 *
+		 * @param bool $has_pending Whether there were any pending notifications rendered.
+		 */
+		do_action( 'woocommerce_after_account_customer_stock_notifications_pending', $has_pending );
+		?>
+	<?php endif; ?>
+<?php endforeach; ?>
+
+<?php if ( ! empty( $active_rows ) ) : ?>
+
 	<?php
 	/**
 	 * Fires before the stock notifications pagination is rendered on My Account.
@@ -204,14 +198,14 @@ do_action( 'woocommerce_before_account_customer_stock_notifications', $has_items
 	do_action( 'woocommerce_before_account_customer_stock_notifications_pagination' );
 	?>

-	<?php if ( $total_pages > 1 ) : ?>
+	<?php if ( '' !== $previous_page_url || '' !== $next_page_url ) : ?>
 		<div class="woocommerce-pagination woocommerce-pagination--without-numbers woocommerce-Pagination" role="navigation" aria-label="<?php esc_attr_e( 'Active stock notifications pagination', 'woocommerce' ); ?>">
-			<?php if ( 1 !== $current_page ) : ?>
-				<a class="woocommerce-button woocommerce-button--previous woocommerce-Button woocommerce-Button--previous button<?php echo esc_attr( $wp_button_class ); ?>" href="<?php echo esc_url( wc_get_endpoint_url( MyAccountEndpoint::ENDPOINT, (string) ( $current_page - 1 ), wc_get_page_permalink( 'myaccount' ) ) ); ?>"><?php esc_html_e( 'Previous', 'woocommerce' ); ?></a>
+			<?php if ( '' !== $previous_page_url ) : ?>
+				<a class="woocommerce-button woocommerce-button--previous woocommerce-Button woocommerce-Button--previous button<?php echo esc_attr( $wp_button_class ); ?>" href="<?php echo esc_url( $previous_page_url ); ?>"><?php esc_html_e( 'Previous', 'woocommerce' ); ?></a>
 			<?php endif; ?>

-			<?php if ( $total_pages !== $current_page ) : ?>
-				<a class="woocommerce-button woocommerce-button--next woocommerce-Button woocommerce-Button--next button<?php echo esc_attr( $wp_button_class ); ?>" href="<?php echo esc_url( wc_get_endpoint_url( MyAccountEndpoint::ENDPOINT, (string) ( $current_page + 1 ), wc_get_page_permalink( 'myaccount' ) ) ); ?>"><?php esc_html_e( 'Next', 'woocommerce' ); ?></a>
+			<?php if ( '' !== $next_page_url ) : ?>
+				<a class="woocommerce-button woocommerce-button--next woocommerce-Button woocommerce-Button--next button<?php echo esc_attr( $wp_button_class ); ?>" href="<?php echo esc_url( $next_page_url ); ?>"><?php esc_html_e( 'Next', 'woocommerce' ); ?></a>
 			<?php endif; ?>
 		</div>
 	<?php endif; ?>
@@ -220,7 +214,7 @@ do_action( 'woocommerce_before_account_customer_stock_notifications', $has_items

 <?php if ( ! $has_items ) : ?>

-	<?php wc_print_notice( esc_html__( "You haven't signed up for any back-in-stock notifications yet.", 'woocommerce' ) . ' <a class="woocommerce-Button wc-forward button' . esc_attr( $wp_button_class ) . '" href="' . esc_url( apply_filters( 'woocommerce_return_to_shop_redirect', wc_get_page_permalink( 'shop' ) ) ) . '">' . esc_html__( 'Browse products', 'woocommerce' ) . '</a>', 'notice' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ?>
+	<?php wc_print_notice( esc_html__( "You haven't signed up for any back-in-stock notifications yet.", 'woocommerce' ) . ' <a class="woocommerce-Button wc-forward button' . esc_attr( $wp_button_class ) . '" href="' . esc_url( apply_filters( 'woocommerce_return_to_shop_redirect', $shop_url ) ) . '">' . esc_html__( 'Browse products', 'woocommerce' ) . '</a>', 'notice' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment ?>

 <?php endif; ?>

diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php
index 6c8909d207d..3c80440b3fe 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountEndpointTests.php
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationCancell
 use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
 use Automattic\WooCommerce\Internal\StockNotifications\Factory;
 use Automattic\WooCommerce\Internal\StockNotifications\Frontend\MyAccountEndpoint;
+use Automattic\WooCommerce\Internal\StockNotifications\Frontend\MyAccountView;
 use Automattic\WooCommerce\Internal\StockNotifications\Frontend\NotificationManagementService;
 use Automattic\WooCommerce\Internal\StockNotifications\Notification;
 use Automattic\WooCommerce\Tests\Internal\StockNotifications\StockNotificationsFeatureTrait;
@@ -109,7 +110,7 @@ class MyAccountEndpointTests extends \WC_Unit_Test_Case {
 	 */
 	private function make_endpoint( ?NotificationManagementService $service = null ): MyAccountEndpoint {
 		$endpoint = new MyAccountEndpoint();
-		$endpoint->init( $service ?? wc_get_container()->get( NotificationManagementService::class ) );
+		$endpoint->init( $service ?? wc_get_container()->get( NotificationManagementService::class ), new MyAccountView() );

 		return $endpoint;
 	}
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountViewTest.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountViewTest.php
new file mode 100644
index 00000000000..44512d42160
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Frontend/MyAccountViewTest.php
@@ -0,0 +1,224 @@
+<?php
+/**
+ * MyAccountViewTest class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\StockNotifications\Frontend;
+
+use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
+use Automattic\WooCommerce\Internal\StockNotifications\Frontend\MyAccountEndpoint;
+use Automattic\WooCommerce\Internal\StockNotifications\Frontend\MyAccountView;
+use Automattic\WooCommerce\Internal\StockNotifications\Notification;
+use Automattic\WooCommerce\Tests\Internal\StockNotifications\StockNotificationsFeatureTrait;
+use WC_Helper_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the MyAccountView class.
+ */
+class MyAccountViewTest extends WC_Unit_Test_Case {
+
+	use StockNotificationsFeatureTrait;
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var MyAccountView
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->enable_stock_notifications_feature();
+		$this->sut = new MyAccountView();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		try {
+			$this->restore_stock_notifications_feature_option();
+		} finally {
+			parent::tearDown();
+		}
+	}
+
+	/**
+	 * Create a saved notification for the given product.
+	 *
+	 * @param int    $product_id Product id.
+	 * @param string $status     Notification status.
+	 * @return Notification
+	 */
+	private function create_notification( int $product_id, string $status ): Notification {
+		$notification = new Notification();
+		$notification->set_product_id( $product_id );
+		$notification->set_user_id( 1 );
+		$notification->set_user_email( 'customer@example.com' );
+		$notification->set_status( $status );
+		$notification->save();
+
+		return $notification;
+	}
+
+	/**
+	 * Default pagination state for a single page.
+	 *
+	 * @param int $current_page 1-indexed current page.
+	 * @param int $total_pages  Total number of pages.
+	 * @return array<string,int>
+	 */
+	private function page( int $current_page = 1, int $total_pages = 1 ): array {
+		return array(
+			'current_page' => $current_page,
+			'total_pages'  => $total_pages,
+			'total_items'  => 1,
+			'per_page'     => 10,
+		);
+	}
+
+	/**
+	 * @testdox Should flatten a pending notification into a row with resend and cancel URLs.
+	 */
+	public function test_pending_row_carries_resend_and_cancel_urls(): void {
+		$product      = WC_Helper_Product::create_simple_product();
+		$notification = $this->create_notification( $product->get_id(), NotificationStatus::PENDING );
+
+		$args = $this->sut->get_template_args( array( $notification ), array(), $this->page() );
+		$row  = $args['pending_rows'][0];
+
+		$this->assertSame( array(), $args['active_rows'] );
+		$this->assertSame( $notification->get_id(), $row['id'] );
+		$this->assertSame( NotificationStatus::PENDING, $row['status'] );
+		$this->assertSame( $product->get_title(), $row['product_name'] );
+		$this->assertSame( $product->get_permalink(), $row['product_url'] );
+		$this->assertSame( '', $row['variation'] );
+		$this->assertSame( $notification->get_date_created()->date( 'c' ), $row['date_iso'] );
+		$this->assertSame( wc_format_datetime( $notification->get_date_created() ), $row['date_display'] );
+		$this->assertSame( MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_RESEND, $notification->get_id() ), $row['resend_url'] );
+		$this->assertSame( 'Resend verification email for ' . $product->get_title(), $row['resend_label'] );
+		$this->assertSame( MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_CANCEL, $notification->get_id() ), $row['cancel_url'] );
+		$this->assertSame( 'Cancel stock notification for ' . $product->get_title(), $row['cancel_label'] );
+	}
+
+	/**
+	 * @testdox Should fall back to empty date strings when the notification has no date created.
+	 */
+	public function test_row_date_fields_fall_back_when_date_created_is_missing(): void {
+		$notification = new Notification();
+		$notification->set_status( NotificationStatus::PENDING );
+
+		$row = $this->sut->get_template_args( array( $notification ), array(), $this->page() )['pending_rows'][0];
+
+		$this->assertSame( '', $row['date_iso'] );
+		$this->assertSame( '', $row['date_display'] );
+	}
+
+	/**
+	 * @testdox Should only offer cancel on an active row.
+	 */
+	public function test_active_row_has_no_resend_url(): void {
+		$product      = WC_Helper_Product::create_simple_product();
+		$notification = $this->create_notification( $product->get_id(), NotificationStatus::ACTIVE );
+
+		$args = $this->sut->get_template_args( array(), array( $notification ), $this->page() );
+		$row  = $args['active_rows'][0];
+
+		$this->assertSame( '', $row['resend_url'] );
+		$this->assertSame( '', $row['resend_label'] );
+		$this->assertNotSame( '', $row['cancel_url'] );
+	}
+
+	/**
+	 * @testdox Should leave both action URLs empty on a row that can neither be resent nor cancelled.
+	 */
+	public function test_sent_row_has_no_action_urls(): void {
+		$product      = WC_Helper_Product::create_simple_product();
+		$notification = $this->create_notification( $product->get_id(), NotificationStatus::SENT );
+
+		$row = $this->sut->get_template_args( array(), array( $notification ), $this->page() )['active_rows'][0];
+
+		$this->assertSame( '', $row['resend_url'] );
+		$this->assertSame( '', $row['cancel_url'] );
+	}
+
+	/**
+	 * @testdox Should label a row for a variation with the parent title and the attribute list.
+	 */
+	public function test_variation_row_uses_parent_title_and_attribute_list(): void {
+		$variable     = WC_Helper_Product::create_variation_product();
+		$variation_id = (int) $variable->get_children()[0];
+		$notification = $this->create_notification( $variation_id, NotificationStatus::ACTIVE );
+
+		$row = $this->sut->get_template_args( array(), array( $notification ), $this->page() )['active_rows'][0];
+
+		$this->assertSame( $variable->get_title(), $row['product_name'] );
+		$this->assertNotSame( '', $row['variation'] );
+		$this->assertSame( 'Cancel stock notification for ' . $variable->get_title() . ' ' . $row['variation'], $row['cancel_label'] );
+	}
+
+	/**
+	 * @testdox Should fall back to a generic label and empty product fields when the product is gone.
+	 */
+	public function test_missing_product_row_falls_back(): void {
+		$notification = $this->create_notification( 999999, NotificationStatus::ACTIVE );
+
+		$row = $this->sut->get_template_args( array(), array( $notification ), $this->page() )['active_rows'][0];
+
+		$this->assertSame( '', $row['product_name'] );
+		$this->assertSame( '', $row['product_url'] );
+		$this->assertSame( 'Cancel stock notification for an unavailable product', $row['cancel_label'] );
+	}
+
+	/**
+	 * @testdox Should carry the current page in the action URLs so the redirect returns there.
+	 */
+	public function test_action_urls_carry_the_current_page(): void {
+		$product      = WC_Helper_Product::create_simple_product();
+		$notification = $this->create_notification( $product->get_id(), NotificationStatus::ACTIVE );
+
+		$row = $this->sut->get_template_args( array(), array( $notification ), $this->page( 3, 4 ) )['active_rows'][0];
+
+		$this->assertSame( MyAccountEndpoint::get_action_url( MyAccountEndpoint::ACTION_CANCEL, $notification->get_id(), 3 ), $row['cancel_url'] );
+	}
+
+	/**
+	 * @testdox Should only build the pagination URLs that lead somewhere.
+	 *
+	 * @testWith [1, 1, false, false]
+	 *           [1, 3, false, true]
+	 *           [2, 3, true, true]
+	 *           [3, 3, true, false]
+	 *           [5, 3, true, false]
+	 *
+	 * @param int  $current_page 1-indexed current page.
+	 * @param int  $total_pages  Total number of pages.
+	 * @param bool $has_previous Whether a previous page URL is expected.
+	 * @param bool $has_next     Whether a next page URL is expected.
+	 */
+	public function test_pagination_urls( int $current_page, int $total_pages, bool $has_previous, bool $has_next ): void {
+		$args = $this->sut->get_template_args( array(), array(), $this->page( $current_page, $total_pages ) );
+
+		$this->assertSame( $has_previous ? MyAccountEndpoint::get_endpoint_url( $current_page - 1 ) : '', $args['previous_page_url'] );
+		$this->assertSame( $has_next ? MyAccountEndpoint::get_endpoint_url( $current_page + 1 ) : '', $args['next_page_url'] );
+		$this->assertSame( $current_page, $args['current_page'] );
+		$this->assertSame( $total_pages, $args['total_pages'] );
+	}
+
+	/**
+	 * @testdox Should report an empty state when there are no notifications at all.
+	 */
+	public function test_empty_state(): void {
+		$args = $this->sut->get_template_args( array(), array(), $this->page() );
+
+		$this->assertSame( array(), $args['pending_rows'] );
+		$this->assertSame( array(), $args['active_rows'] );
+		$this->assertSame( wc_get_page_permalink( 'shop' ), $args['shop_url'] );
+	}
+}