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() );
+ }
+}