Commit a0536feb096 for woocommerce

commit a0536feb096c40c43621844297a4297823367177
Author: Jorge Torres <jorge.torres@automattic.com>
Date:   Mon Jun 1 18:13:20 2026 +0100

    Tighten shopper-collections docblocks and inline comments

    Reword class headers, method docblocks, and inline comments across the
    shopper-collections feature (Saved for Later, Wishlist, Add to Wishlist
    Button, the Store API routes and schemas, the shared ShopperListRenderer,
    and the supporting JS stores/hooks) for the public repo. Removes
    first-person voice, conversational asides, em-dashes, and multi-paragraph
    narration; wraps prose nearer the ~120-column soft limit. Extends the same
    pass to the Add to Wishlist Button block added since the feature merged.
    No functional changes (aside from listing the wishlist items route in the
    client batch opt-out, per review).

diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
index 7e55fb004af..c85220915f8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
@@ -131,14 +131,7 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 			false,
 			isBoolean
 		);
-		// Three signals, each catching a distinct failure mode.
-		// Disabling the `cart_save_for_later` feature unregisters the
-		// saved-for-later block but leaves any prior insertion in the
-		// cart page's post content (the editor renders it as an
-		// "unsupported block" notice) — so presence alone could render
-		// this link with no working destination. Inversely, the feature
-		// can be enabled on cart pages that never inserted the block.
-		// And the REST endpoints behind the click are auth-only.
+		// User has to be logged in, feature enabled, and on the cart page with the block present.
 		const showSaveForLater =
 			isUserLoggedIn &&
 			isSaveForLaterFeatureEnabled &&
@@ -376,16 +369,14 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 										if ( ! saved ) {
 											return;
 										}
-										// removeItem surfaces its own errors
-										// via processErrorResponse; we still
-										// fire the analytics event and a11y
-										// announcement to mirror the regular
-										// remove flow.
+										// `removeItem` surfaces its own errors via `processErrorResponse`. The
+										// analytics event and a11y announcement still fire to mirror the
+										// regular remove flow.
 										await removeItem();
-										// TODO: consider a dedicated
-										// 'cart-save-for-later' store event so
-										// analytics can distinguish a save
-										// from a plain remove.
+										// TODO: emit a dedicated
+										// `cart-save-for-later` store event so
+										// analytics can distinguish a save from
+										// a plain remove.
 										dispatchStoreEvent(
 											'cart-remove-item',
 											{
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/cart/use-save-for-later.ts b/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/cart/use-save-for-later.ts
index 54501e584bf..0fa2f435bb7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/cart/use-save-for-later.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/context/hooks/cart/use-save-for-later.ts
@@ -8,17 +8,9 @@ import { store as noticesStore } from '@wordpress/notices';
 import { cartStore } from '@woocommerce/block-data';

 /**
- * Save a cart line item to the saved-for-later shopper list.
- *
- * Dispatches the wp.data cart store's `saveForLater` thunk, which POSTs the
- * item and emits a `wc-blocks_store_sync_required` event. A Saved for Later
- * block on the same page picks up that event in its iAPI store and applies
- * the new row reactively.
- *
- * Resolves to `true` on success so the caller can chain the cart removal;
- * on failure surfaces an error notice in the cart context and resolves to
- * `false`. The cart removal is the caller's responsibility — keep the two
- * awaits separate so save and remove errors stay distinct.
+ * Save a cart line item to the saved-for-later shopper list. Resolves to `true` on success. On failure,
+ * surfaces an error notice in the cart context and resolves to `false`. Removing the line from the cart
+ * is the caller's responsibility, so save and remove errors can be reported independently.
  */
 export const useSaveForLater = (): {
 	isSaving: boolean;
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/shopper-lists.ts b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/shopper-lists.ts
index 523b1ca8944..a7060837601 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/shopper-lists.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/shopper-lists.ts
@@ -7,11 +7,9 @@ import type { CurrencyResponse } from '@woocommerce/types';
 import type { Store as StoreNotices } from '@woocommerce/stores/store-notices';

 /**
- * Mirror of `Automattic\WooCommerce\StoreApi\Schemas\V1\ShopperListItemSchema::get_properties()`.
- *
- * Keep this in sync with the schema. State here must not include any UI-derived
- * fields — display values belong in block-private stores or PHP SSR.
- * TO DO: decide where UI-derived state lives
+ * Mirror of {@see \Automattic\WooCommerce\StoreApi\Schemas\V1\ShopperListItemSchema::get_properties()}.
+ * Keep in sync with the schema. UI-derived fields do not belong here. Display values are kept in
+ * block-private stores or rendered server-side.
  */
 export type ShopperListItemImage = {
 	id: number;
@@ -70,14 +68,7 @@ export type AddItemPayload = {
 export type Store = {
 	state: {
 		restUrl: string;
-		// TODO: revisit nonce handling when we look at authentication for
-		// the shopper-lists routes. Today PHP seeds this via
-		// `wp_create_nonce( 'wc_store_api' )` and we refresh it from
-		// response headers (see restRequest below). Likely changes once
-		// the routes start enforcing nonces server-side: align with the
-		// cart store's bootstrap-from-response-header pattern, share the
-		// cart's `state.nonce` instead of duplicating, or move to a
-		// caching-friendlier transport.
+		// Shared with the cart routes.
 		nonce: string;
 		lists: Record< string, ShopperListState >;
 	};
@@ -89,7 +80,7 @@ export type Store = {
 	};
 };

-// Stores are locked to prevent 3PD usage until the API is stable.
+// Locked to prevent third-party use until the API stabilizes.
 const universalLock =
 	'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';

@@ -111,15 +102,10 @@ const ensureListState = (
 };

 /**
- * Send a Store API request following the cart store's auth shape:
- * Nonce header, `wc_store_api` action on the server side, cookie auth via
- * `credentials: 'include'`, and `cache: 'no-store'` so user-specific data is
- * never cached.
- *
- * The starter nonce is seeded by PHP via `wp_interactivity_state` and
- * refreshed from the `Nonce` response header on every subsequent request,
- * so the server-side enforcement (landing in a follow-up PR) can be
- * flipped on without rewriting the client.
+ * Send a Store API request using the cart store's auth shape: Nonce header
+ * (`wc_store_api` action), cookie auth via `credentials: 'include'`, and
+ * `cache: 'no-store'` for user-specific data. The nonce is seeded by PHP and
+ * refreshed from the `Nonce` response header on each reply.
  */
 async function restRequest< T >(
 	state: Store[ 'state' ],
@@ -166,11 +152,6 @@ async function restRequest< T >(
 	return json as T | null;
 }

-// Do NOT supply `nonce` / `restUrl` defaults here. iAPI's deep-merge has the
-// JS-supplied state win over the existing (PHP-seeded) state for primitives,
-// so an empty-string default would clobber the values seeded server-side via
-// `wp_interactivity_state`. State for those fields comes purely from PHP. Same
-// reason the cart store doesn't ship state defaults — see cart.ts.
 const { state, actions } = store< Store >(
 	'woocommerce/shopper-lists',
 	{
@@ -198,12 +179,11 @@ const { state, actions } = store< Store >(

 					const items = response.filter( isShopperListItem );

-					// TODO: track in-flight mutation count and skip applying
-					// load results when mutations are pending, so a slow
-					// loadList cannot clobber a fresh add/remove.
+					// TODO: track in-flight mutation count and skip applying load results when mutations
+					// are pending, so a slow loadList cannot overwrite a fresh add or remove.
 					list.items = items;
 				} catch ( error ) {
-					// No user trigger to attach a banner to; log for ops.
+					// Logged for diagnostics.
 					// eslint-disable-next-line no-console
 					console.error( error );
 				} finally {
@@ -237,10 +217,8 @@ const { state, actions } = store< Store >(
 						);
 					}

-					// Merge the returned item by key — replace if present,
-					// append otherwise. Re-saving the same product POSTs
-					// twice and the server merges quantity, so we mirror
-					// that behaviour locally.
+					// Merge the returned item by key: replace if present, append otherwise. The server
+					// merges quantity on duplicate saves, and this mirrors that behaviour client-side.
 					const existingIndex = list.items.findIndex(
 						( i ) => i.key === item.key
 					);
@@ -264,9 +242,8 @@ const { state, actions } = store< Store >(
 					return;
 				}

-				// Pessimistic remove: leave the row in place until the
-				// server confirms, so failures don't flash. Buttons are
-				// disabled meanwhile via the block's `pendingKeys`.
+				// Pessimistic remove: keep the row in place until the server confirms, to avoid a
+				// momentary disappearance on failure. Buttons stay disabled meanwhile via `pendingKeys`.
 				try {
 					yield restRequest(
 						state,
@@ -280,7 +257,7 @@ const { state, actions } = store< Store >(
 					return;
 				}

-				// Re-find — the list may have mutated during the await.
+				// Re-find. The list may have mutated during the await.
 				const removedIndex = list.items.findIndex(
 					( i ) => i.key === key
 				);
@@ -312,16 +289,7 @@ const { state, actions } = store< Store >(
 	{ lock: universalLock }
 );

-// Listen for shopper-list item additions emitted from the wp.data side (e.g.
-// the cart store's saveForLater thunk). Mirrors the cart's iAPI → wp.data
-// sync direction, which also ships a payload (`from_iAPI` carries
-// `quantityChanges`). The event carries the saved item directly so we can
-// splice it in without an extra GET — keeps the merge ordering deterministic
-// and avoids the loadList-vs-mutation race the iAPI store's loadList still
-// has a TODO about.
-//
-// Keeps the discriminator + payload contract in sync with
-// `assets/js/data/cart/thunks.ts::saveForLater`.
+// Syncs wp.data into this iAPI store after a wp.data action (e.g. the cart store's `saveForLater` thunk).
 window.addEventListener( 'wc-blocks_store_sync_required', ( event: Event ) => {
 	const detail = ( event as CustomEvent ).detail as
 		| { type?: string; slug?: string; item?: RawShopperListItem }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/frontend.ts
index 1c639be4d9a..00c5d480d63 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/frontend.ts
@@ -35,16 +35,12 @@ type ButtonConfig = {
 type BlockContext = {
 	productId: number;
 	isVariableType: boolean;
-	// Mid-click flag, gated per-block so the button can be disabled while
-	// the request is in flight. Single-instance block, no `pendingKeys`
-	// map needed (Wishlist/SFL use one because they're per-row).
+	// Mid-click flag so the button can be disabled while the request is in flight.
 	isPending: boolean;
 };

-// The narrow slice of ATCWO's iAPI context this block consumes. Reuses
-// `SelectedAttributes` from the cart store — the same type ATCWO uses for
-// its own `selectedAttributes` context field — so any shape change there
-// (e.g. adding a `taxonomy` field) flows through here automatically.
+// The slice of ATCWO's iAPI context this block reads. Reuses the cart store's `SelectedAttributes`
+// so shape changes flow through automatically.
 type ATCWOContext = {
 	selectedAttributes: SelectedAttributes[];
 };
@@ -79,13 +75,8 @@ const { state } = store< BlockStore >(
 	'woocommerce/add-to-wishlist-button',
 	{
 		state: {
-			// For variable products, the effective product is the selected
-			// variation — resolved through the products store's
-			// `productInContext` derived getter, which already encapsulates
-			// "variation if one is selected, otherwise the parent." Returns
-			// 0 when the current resolution is still the variable parent
-			// (i.e. the shopper hasn't picked attributes yet), which
-			// `isDisabled` reads as "not yet selectable."
+			// Resolves to the selected variation's product ID via the products store. Returns 0 while a
+			// variable product has no selection, which `isDisabled` treats as not yet selectable.
 			get effectiveProductId(): number {
 				const product = productsState.productInContext;
 				if ( ! product ) {
@@ -108,12 +99,8 @@ const { state } = store< BlockStore >(
 					return null;
 				}
 				const context = getContext< BlockContext >();
-				// For non-variable products, id alone uniquely identifies
-				// the wishlist row. For variable products with "any"
-				// attribute slots, several attribute combinations can map
-				// to the same variation product, so we additionally
-				// disambiguate by the shopper's picked attributes — see
-				// `matchVariationItem` for details.
+				// For variable products, several attribute combinations can map to the same variation,
+				// so the picked attributes disambiguate the row. See `matchVariationItem`.
 				if ( ! context.isVariableType ) {
 					return (
 						list.items.find( ( item ) => item.id === id ) ?? null
@@ -174,21 +161,10 @@ const { state } = store< BlockStore >(
 							existing.key
 						);
 					} else {
-						// We inherit ATCWO's iAPI context because this block
-						// is an inner block of ATCWO (enforced by
-						// `ancestor` in `block.json`). That lets us read
-						// the shopper-picked attributes — needed for
-						// variations with "any" slots, where the server
-						// can't resolve the line item without them.
-						//
-						// ATCWO stores them by display label ("Color"), but
-						// the shopper-lists route expects taxonomy slugs
-						// ("pa_color"). Map via the parent product's
-						// `attributes` table; fall back to the raw name for
-						// non-taxonomy custom attributes.
-						//
-						// TODO: drop this mapping once ATCWO exposes the
-						// taxonomy on `selectedAttributes` directly.
+						// ATCWO stores selected attributes by display label ("Color"), but the
+						// shopper-lists route expects taxonomy slugs ("pa_color"). Map via the parent
+						// product's `attributes` table, falling back to the raw name for custom attributes.
+						// TODO: drop this mapping once ATCWO exposes the taxonomy on `selectedAttributes`.
 						const addToCartContext = getContext< ATCWOContext >(
 							'woocommerce/add-to-cart-with-options'
 						);
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/match-variation-item.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/match-variation-item.ts
index 78369a4b093..f58c37e3e87 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/match-variation-item.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/match-variation-item.ts
@@ -1,7 +1,5 @@
 /**
- * Items in the wishlist whose variation values we can compare against the
- * shopper's currently picked attributes. Narrowed from `RawShopperListItem`
- * so this helper stays pure (no iAPI deps) and unit-testable in isolation.
+ * Narrowed `RawShopperListItem` shape so this helper stays pure and unit-testable.
  */
 type MatchableItem = {
 	id: number;
@@ -12,9 +10,7 @@ type MatchableItem = {
 };

 /**
- * The shopper's currently picked attributes — same shape as ATCWO's
- * `selectedAttributes` context entries (also re-exported from the cart store
- * as `SelectedAttributes`).
+ * The shopper's currently picked attributes. Same shape as the cart store's `SelectedAttributes`.
  */
 type SelectedPair = {
 	attribute: string;
@@ -22,21 +18,13 @@ type SelectedPair = {
 };

 /**
- * Decide whether a wishlist item represents the exact variation + attribute
- * combination the shopper has currently picked. For fully-constrained
- * variations a single `id` match is enough, but for "any" attribute slots
- * several attribute combinations can share the same variation product (and
- * therefore the same `item.id`), so we additionally compare the attribute
- * sets. The asymmetric `value` comparison is case-insensitive because the
- * Store API returns each entry's `value` as the term display name ("Red")
- * while ATCWO carries the term slug ("red") in `selectedAttributes`.
- *
- * Edge cases out of scope here (would need a slug→name lookup via the parent
- * product's `terms`): slugs whose shape differs from the display name beyond
- * capitalization, e.g. `"bright-red"` vs `"Bright Red"`.
+ * Whether a wishlist item matches the picked variation and attributes. An `id` match alone is not
+ * enough for "any" attribute slots, where several combinations share one variation product, so the
+ * attribute sets are compared too. The value comparison is case-insensitive because the Store API
+ * returns the term display name ("Red") while ATCWO carries the slug ("red").
  *
  * @param item     Wishlist item from the shopper-lists store.
- * @param id       Effective product/variation id we're comparing against.
+ * @param id       Effective product/variation id to compare against.
  * @param selected Shopper's picked attribute/value pairs.
  */
 export function matchVariationItem(
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/save.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/save.tsx
index cdbc6befb56..c299c223ee0 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/save.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-wishlist-button/save.tsx
@@ -1,7 +1,3 @@
-/**
- * The block is rendered server-side and has no inner blocks, so there's
- * nothing to serialise to `post_content`.
- */
 const Save = (): null => null;

 export default Save;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/edit.tsx
index 2e2d4ea718f..77e79ea3ac8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/edit.tsx
@@ -23,9 +23,6 @@ interface EditProps {
 const MIN_COLUMNS = 2;
 const MAX_COLUMNS = 6;

-// Lives in JS because `__()` is needed for the heading copy. `block.json`
-// strings aren't run through translation, so keeping the template here
-// is the only way to ship a localized default.
 const TEMPLATE: [ string, Record< string, unknown > ][] = [
 	[
 		'core/heading',
@@ -85,11 +82,6 @@ const Edit = ( { attributes, setAttributes }: EditProps ): JSX.Element => {
 		className: 'wc-block-saved-for-later',
 	} );

-	// `allowedBlocks` is read from block.json automatically — passing it
-	// here would just duplicate the declaration. `templateLock: false`
-	// is the default so we omit that too. The className matches the
-	// `<div>` PHP wraps `$content` in on the frontend, so any CSS keyed
-	// off `__header` applies in both contexts.
 	const innerBlocksProps = useInnerBlocksProps(
 		{ className: 'wc-block-saved-for-later__header' },
 		{ template: TEMPLATE }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/frontend.ts
index c78f8148c4e..0713ad1462b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/frontend.ts
@@ -28,10 +28,7 @@ type SavedForLaterConfig = {
 };

 type BlockContext = {
-	// Wrapper-scoped flag: starts as `items.length > 0` from SSR and the
-	// `trackShownItems` callback flips it to `true` the first time the
-	// list has any items at runtime. Lives in iAPI context so it resets
-	// on every full page load.
+	// Tracks whether the list has ever had items in this session. See `trackShownItems` below.
 	hasShownItems: boolean;
 	listItem?: RawShopperListItem;
 	htmlField?: 'price_html' | 'image_html';
@@ -61,10 +58,8 @@ type BlockStore = {
 	};
 };

-// Allow-list for sanitizing the schema's preformatted strings on innerHTML
-// swap. Covers what `wc_price` (sale/discount markup, currency symbol) and
-// `wp_get_attachment_image` / `wc_placeholder_img` emit (responsive image
-// + dimensions + lazy loading).
+// Allow-list for sanitizing the schema's preformatted strings on innerHTML swap. Covers the markup
+// emitted by `wc_price` and `wp_get_attachment_image` / `wc_placeholder_img`.
 const ALLOWED_TAGS = [
 	'a',
 	'b',
@@ -173,12 +168,9 @@ store< BlockStore >(
 				return ! listItem.is_purchasable;
 			},

-			// `data-wp-text` writes its argument as text-content without
-			// running entity decoding, so a name returned by the schema as
-			// `Tom &amp; Jerry` would render literally that way. Bind
-			// templates and SSR text spans to this getter instead of the
-			// raw context field so what the browser shows matches what
-			// PHP wrote on first paint.
+			// `data-wp-text` writes its argument as text-content without entity decoding, so a name like
+			// `Tom &amp; Jerry` would render with the literal entity. Bind templates and SSR text spans
+			// to this getter (not the raw context field) so rendered text matches PHP's first paint.
 			get currentItemDisplayName(): string {
 				const { listItem } = getContext< BlockContext >();
 				return listItem ? decodeEntities( listItem.name ) : '';
@@ -245,14 +237,10 @@ store< BlockStore >(
 					return;
 				}

-				// Map the schema's `variation` shape to the cart's
-				// SelectedAttributes shape. The schema returns the
-				// slug-form attribute under `raw_attribute` (e.g.
-				// `attribute_pa_color`) plus a display label under
-				// `attribute` (e.g. "Color"); the cart matches by the
-				// slug-form, so override `attribute` with `raw_attribute`.
-				// Same swap mini-cart's `changeQuantity` does. Empty for
-				// simple products.
+				// Map the schema's `variation` shape to the cart's `SelectedAttributes` shape. The schema
+				// exposes the slug-form attribute under `raw_attribute` and a display label under
+				// `attribute`. The cart matches by the slug form, so `attribute` is overridden with
+				// `raw_attribute`. Same swap as mini-cart's `changeQuantity`. Empty for simple products.
 				const variation = listItem.variation.map(
 					( { raw_attribute: rawAttribute, value, attribute } ) => ( {
 						attribute: rawAttribute || attribute,
@@ -261,11 +249,9 @@ store< BlockStore >(
 				);
 				const isVariation = listItem.variation_id > 0;

-				// `cartActions.addCartItem` catches its own errors and
-				// surfaces them as store notices, so the yield resolves
-				// the same way on success and failure. Snapshot the
-				// matching line's quantity, run the add, then only remove
-				// from the saved list if it actually grew.
+				// `cartActions.addCartItem` catches its own errors and surfaces them as store notices,
+				// so the yield resolves identically on success and failure. Snapshot the matching line's
+				// quantity, run the add, and only remove from the saved list if the cart line grew.
 				const lookup = {
 					id: listItem.id,
 					...( isVariation && { variation } ),
@@ -300,15 +286,11 @@ store< BlockStore >(
 		},

 		callbacks: {
-			// Wrapper-level watcher: flips `hasShownItems` to `true` the
-			// first time the list has any items. Pairs with `state.isEmpty`
-			// to gate the empty message — a new shopper landing on a page
-			// with nothing saved keeps the flag at its SSR-seeded `false`
-			// and never sees the message; once they save an item (or
-			// landed with items) the flag is `true`, so emptying the list
-			// from that point surfaces the message. The flag never flips
-			// back to `false`, which is what gives the "had-items → now-empty"
-			// transition we want during the session.
+			// Wrapper-level watcher: flips `hasShownItems` to `true` the first time the list has items.
+			// Pairs with `state.isEmpty` to control the empty message. A shopper landing on a page with
+			// nothing saved keeps the SSR-seeded `false` and sees no message. Once items have been seen,
+			// an empty list surfaces the message. The flag never flips back to `false`, producing the
+			// had-items to now-empty transition within a session.
 			trackShownItems: () => {
 				const ctx = getContext< BlockContext >();
 				const list = getList( LIST_SLUG );
@@ -317,16 +299,11 @@ store< BlockStore >(
 				}
 			},

-			// Single shared innerHTML-swap callback for any slot whose
-			// content is one of the schema's preformatted HTML fields.
-			// Mirrors the atomic product-elements `updateValue` callback:
-			// the watched element carries `data-wp-context='{"htmlField":"price_html"}'`
-			// (or `"image_html"`), and this callback reads that field
-			// off the row's `listItem` and pastes its sanitized HTML into
-			// `element.ref`. PHP renders the same HTML server-side, so
-			// hydration is a no-op when the row's listItem hasn't changed,
-			// and a clean swap when it has (e.g. after Remove shifts the
-			// next item into this slot).
+			// Shared innerHTML-swap callback for slots whose content is one of the schema's preformatted
+			// HTML fields. The watched element carries `data-wp-context='{"htmlField":"price_html"}'` (or
+			// `"image_html"`). This reads the named field off the row's `listItem` and writes its
+			// sanitized HTML into `element.ref`. PHP renders the same HTML server-side, so hydration is
+			// a no-op until the row's `listItem` changes.
 			updateInnerHtml: () => {
 				const { ref } = getElement();
 				const { listItem, htmlField } = getContext< BlockContext >();
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/save.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/save.tsx
index 6913e34a3d2..7ed56b68a6e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/save.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/saved-for-later/save.tsx
@@ -3,12 +3,6 @@
  */
 import { InnerBlocks } from '@wordpress/block-editor';

-/**
- * The block is rendered server-side, but the inner blocks (default
- * `core/heading`) must be serialized to `post_content` so user edits
- * persist — returning `null` would drop them. `<InnerBlocks.Content />`
- * emits only the inner blocks' static markup.
- */
 const Save = (): JSX.Element => <InnerBlocks.Content />;

 export default Save;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/edit.tsx
index 38bc1f98bf6..caf1e3702ed 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/edit.tsx
@@ -23,9 +23,6 @@ interface EditProps {
 const MIN_COLUMNS = 2;
 const MAX_COLUMNS = 6;

-// Lives in JS because `__()` is needed for the heading copy. `block.json`
-// strings aren't run through translation, so keeping the template here
-// is the only way to ship a localized default.
 const TEMPLATE: [ string, Record< string, unknown > ][] = [
 	[ 'core/heading', { content: __( 'Wishlist', 'woocommerce' ), level: 2 } ],
 ];
@@ -76,11 +73,6 @@ const Edit = ( { attributes, setAttributes }: EditProps ): JSX.Element => {
 		className: 'wc-block-wishlist',
 	} );

-	// `allowedBlocks` is read from block.json automatically — passing it
-	// here would just duplicate the declaration. `templateLock: false`
-	// is the default so we omit that too. The className matches the
-	// `<div>` PHP wraps `$content` in on the frontend, so any CSS keyed
-	// off `__header` applies in both contexts.
 	const innerBlocksProps = useInnerBlocksProps(
 		{ className: 'wc-block-wishlist__header' },
 		{ template: TEMPLATE }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/frontend.ts
index a9d6451fcc3..9cffc36a3ba 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/frontend.ts
@@ -53,10 +53,8 @@ type BlockStore = {
 	};
 };

-// Allow-list for sanitizing the schema's preformatted strings on innerHTML
-// swap. Covers what `wc_price` (sale/discount markup, currency symbol) and
-// `wp_get_attachment_image` / `wc_placeholder_img` emit (responsive image
-// + dimensions + lazy loading).
+// Allow-list for sanitizing the schema's preformatted strings on innerHTML swap. Covers the markup
+// emitted by `wc_price` and `wp_get_attachment_image` / `wc_placeholder_img`.
 const ALLOWED_TAGS = [
 	'a',
 	'b',
@@ -139,10 +137,9 @@ store< BlockStore >(
 				return !! listItem && !! pendingKeys[ listItem.key ];
 			},

-			// No `hasShownItems` gate: the visitor reached this block
-			// deliberately (My Account endpoint or merchant-placed), so
-			// showing the empty message immediately when the list is
-			// empty is the right signal.
+			// Unlike Saved for Later, Wishlist has no `hasShownItems` first-paint guard. The block is
+			// reached deliberately (My Account endpoint or a merchant-placed instance), so the empty
+			// state should be visible immediately on first paint when the list is empty.
 			get isEmpty(): boolean {
 				const list = getList( LIST_SLUG );
 				if ( ! list ) {
@@ -164,12 +161,9 @@ store< BlockStore >(
 				return ! listItem.is_purchasable;
 			},

-			// `data-wp-text` writes its argument as text-content without
-			// running entity decoding, so a name returned by the schema as
-			// `Tom &amp; Jerry` would render literally that way. Bind
-			// templates and SSR text spans to this getter instead of the
-			// raw context field so what the browser shows matches what
-			// PHP wrote on first paint.
+			// `data-wp-text` writes its argument as text-content without entity decoding, so a name like
+			// `Tom &amp; Jerry` would render with the literal entity. Bind templates and SSR text spans
+			// to this getter (not the raw context field) so rendered text matches PHP's first paint.
 			get currentItemDisplayName(): string {
 				const { listItem } = getContext< BlockContext >();
 				return listItem ? decodeEntities( listItem.name ) : '';
@@ -222,13 +216,10 @@ store< BlockStore >(
 					return;
 				}

-				// Map the schema's `variation` shape to the cart's
-				// SelectedAttributes shape. The schema returns the
-				// slug-form attribute under `raw_attribute` (e.g.
-				// `attribute_pa_color`) plus a display label under
-				// `attribute` (e.g. "Color"); the cart matches by the
-				// slug-form, so override `attribute` with `raw_attribute`.
-				// Empty for simple products.
+				// Map the schema's `variation` shape to the cart's `SelectedAttributes` shape. The schema
+				// exposes the slug-form attribute under `raw_attribute` and a display label under
+				// `attribute`. The cart matches by the slug form, so `attribute` is overridden with
+				// `raw_attribute`. Empty for simple products.
 				const variation = listItem.variation.map(
 					( { raw_attribute: rawAttribute, value, attribute } ) => ( {
 						attribute: rawAttribute || attribute,
@@ -237,14 +228,11 @@ store< BlockStore >(
 				);
 				const isVariation = listItem.variation_id > 0;

-				// Wishlist always adds quantity 1 (no quantity column).
-				// `cartActions.addCartItem` catches its own errors and
-				// surfaces them as store notices, so the yield resolves
-				// the same way on success and failure. Snapshot the
-				// matching line's quantity, run the add, then only remove
-				// from the wishlist if the cart line actually grew — that
-				// guards against partial-stock and silent-failure paths
-				// where we shouldn't drop the wishlist entry.
+				// Wishlist always adds quantity 1 (no quantity column). `cartActions.addCartItem` catches
+				// its own errors and surfaces them as store notices, so the yield resolves identically
+				// on success and failure. Snapshot the matching line's quantity, run the add, and only
+				// remove from the wishlist if the cart line grew. Guards against partial-stock paths
+				// where the wishlist entry must remain.
 				const lookup = {
 					id: listItem.id,
 					...( isVariation && { variation } ),
@@ -279,16 +267,11 @@ store< BlockStore >(
 		},

 		callbacks: {
-			// Single shared innerHTML-swap callback for any slot whose
-			// content is one of the schema's preformatted HTML fields.
-			// Mirrors the atomic product-elements `updateValue` callback:
-			// the watched element carries `data-wp-context='{"htmlField":"price_html"}'`
-			// (or `"image_html"`), and this callback reads that field
-			// off the row's `listItem` and pastes its sanitized HTML into
-			// `element.ref`. PHP renders the same HTML server-side, so
-			// hydration is a no-op when the row's listItem hasn't changed,
-			// and a clean swap when it has (e.g. after Remove shifts the
-			// next item into this slot).
+			// Shared innerHTML-swap callback for slots whose content is one of the schema's preformatted
+			// HTML fields. The watched element carries `data-wp-context='{"htmlField":"price_html"}'` (or
+			// `"image_html"`). This reads the named field off the row's `listItem` and writes its
+			// sanitized HTML into `element.ref`. PHP renders the same HTML server-side, so hydration is
+			// a no-op until the row's `listItem` changes.
 			updateInnerHtml: () => {
 				const { ref } = getElement();
 				const { listItem, htmlField } = getContext< BlockContext >();
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/save.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/save.tsx
index 6913e34a3d2..7ed56b68a6e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/save.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/save.tsx
@@ -3,12 +3,6 @@
  */
 import { InnerBlocks } from '@wordpress/block-editor';

-/**
- * The block is rendered server-side, but the inner blocks (default
- * `core/heading`) must be serialized to `post_content` so user edits
- * persist — returning `null` would drop them. `<InnerBlocks.Content />`
- * emits only the inner blocks' static markup.
- */
 const Save = (): JSX.Element => <InnerBlocks.Content />;

 export default Save;
diff --git a/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts b/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
index a1277513d2f..2ef9d387fe6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
@@ -486,19 +486,10 @@ export const removeItemFromCart =
 	};

 /**
- * Saves a cart line item to the saved-for-later shopper list.
- *
- * On success, emits a `wc-blocks_store_sync_required` event with the saved
- * item in `detail.item` so a `woocommerce/shopper-lists` iAPI store on the
- * same page (rendered by a Saved for Later block) can splice the row
- * into its local state — no extra GET, no race window between a slow
- * refetch and concurrent mutations. Same envelope the cart's iAPI → wp.data
- * sync uses to ship payloads (`detail.type === 'from_iAPI'` carries
- * `quantityChanges`); this is the wp.data → iAPI direction of the same
- * pattern.
- *
- * Removing the item from the cart is the caller's responsibility — keep the
- * two awaits separate so save and remove errors can be reported distinctly.
+ * Save a cart line item to the saved-for-later shopper list. On success, dispatches a
+ * `wc-blocks_store_sync_required` event so a `woocommerce/shopper-lists` iAPI store on the same page can
+ * splice the returned item into local state without an extra GET. Removing the item from the cart is the
+ * caller's responsibility, so save and remove errors can be reported independently.
  *
  * @param {string} cartItemKey Cart item to save.
  */
diff --git a/plugins/woocommerce/client/blocks/assets/js/data/shared-controls.ts b/plugins/woocommerce/client/blocks/assets/js/data/shared-controls.ts
index e1cbd76034a..4ea04bd85fa 100644
--- a/plugins/woocommerce/client/blocks/assets/js/data/shared-controls.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/data/shared-controls.ts
@@ -130,9 +130,9 @@ const preventBatching = [
 	'/wc/store/v1/checkout',
 	'/wc/store/v1/checkout?__experimental_calc_totals=true',
 	'/wc/store/v1/cart/update-item',
-	// Shopper-lists routes don't declare allow_batch yet. Drop these once
-	// the routes opt into batching server-side.
+	// Shopper-lists routes do not declare `allow_batch` server-side yet.
 	'/wc/store/v1/shopper-lists/saved-for-later/items',
+	'/wc/store/v1/shopper-lists/wishlist/items',
 ];

 /**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToWishlistButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToWishlistButton.php
index cc088fd2707..3d9743f0d94 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToWishlistButton.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToWishlistButton.php
@@ -8,21 +8,10 @@ use Automattic\WooCommerce\Blocks\Utils\BlocksSharedState;
 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListRenderer;

 /**
- * Add to Wishlist Button block.
- *
- * Single-product trigger UI for the wishlist. Shipped as an inner block of
- * `woocommerce/add-to-cart-with-options` (ATCWO) via the per-product-type
- * template parts, so it always renders inside the form's iAPI scope and can
- * read its `selectedAttributes` context directly. The `ancestor` restriction
- * in `block.json` prevents merchants from inserting the block outside ATCWO
- * (where it'd lose iAPI scope and the variation-attribute read would break).
- *
- * Hidden for guests and gated by the `product_wishlist` feature flag. On
- * click, toggles the currently configured product (parent or selected
- * variation) in the shopper's wishlist via the shared
- * `woocommerce/shopper-lists` iAPI store. Errors are surfaced through the
- * page's existing `woocommerce/store-notices` region — no inline notices
- * area of its own.
+ * Add to Wishlist Button block. Single-product toggle for the wishlist, shipped as an inner block of
+ * `woocommerce/add-to-cart-with-options` so it renders inside the form's iAPI scope and can read its
+ * selected variation. On click, toggles the configured product in the shopper's wishlist via the shared
+ * `woocommerce/shopper-lists` iAPI store. Logged-in only and behind the `product_wishlist` feature flag.
  */
 final class AddToWishlistButton extends AbstractBlock {
 	/**
@@ -46,8 +35,7 @@ final class AddToWishlistButton extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		// Guests can't have a wishlist — bail before enqueuing assets or
-		// seeding state.
+		// Guests have no personal list. Bail before enqueuing assets or seeding state.
 		if ( ! is_user_logged_in() ) {
 			return '';
 		}
@@ -69,12 +57,7 @@ final class AddToWishlistButton extends AbstractBlock {

 		$items = $this->prefetch_items();

-		// Seed the shared shopper-lists store the same way the Wishlist
-		// block does — restUrl + starter nonce + prefetched items. The
-		// two blocks may both render on the same page (e.g. the merchant
-		// drops the Wishlist block into a sidebar of single-product); iAPI's
-		// deep-merge keeps the first writer's payload, so seeding identical
-		// values here is a no-op when Wishlist already ran.
+		// Seed the shared shopper-lists store with the REST URL, prefetched items, and a bootstrap nonce.
 		wp_interactivity_state(
 			'woocommerce/shopper-lists',
 			array(
@@ -89,10 +72,7 @@ final class AddToWishlistButton extends AbstractBlock {
 			)
 		);

-		// Visible labels flow through `wp_interactivity_config` so the
-		// JS-side getter can pick the right one based on
-		// `state.isInWishlist`. PHP renders the matching one as the
-		// initial server-side label.
+		// Visible labels passed through `wp_interactivity_config` for the JS-side getter.
 		wp_interactivity_config(
 			'woocommerce/add-to-wishlist-button',
 			array(
@@ -163,10 +143,8 @@ final class AddToWishlistButton extends AbstractBlock {
 	}

 	/**
-	 * Prefetch the wishlist items via `rest_do_request()`. Logged-out
-	 * users short-circuit to an empty list — the route requires
-	 * authentication and we don't want to fire an API call that's only
-	 * going to 401.
+	 * Prefetch the wishlist items via `rest_do_request()`. Returns an empty
+	 * list for logged-out users, since the route requires authentication.
 	 *
 	 * @return array<int, array<string, mixed>> Items in the schema response shape.
 	 */
@@ -201,10 +179,8 @@ final class AddToWishlistButton extends AbstractBlock {
 	}

 	/**
-	 * Whether the current product (or its parent, for a variable parent
-	 * with no selection yet) is already in the prefetched wishlist. For
-	 * variable products the SSR star is always empty — we can't know which
-	 * variation the shopper will pick before JS hydrates.
+	 * Whether the current product is already in the prefetched wishlist. Always false for variable
+	 * products, since the selected variation is not known until JS hydrates.
 	 *
 	 * @param array<int, array<string, mixed>> $items   Schema-shape items.
 	 * @param \WC_Product                      $product The product being viewed.
@@ -237,9 +213,7 @@ final class AddToWishlistButton extends AbstractBlock {
 	}

 	/**
-	 * Visible label when the shopper still needs to pick variation
-	 * attributes before the wishlist toggle can resolve to a specific
-	 * variation.
+	 * Visible label shown until the shopper picks variation attributes.
 	 */
 	private function get_select_options_label(): string {
 		return __( 'Select options first', 'woocommerce' );
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/SavedForLater.php b/plugins/woocommerce/src/Blocks/BlockTypes/SavedForLater.php
index b0d5eb8750b..f74b0d926aa 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/SavedForLater.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/SavedForLater.php
@@ -9,25 +9,14 @@ use Automattic\WooCommerce\Internal\ShopperLists\ShopperListRenderer;
 use Automattic\WooCommerce\Proxies\LegacyProxy;

 /**
- * Saved for Later block.
- *
- * Renders the shopper's "Saved for Later" list, wired to the `shopper-lists`
- * Store API endpoints via the shared `woocommerce/shopper-lists` iAPI store.
- * PHP prefetches the list so the first paint is already populated; JS then
- * takes over for adds, removes, and Move-to-cart.
- *
- * The row markup (image, name, price, remove badge, variation overlay) is
- * shared with other shopper-list blocks via `ShopperListRenderer`. This
- * class composes those fragments and adds the bits that are unique to
- * Saved for Later: auto-injection via the Block Hooks API, the
- * `hasShownItems` empty-state gating, the per-row quantity span, and the
- * Move-to-cart action button.
+ * Saved for Later block. Renders the shopper's Saved for Later list from the `shopper-lists` Store API,
+ * sharing state with the cart via the `woocommerce/shopper-lists` iAPI store. Shared row markup lives in
+ * {@see ShopperListRenderer}. Adds Block Hooks auto-injection, the empty-state guard, the quantity span,
+ * and the Move-to-cart action.
  */
 final class SavedForLater extends AbstractBlock {
 	/**
-	 * The list slug this block renders. Constant — when additional list
-	 * types ship as their own blocks (e.g. Wishlist), each one will
-	 * hardcode its own slug.
+	 * Slug of the shopper list this block renders.
 	 */
 	private const LIST_SLUG = 'saved-for-later';

@@ -44,7 +33,7 @@ final class SavedForLater extends AbstractBlock {
 	protected function initialize(): void {
 		parent::initialize();

-		// We do not use `BlockHooksTrait` currently as it has issues with PHPStan.
+		// `BlockHooksTrait` is not used here because of PHPStan issues with the trait's annotations.
 		add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 9, 4 );
 		add_filter( 'hooked_block_woocommerce/saved-for-later', array( $this, 'set_hooked_block_attributes' ), 10, 4 );
 	}
@@ -69,8 +58,7 @@ final class SavedForLater extends AbstractBlock {
 			return $hooked_block_types;
 		}

-		// Don't double-inject if the block is already in the cart page
-		// content.
+		// Skip injection if the block is already present in the cart page content.
 		if ( has_block( $this->get_full_block_name(), $context ) ) {
 			return $hooked_block_types;
 		}
@@ -96,23 +84,17 @@ final class SavedForLater extends AbstractBlock {
 			return $parsed_hooked_block;
 		}

-		// Seed a `core/heading` inner block so freshly-injected instances
-		// ship with the same heading the editor template seeds. We append
-		// unconditionally — extensions are free to hook
-		// `hooked_block_woocommerce/saved-for-later` to add their own
-		// inner blocks, and gating on `empty( innerBlocks )` would silently
-		// suppress our heading whenever any other extension ran first.
+		// Seed a `core/heading` inner block so auto-injected instances carry the editor template's heading.
+		// Append unconditionally: checking `empty( $parsed_hooked_block['innerBlocks'] )` would suppress this
+		// heading whenever another extension already hooked into `hooked_block_woocommerce/saved-for-later`.
 		//
-		// `core/heading` is a static block, so the serialised markup must
-		// match what the editor would have saved (`<h2 class="wp-block-heading">…</h2>`)
-		// or it'll fail block validation when the cart page is opened in the
-		// editor. `attrs.content` mirrors what the editor's template seeds
-		// (`{ content, level }`) so the parsed shape round-trips identically;
-		// the value is the raw string because attrs are JSON-encoded into the
-		// block comment and `esc_html()` would corrupt translations whose text
-		// contains `&`, `<`, etc. The matching `null` push onto `innerContent`
-		// is what makes `WP_Block::render()` walk into the heading when
-		// building `$content`.
+		// `core/heading` is a static block, so the serialised markup must match the editor-saved form
+		// (`<h2 class="wp-block-heading">…</h2>`) or block validation will fail when the cart page is opened
+		// in the editor. `attrs.content` mirrors the editor template's seed (`{ content, level }`) so the
+		// parsed shape round-trips identically. The value is the raw string because attrs are JSON-encoded
+		// into the block comment delimiter, and `esc_html()` would corrupt translations containing `&`, `<`,
+		// etc. The matching `null` push onto `innerContent` is required for `WP_Block::render()` to descend
+		// into the heading when assembling `$content`.
 		$list_heading = __( 'Saved for later', 'woocommerce' );
 		$heading_html = '<h2 class="wp-block-heading">' . esc_html( $list_heading ) . '</h2>';

@@ -146,22 +128,16 @@ final class SavedForLater extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		// Guests have no personal list — bail before enqueuing assets or seeding state.
+		// Guests have no personal list. Bail before enqueuing assets or seeding state.
 		if ( ! is_user_logged_in() ) {
 			return '';
 		}

-		// Set from render() (not Cart::enqueue_data via has_block()) so it works when this
-		// block is auto-injected via the Block Hooks API and isn't in stored post_content.
 		if ( wc_get_container()->get( LegacyProxy::class )->call_function( 'is_cart' ) ) {
 			$this->asset_data_registry->add( 'cartPageHasSavedForLater', true );
 		}

-		// Clamp to the 2-6 range the SCSS `@for $i from 2 through 6` loop and
-		// the editor `RangeControl` both support. `absint()` first defends
-		// against a code-editor override (the attribute can be set to any
-		// JSON value there); the `min`/`max` then keep the value within the
-		// range where a `&.columns-#{$i}` rule actually exists.
+		// Clamp to the 2-6 range supported by the SCSS.
 		$column_count = min( 6, max( 2, absint( $attributes['columnCount'] ?? 5 ) ) );

 		wp_enqueue_script_module( $this->get_full_block_name() );
@@ -169,21 +145,12 @@ final class SavedForLater extends AbstractBlock {
 		$consent = 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WooCommerce';
 		BlocksSharedState::load_store_config( $consent );
 		BlocksSharedState::load_placeholder_image( $consent );
-		// `Move to cart` calls into the shared cart store, which expects
-		// `state.cart.items` and friends. Without this load the cart store
-		// would have no hydrated cart and the action would throw on the
-		// first click.
+		// Required so the Move-to-cart action has a hydrated cart store to dispatch into.
 		BlocksSharedState::load_cart_state( $consent );

 		$items = $this->prefetch_items();

-		// Seed the shared shopper-lists store with the rest URL, the
-		// pre-fetched items, and a starter nonce. The starter nonce is
-		// what the cart store also seeds via `state.nonce` — the JS layer
-		// keeps it fresh by reading the `Nonce` response header on every
-		// subsequent request, so this is just the bootstrap value (and
-		// avoids deadlocking mutations that await `isNonceReady` before
-		// any GET has fired).
+		// Seed the shared shopper-lists store with the REST URL, prefetched items, and a bootstrap nonce.
 		wp_interactivity_state(
 			'woocommerce/shopper-lists',
 			array(
@@ -198,10 +165,7 @@ final class SavedForLater extends AbstractBlock {
 			)
 		);

-		// Templates flow through `wp_interactivity_config` so the JS-side
-		// getters can interpolate them (`%d`, `%s`). Visible strings (empty
-		// state, error, action label) are rendered server-side and toggled
-		// with directives, so they don't need to ride here too.
+		// Sprintf templates passed through `wp_interactivity_config` for JS-side interpolation.
 		wp_interactivity_config(
 			'woocommerce/saved-for-later',
 			array(
@@ -210,25 +174,14 @@ final class SavedForLater extends AbstractBlock {
 			)
 		);

-		// `hasShownItems` seeds the per-block context so the empty message
-		// stays hidden for new shoppers who land on a page with nothing
-		// saved. The JS-side watcher flips it to `true` the first time the
-		// list has any items (whether that's the SSR seed or a runtime add
-		// via "Save for later"), and `state.isEmpty` only flips on when the
-		// flag is set *and* the list is currently empty. The flag lives in
-		// the per-block context, so it naturally resets on every full page
-		// load — no extra Store API field or persisted flag needed.
-		// `data-wp-context---notices` seeds the store-notices namespace
-		// alongside the block's own context on the same wrapper.
+		// `hasShownItems` seeds the per-block context that controls the empty message.
 		$wrapper_attributes = array(
 			'class'                     => 'wc-block-saved-for-later',
 			'data-wp-interactive'       => 'woocommerce/saved-for-later',
 			'data-wp-context'           => (string) wp_json_encode(
 				array(
 					'hasShownItems' => ! empty( $items ),
-					// `stdClass` so it serialises as `{}`, not `[]` —
-					// iAPI's reactive proxy only fires updates on object
-					// writes, not array expandos.
+					// `stdClass` so JSON serializes as `{}` rather than `[]`.
 					'pendingKeys'   => new \stdClass(),
 				)
 			),
@@ -245,9 +198,8 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Prefetch the saved-for-later items via `rest_do_request()`. Logged-out
-	 * users short-circuit to an empty list — the route requires authentication
-	 * and we don't want to fire an API call that's only going to 401.
+	 * Prefetch the saved-for-later items via `rest_do_request()`. Returns an empty
+	 * list for logged-out users, since the route requires authentication.
 	 *
 	 * @return array<int, array<string, mixed>> Items in the schema response shape.
 	 */
@@ -262,11 +214,7 @@ final class SavedForLater extends AbstractBlock {
 		if ( $response->is_error() ) {
 			$error   = $response->as_error();
 			$message = $error instanceof \WP_Error ? $error->get_error_message() : 'Unknown error';
-			// Logged at debug level on purpose: prefetch failures are
-			// often transient (network blips, auth refresh races) and
-			// the user-visible behaviour is the empty state — nothing
-			// for ops to act on. Anyone investigating a regression can
-			// flip the WC logger to debug to surface them.
+			// Logged for diagnostics.
 			wc_get_logger()->debug(
 				sprintf( 'Saved for Later prefetch failed: %s', $message ),
 				array(
@@ -282,19 +230,15 @@ final class SavedForLater extends AbstractBlock {
 			return array();
 		}

-		// The schema casts `prices` and image entries to stdClass so the
-		// JSON response renders objects, not arrays. Round-trip through
-		// JSON encode/decode to normalise everything to nested arrays so
-		// the SSR markup helpers below can treat fields uniformly.
+		// The schema casts `prices` and image entries to stdClass so JSON renders them as objects.
+		// Round-trip through JSON to normalise everything to nested arrays for the SSR markup helpers.
 		$decoded = json_decode( (string) wp_json_encode( $data ), true );
 		return is_array( $decoded ) ? $decoded : array();
 	}

 	/**
-	 * The `<template data-wp-each>` describing how each item is rendered on
-	 * the client. Pre-rendered children sit alongside as `data-wp-each-child`
-	 * elements so first paint is populated. Composes the shared row markup
-	 * with Saved for Later's quantity span and Move-to-cart action button.
+	 * Render the `<template data-wp-each>` used by iAPI to render rows on the client. Pre-rendered
+	 * `data-wp-each-child` elements sit alongside to populate first paint.
 	 *
 	 * @return string
 	 */
@@ -306,8 +250,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Render the SSR markup for each item. JS will reconcile these via
-	 * `data-wp-each-child` after hydration.
+	 * Render the SSR markup for each item. Reconciled by iAPI via `data-wp-each-child` after hydration.
 	 *
 	 * @param array<int, array<string, mixed>> $items Schema-shape items.
 	 * @return string
@@ -321,8 +264,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Render a single SSR item. Composes the shared image / name / price
-	 * markup with the SFL-specific quantity span and Move-to-cart button.
+	 * Render a single SSR item, combining the shared row markup with the quantity span and Move-to-cart button.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -335,8 +277,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Template-mode markup for the quantity span. SFL-specific — Wishlist
-	 * has no quantity column.
+	 * Template-mode markup for the quantity span.
 	 *
 	 * @return string
 	 */
@@ -348,7 +289,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Template-mode markup for the Move-to-cart action button. SFL-specific.
+	 * Template-mode markup for the Move-to-cart action button.
 	 *
 	 * @return string
 	 */
@@ -370,7 +311,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * SSR-mode markup for the quantity span. SFL-specific.
+	 * SSR-mode markup for the quantity span.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -386,10 +327,8 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * SSR-mode markup for the Move-to-cart action button. SFL-specific.
-	 * Always emits the wrapper so iAPI can toggle `hidden` after hydration
-	 * without swapping the row out. Starts hidden when the row isn't
-	 * purchasable.
+	 * SSR-mode markup for the Move-to-cart action button. The wrapper is always emitted so iAPI can toggle
+	 * `hidden` after hydration. Starts hidden when the row is not purchasable.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -421,14 +360,10 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Wrap the inner-block content (heading + any future siblings) in an
-	 * element whose visibility mirrors the empty-state gating: hidden when
-	 * the shopper has never seen items in this session, revealed once
-	 * `context.hasShownItems` flips to `true`. Returns an empty string when
-	 * there's no content to wrap (e.g. merchant deleted the heading and
-	 * saved), so we don't emit an empty `<div>`.
+	 * Wrap the inner-block content in a wrapper that mirrors the empty-state visibility. Hidden until
+	 * `context.hasShownItems` flips to `true`. Returns an empty string when no content needs wrapping.
 	 *
-	 * @param string $content  Rendered inner-block content (typically the heading HTML).
+	 * @param string $content  Rendered inner-block content (usually the heading HTML).
 	 * @param bool   $is_empty Whether the saved-for-later list is empty on initial paint.
 	 * @return string
 	 */
@@ -445,10 +380,8 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Render the empty-state markup. Always present in the DOM so JS can
-	 * toggle it on once the last item is removed. Initially hidden: SSR
-	 * never shows the message, since `state.isEmpty` requires the JS-side
-	 * `hasShownItems` context flag to flip first.
+	 * Render the empty-state markup. Always present in the DOM so iAPI can reveal it once the last item is
+	 * removed. Initially hidden: `state.isEmpty` requires the `hasShownItems` context flag to flip first.
 	 *
 	 * @return string
 	 */
@@ -461,10 +394,8 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Sprintf template for the per-row quantity label. Used both by PHP SSR
-	 * (`render_ssr_quantity()`) and by the JS-side getter (via
-	 * `wp_interactivity_config`) so both paths produce the same string after
-	 * `%d` interpolation.
+	 * Sprintf template for the per-row quantity label. Shared between PHP SSR and the JS-side getter
+	 * (seeded via `wp_interactivity_config`) so both paths produce identical output.
 	 */
 	private function get_quantity_label_template(): string {
 		/* translators: %d: quantity of saved items. */
@@ -472,8 +403,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Sprintf template for the per-row remove button's aria-label. Same dual
-	 * use as the quantity template.
+	 * Sprintf template for the per-row remove button's aria-label. Shared between PHP SSR and JS.
 	 */
 	private function get_remove_label_template(): string {
 		/* translators: %s: product name. */
@@ -481,8 +411,7 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Visible label for the move-to-cart action button, used by both the
-	 * iAPI `<template>` and the SSR per-row markup.
+	 * Visible label for the Move-to-cart action button. Used by the iAPI `<template>` and the SSR markup.
 	 */
 	private function get_move_to_cart_label(): string {
 		return __( 'Move to cart', 'woocommerce' );
@@ -501,16 +430,8 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Get the frontend style handle for this block type.
-	 *
-	 * Returning null lets WP use the `style` array from block.json, which
-	 * lists this block's own stylesheet plus the atomic
-	 * product-image / product-price / product-button stylesheets we
-	 * borrow class names from. We can't render those atomic blocks as
-	 * inner blocks (they rely on WP_Query / $post loop context, which
-	 * this block doesn't have — it hydrates from a Store API call), so
-	 * declaring them as style dependencies is the only way to get WP
-	 * to enqueue their CSS whenever Saved for Later renders.
+	 * Frontend style handle. Returns null so WP loads the `style` array from block.json, which lists this
+	 * block's stylesheet and the atomic product-image/price/button stylesheets whose classes it reuses.
 	 *
 	 * @return null
 	 */
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php b/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php
index 1a26fb39d39..436a34270f7 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php
@@ -8,24 +8,14 @@ use Automattic\WooCommerce\Blocks\Utils\BlocksSharedState;
 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListRenderer;

 /**
- * Wishlist block.
- *
- * Renders the shopper's wishlist, wired to the `shopper-lists` Store API
- * endpoints via the shared `woocommerce/shopper-lists` iAPI store. PHP
- * prefetches the list so the first paint is already populated; JS then
- * takes over for adds, removes, and the per-row "Add to cart" action.
- *
- * Unlike Saved for Later, this block is merchant-placed — no Block Hooks
- * API integration. It's rendered by the `/my-account/wishlist/` endpoint
- * (gated by the `product_wishlist` feature flag) and can also be placed
- * on any other page or template. "Add to cart" mirrors Saved for Later's
- * Move-to-cart flow: add the product to the cart, then remove it from the
- * wishlist on confirmed success.
+ * Wishlist block. Renders the shopper's wishlist via the `shopper-lists` Store API and the shared
+ * `woocommerce/shopper-lists` iAPI store. Merchant-placed (no Block Hooks integration), and also rendered
+ * by the `/my-account/wishlist/` endpoint when the `product_wishlist` feature flag is enabled. The Add to
+ * cart action adds the product to the cart and removes the row from the wishlist on confirmed success.
  */
 final class Wishlist extends AbstractBlock {
 	/**
-	 * The list slug this block renders. Constant — when additional list
-	 * types ship as their own blocks, each one hardcodes its own slug.
+	 * Slug of the shopper list this block renders.
 	 */
 	private const LIST_SLUG = 'wishlist';

@@ -45,20 +35,13 @@ final class Wishlist extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		// Guests have no personal list — bail before enqueuing assets or
-		// seeding state. The My Account endpoint isn't reachable for
-		// guests, but the block can also be placed by a merchant on any
-		// page, where this guard is what stops it from rendering an
-		// empty shell for logged-out visitors.
+		// Guests have no personal list. The My Account endpoint is unreachable for guests, and the same
+		// guard is needed when a merchant places this block on any other page.
 		if ( ! is_user_logged_in() ) {
 			return '';
 		}

-		// Clamp to the 2-6 range the SCSS `@for $i from 2 through 6` loop
-		// and the editor `RangeControl` both support. `absint()` first
-		// defends against a code-editor override (the attribute can be set
-		// to any JSON value there); the `min`/`max` then keep the value
-		// within the range where a `&.columns-#{$i}` rule actually exists.
+		// Clamp to the 2-6 range supported by the SCSS.
 		$column_count = min( 6, max( 2, absint( $attributes['columnCount'] ?? 5 ) ) );

 		wp_enqueue_script_module( $this->get_full_block_name() );
@@ -66,21 +49,12 @@ final class Wishlist extends AbstractBlock {
 		$consent = 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WooCommerce';
 		BlocksSharedState::load_store_config( $consent );
 		BlocksSharedState::load_placeholder_image( $consent );
-		// `Add to cart` calls into the shared cart store, which expects
-		// `state.cart.items` and friends. Without this load the cart store
-		// would have no hydrated cart and the action would throw on the
-		// first click.
+		// Required so the Add to cart action has a hydrated cart store to dispatch into.
 		BlocksSharedState::load_cart_state( $consent );

 		$items = $this->prefetch_items();

-		// Seed the shared shopper-lists store with the rest URL, the
-		// pre-fetched items, and a starter nonce. The starter nonce is
-		// what the cart store also seeds via `state.nonce` — the JS layer
-		// keeps it fresh by reading the `Nonce` response header on every
-		// subsequent request, so this is just the bootstrap value (and
-		// avoids deadlocking mutations that await `isNonceReady` before
-		// any GET has fired).
+		// Seed the shared shopper-lists store with the REST URL, prefetched items, and a bootstrap nonce.
 		wp_interactivity_state(
 			'woocommerce/shopper-lists',
 			array(
@@ -95,9 +69,7 @@ final class Wishlist extends AbstractBlock {
 			)
 		);

-		// Only the remove-button aria-label template needs JS-side
-		// interpolation; visible strings (empty state, action label) are
-		// rendered server-side and toggled with directives.
+		// Only the remove-button aria-label template needs JS-side interpolation.
 		wp_interactivity_config(
 			'woocommerce/wishlist',
 			array(
@@ -105,22 +77,13 @@ final class Wishlist extends AbstractBlock {
 			)
 		);

-		// No `hasShownItems` flag: unlike Saved for Later (which auto-
-		// renders on every cart visit and must avoid flashing an empty
-		// message before a runtime save lands), Wishlist is reached
-		// deliberately — by the My Account endpoint or because a merchant
-		// placed it. Showing the empty message immediately is the right
-		// signal: the visitor came to look at their wishlist, and it's
-		// empty. `data-wp-context---notices` seeds the store-notices
-		// namespace alongside the block's own context on the same wrapper.
+		// No `hasShownItems` flag here, unlike Saved for Later: the empty message shows immediately.
 		$wrapper_attributes = array(
 			'class'                     => 'wc-block-wishlist',
 			'data-wp-interactive'       => 'woocommerce/wishlist',
 			'data-wp-context'           => (string) wp_json_encode(
 				array(
-					// `stdClass` so it serialises as `{}`, not `[]` —
-					// iAPI's reactive proxy only fires updates on object
-					// writes, not array expandos.
+					// `stdClass` so JSON serializes as `{}` rather than `[]`.
 					'pendingKeys' => new \stdClass(),
 				)
 			),
@@ -135,10 +98,8 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Prefetch the wishlist items via `rest_do_request()`. Logged-out
-	 * users short-circuit to an empty list — the route requires
-	 * authentication and we don't want to fire an API call that's only
-	 * going to 401.
+	 * Prefetch the wishlist items via `rest_do_request()`. Returns an empty
+	 * list for logged-out users, since the route requires authentication.
 	 *
 	 * @return array<int, array<string, mixed>> Items in the schema response shape.
 	 */
@@ -153,10 +114,7 @@ final class Wishlist extends AbstractBlock {
 		if ( $response->is_error() ) {
 			$error   = $response->as_error();
 			$message = $error instanceof \WP_Error ? $error->get_error_message() : 'Unknown error';
-			// Logged at debug level on purpose: prefetch failures are
-			// often transient (network blips, auth refresh races) and
-			// the user-visible behaviour is the empty state — nothing
-			// for ops to act on.
+			// Logged for diagnostics.
 			wc_get_logger()->debug(
 				sprintf( 'Wishlist prefetch failed: %s', $message ),
 				array(
@@ -172,20 +130,15 @@ final class Wishlist extends AbstractBlock {
 			return array();
 		}

-		// The schema casts `prices` and image entries to stdClass so the
-		// JSON response renders objects, not arrays. Round-trip through
-		// JSON encode/decode to normalise everything to nested arrays so
-		// the SSR markup helpers can treat fields uniformly.
+		// The schema casts `prices` and image entries to stdClass so JSON renders them as objects.
+		// Round-trip through JSON to normalise everything to nested arrays for the SSR markup helpers.
 		$decoded = json_decode( (string) wp_json_encode( $data ), true );
 		return is_array( $decoded ) ? $decoded : array();
 	}

 	/**
-	 * The `<template data-wp-each>` describing how each item is rendered
-	 * on the client. Pre-rendered children sit alongside as
-	 * `data-wp-each-child` elements so first paint is populated. Composes
-	 * the shared row markup with the Wishlist-specific "Add to cart"
-	 * action button.
+	 * Render the `<template data-wp-each>` used by iAPI to render rows on the client. Pre-rendered
+	 * `data-wp-each-child` elements sit alongside to populate first paint.
 	 *
 	 * @return string
 	 */
@@ -196,8 +149,7 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Render the SSR markup for each item. JS will reconcile these via
-	 * `data-wp-each-child` after hydration.
+	 * Render the SSR markup for each item. Reconciled by iAPI via `data-wp-each-child` after hydration.
 	 *
 	 * @param array<int, array<string, mixed>> $items Schema-shape items.
 	 * @return string
@@ -211,8 +163,7 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Render a single SSR item. Composes the shared image / name / price
-	 * markup with the Wishlist-specific "Add to cart" button.
+	 * Render a single SSR item, combining the shared row markup with the Add to cart button.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -224,9 +175,8 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Template-mode markup for the "Add to cart" action button. iAPI
-	 * substitutes the per-row state through `data-wp-bind--hidden` and
-	 * `data-wp-bind--disabled`.
+	 * Template-mode markup for the Add to cart action button. iAPI substitutes the per-row state through
+	 * `data-wp-bind--hidden` and `data-wp-bind--disabled`.
 	 *
 	 * @return string
 	 */
@@ -248,9 +198,8 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * SSR-mode markup for the "Add to cart" action button. Always emits
-	 * the wrapper so iAPI can toggle `hidden` after hydration without
-	 * swapping the row out. Starts hidden when the row isn't purchasable.
+	 * SSR-mode markup for the Add to cart action button. The wrapper is always emitted so iAPI can toggle
+	 * `hidden` after hydration. Starts hidden when the row is not purchasable.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -282,13 +231,10 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Wrap the inner-block content (heading + any future siblings) in a
-	 * div. Unlike Saved for Later, no `hasShownItems` gating — the header
-	 * is always shown when there's content for it. Returns an empty
-	 * string when there's no content to wrap, so we don't emit an empty
-	 * `<div>`.
+	 * Wrap the inner-block content in a div. The header is always shown when content is present, with no
+	 * `hasShownItems` guard. Returns an empty string when there is no content, to avoid an empty `<div>`.
 	 *
-	 * @param string $content Rendered inner-block content (typically the heading HTML).
+	 * @param string $content Rendered inner-block content (usually the heading HTML).
 	 * @return string
 	 */
 	private function render_header_markup( string $content ): string {
@@ -299,9 +245,8 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Render the empty-state markup. Visible on first paint when the
-	 * list is empty (no `hasShownItems` gate), then iAPI takes over via
-	 * `state.isEmpty` for runtime transitions.
+	 * Render the empty-state markup. Visible on first paint when the list is empty. iAPI handles runtime
+	 * transitions via `state.isEmpty`.
 	 *
 	 * @param array<int, array<string, mixed>> $items Schema-shape items.
 	 * @return string
@@ -315,10 +260,8 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Sprintf template for the per-row remove button's aria-label. Used
-	 * both by PHP SSR and by the JS-side getter (via
-	 * `wp_interactivity_config`) so both paths produce the same string
-	 * after `%s` interpolation.
+	 * Sprintf template for the per-row remove button aria-label. Shared between PHP SSR and the JS-side
+	 * getter (seeded via `wp_interactivity_config`) so both paths produce identical output.
 	 */
 	private function get_remove_label_template(): string {
 		/* translators: %s: product name. */
@@ -326,8 +269,7 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Visible label for the add-to-cart action button, used by both the
-	 * iAPI `<template>` and the SSR per-row markup.
+	 * Visible label for the Add to cart action button. Used by the iAPI `<template>` and the SSR markup.
 	 */
 	private function get_add_to_cart_label(): string {
 		return __( 'Add to cart', 'woocommerce' );
@@ -346,12 +288,8 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Get the frontend style handle for this block type.
-	 *
-	 * Returning null lets WP use the `style` array from block.json, which
-	 * lists this block's own stylesheet plus the atomic
-	 * product-image / product-price / product-button stylesheets we
-	 * borrow class names from.
+	 * Frontend style handle. Returns null so WP loads the `style` array from block.json, which lists this
+	 * block's stylesheet and the atomic product-image/price/button stylesheets whose classes it reuses.
 	 *
 	 * @return null
 	 */
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php
index 48fa653dfff..13e9441535f 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php
@@ -70,8 +70,6 @@ class ShopperList {
 	 * @return self|false
 	 */
 	public static function get_by_slug( string $slug, ?int $user_id = null ) {
-		// Gate disabled or unknown slugs upfront so previously-persisted lists
-		// don't bypass the feature flag (the Store API surfaces this as 404).
 		if ( ! wc_get_container()->get( ShopperListsController::class )->is_enabled( $slug ) ) {
 			return false;
 		}
@@ -87,7 +85,7 @@ class ShopperList {
 			return self::from_array( $stored, $user_id );
 		}

-		// In-memory list; saved on the first save().
+		// In-memory list. Persisted on the first save().
 		return new self(
 			$user_id,
 			$slug,
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
index 139a05d8d62..85a784c374c 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListItem.php
@@ -166,9 +166,9 @@ class ShopperListItem {
 	/**
 	 * Resolve and validate the variation attribute array against the variation product.
 	 *
-	 * Mirrors {@see CartController::parse_variation_data()}: specific values come from
-	 * the variation (server-authoritative); "any" slots must be supplied by the caller
-	 * with a value that exists on the parent product.
+	 * Mirrors {@see CartController::parse_variation_data()}: specific values come from the variation
+	 * (server-authoritative). "Any" slots must be supplied by the caller with a value that exists on the
+	 * parent product.
 	 *
 	 * @throws \InvalidArgumentException When the supplied variation attributes are
 	 *                                   missing required values or don't match the
@@ -245,7 +245,7 @@ class ShopperListItem {
 	}

 	/**
-	 * Storage key — also used as the response identifier.
+	 * Storage key. Also used as the response identifier.
 	 */
 	public function get_key(): string {
 		return $this->key;
@@ -307,9 +307,8 @@ class ShopperListItem {
 	}

 	/**
-	 * Whether the row serves live product data. True when the product (and its
-	 * parent, for variations) is `publish`; password-gated products still
-	 * qualify since their page renders behind a prompt.
+	 * Whether the row serves live product data. True when the product (and its parent, for variations) is
+	 * `publish`. Password-protected products still qualify since their page renders behind a prompt.
 	 */
 	public function is_live(): bool {
 		$product = $this->get_product();
@@ -330,10 +329,9 @@ class ShopperListItem {
 	}

 	/**
-	 * Whether the product can be added to the cart. Mirrors the catalog gate
-	 * (`is_purchasable()` && `is_in_stock()`), but additionally requires the
-	 * row to be live and rejects password-gated products (self or parent) —
-	 * cart-add can't prompt for a password.
+	 * Whether the product can be added to the cart. Requires `is_purchasable()`, `is_in_stock()`, and
+	 * a live row. Password-protected products (self or parent) are rejected because the cart-add flow
+	 * cannot prompt for a password.
 	 */
 	public function is_purchasable(): bool {
 		$product = $this->get_product();
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListRenderer.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListRenderer.php
index a41a6549afa..383dde71db6 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListRenderer.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListRenderer.php
@@ -5,32 +5,19 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\ShopperLists;

 /**
- * Shared markup helpers for blocks that render a shopper-list item card
- * (Saved for Later, Wishlist, …). Static helpers, not an abstract base —
- * the two blocks' lifecycles diverge enough (auto-injected vs merchant-
- * placed, different actions, different empty-state gating) that inheritance
- * is not a clean fit. Consumers stitch the fragments together with their
- * own quantity / action button / heading bits.
- *
- * Any change here is co-reviewed with every consuming block — drift in the
- * shared row shape will break first paint for whoever didn't get the memo.
+ * Markup helpers for the shopper collection blocks.
  */
 final class ShopperListRenderer {

 	/**
-	 * Shared CSS root class for the row. Each section helper outputs
-	 * BEM-style modifiers off this base (`__image-slot`, `__remove`, …).
+	 * Shared CSS root class for the row.
 	 */
 	public const ROW_CLASS = 'wc-block-shopper-list-item';

 	/**
-	 * Wrap `$inner` in the block's outer `<section><ul>…</ul></section>`
-	 * grid scaffold. `$wrapper_attrs` are merged with the block's wrapper
-	 * attributes via `get_block_wrapper_attributes()`.
-	 *
-	 * Trust contract: callers are responsible for ensuring `$inner` and
-	 * `$before_list` contain only safe, escaped HTML — typically composed
-	 * from the section helpers below, never from raw schema/request input.
+	 * Wrap `$inner` in the block's outer `<section><ul>…</ul></section>` grid scaffold. `$wrapper_attrs`
+	 * are merged with the block's wrapper attributes via `get_block_wrapper_attributes()`. Callers must
+	 * ensure `$inner` and `$before_list` contain only pre-escaped markup. Both are emitted verbatim.
 	 *
 	 * @param array<string, mixed> $wrapper_attrs Attributes for the outer `<section>`.
 	 * @param string               $list_class    Class attribute for the inner `<ul>`.
@@ -49,12 +36,8 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Wrap `$row_inner_markup` in a `<template data-wp-each>` element that
-	 * iAPI uses to render new rows. `$row_inner_markup` is the inner HTML
-	 * for the `<li>` — everything between `<li>` and `</li>`.
-	 *
-	 * Trust contract: caller is responsible for ensuring `$row_inner_markup`
-	 * contains only safe, escaped HTML.
+	 * Wrap `$row_inner_markup` in a `<template data-wp-each>` element used by iAPI to render new rows.
+	 * Callers must ensure `$row_inner_markup` contains only pre-escaped markup. It is emitted verbatim.
 	 *
 	 * @param string $row_inner_markup Inner markup for the `<li>`.
 	 * @return string
@@ -68,13 +51,9 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Wrap `$row_inner_markup` in an SSR `<li data-wp-each-child>` element
-	 * seeded with the per-row iAPI context derived from `$item`. iAPI's
-	 * hydration treats this as a no-op diff against the `<template>` if
-	 * the inner markup matches.
-	 *
-	 * Trust contract: caller is responsible for ensuring `$row_inner_markup`
-	 * contains only safe, escaped HTML.
+	 * Wrap `$row_inner_markup` in an SSR `<li data-wp-each-child>` element seeded with the per-row iAPI
+	 * context from `$item`. Hydration is a no-op diff against the `<template>` when the inner markup
+	 * matches. Callers must ensure `$row_inner_markup` contains only pre-escaped markup.
 	 *
 	 * @param array<string, mixed> $item             Schema-shape item.
 	 * @param string               $row_inner_markup Inner markup for the `<li>`.
@@ -82,7 +61,7 @@ final class ShopperListRenderer {
 	 */
 	public static function render_each_child( array $item, string $row_inner_markup ): string {
 		$context = array( 'listItem' => $item );
-		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_interactivity_data_wp_context() returns a safely-encoded attribute pair; $row_inner_markup is composed of escaped fragments from the section helpers below.
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_interactivity_data_wp_context() returns a safely-encoded attribute pair, and $row_inner_markup is composed of escaped fragments from the section helpers below.
 		return sprintf(
 			'<li class="%1$s" data-wp-each-child %2$s>%3$s</li>',
 			esc_attr( self::ROW_CLASS ),
@@ -92,8 +71,7 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Render the image + title + price triplet for the template-mode row
-	 * (no static attrs; bindings only). Identical between consumer blocks.
+	 * Render the image + title + price triplet for the template-mode row (bindings only, no static attrs).
 	 *
 	 * @return string
 	 */
@@ -124,10 +102,8 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Render the image + title + price triplet for the SSR-mode row, with
-	 * values populated from `$item` and `$remove_aria_label_template`. The
-	 * binding directives match the template-mode markup so iAPI's hydration
-	 * is a no-op diff after first paint.
+	 * Render the image + title + price triplet for the SSR-mode row, populated from `$item`. Binding
+	 * directives match the template-mode markup so iAPI hydration is a no-op diff after first paint.
 	 *
 	 * @param array<string, mixed> $item                        Schema-shape item.
 	 * @param string               $remove_aria_label_template  Sprintf template for the remove button's aria-label. `%s` is replaced with the product name.
@@ -143,10 +119,9 @@ final class ShopperListRenderer {
 		$variation_label = self::get_variation_label( $item );
 		$remove_aria     = sprintf( $remove_aria_label_template, $alt );
 		$is_price_hidden = '' === $price_html;
-		// Tombstone rows (`is_live=false` or empty permalink) render `<a>`
-		// without an href — keeps the element shape stable for iAPI
-		// reconciliation against the live-row template, and the CSS in the
-		// shared partial drops link affordances when the anchor has no href.
+		// Tombstone rows (`is_live=false` or empty permalink) emit `<a>` without an href so the element
+		// shape stays stable for iAPI reconciliation against the live-row template. The stylesheet drops
+		// link affordances when the anchor has no href.
 		$href_attr = $is_live && '' !== $permalink ? 'href="' . esc_url( $permalink ) . '"' : '';

 		ob_start();
@@ -203,11 +178,9 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Empty-state `<li>` that the block toggles on once `state.isEmpty`
-	 * flips. `$start_hidden = true` makes SSR ship with `hidden` so the
-	 * message doesn't flash for shoppers whose list is being populated
-	 * client-side. `$start_hidden = false` is for blocks (e.g. Wishlist)
-	 * where the message should show on first paint when the list is empty.
+	 * Empty-state `<li>` that the block reveals once `state.isEmpty` flips. With `$start_hidden = true`
+	 * the `<li>` carries `hidden` on first paint (Saved for Later, populated asynchronously). Pass false
+	 * when the empty state should be visible immediately on first paint (Wishlist).
 	 *
 	 * @param string $message      Visible empty-state message.
 	 * @param string $css_class    Class attribute for the `<li>`.
@@ -224,9 +197,8 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Render the iAPI store-notices region used by the row-level error
-	 * banners. Mirrors `AddToCartWithOptions::render_interactivity_notices_region()`
-	 * — keep in sync if the shape changes.
+	 * Render the iAPI store-notices region used by row-level error banners. Mirrors
+	 * {@see AddToCartWithOptions::render_interactivity_notices_region()}. Keep in sync if the shape changes.
 	 *
 	 * @param string $wrapper_class Class attribute for the outer `<div>`.
 	 * @return string
@@ -267,11 +239,9 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Markup for the trash icon used in the remove-item button. Mirrors the
-	 * `trash` icon from `@wordpress/icons` that the cart line item uses for
-	 * `wc-block-cart-item__remove-link`, inlined here so SSR first paint
-	 * matches what JS would render after hydration. `currentColor` lets the
-	 * surrounding badge wrapper drive the fill.
+	 * Markup for the trash icon used in the remove-item button. Inlines the
+	 * `trash` icon from `@wordpress/icons` so SSR first paint matches the
+	 * post-hydration JS render. `currentColor` lets the wrapper drive the fill.
 	 *
 	 * @return string
 	 */
@@ -280,10 +250,8 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Markup for the empty-star icon. Mirrors `starEmpty` from
-	 * `@wordpress/icons`, inlined here so SSR first paint matches what JS
-	 * renders after hydration. `currentColor` lets the surrounding button
-	 * drive the fill.
+	 * Markup for the empty-star icon. Inlines the `starEmpty` icon from
+	 * `@wordpress/icons` so SSR first paint matches the post-hydration JS render.
 	 *
 	 * @return string
 	 */
@@ -292,10 +260,8 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Markup for the filled-star icon. Mirrors `starFilled` from
-	 * `@wordpress/icons`, inlined here so SSR first paint matches what JS
-	 * renders after hydration. `currentColor` lets the surrounding button
-	 * drive the fill.
+	 * Markup for the filled-star icon. Inlines the `starFilled` icon from
+	 * `@wordpress/icons` so SSR first paint matches the post-hydration JS render.
 	 *
 	 * @return string
 	 */
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListsController.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListsController.php
index 18e403a55ee..7dc226b8853 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListsController.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListsController.php
@@ -8,10 +8,10 @@ use Automattic\WooCommerce\Internal\RegisterHooksInterface;
 use Automattic\WooCommerce\Utilities\FeaturesUtil;

 /**
- * Tracks which shopper-list types are turned on and registers the
- * user-facing pieces that depend on each.
+ * Tracks which shopper-list types are enabled and registers the user-facing
+ * endpoints, menu items, and rewrite rules that depend on each.
  *
- * @internal Just for internal use.
+ * @internal
  */
 final class ShopperListsController implements RegisterHooksInterface {

@@ -24,19 +24,17 @@ final class ShopperListsController implements RegisterHooksInterface {
 	);

 	/**
-	 * Wishlist My Account endpoint slug. Wrapped in a method (rather than
-	 * a constant) so a future filter or settings hook can override it
-	 * without touching every call site.
+	 * Wishlist My Account endpoint slug.
 	 */
 	public function get_wishlist_endpoint(): string {
 		return 'wishlist';
 	}

 	/**
-	 * Whether a given list type is on, or whether any list type is on
+	 * Whether a given list type is enabled, or whether any list type is enabled
 	 * when no slug is passed.
 	 *
-	 * @param string|null $list_slug List slug, or null to ask about any type.
+	 * @param string|null $list_slug List slug, or null to check any list type.
 	 */
 	public function is_enabled( ?string $list_slug = null ): bool {
 		if ( null === $list_slug ) {
@@ -149,12 +147,10 @@ final class ShopperListsController implements RegisterHooksInterface {
 	}

 	/**
-	 * Render the wishlist endpoint by dispatching to the
-	 * `woocommerce/wishlist` block. The block handles the empty state,
-	 * logged-out guard, asset enqueues, and item rendering.
+	 * Render the `woocommerce/wishlist` block.
 	 */
 	public function render_wishlist_endpoint(): void {
-		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- the block string is a static literal; `do_blocks()` invokes the registered block's render callback, which is responsible for its own escaping.
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- the block string is a static literal, and `do_blocks()` invokes the registered block's render callback, which is responsible for its own escaping.
 		echo do_blocks( '<!-- wp:woocommerce/wishlist /-->' );
 	}
 }
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItems.php b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItems.php
index ff4985e906e..85c821f465a 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItems.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItems.php
@@ -14,7 +14,7 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
  * POST saves an item to the list either from an existing cart line or from direct item payload fields.
  */
 class ShopperListItems extends AbstractRoute {
-	// Stopgap CSRF guard, replaced once the upstream trait lands on trunk.
+	// Temporary CSRF guard. Will be removed once a Store API-wide trait lands on trunk.
 	use ShopperListsNonceCheck;

 	/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItemsByKey.php b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItemsByKey.php
index ab44a9ff839..050445b3557 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItemsByKey.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListItemsByKey.php
@@ -10,7 +10,7 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
  * DELETE /shopper-lists/{slug}/items/{key}.
  */
 class ShopperListItemsByKey extends AbstractRoute {
-	// Stopgap CSRF guard, replaced once the upstream trait lands on trunk.
+	// Temporary CSRF guard. Will be removed once a Store API-wide trait lands on trunk.
 	use ShopperListsNonceCheck;

 	/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperLists.php b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperLists.php
index 18302622d76..d7aacdb84d9 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperLists.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperLists.php
@@ -6,7 +6,7 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1;
 use Automattic\WooCommerce\Internal\ShopperLists\ShopperList;

 /**
- * GET /shopper-lists — collection of the current user's shopper lists.
+ * GET /shopper-lists: collection of the current user's shopper lists.
  */
 class ShopperLists extends AbstractRoute {
 	/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsBySlug.php b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsBySlug.php
index 6769834e54a..e2500409548 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsBySlug.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsBySlug.php
@@ -7,7 +7,7 @@ use Automattic\WooCommerce\Internal\ShopperLists\ShopperList;
 use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;

 /**
- * GET /shopper-lists/{slug} — metadata for a single list.
+ * GET /shopper-lists/{slug}: metadata for a single list.
  */
 class ShopperListsBySlug extends AbstractRoute {
 	/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsNonceCheck.php b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsNonceCheck.php
index d542f30dcf7..772f1e7667e 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsNonceCheck.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsNonceCheck.php
@@ -4,14 +4,9 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\StoreApi\Routes\V1;

 /**
- * Stopgap CSRF guard for the write-capable shopper-lists routes.
- *
- * Enforces a `wc_store_api` Nonce header on writes and refreshes the
- * client nonce via response headers on every reply. Same shape as the
- * cart's existing flow, scoped to the nonce concern.
- *
- * To be replaced by a reusable Store API-wide nonce trait once that
- * lands on trunk.
+ * CSRF guard for the write-capable shopper-lists routes. Enforces a `wc_store_api` Nonce header on writes
+ * and refreshes the client nonce via response headers on every reply. Temporary, until a reusable
+ * Store API-wide nonce trait lands on trunk.
  *
  * @internal
  */
@@ -24,8 +19,8 @@ trait ShopperListsNonceCheck {
 	private static $store_api_nonce_action = 'wc_store_api';

 	/**
-	 * Override of {@see AbstractRoute::get_response} that enforces the
-	 * `wc_store_api` Nonce header on writes and refreshes it on every reply.
+	 * Enforce the `wc_store_api` Nonce header on writes and refresh it on every reply.
+	 * Overrides {@see AbstractRoute::get_response}.
 	 *
 	 * @param \WP_REST_Request $request Request object.
 	 * @phpstan-param \WP_REST_Request<array<string, mixed>> $request
diff --git a/plugins/woocommerce/src/StoreApi/RoutesController.php b/plugins/woocommerce/src/StoreApi/RoutesController.php
index 6de8e81baed..7dd316b3e8c 100644
--- a/plugins/woocommerce/src/StoreApi/RoutesController.php
+++ b/plugins/woocommerce/src/StoreApi/RoutesController.php
@@ -77,7 +77,7 @@ class RoutesController {
 				Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class,
 			],
 			'shopper_lists' => [
-				// Gated by ShopperListsController — registered only when at least one shopper-list feature is enabled.
+				// Registered only when at least one shopper-list feature flag is enabled (see ShopperListsController).
 				Routes\V1\ShopperLists::IDENTIFIER       => Routes\V1\ShopperLists::class,
 				Routes\V1\ShopperListsBySlug::IDENTIFIER => Routes\V1\ShopperListsBySlug::class,
 				Routes\V1\ShopperListItems::IDENTIFIER   => Routes\V1\ShopperListItems::class,
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ShopperListItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ShopperListItemSchema.php
index 9fdc2a61ba7..45aac76e4fa 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ShopperListItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ShopperListItemSchema.php
@@ -15,7 +15,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
  * item reports `is_live`, and falls back to at-save snapshot data otherwise.
  */
 class ShopperListItemSchema extends AbstractSchema {
-	// We only call format_variation_data(); see phpstan.neon for the related suppressions.
+	// Only `format_variation_data()` is used from this trait. See phpstan.neon for related suppressions.
 	use ProductItemTrait;

 	/**
@@ -264,10 +264,8 @@ class ShopperListItemSchema extends AbstractSchema {
 	}

 	/**
-	 * Get the main image for a shopper list item.
-	 *
-	 * Returns the product's main image only — shopper list rows are compact and
-	 * the gallery isn't needed at the row level.
+	 * Get the main image for a shopper list item. Only the product's main image is returned. The
+	 * gallery is not exposed at the row level.
 	 *
 	 * @param \WC_Product $product Live product instance.
 	 * @return array
@@ -283,13 +281,10 @@ class ShopperListItemSchema extends AbstractSchema {
 	}

 	/**
-	 * Get the thumbnail image HTML for a shopper list item, falling back to the
-	 * WooCommerce placeholder when the product has no image or has been deleted.
-	 *
-	 * Pre-formatting on the server lets renderers (PHP SSR + JS hydration)
-	 * consume one canonical string instead of each side composing the markup
-	 * from the structured `images` array. Mirrors the pattern WC uses in
-	 * `ProductSchema::price_html` / `ProductImage::render`.
+	 * Get the thumbnail image HTML for a shopper list item, with a WooCommerce
+	 * placeholder fallback when the product has no image or has been deleted.
+	 * Pre-formatted on the server so SSR and JS hydration consume one canonical
+	 * string. Follows the pattern used by `ProductSchema::price_html`.
 	 *
 	 * @param \WC_Product|null $product Live product instance, or null for tombstones.
 	 * @return string
@@ -303,10 +298,7 @@ class ShopperListItemSchema extends AbstractSchema {
 	}

 	/**
-	 * Compute live prices for the saved item.
-	 *
-	 * We don't extend ProductSchema because saved items aren't products. The shape
-	 * here is a thin subset of cart-item prices.
+	 * Compute live prices for the saved item. Returns a thin subset of cart-item prices.
 	 *
 	 * @param \WC_Product $product Live product instance.
 	 * @return array