diff --git a/plugins/woocommerce-admin/client/two-column-tasks/footer-slot/footer-slot.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/footer-slot/footer-slot.tsx
similarity index 100%
rename from plugins/woocommerce-admin/client/two-column-tasks/footer-slot/footer-slot.tsx
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/footer-slot/footer-slot.tsx
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/footer-slot/index.ts b/plugins/woocommerce-admin/client/task-lists/setup-task-list/footer-slot/index.ts
similarity index 100%
rename from plugins/woocommerce-admin/client/two-column-tasks/footer-slot/index.ts
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/footer-slot/index.ts
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/footer-slot/utils.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/footer-slot/utils.tsx
similarity index 100%
rename from plugins/woocommerce-admin/client/two-column-tasks/footer-slot/utils.tsx
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/footer-slot/utils.tsx
diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/index.ts b/plugins/woocommerce-admin/client/task-lists/setup-task-list/index.ts
new file mode 100644
index 00000000000..ea833dd9351
--- /dev/null
+++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/index.ts
@@ -0,0 +1,2 @@
+export * from './setup-task-list';
+export * from './components/task-list-placeholder';
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/setup-task-list.tsx
similarity index 92%
rename from plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/setup-task-list.tsx
index e11fc9bd392..d6da84ecd61 100644
--- a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/setup-task-list.tsx
@@ -24,13 +24,12 @@ import { useLayoutContext } from '@woocommerce/admin-layout';
/**
* Internal dependencies
*/
-import '../tasks/task-list.scss';
-import taskHeaders from './task-headers';
-import DismissModal from './dismiss-modal';
-import TaskListCompleted from './completed';
-import { ProgressHeader } from '~/task-lists/progress-header';
-import { TaskListItemTwoColumn } from './task-list-item-two-column';
-import { TaskListCompletedHeader } from './completed-header';
+import { taskHeaders } 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';
+import { TaskListItem } from './components/task-list-item';
+import { TaskListCompletedHeader } from './components/task-list-completed-header';
import { ExperimentalWooTaskListFooter } from './footer-slot';
import {
@@ -40,20 +39,18 @@ import {
export type TaskListProps = TaskListType & {
eventName?: string;
- twoColumns?: boolean;
query: {
task?: string;
};
cesHeader?: boolean;
};
-export const TaskList: React.FC< TaskListProps > = ( {
+export const SetupTaskList: React.FC< TaskListProps > = ( {
query,
id,
eventName,
eventPrefix,
tasks,
- twoColumns,
keepCompletedTaskList,
isComplete,
displayProgressHeader,
@@ -271,7 +268,6 @@ export const TaskList: React.FC< TaskListProps > = ( {
) }
>
@@ -292,8 +288,7 @@ export const TaskList: React.FC< TaskListProps > = ( {
) : null }
= ( {
{ visibleTasks.map( ( task, index ) => {
return (
- = ( {
);
};
-export default TaskList;
+export default SetupTaskList;
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/style.scss b/plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss
similarity index 94%
rename from plugins/woocommerce-admin/client/two-column-tasks/style.scss
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss
index ed430c7d43e..b92bd129595 100644
--- a/plugins/woocommerce-admin/client/two-column-tasks/style.scss
+++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss
@@ -95,13 +95,7 @@
}
}
-.two-columns .woocommerce-task-dashboard__container.two-column-experiment {
- .svg-background {
- top: 15%;
- }
-}
-
-.woocommerce-task-dashboard__container.two-column-experiment {
+.woocommerce-task-dashboard__container.setup-task-list {
max-width: 1032px;
display: flex;
flex-direction: row;
@@ -150,19 +144,8 @@
}
}
- &:not(.two-columns) {
- @include single-column;
- }
-
- &.two-columns .svg-background {
- top: 50%;
- bottom: 50%;
- margin-top: auto;
- margin-bottom: auto;
- }
ul {
- display: flex;
li {
display: block;
width: 100%;
@@ -217,6 +200,9 @@
width: 100%;
}
+ @include single-column;
+
+
@for $i from 1 through 10 {
.woocommerce-task-list__item:not(.complete).index-#{$i} .woocommerce-task__icon::after {
content: '#{$i}';
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-completion-slot/index.ts b/plugins/woocommerce-admin/client/task-lists/setup-task-list/task-list-completion-slot/index.ts
similarity index 100%
rename from plugins/woocommerce-admin/client/two-column-tasks/task-list-completion-slot/index.ts
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/task-list-completion-slot/index.ts
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-completion-slot/task-list-completion-slot.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/task-list-completion-slot/task-list-completion-slot.tsx
similarity index 100%
rename from plugins/woocommerce-admin/client/two-column-tasks/task-list-completion-slot/task-list-completion-slot.tsx
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/task-list-completion-slot/task-list-completion-slot.tsx
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-completion-slot/utils.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/task-list-completion-slot/utils.tsx
similarity index 100%
rename from plugins/woocommerce-admin/client/two-column-tasks/task-list-completion-slot/utils.tsx
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/task-list-completion-slot/utils.tsx
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx
similarity index 94%
rename from plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx
rename to plugins/woocommerce-admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx
index a55570387e5..2624c30ccfc 100644
--- a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/test/setup-task-list.test.tsx
@@ -8,7 +8,7 @@ import { TaskType } from '@woocommerce/data';
/**
* Internal dependencies
*/
-import { TaskList } from '../task-list';
+import { SetupTaskList } from '../setup-task-list';
jest.mock( '@woocommerce/tracks', () => ( {
recordEvent: jest.fn(),
@@ -27,10 +27,12 @@ jest.mock( '@woocommerce/components', () => ( {
.fn()
.mockImplementation( () => task_list_menu
),
} ) );
-jest.mock( '../task-headers', () => ( {
- optional: () => optional_header
,
- required: () => required_header
,
- completed: () => completed_header
,
+jest.mock( '../components/task-headers', () => ( {
+ taskHeaders: {
+ optional: () => optional_header
,
+ required: () => required_header
,
+ completed: () => completed_header
,
+ },
} ) );
jest.mock( '@woocommerce/data', () => ( {
...jest.requireActual( '@woocommerce/data' ),
@@ -155,7 +157,7 @@ describe( 'TaskList', () => {
it( 'should trigger tasklist_view event on initial render for setup task list', () => {
render(
- {
it( 'should trigger tasklist_view event on initial render for setup task list with eventPrefix if eventName is undefined', () => {
render(
- {
it( 'should trigger {id}_tasklist_view event on initial render for setup task list if id is not setup', () => {
render(
- {
it( 'should render the task header of the first uncompleted task', () => {
const { queryByText } = render(
- {
it( 'should render all tasks', () => {
const { queryByText } = render(
- {
it( 'should not display isDismissed tasks', () => {
const dismissedTask = [ { ...tasks.setup[ 0 ], isDismissed: true } ];
const { queryByText } = render(
- = ( { query } ) => {
+export const TaskLists: React.FC< TaskListsProps > = ( { query } ) => {
const { task } = query;
const { hideTaskList } = useDispatch( ONBOARDING_STORE_NAME );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
@@ -108,7 +110,7 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => {
const taskListIds = getAdminSetting( 'visibleTaskListIds', [] );
const TaskListPlaceholderComponent =
taskListIds[ 0 ] === 'setup'
- ? TwoColumnTaskListPlaceholder
+ ? SetupTaskListPlaceholder
: TasksPlaceholder;
if ( isResolving ) {
@@ -118,21 +120,17 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => {
return (
<>
{ taskLists
- .filter(
- ( { id }: TaskListType ) => ! id.endsWith( 'two_column' )
- )
.filter( ( { isVisible }: TaskListType ) => isVisible )
.map( ( taskList: TaskListType ) => {
const { id, isHidden, isToggleable } = taskList;
const TaskListComponent =
- id === 'setup' ? TwoColumnTaskList : TaskList;
+ id === 'setup' ? SetupTaskList : TaskList;
return (
{ isToggleable && (
diff --git a/plugins/woocommerce-admin/client/tasks/test/tasks.test.tsx b/plugins/woocommerce-admin/client/task-lists/test/tasks.test.tsx
similarity index 85%
rename from plugins/woocommerce-admin/client/tasks/test/tasks.test.tsx
rename to plugins/woocommerce-admin/client/task-lists/test/tasks.test.tsx
index 583c04e1688..666146ddfb5 100644
--- a/plugins/woocommerce-admin/client/tasks/test/tasks.test.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/test/tasks.test.tsx
@@ -9,10 +9,10 @@ import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
-import { Tasks } from '../tasks';
-import { TaskProps } from '../task';
-import { TaskListProps } from '../task-list';
-import { TaskListProps as TwoColumnTaskListProps } from '../../two-column-tasks/task-list';
+import { TaskLists } from '../task-lists';
+import { TaskProps } from '../components/task';
+import { TaskListProps } from '../components/task-list';
+import { TaskListProps as SetupTaskListProps } from '../setup-task-list/setup-task-list';
jest.mock( '@wordpress/data', () => {
// Require the original module to not be mocked...
@@ -29,21 +29,21 @@ jest.mock( '@wordpress/data', () => {
jest.mock( '@woocommerce/explat' );
jest.mock( '@woocommerce/tracks' );
-jest.mock( '../task-list', () => ( {
+jest.mock( '../components/task-list', () => ( {
TaskList: ( { id }: TaskListProps ) => task-list:{ id }
,
} ) );
-jest.mock( '../../two-column-tasks/task-list', () => ( {
- TaskList: ( { id }: TwoColumnTaskListProps ) => (
- two-column-list:{ id }
+jest.mock( '../setup-task-list', () => ( {
+ SetupTaskList: ( { id }: SetupTaskListProps ) => (
+ setup-task-list:{ id }
),
} ) );
-jest.mock( '../task', () => ( {
+jest.mock( '../components/task', () => ( {
Task: ( { query }: TaskProps ) => task:{ query.task }
,
} ) );
-jest.mock( '../placeholder', () => ( {
+jest.mock( '../components/placeholder', () => ( {
TasksPlaceholder: () => task-placeholder
,
} ) );
@@ -83,14 +83,14 @@ describe( 'Task', () => {
it( 'should render if no current task and finished resolving', () => {
const { queryByText } = render(
-
+
);
waitFor( () => {
expect( queryByText( 'task-list:main' ) ).toBeInTheDocument();
expect( queryByText( 'task-list:extended' ) ).toBeInTheDocument();
expect(
- queryByText( 'two-column-list:setup' )
+ queryByText( 'setup-task-list:setup' )
).not.toBeInTheDocument();
} );
} );
@@ -98,7 +98,7 @@ describe( 'Task', () => {
it( 'should render the task component if query has an existing task', () => {
const { queryByText } = render(
-
+
);
expect( queryByText( 'task:main-task-1' ) ).toBeInTheDocument();
@@ -107,7 +107,7 @@ describe( 'Task', () => {
it( 'should not render anything if query has task, but task does not exist', () => {
const { queryByText } = render(
-
+
);
expect(
@@ -122,7 +122,7 @@ describe( 'Task', () => {
} ) );
const { queryByText } = render(
-
+
);
expect( queryByText( 'task-placeholder' ) ).toBeInTheDocument();
@@ -144,7 +144,7 @@ describe( 'Task', () => {
} ) );
const { queryByText } = render(
-
+
);
expect( queryByText( 'Show things to do next' ) ).toBeInTheDocument();
@@ -153,7 +153,7 @@ describe( 'Task', () => {
it( 'should class updateOptions with default data on render', () => {
render(
-
+
);
waitFor( () => {
@@ -180,7 +180,7 @@ describe( 'Task', () => {
} ) );
const { getByText } = render(
-
+
);
act( () => {
@@ -209,7 +209,7 @@ describe( 'Task', () => {
} ) );
const { getByText } = render(
-
+
);
act( () => {
diff --git a/plugins/woocommerce-admin/client/tasks/index.ts b/plugins/woocommerce-admin/client/tasks/index.ts
deleted file mode 100644
index 965ee2d89d9..00000000000
--- a/plugins/woocommerce-admin/client/tasks/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Internal dependencies
- */
-import './fills';
-import './deprecated-tasks';
-
-export * from './tasks';
-export * from './placeholder';
-export * from './reminder-bar';
-export * from './hooks/useActiveSetupList';
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/extended-task.tsx b/plugins/woocommerce-admin/client/two-column-tasks/extended-task.tsx
deleted file mode 100644
index dd0bc2df026..00000000000
--- a/plugins/woocommerce-admin/client/two-column-tasks/extended-task.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { MenuGroup, MenuItem } from '@wordpress/components';
-import { check } from '@wordpress/icons';
-import { Fragment, useEffect } from '@wordpress/element';
-import { useDispatch, useSelect } from '@wordpress/data';
-import {
- ONBOARDING_STORE_NAME,
- OPTIONS_STORE_NAME,
- TaskListType,
- TaskType,
-} from '@woocommerce/data';
-import { recordEvent } from '@woocommerce/tracks';
-
-/**
- * Internal dependencies
- */
-import { DisplayOption } from '~/activity-panel/display-options';
-import { TaskList } from '../tasks/task-list';
-import { TasksPlaceholder } from '../tasks/placeholder';
-import '../tasks/tasks.scss';
-
-export type TasksProps = {
- query: { task?: string };
-};
-
-const ExtendedTask: React.FC< TasksProps > = ( { query } ) => {
- const { hideTaskList } = useDispatch( ONBOARDING_STORE_NAME );
- const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
- const { task } = query;
-
- const {
- isResolving,
- taskLists,
- }: {
- isResolving: boolean;
- taskLists: TaskListType[];
- } = useSelect( ( select ) => {
- return {
- isResolving: select( ONBOARDING_STORE_NAME ).isResolving(
- 'getTaskListsByIds'
- ),
- taskLists: select( ONBOARDING_STORE_NAME ).getTaskListsByIds( [
- 'extended_two_column',
- ] ),
- };
- } );
- const toggleTaskList = ( taskList: TaskListType ) => {
- const { id, isHidden } = taskList;
- const newValue = ! isHidden;
-
- recordEvent(
- newValue ? `${ id }_tasklist_hide` : `${ id }_tasklist_show`,
- {}
- );
-
- hideTaskList( id );
- };
-
- useEffect( () => {
- updateOptions( {
- woocommerce_task_list_prompt_shown: true,
- } );
- }, [ taskLists, isResolving ] );
-
- // If a task detail is being shown, we shouldn't show the extended tasklist.
- if ( task ) {
- return null;
- }
-
- if ( isResolving ) {
- return ;
- }
-
- const extendedTaskList = taskLists[ 0 ];
-
- if ( ! extendedTaskList || extendedTaskList.tasks.length === 0 ) {
- return null;
- }
-
- const completedTasks = extendedTaskList.tasks.filter(
- ( extendedTaskListTask: TaskType ) => {
- return extendedTaskListTask.isComplete;
- }
- );
-
- // Use custom isComplete since we're manually adding tasks
- // to the extended task list.
- const isComplete = completedTasks.length === extendedTaskList.tasks.length;
-
- const {
- id,
- eventPrefix,
- isHidden,
- isVisible,
- isToggleable,
- title,
- tasks,
- displayProgressHeader,
- keepCompletedTaskList,
- } = extendedTaskList;
-
- if ( ! isVisible ) {
- return null;
- }
-
- return (
-
-
- { isToggleable && (
-
-
-
-
-
- ) }
-
- );
-};
-
-export default ExtendedTask;
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/index.js b/plugins/woocommerce-admin/client/two-column-tasks/index.js
deleted file mode 100644
index 81c8ca7203a..00000000000
--- a/plugins/woocommerce-admin/client/two-column-tasks/index.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { useSelect } from '@wordpress/data';
-import { ONBOARDING_STORE_NAME } from '@woocommerce/data';
-
-/**
- * Internal dependencies
- */
-import '../tasks/tasks.scss';
-import './style.scss';
-import TaskList from './task-list';
-import TaskListPlaceholder from './placeholder';
-import { Task } from '../tasks/task';
-
-const TaskDashboard = ( { query, twoColumns } ) => {
- const { task } = query;
-
- const { isResolving, taskLists } = useSelect( ( select ) => {
- return {
- taskLists: select( ONBOARDING_STORE_NAME ).getTaskListsByIds( [
- 'setup_two_column',
- 'extended_two_column',
- ] ),
- isResolving: select( ONBOARDING_STORE_NAME ).isResolving(
- 'getTaskListsByIds'
- ),
- };
- } );
- const getCurrentTask = () => {
- if ( ! task ) {
- return null;
- }
-
- const tasks = taskLists.reduce(
- ( acc, taskList ) => [ ...acc, ...taskList.tasks ],
- []
- );
-
- const currentTask = tasks.find( ( t ) => t.id === task );
-
- if ( ! currentTask ) {
- return null;
- }
-
- return currentTask;
- };
-
- const currentTask = getCurrentTask();
-
- if ( task && ! currentTask ) {
- return null;
- }
-
- if ( isResolving || ! taskLists[ 0 ] ) {
- return ;
- }
-
- if ( currentTask ) {
- return (
-
-
-
- );
- }
- // List of task items to be shown on the main task list.
- // Any other remaining tasks will be moved to the extended task list.
- const taskList = taskLists[ 0 ];
- const setupTasks = taskList.tasks;
-
- const completedTasks = setupTasks.filter(
- ( setupTask ) => setupTask.isComplete
- );
- const isTaskListComplete = setupTasks.length === completedTasks.length;
-
- const dismissedTasks = setupTasks.filter(
- ( setupTask ) => setupTask.isDismissed
- );
-
- return (
- <>
- { setupTasks && ( taskList.isVisible || task ) && (
-
- ) }
- >
- );
-};
-
-export default TaskDashboard;
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-headers.ts b/plugins/woocommerce-admin/client/two-column-tasks/task-headers.ts
deleted file mode 100644
index 6e4ee244eb7..00000000000
--- a/plugins/woocommerce-admin/client/two-column-tasks/task-headers.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Internal dependencies
- */
-import StoreDetailsHeader from './headers/store-details';
-import TaxHeader from './headers/tax';
-import MarketingHeader from './headers/marketing';
-import AppearanceHeader from './headers/appearance';
-import ShippingHeader from './headers/shipping';
-import ProductsHeader from './headers/products';
-import PurchaseHeader from './headers/purchase';
-import PaymentsHeader from './headers/payments';
-import WoocommercePaymentsHeader from './headers/woocommerce-payments';
-
-const taskHeaders: Record< string, React.ElementType > = {
- store_details: StoreDetailsHeader,
- tax: TaxHeader,
- shipping: ShippingHeader,
- marketing: MarketingHeader,
- appearance: AppearanceHeader,
- payments: PaymentsHeader,
- products: ProductsHeader,
- purchase: PurchaseHeader,
- 'woocommerce-payments': WoocommercePaymentsHeader,
-};
-
-export default taskHeaders;
diff --git a/plugins/woocommerce/changelog/dev-clean-up-task-list b/plugins/woocommerce/changelog/dev-clean-up-task-list
new file mode 100644
index 00000000000..7e60d34da07
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-clean-up-task-list
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Cleanup task list and organize tasks file structure
diff --git a/plugins/woocommerce/changelog/fix-37806 b/plugins/woocommerce/changelog/fix-37806
new file mode 100644
index 00000000000..fe00a2ff37d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-37806
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+When deleting an administrator user, any existing webhook(s) owned to the user being deleted are re-assigned to the nominated user if the "Attribute all content to" option is chosen, or re-assigned to user id zero. This helps avoid `woocommerce_rest_cannot_view` webhook payload errors.
diff --git a/plugins/woocommerce/changelog/update-update-rin-transformers-doc b/plugins/woocommerce/changelog/update-update-rin-transformers-doc
new file mode 100644
index 00000000000..8e03ffd0ae7
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-update-rin-transformers-doc
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Update transformers doc with examples
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 5b83720053b..9ca227c7ee6 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -18,6 +18,7 @@ use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as ProductDownloadDirectories;
use Automattic\WooCommerce\Internal\RestockRefundedItemsAdjuster;
use Automattic\WooCommerce\Internal\Settings\OptionSanitizer;
+use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
/**
@@ -240,6 +241,7 @@ final class WooCommerce {
$container->get( OptionSanitizer::class );
$container->get( BatchProcessingController::class );
$container->get( FeaturesController::class );
+ $container->get( WebhookUtil::class );
}
/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php
index e8b417e3a8f..adb0f36be22 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-webhook-data-store.php
@@ -282,6 +282,7 @@ class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {
$exclude = '';
$date_created = '';
$date_modified = '';
+ $user_id = '';
if ( ! empty( $args['include'] ) ) {
$args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) );
@@ -293,6 +294,10 @@ class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {
$exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')';
}
+ if ( ! empty( $args['user_id'] ) ) {
+ $user_id = $wpdb->prepare( 'AND `user_id` = %d', absint( $args['user_id'] ) );
+ }
+
if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
$args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
$args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];
@@ -326,6 +331,7 @@ class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {
{$exclude}
{$date_created}
{$date_modified}
+ {$user_id}
{$order}
{$limit}
{$offset}"
@@ -349,6 +355,7 @@ class WC_Webhook_Data_Store implements WC_Webhook_Data_Store_Interface {
{$exclude}
{$date_created}
{$date_modified}
+ {$user_id}
{$order}
{$limit}
{$offset}"
diff --git a/plugins/woocommerce/src/Admin/API/OnboardingTasks.php b/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
index a7e80da382d..9f637873ed7 100644
--- a/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
+++ b/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
@@ -295,10 +295,14 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
/**
* Check if a given request has access to manage woocommerce.
*
+ * @deprecated 7.8.0 snooze task is deprecated.
+ *
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function snooze_task_permissions_check( $request ) {
+ wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.8.0' );
+
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to snooze onboarding tasks.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -816,11 +820,15 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
/**
* Snooze an onboarding task.
*
+ * @deprecated 7.8.0 snooze task is deprecated.
+ *
* @param WP_REST_Request $request Request data.
*
* @return WP_REST_Response|WP_Error
*/
public function snooze_task( $request ) {
+ wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.8.0' );
+
$task_id = $request->get_param( 'id' );
$task_list_id = $request->get_param( 'task_list_id' );
$duration = $request->get_param( 'duration' );
@@ -853,10 +861,14 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
/**
* Undo snooze of a single task.
*
+ * @deprecated 7.8.0 undo snooze task is deprecated.
+ *
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function undo_snooze_task( $request ) {
+ wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.8.0' );
+
$id = $request->get_param( 'id' );
$task = TaskLists::get_task( $id );
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php
index e3e2f91a72a..8385ee9865a 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php
@@ -152,45 +152,6 @@ class TaskLists {
),
)
);
- self::add_list(
- array(
- 'id' => 'setup_two_column',
- 'hidden_id' => 'setup',
- 'title' => __( 'Get ready to start selling', 'woocommerce' ),
- 'tasks' => array(
- 'Products',
- 'WooCommercePayments',
- 'Payments',
- 'Tax',
- 'Shipping',
- 'Marketing',
- 'Appearance',
- ),
- 'event_prefix' => 'tasklist_',
- )
- );
- self::add_list(
- array(
- 'id' => 'extended_two_column',
- 'hidden_id' => 'extended',
- 'title' => __( 'Things to do next', 'woocommerce' ),
- 'sort_by' => array(
- array(
- 'key' => 'is_complete',
- 'order' => 'asc',
- ),
- array(
- 'key' => 'level',
- 'order' => 'asc',
- ),
- ),
- 'tasks' => array(
- 'AdditionalPayments',
- 'GetMobileApp',
- ),
- 'event_prefix' => 'extended_tasklist_',
- )
- );
if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
self::add_task(
@@ -200,13 +161,6 @@ class TaskLists {
)
);
- self::add_task(
- 'extended_two_column',
- new ReviewShippingOptions(
- self::get_list( 'extended_two_column' )
- )
- );
-
// Tasklist that will never be shown in homescreen,
// used for having tasks that are accessed by other means.
self::add_list(
@@ -225,7 +179,6 @@ class TaskLists {
if ( ! wp_is_mobile() ) { // Permit In-App Marketplace Tour on desktops only.
$tour_task = new TourInAppMarketplace();
self::add_task( 'extended', $tour_task );
- self::add_task( 'extended_two_column', $tour_task );
}
if ( has_filter( 'woocommerce_admin_experimental_onboarding_tasklists' ) ) {
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php
index 9465bb3e94a..ab1aa4dd002 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php
@@ -91,9 +91,6 @@ class AdditionalPayments extends Payments {
// Hide task if WC Pay is not installed via OBW, or is not connected, or the store is located in a country that is not supported by WC Pay.
return false;
}
- if ( $this->get_parent_id() === 'extended_two_column' && WooCommercePayments::is_connected() ) {
- return false;
- }
return true;
}
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php
index 9c7fc60ae38..409a69fefd7 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php
@@ -92,7 +92,7 @@ class WooCommercePayments extends Task {
return ! $payments->is_complete() && // Do not re-display the task if the "add payments" task has already been completed.
self::is_installed() &&
self::is_supported() &&
- ( $this->get_parent_id() !== 'setup_two_column' || ! self::is_connected() );
+ ! self::is_connected();
}
/**
diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md
index 96c8a5017d5..ca447a299c1 100644
--- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md
+++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md
@@ -2,9 +2,9 @@
An option transformer is a class that transforms the given option value into a different value for the comparison operation.
-Transformers run in the order they are defined. Each transformer passes down the value it transformed to the next transformer to consume.
+Transformers run in the order in which they are defined, and each transformer passes down the value it transformed to the next transformer for consumption.
-Definition example:
+**Definition example**: transformers are always used with `option` rule.
```
{
@@ -37,17 +37,17 @@ Definition example:
}
```
-### ArrayColumn (array_column)
+## array_column
-This uses PHP's built-in `array_column` to select values by a given array key.
+PHP's built-in `array_column` to select values from a single column. For more information about how array_column works, please see PHP's [official documentation](https://www.php.net/manual/en/function.array-column.php).
-Arguments:
+#### Arguments:
| name | description |
| ---- | -------------- |
| key | array key name |
-Definition:
+#### Definition:
```php
"transformers": [
@@ -60,13 +60,49 @@ Definition:
],
```
-### ArrayFlatten (array_flatten)
+#### Example:
-This flattens a nested array.
+Given the following data
-Arguments: N/A
+```php
+array(
+ array("industry" => "media" ),
+ array("industry" => "software" )
+);
+```
-Definition:
+Use `array_column` to extract `array("media", "software")` then choose the first element with `dot_notation`.
+
+```php
+"transformers": [
+ {
+ "use": "array_column",
+ "arguments": {
+ "key": "industry"
+ }
+ },
+ {
+ "use": "dot_noation",
+ "arguments": {
+ "key": "0"
+ }
+ }
+],
+```
+
+**Output**: "media"
+
+
+
+
+
+## array_flatten
+
+Flattens a nested array.
+
+#### Arguments: N/A
+
+#### Definition:
```php
"transformers": [
@@ -76,13 +112,50 @@ Definition:
],
```
-### ArrayKeys (array_keys)
+#### Example:
-This uses PHP's built-in `array_keys` to return keys from an array.
+Given the follwoing data
-Arguments: N/A
+```php
+array(
+ array(
+ 'member1',
+ ),
+ array(
+ 'member2',
+ ),
+ array(
+ 'member3',
+ ),
+);
+```
-Definition:
+Use `array_flatten` to extract `array("member1", "member2", "member3")` then use `array_search` to make sure it has `member2`
+
+
+```php
+"transformers": [
+ {
+ "use": "array_flatten",
+ },
+ {
+ "use": "array_search",
+ "arguments": {
+ "key": "member2"
+ }
+ }
+],
+```
+
+**Output**: true
+
+## array_keys
+
+PHP's built-in `array_keys` to return keys from an array. For more information about how `array_keys` works, please see PHP’s [official documentation](https://www.php.net/manual/en/function.array-column.php).
+
+#### Arguments: N/A
+
+####Definition:
```php
"transformers": [
@@ -92,16 +165,47 @@ Definition:
],
```
-### ArraySearch (array_search)
+#### Example:
-This uses PHP's built-in `array_search` to search a value in an array.
+Given the follwing data
+
+```php
+array(
+ "name" => "tester",
+ "address" => "test",
+ "supports_version_2" => true
+)
+```
+
+Use `array_keys` to extract `array("name", "address", "supports_version_2")` and then use `array_search` to make sure it has `supports_version_2`
+
+```php
+"transformers": [
+ {
+ "use": "array_keys",
+ },
+ {
+ "use": "array_search",
+ "arguments": {
+ "key": "member2"
+ }
+ }
+],
+```
+
+**Output**: true
+
+## array_search
+
+PHP's built-in `array_search` to search a value in an array. For more information about how `array_search` works, please see PHP’s [official documentation](https://www.php.net/manual/en/function.array-search.php).
+
+#### Arguments:
-Arguments:
|name|description|
|----|---------|
| value | a value to search in the given array |
-Definition:
+#### Definition:
```php
"transformers": [
@@ -114,13 +218,18 @@ Definition:
],
```
-### ArrayValues (array_values)
+#### Examples
-This uses PHP's built-in array_values to return values from an array.
+See examples from [array_flatten](#array_flatten) and [array_keys](#array_keys)
-Arguments: N/A
+## array_values
-Definition:
+PHP's built-in array_values to return values from an array. For more information about how `array_values` works, please see PHP’s [official documentation](https://www.php.net/manual/en/function.array-values).
+
+
+#### Arguments: N/A
+
+#### Definition:
```php
"transformers": [
@@ -130,20 +239,37 @@ Definition:
],
```
-### DotNotation (dot_notation)
+#### Example:
-This uses dot notation to select a value in an array. Dot notation lets you access an array as if it is an object.
-
-Let's say we have the following array.
+Given the follwoing data
```php
-$items = [
- 'name' => 'name',
- 'members' => ['member1', 'member2']
-];
+array (
+ "size" => "x-large"
+)
```
-Example definition to select `$items['name']`:
+Use `array_values` to extract `array("x-large")`
+
+```php
+"transformers": [
+ {
+ "use": "array_values",
+ }
+],
+```
+
+**Output:** "x-large"
+
+
+## dot_notation
+
+Uses dot notation to select a value in an array. Dot notation lets you access an array as if it is an object.
+
+#### Arguments: N/A
+
+#### Definition:
+
```php
"transformers": [
@@ -156,26 +282,56 @@ Example definition to select `$items['name']`:
],
```
-Example definition to select `$items['members'][0]`:
+#### Example:
+
+
+
+Given the follwoing data
+
+```php
+array(
+ 'name' => 'john',
+ 'members' => ['member1', 'member2']
+);
+```
+
+Select `name` field.
```php
"transformers": [
{
"use": "dot_notation",
"arguments": {
- "path": "members.0"
+ "path": "name"
}
}
],
```
-### Count (count)
+**Output:** "john"
-This uses PHP's built-in count to return the number of values from a countable, such as an array.
+Select `member2`. You can access array items with an index.
-Arguments: N/A
+```php
+"transformers": [
+ {
+ "use": "dot_notation",
+ "arguments": {
+ "path": "members.1"
+ }
+ }
+],
+```
-Definition:
+**Output:**: "member2"
+
+## count
+
+PHP's built-in count to return the number of values from a countable, such as an array.
+
+#### Arguments: N/A
+
+#### Definition:
```php
"transformers": [
@@ -184,3 +340,29 @@ Definition:
}
],
```
+
+#### Example:
+
+Given the follwing list of usernames
+
+```php
+array(
+ "username1",
+ "username2",
+ "username3"
+)
+```
+
+Let's count # of users with `count`
+
+```php
+"transformers": [
+ {
+ "use": "count",
+ }
+],
+```
+
+**Output:** 3
+
+
diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/UtilsClassesServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/UtilsClassesServiceProvider.php
index c6d6ea53fd3..52e25ba49f8 100644
--- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/UtilsClassesServiceProvider.php
+++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/UtilsClassesServiceProvider.php
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\Utilities\HtmlSanitizer;
+use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
@@ -31,6 +32,7 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
OrderUtil::class,
PluginUtil::class,
COTMigrationUtil::class,
+ WebhookUtil::class,
);
/**
@@ -44,5 +46,6 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
->addArgument( LegacyProxy::class );
$this->share( COTMigrationUtil::class )
->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
+ $this->share( WebhookUtil::class );
}
}
diff --git a/plugins/woocommerce/src/Internal/Utilities/WebhookUtil.php b/plugins/woocommerce/src/Internal/Utilities/WebhookUtil.php
new file mode 100644
index 00000000000..e6ff21320f5
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Utilities/WebhookUtil.php
@@ -0,0 +1,136 @@
+get_webhook_ids_for_user( $old_user_id );
+
+ foreach ( $webhook_ids as $webhook_id ) {
+ $webhook = new \WC_Webhook( $webhook_id );
+ $webhook->set_user_id( $new_user_id ?? 0 );
+ $webhook->save();
+ }
+ }
+
+ /**
+ * When users are about to be deleted show an informative text if they have webhooks assigned.
+ *
+ * @param \WP_User $current_user The current logged in user.
+ * @param array $userids Array with the ids of the users that are about to be deleted.
+ * @return void
+ * @since 7.8.0
+ */
+ private function maybe_render_user_with_webhooks_warning( \WP_User $current_user, array $userids ): void {
+ global $wpdb;
+
+ $at_least_one_user_with_webhooks = false;
+
+ foreach ( $userids as $user_id ) {
+ $webhook_ids = $this->get_webhook_ids_for_user( $user_id );
+ if ( empty( $webhook_ids ) ) {
+ continue;
+ }
+
+ $at_least_one_user_with_webhooks = true;
+
+ $user_data = get_userdata( $user_id );
+ $user_login = false === $user_data ? '' : $user_data->user_login;
+ $webhooks_count = count( $webhook_ids );
+
+ $text = sprintf(
+ /* translators: 1 = user id, 2 = user login, 3 = webhooks count */
+ _nx(
+ 'User #%1$s %2$s has created %3$d WooCommerce webhook.',
+ 'User #%1$s %2$s has created %3$d WooCommerce webhooks.',
+ $webhooks_count,
+ 'user webhook count',
+ 'woocommerce'
+ ),
+ $user_id,
+ $user_login,
+ $webhooks_count
+ );
+
+ echo '' . esc_html( $text ) . '
';
+ }
+
+ if ( ! $at_least_one_user_with_webhooks ) {
+ return;
+ }
+
+ $webhooks_settings_url = esc_url_raw( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) );
+
+ // This block of code is copied from WordPress' users.php.
+ // phpcs:disable WooCommerce.Commenting.CommentHooks, WordPress.DB.PreparedSQL.NotPrepared
+ $users_have_content = (bool) apply_filters( 'users_have_additional_content', false, $userids );
+ if ( ! $users_have_content ) {
+ if ( $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_author IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
+ $users_have_content = true;
+ } elseif ( $wpdb->get_var( "SELECT link_id FROM {$wpdb->links} WHERE link_owner IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
+ $users_have_content = true;
+ }
+ }
+ // phpcs:enable WooCommerce.Commenting.CommentHooks, WordPress.DB.PreparedSQL.NotPrepared
+
+ if ( $users_have_content ) {
+ $text = __( 'If the "Delete all content" option is selected, the affected WooCommerce webhooks will not be deleted and will be attributed to user id 0.
', 'woocommerce' );
+ } else {
+ $text = __( 'The affected WooCommerce webhooks will not be deleted and will be attributed to user id 0.
', 'woocommerce' );
+ }
+
+ $text .= sprintf(
+ /* translators: 1 = url of the WooCommerce webhooks settings page */
+ __( 'After that they can be reassigned to the logged-in user by going to the WooCommerce webhooks settings page and re-saving them.', 'woocommerce' ),
+ $webhooks_settings_url
+ );
+
+ echo '' . wp_kses_post( $text ) . '
';
+ }
+
+ /**
+ * Get the ids of the webhooks assigned to a given user.
+ *
+ * @param int $user_id User id.
+ * @return int[] Array of webhook ids.
+ */
+ private function get_webhook_ids_for_user( int $user_id ): array {
+ $data_store = \WC_Data_Store::load( 'webhook' );
+ return $data_store->search_webhooks(
+ array(
+ 'user_id' => $user_id,
+ )
+ );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php
index a17b544d1fb..f1f51120993 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php
@@ -34,4 +34,82 @@ class WC_Webhook_Test extends WC_Unit_Test_Case {
$this->assertFalse( $call_is_valid_function->call( $webhook, $product->get_id() ) );
}
+ /**
+ * @testDox Check that a deleted administrator user (with content re-assigned to another user)
+ * does not cause webhook payloads to fail.
+ */
+ public function test_payload_for_deleted_user_id_with_reassign() {
+ $admin_user_id_1 = wp_insert_user(
+ array(
+ 'user_login' => 'test_admin',
+ 'user_pass' => 'password',
+ 'role' => 'administrator',
+ )
+ );
+
+ $webhook = new WC_Webhook();
+ $webhook->set_topic( 'order.created' );
+ $webhook->set_user_id( $admin_user_id_1 );
+ $webhook->save();
+
+ $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
+
+ $payload = $webhook->build_payload( $order->get_id() );
+ $this->assertArrayNotHasKey( 'code', $payload );
+ $this->assertArrayHasKey( 'id', $payload );
+ $this->assertSame( $order->get_id(), $payload['id'] );
+
+ // Create a second admin user and delete the first one, reassigning existing content to the second user.
+ $admin_user_id_2 = wp_insert_user(
+ array(
+ 'user_login' => 'test_admin2',
+ 'user_pass' => 'password',
+ 'role' => 'administrator',
+ )
+ );
+ wp_delete_user( $admin_user_id_1, $admin_user_id_2 );
+
+ // Re-load the webhook from the database.
+ $webhook = new WC_Webhook( $webhook->get_id() );
+ // Confirm user_id has been updated to the second admin user.
+ $this->assertSame( $admin_user_id_2, $webhook->get_user_id() );
+
+ $this->assertArrayNotHasKey( 'code', $payload );
+ $this->assertArrayHasKey( 'id', $payload );
+ $this->assertSame( $order->get_id(), $payload['id'] );
+ }
+
+ /**
+ * @testDox Check that a deleted administrator user (without content re-assigned to another user)
+ * has all webhooks changed to user_id zero.
+ */
+ public function test_payload_for_deleted_user_id_without_reassign() {
+ $admin_user_id = wp_insert_user(
+ array(
+ 'user_login' => 'test_admin',
+ 'user_pass' => 'password',
+ 'role' => 'administrator',
+ )
+ );
+
+ $webhook1 = new WC_Webhook();
+ $webhook1->set_topic( 'order.created' );
+ $webhook1->set_user_id( $admin_user_id );
+ $webhook1->save();
+
+ $webhook2 = new WC_Webhook();
+ $webhook2->set_topic( 'order.created' );
+ $webhook2->set_user_id( 999 );
+ $webhook2->save();
+
+ wp_delete_user( $admin_user_id );
+
+ // Re-load the webhooks from the database.
+ $webhook1 = new WC_Webhook( $webhook1->get_id() );
+ $webhook2 = new WC_Webhook( $webhook2->get_id() );
+ // Confirm user_id has been updated to zero for the first webhook only.
+ $this->assertSame( 0, $webhook1->get_user_id() );
+ $this->assertSame( 999, $webhook2->get_user_id() );
+ }
+
}
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-webhook-data-store-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-webhook-data-store-test.php
new file mode 100644
index 00000000000..92b736cc42b
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-webhook-data-store-test.php
@@ -0,0 +1,49 @@
+assertEmpty(
+ $store->search_webhooks(
+ array(
+ 'user_id' => 1,
+ )
+ )
+ );
+
+ $webhook1 = new WC_Webhook();
+ $webhook1->set_user_id( 1 );
+ $webhook1->save();
+
+ $webhook2 = new WC_Webhook();
+ $webhook2->set_user_id( 2 );
+ $webhook2->save();
+
+ $this->assertSame(
+ array( $webhook1->get_id() ),
+ $store->search_webhooks(
+ array(
+ 'user_id' => 1,
+ )
+ )
+ );
+ $this->assertSame(
+ array( $webhook2->get_id() ),
+ $store->search_webhooks(
+ array(
+ 'user_id' => 2,
+ )
+ )
+ );
+ }
+
+}