Commit 76a969f8ed8 for woocommerce

commit 76a969f8ed8ce818caa5bdaf237d4a2fdc4e3fb7
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Mon Jun 1 19:45:44 2026 +0100

    Revert "Tighten shopper-collections docblocks and inline comments" (#65448)

    This reverts commit a0536feb096c40c43621844297a4297823367177.

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 c85220915f8..7e55fb004af 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,7 +131,14 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 			false,
 			isBoolean
 		);
-		// User has to be logged in, feature enabled, and on the cart page with the block present.
+		// 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.
 		const showSaveForLater =
 			isUserLoggedIn &&
 			isSaveForLaterFeatureEnabled &&
@@ -369,14 +376,16 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
 										if ( ! saved ) {
 											return;
 										}
-										// `removeItem` surfaces its own errors via `processErrorResponse`. The
-										// analytics event and a11y announcement still fire to mirror the
-										// regular remove flow.
+										// removeItem surfaces its own errors
+										// via processErrorResponse; we still
+										// fire the analytics event and a11y
+										// announcement to mirror the regular
+										// remove flow.
 										await removeItem();
-										// TODO: emit a dedicated
-										// `cart-save-for-later` store event so
-										// analytics can distinguish a save from
-										// a plain remove.
+										// TODO: consider 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 0fa2f435bb7..54501e584bf 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,9 +8,17 @@ 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. 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.
+ * 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.
  */
 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 a7060837601..523b1ca8944 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,9 +7,11 @@ import type { CurrencyResponse } from '@woocommerce/types';
 import type { Store as StoreNotices } from '@woocommerce/stores/store-notices';

 /**
- * 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.
+ * 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
  */
 export type ShopperListItemImage = {
 	id: number;
@@ -68,7 +70,14 @@ export type AddItemPayload = {
 export type Store = {
 	state: {
 		restUrl: string;
-		// Shared with the cart routes.
+		// 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.
 		nonce: string;
 		lists: Record< string, ShopperListState >;
 	};
@@ -80,7 +89,7 @@ export type Store = {
 	};
 };

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

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

 /**
- * 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.
+ * 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.
  */
 async function restRequest< T >(
 	state: Store[ 'state' ],
@@ -152,6 +166,11 @@ 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',
 	{
@@ -179,11 +198,12 @@ 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 overwrite a fresh add or remove.
+					// 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.
 					list.items = items;
 				} catch ( error ) {
-					// Logged for diagnostics.
+					// No user trigger to attach a banner to; log for ops.
 					// eslint-disable-next-line no-console
 					console.error( error );
 				} finally {
@@ -217,8 +237,10 @@ const { state, actions } = store< Store >(
 						);
 					}

-					// 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.
+					// 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.
 					const existingIndex = list.items.findIndex(
 						( i ) => i.key === item.key
 					);
@@ -242,8 +264,9 @@ const { state, actions } = store< Store >(
 					return;
 				}

-				// Pessimistic remove: keep the row in place until the server confirms, to avoid a
-				// momentary disappearance on failure. Buttons stay disabled meanwhile via `pendingKeys`.
+				// 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`.
 				try {
 					yield restRequest(
 						state,
@@ -257,7 +280,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
 				);
@@ -289,7 +312,16 @@ const { state, actions } = store< Store >(
 	{ lock: universalLock }
 );

-// Syncs wp.data into this iAPI store after a wp.data action (e.g. the cart store's `saveForLater` thunk).
+// 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`.
 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 00c5d480d63..1c639be4d9a 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,12 +35,16 @@ type ButtonConfig = {
 type BlockContext = {
 	productId: number;
 	isVariableType: boolean;
-	// Mid-click flag so the button can be disabled while the request is in flight.
+	// 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).
 	isPending: boolean;
 };

-// The slice of ATCWO's iAPI context this block reads. Reuses the cart store's `SelectedAttributes`
-// so shape changes flow through automatically.
+// 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.
 type ATCWOContext = {
 	selectedAttributes: SelectedAttributes[];
 };
@@ -75,8 +79,13 @@ const { state } = store< BlockStore >(
 	'woocommerce/add-to-wishlist-button',
 	{
 		state: {
-			// 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.
+			// 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."
 			get effectiveProductId(): number {
 				const product = productsState.productInContext;
 				if ( ! product ) {
@@ -99,8 +108,12 @@ const { state } = store< BlockStore >(
 					return null;
 				}
 				const context = getContext< BlockContext >();
-				// For variable products, several attribute combinations can map to the same variation,
-				// so the picked attributes disambiguate the row. See `matchVariationItem`.
+				// 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.
 				if ( ! context.isVariableType ) {
 					return (
 						list.items.find( ( item ) => item.id === id ) ?? null
@@ -161,10 +174,21 @@ const { state } = store< BlockStore >(
 							existing.key
 						);
 					} else {
-						// 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`.
+						// 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.
 						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 f58c37e3e87..78369a4b093 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,5 +1,7 @@
 /**
- * Narrowed `RawShopperListItem` shape so this helper stays pure and unit-testable.
+ * 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.
  */
 type MatchableItem = {
 	id: number;
@@ -10,7 +12,9 @@ type MatchableItem = {
 };

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

 /**
- * 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").
+ * 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"`.
  *
  * @param item     Wishlist item from the shopper-lists store.
- * @param id       Effective product/variation id to compare against.
+ * @param id       Effective product/variation id we're comparing 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 c299c223ee0..cdbc6befb56 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,3 +1,7 @@
+/**
+ * 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 77e79ea3ac8..2e2d4ea718f 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,6 +23,9 @@ 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',
@@ -82,6 +85,11 @@ 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 0713ad1462b..c78f8148c4e 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,7 +28,10 @@ type SavedForLaterConfig = {
 };

 type BlockContext = {
-	// Tracks whether the list has ever had items in this session. See `trackShownItems` below.
+	// 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.
 	hasShownItems: boolean;
 	listItem?: RawShopperListItem;
 	htmlField?: 'price_html' | 'image_html';
@@ -58,8 +61,10 @@ type BlockStore = {
 	};
 };

-// 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`.
+// 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).
 const ALLOWED_TAGS = [
 	'a',
 	'b',
@@ -168,9 +173,12 @@ store< BlockStore >(
 				return ! listItem.is_purchasable;
 			},

-			// `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.
+			// `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.
 			get currentItemDisplayName(): string {
 				const { listItem } = getContext< BlockContext >();
 				return listItem ? decodeEntities( listItem.name ) : '';
@@ -237,10 +245,14 @@ store< BlockStore >(
 					return;
 				}

-				// 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.
+				// 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.
 				const variation = listItem.variation.map(
 					( { raw_attribute: rawAttribute, value, attribute } ) => ( {
 						attribute: rawAttribute || attribute,
@@ -249,9 +261,11 @@ store< BlockStore >(
 				);
 				const isVariation = listItem.variation_id > 0;

-				// `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.
+				// `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.
 				const lookup = {
 					id: listItem.id,
 					...( isVariation && { variation } ),
@@ -286,11 +300,15 @@ store< BlockStore >(
 		},

 		callbacks: {
-			// 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.
+			// 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.
 			trackShownItems: () => {
 				const ctx = getContext< BlockContext >();
 				const list = getList( LIST_SLUG );
@@ -299,11 +317,16 @@ store< BlockStore >(
 				}
 			},

-			// 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.
+			// 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).
 			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 7ed56b68a6e..6913e34a3d2 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,6 +3,12 @@
  */
 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 caf1e3702ed..38bc1f98bf6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/edit.tsx
@@ -23,6 +23,9 @@ 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 } ],
 ];
@@ -73,6 +76,11 @@ 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 9cffc36a3ba..a9d6451fcc3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/frontend.ts
@@ -53,8 +53,10 @@ type BlockStore = {
 	};
 };

-// 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`.
+// 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).
 const ALLOWED_TAGS = [
 	'a',
 	'b',
@@ -137,9 +139,10 @@ store< BlockStore >(
 				return !! listItem && !! pendingKeys[ listItem.key ];
 			},

-			// 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.
+			// 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.
 			get isEmpty(): boolean {
 				const list = getList( LIST_SLUG );
 				if ( ! list ) {
@@ -161,9 +164,12 @@ store< BlockStore >(
 				return ! listItem.is_purchasable;
 			},

-			// `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.
+			// `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.
 			get currentItemDisplayName(): string {
 				const { listItem } = getContext< BlockContext >();
 				return listItem ? decodeEntities( listItem.name ) : '';
@@ -216,10 +222,13 @@ store< BlockStore >(
 					return;
 				}

-				// 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.
+				// 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.
 				const variation = listItem.variation.map(
 					( { raw_attribute: rawAttribute, value, attribute } ) => ( {
 						attribute: rawAttribute || attribute,
@@ -228,11 +237,14 @@ 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 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.
+				// 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.
 				const lookup = {
 					id: listItem.id,
 					...( isVariation && { variation } ),
@@ -267,11 +279,16 @@ store< BlockStore >(
 		},

 		callbacks: {
-			// 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.
+			// 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).
 			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 7ed56b68a6e..6913e34a3d2 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/save.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/wishlist/save.tsx
@@ -3,6 +3,12 @@
  */
 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 2ef9d387fe6..a1277513d2f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/data/cart/thunks.ts
@@ -486,10 +486,19 @@ export const removeItemFromCart =
 	};

 /**
- * 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.
+ * 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.
  *
  * @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 4ea04bd85fa..e1cbd76034a 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 do not declare `allow_batch` server-side yet.
+	// Shopper-lists routes don't declare allow_batch yet. Drop these once
+	// the routes opt into batching server-side.
 	'/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 3d9743f0d94..cc088fd2707 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToWishlistButton.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToWishlistButton.php
@@ -8,10 +8,21 @@ use Automattic\WooCommerce\Blocks\Utils\BlocksSharedState;
 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListRenderer;

 /**
- * 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.
+ * 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.
  */
 final class AddToWishlistButton extends AbstractBlock {
 	/**
@@ -35,7 +46,8 @@ final class AddToWishlistButton 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 can't have a wishlist — bail before enqueuing assets or
+		// seeding state.
 		if ( ! is_user_logged_in() ) {
 			return '';
 		}
@@ -57,7 +69,12 @@ final class AddToWishlistButton extends AbstractBlock {

 		$items = $this->prefetch_items();

-		// Seed the shared shopper-lists store with the REST URL, prefetched items, and a bootstrap nonce.
+		// 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.
 		wp_interactivity_state(
 			'woocommerce/shopper-lists',
 			array(
@@ -72,7 +89,10 @@ final class AddToWishlistButton extends AbstractBlock {
 			)
 		);

-		// Visible labels passed through `wp_interactivity_config` for the JS-side getter.
+		// 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.
 		wp_interactivity_config(
 			'woocommerce/add-to-wishlist-button',
 			array(
@@ -143,8 +163,10 @@ final class AddToWishlistButton extends AbstractBlock {
 	}

 	/**
-	 * Prefetch the wishlist items via `rest_do_request()`. Returns an empty
-	 * list for logged-out users, since the route requires authentication.
+	 * 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.
 	 *
 	 * @return array<int, array<string, mixed>> Items in the schema response shape.
 	 */
@@ -179,8 +201,10 @@ final class AddToWishlistButton extends AbstractBlock {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param array<int, array<string, mixed>> $items   Schema-shape items.
 	 * @param \WC_Product                      $product The product being viewed.
@@ -213,7 +237,9 @@ final class AddToWishlistButton extends AbstractBlock {
 	}

 	/**
-	 * Visible label shown until the shopper picks variation attributes.
+	 * Visible label when the shopper still needs to pick variation
+	 * attributes before the wishlist toggle can resolve to a specific
+	 * variation.
 	 */
 	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 f74b0d926aa..b0d5eb8750b 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/SavedForLater.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/SavedForLater.php
@@ -9,14 +9,25 @@ use Automattic\WooCommerce\Internal\ShopperLists\ShopperListRenderer;
 use Automattic\WooCommerce\Proxies\LegacyProxy;

 /**
- * 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.
+ * 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.
  */
 final class SavedForLater extends AbstractBlock {
 	/**
-	 * Slug of the shopper list this block renders.
+	 * 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.
 	 */
 	private const LIST_SLUG = 'saved-for-later';

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

-		// `BlockHooksTrait` is not used here because of PHPStan issues with the trait's annotations.
+		// We do not use `BlockHooksTrait` currently as it has issues with PHPStan.
 		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 );
 	}
@@ -58,7 +69,8 @@ final class SavedForLater extends AbstractBlock {
 			return $hooked_block_types;
 		}

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

-		// 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`.
+		// 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.
 		//
-		// `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`.
+		// `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`.
 		$list_heading = __( 'Saved for later', 'woocommerce' );
 		$heading_html = '<h2 class="wp-block-heading">' . esc_html( $list_heading ) . '</h2>';

@@ -128,16 +146,22 @@ 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 supported by the SCSS.
+		// 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.
 		$column_count = min( 6, max( 2, absint( $attributes['columnCount'] ?? 5 ) ) );

 		wp_enqueue_script_module( $this->get_full_block_name() );
@@ -145,12 +169,21 @@ 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 );
-		// Required so the Move-to-cart action has a hydrated cart store to dispatch into.
+		// `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.
 		BlocksSharedState::load_cart_state( $consent );

 		$items = $this->prefetch_items();

-		// Seed the shared shopper-lists store with the REST URL, prefetched items, and a bootstrap nonce.
+		// 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).
 		wp_interactivity_state(
 			'woocommerce/shopper-lists',
 			array(
@@ -165,7 +198,10 @@ final class SavedForLater extends AbstractBlock {
 			)
 		);

-		// Sprintf templates passed through `wp_interactivity_config` for JS-side interpolation.
+		// 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.
 		wp_interactivity_config(
 			'woocommerce/saved-for-later',
 			array(
@@ -174,14 +210,25 @@ final class SavedForLater extends AbstractBlock {
 			)
 		);

-		// `hasShownItems` seeds the per-block context that controls the empty message.
+		// `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.
 		$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 JSON serializes as `{}` rather than `[]`.
+					// `stdClass` so it serialises as `{}`, not `[]` —
+					// iAPI's reactive proxy only fires updates on object
+					// writes, not array expandos.
 					'pendingKeys'   => new \stdClass(),
 				)
 			),
@@ -198,8 +245,9 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * Prefetch the saved-for-later items via `rest_do_request()`. Returns an empty
-	 * list for logged-out users, since the route requires authentication.
+	 * 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.
 	 *
 	 * @return array<int, array<string, mixed>> Items in the schema response shape.
 	 */
@@ -214,7 +262,11 @@ 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 for diagnostics.
+			// 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.
 			wc_get_logger()->debug(
 				sprintf( 'Saved for Later prefetch failed: %s', $message ),
 				array(
@@ -230,15 +282,19 @@ final class SavedForLater extends AbstractBlock {
 			return array();
 		}

-		// 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.
+		// 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.
 		$decoded = json_decode( (string) wp_json_encode( $data ), true );
 		return is_array( $decoded ) ? $decoded : array();
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @return string
 	 */
@@ -250,7 +306,8 @@ final class SavedForLater extends AbstractBlock {
 	}

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

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

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

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

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

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -360,10 +421,14 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * 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.
+	 * 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>`.
 	 *
-	 * @param string $content  Rendered inner-block content (usually the heading HTML).
+	 * @param string $content  Rendered inner-block content (typically the heading HTML).
 	 * @param bool   $is_empty Whether the saved-for-later list is empty on initial paint.
 	 * @return string
 	 */
@@ -380,8 +445,10 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @return string
 	 */
@@ -394,8 +461,10 @@ final class SavedForLater extends AbstractBlock {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 */
 	private function get_quantity_label_template(): string {
 		/* translators: %d: quantity of saved items. */
@@ -403,7 +472,8 @@ final class SavedForLater extends AbstractBlock {
 	}

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

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

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @return null
 	 */
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php b/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php
index 436a34270f7..1a26fb39d39 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Wishlist.php
@@ -8,14 +8,24 @@ use Automattic\WooCommerce\Blocks\Utils\BlocksSharedState;
 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListRenderer;

 /**
- * 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.
+ * 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.
  */
 final class Wishlist extends AbstractBlock {
 	/**
-	 * Slug of the shopper list this block renders.
+	 * The list slug this block renders. Constant — when additional list
+	 * types ship as their own blocks, each one hardcodes its own slug.
 	 */
 	private const LIST_SLUG = 'wishlist';

@@ -35,13 +45,20 @@ final class Wishlist extends AbstractBlock {
 	 * @return string Rendered block type output.
 	 */
 	protected function render( $attributes, $content, $block ) {
-		// 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.
+		// 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.
 		if ( ! is_user_logged_in() ) {
 			return '';
 		}

-		// Clamp to the 2-6 range supported by the SCSS.
+		// 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.
 		$column_count = min( 6, max( 2, absint( $attributes['columnCount'] ?? 5 ) ) );

 		wp_enqueue_script_module( $this->get_full_block_name() );
@@ -49,12 +66,21 @@ 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 );
-		// Required so the Add to cart action has a hydrated cart store to dispatch into.
+		// `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.
 		BlocksSharedState::load_cart_state( $consent );

 		$items = $this->prefetch_items();

-		// Seed the shared shopper-lists store with the REST URL, prefetched items, and a bootstrap nonce.
+		// 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).
 		wp_interactivity_state(
 			'woocommerce/shopper-lists',
 			array(
@@ -69,7 +95,9 @@ final class Wishlist extends AbstractBlock {
 			)
 		);

-		// Only the remove-button aria-label template needs JS-side interpolation.
+		// 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.
 		wp_interactivity_config(
 			'woocommerce/wishlist',
 			array(
@@ -77,13 +105,22 @@ final class Wishlist extends AbstractBlock {
 			)
 		);

-		// No `hasShownItems` flag here, unlike Saved for Later: the empty message shows immediately.
+		// 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.
 		$wrapper_attributes = array(
 			'class'                     => 'wc-block-wishlist',
 			'data-wp-interactive'       => 'woocommerce/wishlist',
 			'data-wp-context'           => (string) wp_json_encode(
 				array(
-					// `stdClass` so JSON serializes as `{}` rather than `[]`.
+					// `stdClass` so it serialises as `{}`, not `[]` —
+					// iAPI's reactive proxy only fires updates on object
+					// writes, not array expandos.
 					'pendingKeys' => new \stdClass(),
 				)
 			),
@@ -98,8 +135,10 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Prefetch the wishlist items via `rest_do_request()`. Returns an empty
-	 * list for logged-out users, since the route requires authentication.
+	 * 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.
 	 *
 	 * @return array<int, array<string, mixed>> Items in the schema response shape.
 	 */
@@ -114,7 +153,10 @@ 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 for diagnostics.
+			// 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.
 			wc_get_logger()->debug(
 				sprintf( 'Wishlist prefetch failed: %s', $message ),
 				array(
@@ -130,15 +172,20 @@ final class Wishlist extends AbstractBlock {
 			return array();
 		}

-		// 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.
+		// 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.
 		$decoded = json_decode( (string) wp_json_encode( $data ), true );
 		return is_array( $decoded ) ? $decoded : array();
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @return string
 	 */
@@ -149,7 +196,8 @@ final class Wishlist extends AbstractBlock {
 	}

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

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

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param array<string, mixed> $item Schema-shape item.
 	 * @return string
@@ -231,10 +282,13 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * 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>`.
+	 * 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>`.
 	 *
-	 * @param string $content Rendered inner-block content (usually the heading HTML).
+	 * @param string $content Rendered inner-block content (typically the heading HTML).
 	 * @return string
 	 */
 	private function render_header_markup( string $content ): string {
@@ -245,8 +299,9 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * Render the empty-state markup. Visible on first paint when the list is empty. iAPI handles runtime
-	 * transitions via `state.isEmpty`.
+	 * 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.
 	 *
 	 * @param array<int, array<string, mixed>> $items Schema-shape items.
 	 * @return string
@@ -260,8 +315,10 @@ final class Wishlist extends AbstractBlock {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 */
 	private function get_remove_label_template(): string {
 		/* translators: %s: product name. */
@@ -269,7 +326,8 @@ final class Wishlist extends AbstractBlock {
 	}

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

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @return null
 	 */
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php
index 13e9441535f..48fa653dfff 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperList.php
@@ -70,6 +70,8 @@ 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;
 		}
@@ -85,7 +87,7 @@ class ShopperList {
 			return self::from_array( $stored, $user_id );
 		}

-		// In-memory list. Persisted on the first save().
+		// In-memory list; saved 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 85a784c374c..139a05d8d62 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,8 +307,9 @@ class ShopperListItem {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 */
 	public function is_live(): bool {
 		$product = $this->get_product();
@@ -329,9 +330,10 @@ class ShopperListItem {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 */
 	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 383dde71db6..a41a6549afa 100644
--- a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListRenderer.php
+++ b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListRenderer.php
@@ -5,19 +5,32 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\ShopperLists;

 /**
- * Markup helpers for the shopper collection blocks.
+ * 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.
  */
 final class ShopperListRenderer {

 	/**
-	 * Shared CSS root class for the row.
+	 * Shared CSS root class for the row. Each section helper outputs
+	 * BEM-style modifiers off this base (`__image-slot`, `__remove`, …).
 	 */
 	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()`. Callers must
-	 * ensure `$inner` and `$before_list` contain only pre-escaped markup. Both are emitted verbatim.
+	 * 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.
 	 *
 	 * @param array<string, mixed> $wrapper_attrs Attributes for the outer `<section>`.
 	 * @param string               $list_class    Class attribute for the inner `<ul>`.
@@ -36,8 +49,12 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param string $row_inner_markup Inner markup for the `<li>`.
 	 * @return string
@@ -51,9 +68,13 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param array<string, mixed> $item             Schema-shape item.
 	 * @param string               $row_inner_markup Inner markup for the `<li>`.
@@ -61,7 +82,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, and $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; $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 ),
@@ -71,7 +92,8 @@ final class ShopperListRenderer {
 	}

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

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @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.
@@ -119,9 +143,10 @@ 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) 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.
+		// 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.
 		$href_attr = $is_live && '' !== $permalink ? 'href="' . esc_url( $permalink ) . '"' : '';

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

 	/**
-	 * 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).
+	 * 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.
 	 *
 	 * @param string $message      Visible empty-state message.
 	 * @param string $css_class    Class attribute for the `<li>`.
@@ -197,8 +224,9 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param string $wrapper_class Class attribute for the outer `<div>`.
 	 * @return string
@@ -239,9 +267,11 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @return string
 	 */
@@ -250,8 +280,10 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Markup for the empty-star icon. Inlines the `starEmpty` icon from
-	 * `@wordpress/icons` so SSR first paint matches the post-hydration JS render.
+	 * 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.
 	 *
 	 * @return string
 	 */
@@ -260,8 +292,10 @@ final class ShopperListRenderer {
 	}

 	/**
-	 * Markup for the filled-star icon. Inlines the `starFilled` icon from
-	 * `@wordpress/icons` so SSR first paint matches the post-hydration JS render.
+	 * 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.
 	 *
 	 * @return string
 	 */
diff --git a/plugins/woocommerce/src/Internal/ShopperLists/ShopperListsController.php b/plugins/woocommerce/src/Internal/ShopperLists/ShopperListsController.php
index 7dc226b8853..18e403a55ee 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 enabled and registers the user-facing
- * endpoints, menu items, and rewrite rules that depend on each.
+ * Tracks which shopper-list types are turned on and registers the
+ * user-facing pieces that depend on each.
  *
- * @internal
+ * @internal Just for internal use.
  */
 final class ShopperListsController implements RegisterHooksInterface {

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

 	/**
-	 * Wishlist My Account endpoint slug.
+	 * 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.
 	 */
 	public function get_wishlist_endpoint(): string {
 		return 'wishlist';
 	}

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

 	/**
-	 * Render the `woocommerce/wishlist` block.
+	 * 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.
 	 */
 	public function render_wishlist_endpoint(): void {
-		// 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.
+		// 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.
 		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 85c821f465a..ff4985e906e 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 {
-	// Temporary CSRF guard. Will be removed once a Store API-wide trait lands on trunk.
+	// Stopgap CSRF guard, replaced once the upstream 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 050445b3557..ab44a9ff839 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 {
-	// Temporary CSRF guard. Will be removed once a Store API-wide trait lands on trunk.
+	// Stopgap CSRF guard, replaced once the upstream 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 d7aacdb84d9..18302622d76 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 e2500409548..6769834e54a 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 772f1e7667e..d542f30dcf7 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsNonceCheck.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/ShopperListsNonceCheck.php
@@ -4,9 +4,14 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\StoreApi\Routes\V1;

 /**
- * 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.
+ * 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.
  *
  * @internal
  */
@@ -19,8 +24,8 @@ trait ShopperListsNonceCheck {
 	private static $store_api_nonce_action = 'wc_store_api';

 	/**
-	 * Enforce the `wc_store_api` Nonce header on writes and refresh it on every reply.
-	 * Overrides {@see AbstractRoute::get_response}.
+	 * Override of {@see AbstractRoute::get_response} that enforces the
+	 * `wc_store_api` Nonce header on writes and refreshes it on every reply.
 	 *
 	 * @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 7dd316b3e8c..6de8e81baed 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' => [
-				// Registered only when at least one shopper-list feature flag is enabled (see ShopperListsController).
+				// Gated by ShopperListsController — registered only when at least one shopper-list feature is enabled.
 				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 45aac76e4fa..9fdc2a61ba7 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 {
-	// Only `format_variation_data()` is used from this trait. See phpstan.neon for related suppressions.
+	// We only call format_variation_data(); see phpstan.neon for the related suppressions.
 	use ProductItemTrait;

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

 	/**
-	 * 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.
+	 * 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.
 	 *
 	 * @param \WC_Product $product Live product instance.
 	 * @return array
@@ -281,10 +283,13 @@ class ShopperListItemSchema extends AbstractSchema {
 	}

 	/**
-	 * 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`.
+	 * 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`.
 	 *
 	 * @param \WC_Product|null $product Live product instance, or null for tombstones.
 	 * @return string
@@ -298,7 +303,10 @@ class ShopperListItemSchema extends AbstractSchema {
 	}

 	/**
-	 * Compute live prices for the saved item. Returns a thin subset of cart-item prices.
+	 * 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.
 	 *
 	 * @param \WC_Product $product Live product instance.
 	 * @return array