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 : ?>
- —
- <?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 : ?>
—
<?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'] ) : ?>
—
<?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'] );
+ }
+}