Commit d2ade4d4773 for woocommerce

commit d2ade4d4773eb5a554845e9d5bd3684877ecc290
Author: Miroslav Mitev <m1r0@users.noreply.github.com>
Date:   Wed Jul 15 14:37:12 2026 +0300

    Support custom header images for third-party onboarding tasks (#66500)

    * Add task image metadata API and use it in the dashboard widget

    Add Task::get_image_url() and Task::get_image_alt() with empty defaults,
    exposed as imageUrl/imageAlt in Task::get_json(). Migrate the core task
    illustrations into their task classes and let DeprecatedExtendedTask accept
    image_url/image_alt args, so third-party tasks can supply their own image.
    Replace the dashboard setup widget's hard-coded core-ID image map with the
    task-provided metadata, falling back to the generic setup illustration.

    * Render task image metadata on My Home task headers

    Add imageUrl/imageAlt to the onboarding TaskType and introduce a generic
    DefaultTaskHeader that renders a task's image, title, content and CTA. The
    setup task list falls back to it for tasks that provide image metadata but
    have no dedicated header component or WooOnboardingTaskListHeader SlotFill.

    * Add changelog entries for onboarding task image metadata

    * Fix non-descriptive link text in onboarding tasks doc

    * Remove extra blank line in DeprecatedExtendedTask

    * Convert DefaultTaskHeaderProps type alias to interface

    * Add tests for SetupTaskList DefaultTaskHeader fallback wiring

diff --git a/packages/js/data/changelog/add-onboarding-task-image-metadata b/packages/js/data/changelog/add-onboarding-task-image-metadata
new file mode 100644
index 00000000000..6528f228066
--- /dev/null
+++ b/packages/js/data/changelog/add-onboarding-task-image-metadata
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add optional imageUrl and imageAlt fields to the onboarding TaskType.
diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts
index 03a0e8d34e8..d1ee85cd6ae 100644
--- a/packages/js/data/src/onboarding/types.ts
+++ b/packages/js/data/src/onboarding/types.ts
@@ -6,6 +6,8 @@ import { Plugin } from '../plugins/types';
 export type TaskType = {
 	actionLabel?: string;
 	actionUrl?: string;
+	imageUrl?: string;
+	imageAlt?: string;
 	content: string;
 	id: string;
 	parentId: string;
diff --git a/plugins/woocommerce/changelog/add-onboarding-task-image-metadata b/plugins/woocommerce/changelog/add-onboarding-task-image-metadata
new file mode 100644
index 00000000000..7d2bb943e29
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-onboarding-task-image-metadata
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add contextual image metadata to onboarding tasks via Task::get_image_url() and Task::get_image_alt(), so third-party tasks can supply their own illustration for My Home and the legacy dashboard setup widget.
diff --git a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/default.tsx b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/default.tsx
new file mode 100644
index 00000000000..2c3f246fb67
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/default.tsx
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import { Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { TaskType } from '@woocommerce/data';
+
+interface DefaultTaskHeaderProps {
+	task: TaskType;
+	goToTask: () => void;
+}
+
+/**
+ * Generic task header used for tasks that provide contextual image metadata
+ * (imageUrl/imageAlt) but do not register a dedicated header component or a
+ * WooOnboardingTaskListHeader SlotFill. This lets third-party tasks render a
+ * header on My Home using the same metadata consumed by the legacy dashboard
+ * setup widget.
+ */
+const DefaultTaskHeader = ( { task, goToTask }: DefaultTaskHeaderProps ) => {
+	if ( ! task.imageUrl ) {
+		return null;
+	}
+
+	return (
+		<div className="woocommerce-task-header__contents-container">
+			<img
+				alt={ task.imageAlt || '' }
+				src={ task.imageUrl }
+				className="svg-background"
+			/>
+			<div className="woocommerce-task-header__contents">
+				<h1>{ task.title }</h1>
+				<p>{ task.content }</p>
+				<Button
+					variant={ task.isComplete ? 'secondary' : 'primary' }
+					onClick={ goToTask }
+				>
+					{ task.actionLabel || __( "Let's go", 'woocommerce' ) }
+				</Button>
+			</div>
+		</div>
+	);
+};
+
+export default DefaultTaskHeader;
diff --git a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/index.ts b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/index.ts
index 57987ded116..063eae94210 100644
--- a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/index.ts
+++ b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/index.ts
@@ -13,6 +13,8 @@ import PaymentsHeader from './payments';
 import WoocommercePaymentsHeader from './woocommerce-payments';
 import LaunchYourStoreHeader from './launch-your-store';

+export { default as DefaultTaskHeader } from './default';
+
 export const taskHeaders: Record< string, React.ElementType > = {
 	store_details: StoreDetailsHeader,
 	'customize-store': CustomizeStoreHeader,
diff --git a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/test/default.test.tsx b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/test/default.test.tsx
new file mode 100644
index 00000000000..4f0aa09f38f
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/components/task-headers/test/default.test.tsx
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { TaskType } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import DefaultTaskHeader from '../default';
+
+jest.mock( '@wordpress/components', () => ( {
+	Button: ( {
+		children,
+		onClick,
+	}: {
+		children: React.ReactNode;
+		onClick?: () => void;
+	} ) => <button onClick={ onClick }>{ children }</button>,
+} ) );
+
+const baseTask = {
+	id: 'third-party-task',
+	title: 'Third party task',
+	content: 'A task added by an extension.',
+	actionLabel: 'Get started',
+	imageUrl: 'https://example.com/custom-illustration.png',
+	imageAlt: 'Custom extension illustration',
+	isComplete: false,
+} as TaskType;
+
+describe( 'DefaultTaskHeader', () => {
+	it( 'renders the task title, content, image and action button', () => {
+		const goToTask = jest.fn();
+		render( <DefaultTaskHeader task={ baseTask } goToTask={ goToTask } /> );
+
+		expect( screen.getByText( 'Third party task' ) ).toBeInTheDocument();
+		expect(
+			screen.getByText( 'A task added by an extension.' )
+		).toBeInTheDocument();
+
+		const image = screen.getByRole( 'img', {
+			name: 'Custom extension illustration',
+		} );
+		expect( image ).toHaveAttribute(
+			'src',
+			'https://example.com/custom-illustration.png'
+		);
+
+		expect(
+			screen.getByRole( 'button', { name: 'Get started' } )
+		).toBeInTheDocument();
+	} );
+
+	it( 'calls goToTask when the action button is clicked', async () => {
+		const goToTask = jest.fn();
+		render( <DefaultTaskHeader task={ baseTask } goToTask={ goToTask } /> );
+
+		await userEvent.click(
+			screen.getByRole( 'button', { name: 'Get started' } )
+		);
+		expect( goToTask ).toHaveBeenCalled();
+	} );
+
+	it( 'falls back to the default action label when none is provided', () => {
+		const goToTask = jest.fn();
+		const task = { ...baseTask, actionLabel: undefined } as TaskType;
+		render( <DefaultTaskHeader task={ task } goToTask={ goToTask } /> );
+
+		expect(
+			screen.getByRole( 'button', { name: "Let's go" } )
+		).toBeInTheDocument();
+	} );
+
+	it( 'renders nothing when the task has no image', () => {
+		const goToTask = jest.fn();
+		const task = { ...baseTask, imageUrl: undefined } as TaskType;
+		const { container } = render(
+			<DefaultTaskHeader task={ task } goToTask={ goToTask } />
+		);
+
+		expect( container ).toBeEmptyDOMElement();
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/setup-task-list.tsx b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/setup-task-list.tsx
index b316ce77562..8946da89980 100644
--- a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/setup-task-list.tsx
+++ b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/setup-task-list.tsx
@@ -23,7 +23,7 @@ import { useLayoutContext } from '@woocommerce/admin-layout';
 /**
  * Internal dependencies
  */
-import { taskHeaders } from './components/task-headers';
+import { taskHeaders, DefaultTaskHeader } from './components/task-headers';
 import DismissModal from './components/dismiss-modal';
 import TaskListCompleted from './components/task-list-completed';
 import { ProgressHeader } from '~/task-lists/components/progress-header';
@@ -231,7 +231,11 @@ export const SetupTaskList = ( {
 	};

 	const showTaskHeader = ( task: TaskType ) => {
-		if ( taskHeaders[ task.id ] || hasTaskListHeaderSlotFills ) {
+		if (
+			taskHeaders[ task.id ] ||
+			hasTaskListHeaderSlotFills ||
+			task.imageUrl
+		) {
 			setHeaderData( {
 				task,
 				goToTask: () => goToTask( task ),
@@ -312,7 +316,8 @@ export const SetupTaskList = ( {
 							) : (
 								headerData?.task &&
 								createElement(
-									taskHeaders[ headerData.task.id ],
+									taskHeaders[ headerData.task.id ] ??
+										DefaultTaskHeader,
 									headerData
 								)
 							) }
diff --git a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx
index 9e8515dc2bb..e44a29bb409 100644
--- a/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx
+++ b/plugins/woocommerce/client/admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx
@@ -33,6 +33,7 @@ jest.mock( '../components/task-headers', () => ( {
 		required: () => <div>required_header</div>,
 		completed: () => <div>completed_header</div>,
 	},
+	DefaultTaskHeader: () => <div>default_header</div>,
 } ) );
 jest.mock( '@woocommerce/data', () => ( {
 	...jest.requireActual( '@woocommerce/data' ),
@@ -288,4 +289,69 @@ describe( 'TaskList', () => {
 			queryByText( dismissedTask[ 0 ].title )
 		).not.toBeInTheDocument();
 	} );
+
+	it( 'should fall back to the DefaultTaskHeader for a task that has an image but no dedicated header or slot fill', () => {
+		const thirdPartyTask = {
+			...tasks.extension[ 0 ],
+			imageUrl: 'https://example.com/custom-illustration.png',
+			imageAlt: 'Custom illustration',
+		};
+		const { queryByText } = render(
+			<SetupTaskList
+				id="extended"
+				tasks={ [ thirdPartyTask ] }
+				title="List title"
+				query={ {} }
+				isComplete={ false }
+				isHidden={ false }
+				eventPrefix={ '' }
+				displayProgressHeader={ false }
+				keepCompletedTaskList="no"
+				isVisible={ true }
+			/>
+		);
+		expect( queryByText( 'default_header' ) ).toBeInTheDocument();
+	} );
+
+	it( 'should not render any task header for a task without an image, dedicated header, or slot fill', () => {
+		const { queryByText } = render(
+			<SetupTaskList
+				id="extended"
+				tasks={ [ ...tasks.extension ] }
+				title="List title"
+				query={ {} }
+				isComplete={ false }
+				isHidden={ false }
+				eventPrefix={ '' }
+				displayProgressHeader={ false }
+				keepCompletedTaskList="no"
+				isVisible={ true }
+			/>
+		);
+		expect( queryByText( 'default_header' ) ).not.toBeInTheDocument();
+	} );
+
+	it( 'should prefer a dedicated task header over the DefaultTaskHeader even when the task has an image', () => {
+		const taskWithImage = {
+			...tasks.setup[ 0 ],
+			imageUrl: 'https://example.com/custom-illustration.png',
+			imageAlt: 'Custom illustration',
+		};
+		const { queryByText } = render(
+			<SetupTaskList
+				id="extended"
+				tasks={ [ taskWithImage ] }
+				title="List title"
+				query={ {} }
+				isComplete={ false }
+				isHidden={ false }
+				eventPrefix={ '' }
+				displayProgressHeader={ false }
+				keepCompletedTaskList="no"
+				isVisible={ true }
+			/>
+		);
+		expect( queryByText( 'optional_header' ) ).toBeInTheDocument();
+		expect( queryByText( 'default_header' ) ).not.toBeInTheDocument();
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/docs/features/onboarding-tasks.md b/plugins/woocommerce/client/admin/docs/features/onboarding-tasks.md
index 17fa34b0ceb..3f4373b2b26 100644
--- a/plugins/woocommerce/client/admin/docs/features/onboarding-tasks.md
+++ b/plugins/woocommerce/client/admin/docs/features/onboarding-tasks.md
@@ -246,6 +246,8 @@ TaskLists::add_task(
 - `$task->get_additional_data(): mixed|null`: Returns additional data associated with the task. It can be any type of data, such as arrays, objects, or simple values.
 - `$task->get_action_label(): string`: Returns the label for the action button of the task.
 - `$task->get_action_url(): string|null`: Returns the URL associated with the task's action.
+- `$task->get_image_url(): string`: Returns the contextual image URL for the task.
+- `$task->get_image_alt(): string`: Returns the alt text for the contextual image.
 - `$task->is_dismissable(): bool`: Checks if the task is dismissable.
 - `$task->is_dismissed(): bool`: Checks if the task is dismissed.
 - `$task->dismiss(): bool`: Dismisses the task.
@@ -271,6 +273,8 @@ TaskLists::add_task(
     - `additionalInfo` (object) - Additional extensible information about the task.
     - `actionLabel` (string) - The label used for the action button.
     - `actionUrl` (string) - The URL used when clicking the task if no task card is required.
+    - `imageUrl` (string) - The contextual image URL for the task.
+    - `imageAlt` (string) - The alt text for the contextual image.
     - `isComplete` (bool) - If the task has been completed or not.
     - `time` (string) - Length of time to complete the task.
     - `level` (integer) - A priority for task list sorting.
@@ -323,7 +327,7 @@ The following REST endpoints are available to interact with tasks. For ease of u

 ### SlotFills

-The task UI can be supplemented by registering plugins that fill the provided task slots. Learn more about slot fills in the [SlotFill documentation](https://developer.wordpress.org/block-editor/reference-guides/slotfills/) and [here](https://developer.wordpress.org/block-editor/reference-guides/components/slot-fill/).
+The task UI can be supplemented by registering plugins that fill the provided task slots. Learn more about slot fills in the [SlotFill documentation](https://developer.wordpress.org/block-editor/reference-guides/slotfills/) and the [SlotFill component reference](https://developer.wordpress.org/block-editor/reference-guides/components/slot-fill/).

 ### Task content

diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php
index 4c520e71359..cc9c075fdd6 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard-setup.php
@@ -95,59 +95,25 @@ if ( ! class_exists( 'WC_Admin_Dashboard_Setup', false ) ) :
 		 * @return array
 		 */
 		private function get_task_header( $task ) {
-			$asset_url           = WC()->plugin_url() . '/assets/';
-			$task_json           = $task->get_json();
-			$default_task_header = array(
+			$asset_url = WC()->plugin_url() . '/assets/';
+			$task_json = $task->get_json();
+
+			$image_url = method_exists( $task, 'get_image_url' ) ? (string) $task->get_image_url() : '';
+			$image_alt = method_exists( $task, 'get_image_alt' ) ? (string) $task->get_image_alt() : '';
+
+			// Fallback to the generic WooCommerce setup illustration.
+			if ( '' === $image_url ) {
+				$image_url = $asset_url . 'images/dashboard-widget-setup.png';
+				$image_alt = __( 'WooCommerce setup illustration', 'woocommerce' );
+			}
+
+			return array(
 				'title'        => $task->get_title(),
 				'content'      => $task_json['content'] ?? '',
 				'button_label' => $task_json['actionLabel'] ?? $task->get_title(),
-				'image_url'    => $asset_url . 'images/dashboard-widget-setup.png',
-				'image_alt'    => __( 'WooCommerce setup illustration', 'woocommerce' ),
-			);
-			$task_images         = array(
-				'store_details'        => array(
-					'image_url' => $asset_url . 'images/task_list/store-details-illustration.png',
-					'image_alt' => __( 'Store location illustration', 'woocommerce' ),
-				),
-				'customize-store'      => array(
-					'image_url' => $asset_url . 'images/task_list/customize-store-illustration.svg',
-					'image_alt' => __( 'Customize your store illustration', 'woocommerce' ),
-				),
-				'tax'                  => array(
-					'image_url' => $asset_url . 'images/task_list/tax-illustration.svg',
-					'image_alt' => __( 'Tax illustration', 'woocommerce' ),
-				),
-				'shipping'             => array(
-					'image_url' => $asset_url . 'images/task_list/shipping-illustration.svg',
-					'image_alt' => __( 'Shipping illustration', 'woocommerce' ),
-				),
-				'marketing'            => array(
-					'image_url' => $asset_url . 'images/task_list/sales-illustration.svg',
-					'image_alt' => __( 'Marketing illustration', 'woocommerce' ),
-				),
-				'payments'             => array(
-					'image_url' => $asset_url . 'images/task_list/payment-illustration.svg',
-					'image_alt' => __( 'Payment illustration', 'woocommerce' ),
-				),
-				'woocommerce-payments' => array(
-					'image_url' => $asset_url . 'images/task_list/payment-illustration.svg',
-					'image_alt' => __( 'Payment illustration', 'woocommerce' ),
-				),
-				'products'             => array(
-					'image_url' => $asset_url . 'images/task_list/sales-section-illustration.svg',
-					'image_alt' => __( 'Products illustration', 'woocommerce' ),
-				),
-				'purchase'             => array(
-					'image_url' => $asset_url . 'images/task_list/purchase-illustration.png',
-					'image_alt' => __( 'Purchase illustration', 'woocommerce' ),
-				),
-				'launch-your-store'    => array(
-					'image_url' => $asset_url . 'images/task_list/launch-your-store-illustration.svg',
-					'image_alt' => __( 'Launch your store illustration', 'woocommerce' ),
-				),
+				'image_url'    => $image_url,
+				'image_alt'    => $image_alt,
 			);
-
-			return array_merge( $default_task_header, $task_images[ $task->get_id() ] ?? array() );
 		}

 		/**
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTask.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTask.php
index 2b101d5e930..5aafca682e9 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTask.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTask.php
@@ -79,6 +79,19 @@ class DeprecatedExtendedTask extends Task {
 	 */
 	public $title = '';

+	/**
+	 * Contextual image URL.
+	 *
+	 * @var string
+	 */
+	public $image_url = '';
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @var string
+	 */
+	public $image_alt = '';

 	/**
 	 * Constructor.
@@ -101,6 +114,8 @@ class DeprecatedExtendedTask extends Task {
 				'title'           => '',
 				'is_complete'     => false,
 				'time'            => null,
+				'image_url'       => '',
+				'image_alt'       => '',
 			)
 		);

@@ -114,6 +129,8 @@ class DeprecatedExtendedTask extends Task {
 		$this->level           = $task_args['level'];
 		$this->time            = $task_args['time'];
 		$this->title           = $task_args['title'];
+		$this->image_url       = $task_args['image_url'];
+		$this->image_alt       = $task_args['image_alt'];
 	}

 	/**
@@ -170,6 +187,24 @@ class DeprecatedExtendedTask extends Task {
 		return $this->time;
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return $this->image_url;
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return $this->image_alt;
+	}
+
 	/**
 	 * Check if a task is snoozeable.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
index 8f83aa525bb..8e15df413e8 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php
@@ -217,6 +217,24 @@ abstract class Task {
 		return null;
 	}

+	/**
+	 * Contextual image URL for the task.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return '';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return '';
+	}
+
 	/**
 	 * Check if a task is dismissable.
 	 *
@@ -534,6 +552,8 @@ abstract class Task {
 			'additionalInfo'  => $this->get_additional_info(),
 			'actionLabel'     => $this->get_action_label(),
 			'actionUrl'       => $this->get_action_url(),
+			'imageUrl'        => $this->get_image_url(),
+			'imageAlt'        => $this->get_image_alt(),
 			'isComplete'      => $is_complete,
 			'isInProgress'    => $this->is_in_progress(),
 			'inProgressLabel' => $this->in_progress_label(),
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php
index 5b4061553ee..c9be7f381a2 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php
@@ -62,13 +62,31 @@ class CustomizeStore extends Task {
 		return 'customize-store';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/customize-store-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Customize your store illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
 	 * @return string
 	 */
 	public function get_title() {
-		return __( 'Customize your store ', 'woocommerce' );
+		return __( 'Customize your store', 'woocommerce' );
 	}

 	/**
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/LaunchYourStore.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/LaunchYourStore.php
index 3b3bad0a278..8a957231c56 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/LaunchYourStore.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/LaunchYourStore.php
@@ -28,6 +28,24 @@ class LaunchYourStore extends Task {
 		return 'launch-your-store';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/launch-your-store-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Launch your store illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php
index bfd4117e4a2..d11d50df58b 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php
@@ -55,6 +55,24 @@ class Marketing extends Task {
 		return 'marketing';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/sales-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Marketing illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php
index 6c1d6370162..94ab3c349c4 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php
@@ -33,6 +33,24 @@ class Payments extends Task {
 		return 'payments';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/payment-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Payment illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
index 59ce45eb436..978c3fe95de 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
@@ -53,6 +53,24 @@ class Products extends Task {
 		return 'products';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/sales-section-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Products illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php
index e38585e76ea..3e8bf0aef43 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php
@@ -38,6 +38,24 @@ class Shipping extends Task {
 		return 'shipping';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/shipping-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Shipping illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreDetails.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreDetails.php
index c8e650a8562..9f33fbc9080 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreDetails.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreDetails.php
@@ -18,6 +18,24 @@ class StoreDetails extends Task {
 		return 'store_details';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/store-details-illustration.png';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Store location illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
index 9625b36607b..22f6a95be6e 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php
@@ -61,6 +61,24 @@ class Tax extends Task {
 		return 'tax';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/tax-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Tax illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php
index 89ba63014b3..8d8c3801f41 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php
@@ -34,6 +34,24 @@ class WooCommercePayments extends Task {
 		return 'woocommerce-payments';
 	}

+	/**
+	 * Contextual image URL.
+	 *
+	 * @return string
+	 */
+	public function get_image_url() {
+		return WC()->plugin_url() . '/assets/images/task_list/payment-illustration.svg';
+	}
+
+	/**
+	 * Alt text for the contextual image.
+	 *
+	 * @return string
+	 */
+	public function get_image_alt() {
+		return __( 'Payment illustration', 'woocommerce' );
+	}
+
 	/**
 	 * Title.
 	 *
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php
index 8af9294f657..562dc354050 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php
@@ -117,6 +117,8 @@ class WC_Admin_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case {
 		$this->assertArrayHasKey( 'content', $json );
 		$this->assertArrayHasKey( 'actionLabel', $json );
 		$this->assertArrayHasKey( 'actionUrl', $json );
+		$this->assertArrayHasKey( 'imageUrl', $json );
+		$this->assertArrayHasKey( 'imageAlt', $json );
 		$this->assertArrayHasKey( 'isComplete', $json );
 		$this->assertArrayHasKey( 'canView', $json );
 		$this->assertArrayHasKey( 'time', $json );
@@ -127,6 +129,25 @@ class WC_Admin_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case {
 		$this->assertArrayHasKey( 'snoozedUntil', $json );
 	}

+	/**
+	 * Tests that a task exposes empty image metadata by default.
+	 */
+	public function test_default_image_metadata() {
+		$task = new TestTask(
+			new TaskList( array( 'id' => 'setup' ) ),
+			array(
+				'id' => 'wc-unit-test-task',
+			)
+		);
+
+		$this->assertSame( '', $task->get_image_url() );
+		$this->assertSame( '', $task->get_image_alt() );
+
+		$json = $task->get_json();
+		$this->assertSame( '', $json['imageUrl'] );
+		$this->assertSame( '', $json['imageAlt'] );
+	}
+
 	/**
 	 * Tests that a task can be actioned.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
index ff707fe64a3..93bf1aee23c 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
@@ -262,6 +262,12 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
 						public function in_progress_label() {
 							return 'Test account';
 						}
+						public function get_image_url() {
+							return WC()->plugin_url() . '/assets/images/task_list/payment-illustration.svg';
+						}
+						public function get_image_alt() {
+							return 'Payment illustration';
+						}
 					},
 				);
 			}
@@ -310,6 +316,111 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
 		}
 	}

+	/**
+	 * Tests the widget renders a task-provided contextual image.
+	 *
+	 * @testdox Widget output uses the task's own image_url and image_alt when provided.
+	 */
+	public function test_widget_output_uses_task_provided_image() {
+		// phpcs:disable Squiz.Commenting
+		$task_list = new class() {
+			public function is_complete() {
+				return false;
+			}
+			public function is_hidden() {
+				return false;
+			}
+			public function get_viewable_tasks() {
+				return array(
+					new class() extends \Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task {
+						public function get_id() {
+							return 'third-party-task';
+						}
+						public function get_title() {
+							return 'Third party task';
+						}
+						public function get_content() {
+							return 'A task added by an extension.';
+						}
+						public function get_time() {
+							return '5 minutes';
+						}
+						public function is_complete() {
+							return false;
+						}
+						public function get_image_url() {
+							return 'https://example.com/custom-illustration.png';
+						}
+						public function get_image_alt() {
+							return 'Custom extension illustration';
+						}
+					},
+				);
+			}
+		};
+		// phpcs:enable Squiz.Commenting
+
+		$widget = $this->get_widget();
+		$widget->set_task_list( $task_list );
+
+		ob_start();
+		$widget->render();
+		$html = ob_get_clean();
+
+		$this->assertStringContainsString( 'https://example.com/custom-illustration.png', $html );
+		$this->assertStringContainsString( 'Custom extension illustration', $html );
+		$this->assertStringNotContainsString( 'dashboard-widget-setup.png', $html );
+	}
+
+	/**
+	 * Tests the widget falls back to the default image when the task has none.
+	 *
+	 * @testdox Widget output falls back to the generic setup image when the task provides no image.
+	 */
+	public function test_widget_output_falls_back_to_default_image() {
+		// phpcs:disable Squiz.Commenting
+		$task_list = new class() {
+			public function is_complete() {
+				return false;
+			}
+			public function is_hidden() {
+				return false;
+			}
+			public function get_viewable_tasks() {
+				return array(
+					new class() extends \Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task {
+						public function get_id() {
+							return 'third-party-task';
+						}
+						public function get_title() {
+							return 'Third party task';
+						}
+						public function get_content() {
+							return 'A task added by an extension.';
+						}
+						public function get_time() {
+							return '5 minutes';
+						}
+						public function is_complete() {
+							return false;
+						}
+					},
+				);
+			}
+		};
+		// phpcs:enable Squiz.Commenting
+
+		$widget = $this->get_widget();
+		$widget->set_task_list( $task_list );
+
+		ob_start();
+		$widget->render();
+		$html = ob_get_clean();
+
+		$this->assertStringContainsString( 'dashboard-widget-setup.png', $html );
+		$this->assertStringContainsString( 'WooCommerce setup illustration', $html );
+	}
+
 	/**
 	 * Tests get_button_link redirects to core profiler when it needs completion.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTaskTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTaskTest.php
new file mode 100644
index 00000000000..786fbd53934
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/OnboardingTasks/DeprecatedExtendedTaskTest.php
@@ -0,0 +1,54 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\Features\OnboardingTasks;
+
+use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
+use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskList;
+use WC_Unit_Test_Case;
+
+/**
+ * DeprecatedExtendedTask test.
+ *
+ * @class DeprecatedExtendedTaskTest.
+ */
+class DeprecatedExtendedTaskTest extends WC_Unit_Test_Case {
+
+	/**
+	 * Tests that image metadata provided via task args is exposed.
+	 */
+	public function test_exposes_image_metadata_from_args() {
+		$task = new DeprecatedExtendedTask(
+			new TaskList( array( 'id' => 'extended' ) ),
+			array(
+				'id'        => 'third-party-task',
+				'title'     => 'Third party task',
+				'image_url' => 'https://example.com/image.png',
+				'image_alt' => 'Third party illustration',
+			)
+		);
+
+		$this->assertSame( 'https://example.com/image.png', $task->get_image_url() );
+		$this->assertSame( 'Third party illustration', $task->get_image_alt() );
+
+		$json = $task->get_json();
+		$this->assertSame( 'https://example.com/image.png', $json['imageUrl'] );
+		$this->assertSame( 'Third party illustration', $json['imageAlt'] );
+	}
+
+	/**
+	 * Tests that image metadata defaults to empty strings when not provided.
+	 */
+	public function test_defaults_to_empty_image_metadata() {
+		$task = new DeprecatedExtendedTask(
+			new TaskList( array( 'id' => 'extended' ) ),
+			array(
+				'id'    => 'third-party-task',
+				'title' => 'Third party task',
+			)
+		);
+
+		$this->assertSame( '', $task->get_image_url() );
+		$this->assertSame( '', $task->get_image_alt() );
+	}
+}