@@ -162,7 +250,9 @@ function CustomerFeedbackModal( {
CustomerFeedbackModal.propTypes = {
recordScoreCallback: PropTypes.func.isRequired,
- label: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ firstQuestion: PropTypes.string.isRequired,
+ secondQuestion: PropTypes.string.isRequired,
defaultScore: PropTypes.number,
onCloseModal: PropTypes.func,
};
diff --git a/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx b/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx
index 40829821a4e..b4136fd3fe6 100644
--- a/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx
+++ b/packages/js/customer-effort-score/src/customer-feedback-modal/test/index.tsx
@@ -16,7 +16,9 @@ describe( 'CustomerFeedbackModal', () => {
render(
);
@@ -33,13 +35,15 @@ describe( 'CustomerFeedbackModal', () => {
render(
);
await screen.findByRole( 'dialog' ); // Wait for the modal to render.
- fireEvent.click( screen.getByRole( 'button', { name: /send/i } ) ); // Press send button.
+ fireEvent.click( screen.getByRole( 'button', { name: /share/i } ) ); // Press send button.
// Wait for error message.
await screen.findByRole( 'alert' );
@@ -51,7 +55,9 @@ describe( 'CustomerFeedbackModal', () => {
render(
);
@@ -59,17 +65,21 @@ describe( 'CustomerFeedbackModal', () => {
await screen.findByRole( 'dialog' );
expect(
- screen.queryByLabelText( 'Comments (optional)' )
+ screen.queryByLabelText(
+ 'How is that screen useful to you? What features would you add or change?'
+ )
).not.toBeInTheDocument();
} );
- it.each( [ 'Very difficult', 'Somewhat difficult' ] )(
+ it.each( [ 'Strongly disagree', 'Disagree' ] )(
'should toggle the comments field when %s is selected',
async ( labelText ) => {
render(
);
@@ -77,18 +87,22 @@ describe( 'CustomerFeedbackModal', () => {
await screen.findByRole( 'dialog' );
// Select the option.
- fireEvent.click( screen.getByLabelText( labelText ) );
+ fireEvent.click( screen.getAllByLabelText( labelText )[ 0 ] );
// Wait for comments field to show.
- await screen.findByLabelText( 'Comments (optional)' );
+ await screen.findByLabelText(
+ 'How is that screen useful to you? What features would you add or change?'
+ );
// Select neutral score.
- fireEvent.click( screen.getByLabelText( 'Neutral' ) );
+ fireEvent.click( screen.getAllByLabelText( 'Neutral' )[ 0 ] );
// Wait for comments field to hide.
await waitFor( () => {
expect(
- screen.queryByLabelText( 'Comments (optional)' )
+ screen.queryByLabelText(
+ 'How is that screen useful to you? What features would you add or change?'
+ )
).not.toBeInTheDocument();
} );
}
diff --git a/packages/js/customer-effort-score/src/style.scss b/packages/js/customer-effort-score/src/style.scss
index 6c7397a20f8..adabb8fdba9 100644
--- a/packages/js/customer-effort-score/src/style.scss
+++ b/packages/js/customer-effort-score/src/style.scss
@@ -1,7 +1,7 @@
@import 'customer-feedback-simple/customer-feedback-simple.scss';
.woocommerce-customer-effort-score__selection {
- margin: 1em 0;
+ margin: 1em 0 1.5em 0;
.components-base-control__field {
display: flex;
@@ -11,6 +11,13 @@
@include breakpoint( '>600px' ) {
flex-direction: row;
+ justify-content: center;
+ }
+
+ .components-h-stack {
+ @include breakpoint( '>600px' ) {
+ flex-direction: row;
+ }
}
}
@@ -84,10 +91,14 @@
}
.woocommerce-customer-effort-score__comments {
+ margin-bottom: 1.5em;
+
label {
display: block;
color: inherit;
font-weight: bold;
+ text-transform: none;
+ font-size: 14px;
}
textarea {
@@ -102,3 +113,8 @@
margin-left: 1em;
}
}
+
+.woocommerce-customer-effort-score .woocommerce-customer-effort-score__intro {
+ max-width: 550px;
+ margin: 0 0 1.5em 0;
+}
diff --git a/packages/js/customer-effort-score/src/test/index.tsx b/packages/js/customer-effort-score/src/test/index.tsx
index 59f54f2afce..e8bd5e774cc 100644
--- a/packages/js/customer-effort-score/src/test/index.tsx
+++ b/packages/js/customer-effort-score/src/test/index.tsx
@@ -35,7 +35,9 @@ describe( 'CustomerEffortScore', () => {
render(
@@ -45,7 +47,7 @@ describe( 'CustomerEffortScore', () => {
// Notice status.
expect.any( String ),
// Notice message.
- 'label',
+ 'title',
// Notice options.
expect.objectContaining( {
icon,
@@ -63,7 +65,9 @@ describe( 'CustomerEffortScore', () => {
const { rerender } = render(
);
@@ -71,7 +75,9 @@ describe( 'CustomerEffortScore', () => {
rerender(
);
@@ -82,7 +88,9 @@ describe( 'CustomerEffortScore', () => {
render(
);
@@ -121,7 +129,9 @@ describe( 'CustomerEffortScore', () => {
render(
);
diff --git a/packages/js/data/changelog/add-35175 b/packages/js/data/changelog/add-35175
new file mode 100644
index 00000000000..e5dbd235dee
--- /dev/null
+++ b/packages/js/data/changelog/add-35175
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add missing reviews property to product data
diff --git a/packages/js/data/changelog/add-35771 b/packages/js/data/changelog/add-35771
new file mode 100644
index 00000000000..28a5bdaefb9
--- /dev/null
+++ b/packages/js/data/changelog/add-35771
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add product variations data store
diff --git a/packages/js/data/changelog/add-35772 b/packages/js/data/changelog/add-35772
new file mode 100644
index 00000000000..289817caff8
--- /dev/null
+++ b/packages/js/data/changelog/add-35772
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Update attributes type for product variations data store
diff --git a/packages/js/data/changelog/add-in-app-tour-track-events b/packages/js/data/changelog/add-in-app-tour-track-events
new file mode 100644
index 00000000000..f9d60be1200
--- /dev/null
+++ b/packages/js/data/changelog/add-in-app-tour-track-events
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add an attribute to an onboarding task to indicate whether to track a task view.
diff --git a/packages/js/data/changelog/dev-adjust-sync b/packages/js/data/changelog/dev-adjust-sync
new file mode 100644
index 00000000000..395bc6d8d2d
--- /dev/null
+++ b/packages/js/data/changelog/dev-adjust-sync
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Dev dependency bump
diff --git a/packages/js/data/changelog/fix-34929 b/packages/js/data/changelog/fix-34929
new file mode 100644
index 00000000000..c01aa5a1f0b
--- /dev/null
+++ b/packages/js/data/changelog/fix-34929
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add is_read query option for notes data store
diff --git a/packages/js/data/package.json b/packages/js/data/package.json
index 5dfa2234cd3..cd803090447 100644
--- a/packages/js/data/package.json
+++ b/packages/js/data/package.json
@@ -57,7 +57,7 @@
"@types/lodash": "^4.14.182",
"@types/md5": "^2.3.2",
"@types/qs": "^6.9.7",
- "@types/react": "^17.0.0",
+ "@types/react": "^17.0.2",
"@types/wordpress__compose": "^4.0.1",
"@types/wordpress__core-data": "^2.4.5",
"@types/wordpress__data": "^6.0.0",
diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts
index be23c55eff3..f386b083731 100644
--- a/packages/js/data/src/index.ts
+++ b/packages/js/data/src/index.ts
@@ -24,6 +24,7 @@ export { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones';
export { EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME } from './product-tags';
export { EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME } from './product-categories';
export { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME } from './product-attribute-terms';
+export { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME } from './product-variations';
export { PaymentGateway } from './payment-gateways/types';
// Export hooks
@@ -76,6 +77,7 @@ export * from './countries/types';
export * from './onboarding/types';
export * from './plugins/types';
export * from './products/types';
+export { ProductVariation } from './product-variations/types';
export {
QueryProductAttribute,
ProductAttributeSelectors,
@@ -115,6 +117,7 @@ import type { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones';
import type { EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME } from './product-tags';
import type { EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME } from './product-categories';
import type { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME } from './product-attribute-terms';
+import type { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME } from './product-variations';
export type WCDataStoreName =
| typeof REVIEWS_STORE_NAME
@@ -136,7 +139,8 @@ export type WCDataStoreName =
| typeof EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
| typeof EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME
- | typeof EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME;
+ | typeof EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME
+ | typeof EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME;
/**
* Internal dependencies
@@ -154,6 +158,7 @@ import { ShippingZonesSelectors } from './shipping-zones/types';
import { ProductTagSelectors } from './product-tags/types';
import { ProductCategorySelectors } from './product-categories/types';
import { ProductAttributeTermsSelectors } from './product-attribute-terms/types';
+import { ProductVariationSelectors } from './product-variations/types';
// As we add types to all the package selectors we can fill out these unknown types with real ones. See one
// of the already typed selectors for an example of how you can do this.
@@ -193,6 +198,8 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME
? ProductCategorySelectors
: T extends typeof EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
? ProductAttributeTermsSelectors
+ : T extends typeof EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
+ ? ProductVariationSelectors
: T extends typeof ORDERS_STORE_NAME
? OrdersSelectors
: T extends typeof EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME
@@ -209,6 +216,7 @@ export { ActionDispatchers as ProductAttributesActions } from './product-attribu
export { ActionDispatchers as ProductTagsActions } from './product-tags/types';
export { ActionDispatchers as ProductCategoryActions } from './product-categories/types';
export { ActionDispatchers as ProductAttributeTermsActions } from './product-attribute-terms/types';
+export { ActionDispatchers as ProductVariationsActions } from './product-variations/types';
export { ActionDispatchers as ProductsStoreActions } from './products/actions';
export { ActionDispatchers as ProductShippingClassesActions } from './product-shipping-classes/types';
export { ActionDispatchers as ShippingZonesActions } from './shipping-zones/types';
diff --git a/packages/js/data/src/notes/types.ts b/packages/js/data/src/notes/types.ts
index 4fd8c183f7a..8494970fcbd 100644
--- a/packages/js/data/src/notes/types.ts
+++ b/packages/js/data/src/notes/types.ts
@@ -52,6 +52,7 @@ export type Note = {
// [Notes.php](https://github.com/woocommerce/woocommerce/blob/af97aaf41067bcd0b7ff12df9b6169f97c326c0f/plugins/woocommerce/src/Admin/API/Notes.php#L629-L699)
export type NoteQuery = Partial< {
context: string;
+ is_read: boolean;
order: 'asc' | 'desc';
orderby: 'note_id' | 'date' | 'type' | 'title' | 'status';
page: number;
diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts
index 24060daa19d..7cbf182201c 100644
--- a/packages/js/data/src/onboarding/types.ts
+++ b/packages/js/data/src/onboarding/types.ts
@@ -25,6 +25,7 @@ export type TaskType = {
isActioned: boolean;
eventPrefix: string;
level: number;
+ recordViewEvent: boolean;
additionalData?: {
woocommerceTaxCountries?: string[];
taxJarActivated?: boolean;
diff --git a/packages/js/data/src/product-variations/constants.ts b/packages/js/data/src/product-variations/constants.ts
new file mode 100644
index 00000000000..45f88e4ca41
--- /dev/null
+++ b/packages/js/data/src/product-variations/constants.ts
@@ -0,0 +1,4 @@
+export const STORE_NAME = 'wc/admin/products/variations';
+
+export const WC_PRODUCT_VARIATIONS_NAMESPACE =
+ '/wc/v3/products/{product_id}/variations';
diff --git a/packages/js/data/src/product-variations/index.ts b/packages/js/data/src/product-variations/index.ts
new file mode 100644
index 00000000000..4a6a444c202
--- /dev/null
+++ b/packages/js/data/src/product-variations/index.ts
@@ -0,0 +1,14 @@
+/**
+ * Internal dependencies
+ */
+import { STORE_NAME, WC_PRODUCT_VARIATIONS_NAMESPACE } from './constants';
+import { createCrudDataStore } from '../crud';
+
+createCrudDataStore( {
+ storeName: STORE_NAME,
+ resourceName: 'ProductVariation',
+ pluralResourceName: 'ProductVariations',
+ namespace: WC_PRODUCT_VARIATIONS_NAMESPACE,
+} );
+
+export const EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME = STORE_NAME;
diff --git a/packages/js/data/src/product-variations/types.ts b/packages/js/data/src/product-variations/types.ts
new file mode 100644
index 00000000000..2369bce1d8c
--- /dev/null
+++ b/packages/js/data/src/product-variations/types.ts
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import { DispatchFromMap } from '@automattic/data-stores';
+
+/**
+ * Internal dependencies
+ */
+import { CrudActions, CrudSelectors } from '../crud/types';
+import { Product, ProductQuery, ReadOnlyProperties } from '../products/types';
+
+export type ProductVariationAttribute = {
+ id: number;
+ name: string;
+ option: string;
+};
+
+export type ProductVariation = Omit<
+ Product,
+ 'name' | 'slug' | 'attributes'
+> & {
+ attributes: ProductVariationAttribute[];
+};
+
+type Query = Omit< ProductQuery, 'name' >;
+
+type MutableProperties = Partial<
+ Omit< ProductVariation, ReadOnlyProperties >
+>;
+
+type ProductVariationActions = CrudActions<
+ 'ProductVariation',
+ ProductVariation,
+ MutableProperties
+>;
+
+export type ProductVariationSelectors = CrudSelectors<
+ 'ProductVariation',
+ 'ProductVariations',
+ ProductVariation,
+ Query,
+ MutableProperties
+>;
+
+export type ActionDispatchers = DispatchFromMap< ProductVariationActions >;
diff --git a/packages/js/data/src/products/types.ts b/packages/js/data/src/products/types.ts
index 961261a771f..7b3f6154634 100644
--- a/packages/js/data/src/products/types.ts
+++ b/packages/js/data/src/products/types.ts
@@ -45,79 +45,80 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit<
Schema.Post,
'status' | 'categories'
> & {
- id: number;
- name: string;
- slug: string;
- permalink: string;
+ attributes: ProductAttribute[];
+ average_rating: string;
+ backordered: boolean;
+ backorders: 'no' | 'notify' | 'yes';
+ backorders_allowed: boolean;
+ button_text: string;
+ categories: Pick< ProductCategory, 'id' | 'name' | 'slug' >[];
date_created: string;
date_created_gmt: string;
date_modified: string;
date_modified_gmt: string;
- type: Type;
- status: Status;
- featured: boolean;
- description: string;
- short_description: string;
- sku: string;
date_on_sale_from_gmt: string | null;
date_on_sale_to_gmt: string | null;
- virtual: boolean;
+ description: string;
+ dimensions: ProductDimensions;
+ download_expiry: number;
+ download_limit: number;
downloadable: boolean;
downloads: ProductDownload[];
- download_limit: number;
- download_expiry: number;
external_url: string;
- button_text: string;
- tax_status: 'taxable' | 'shipping' | 'none';
- tax_class: 'standard' | 'reduced-rate' | 'zero-rate' | undefined;
- manage_stock: boolean;
- stock_quantity: number;
+ featured: boolean;
+ id: number;
low_stock_amount: number;
- stock_status: 'instock' | 'outofstock' | 'onbackorder';
- backorders: 'no' | 'notify' | 'yes';
+ manage_stock: boolean;
+ name: string;
+ on_sale: boolean;
+ permalink: string;
price: string;
price_html: string;
- regular_price: string;
- sale_price: string;
- on_sale: boolean;
purchasable: boolean;
- total_sales: number;
- backorders_allowed: boolean;
- backordered: boolean;
- shipping_required: boolean;
- shipping_taxable: boolean;
- shipping_class: string;
- shipping_class_id: number;
- average_rating: string;
+ regular_price: string;
rating_count: number;
related_ids: number[];
+ reviews_allowed: boolean;
+ sale_price: string;
+ shipping_class: string;
+ shipping_class_id: number;
+ shipping_required: boolean;
+ shipping_taxable: boolean;
+ short_description: string;
+ slug: string;
+ sku: string;
+ status: Status;
+ stock_quantity: number;
+ stock_status: 'instock' | 'outofstock' | 'onbackorder';
+ tax_class: 'standard' | 'reduced-rate' | 'zero-rate' | undefined;
+ tax_status: 'taxable' | 'shipping' | 'none';
+ total_sales: number;
+ type: Type;
variations: number[];
- attributes: ProductAttribute[];
- dimensions: ProductDimensions;
+ virtual: boolean;
weight: string;
- categories: Pick< ProductCategory, 'id' | 'name' | 'slug' >[];
};
export const productReadOnlyProperties = [
- 'id',
- 'permalink',
+ 'average_rating',
+ 'backordered',
+ 'backorders_allowed',
'date_created',
'date_created_gmt',
'date_modified',
'date_modified_gmt',
+ 'id',
+ 'on_sale',
+ 'permalink',
'price',
'price_html',
- 'on_sale',
'purchasable',
- 'total_sales',
- 'backorders_allowed',
- 'backordered',
- 'shipping_required',
- 'shipping_taxable',
- 'shipping_class_id',
- 'average_rating',
'rating_count',
'related_ids',
+ 'shipping_class_id',
+ 'shipping_required',
+ 'shipping_taxable',
+ 'total_sales',
'variations',
] as const;
diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json
index e95728acd7c..d643cdc4553 100644
--- a/packages/js/e2e-utils/package.json
+++ b/packages/js/e2e-utils/package.json
@@ -30,8 +30,8 @@
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.12.7",
- "@typescript-eslint/eslint-plugin": "^5.3.0",
- "@typescript-eslint/parser": "^5.3.0",
+ "@typescript-eslint/eslint-plugin": "^5.43.0",
+ "@typescript-eslint/parser": "^5.43.0",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-e2e-builds": "workspace:*",
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
diff --git a/packages/js/experimental/changelog/fix-34929 b/packages/js/experimental/changelog/fix-34929
new file mode 100644
index 00000000000..2fedcc1e932
--- /dev/null
+++ b/packages/js/experimental/changelog/fix-34929
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Check for note actions before checking length
diff --git a/packages/js/experimental/src/inbox-note/inbox-note.tsx b/packages/js/experimental/src/inbox-note/inbox-note.tsx
index 1df953e5fa0..0ad957e921f 100644
--- a/packages/js/experimental/src/inbox-note/inbox-note.tsx
+++ b/packages/js/experimental/src/inbox-note/inbox-note.tsx
@@ -171,7 +171,7 @@ const InboxNoteCard: React.FC< InboxNoteProps > = ( {
const actionWrapperClassName = classnames(
'woocommerce-inbox-message__actions',
{
- 'has-multiple-actions': note.actions.length > 1,
+ 'has-multiple-actions': note.actions?.length > 1,
}
);
diff --git a/packages/js/explat/changelog/dev-adjust-sync b/packages/js/explat/changelog/dev-adjust-sync
new file mode 100644
index 00000000000..395bc6d8d2d
--- /dev/null
+++ b/packages/js/explat/changelog/dev-adjust-sync
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Dev dependency bump
diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json
index 918ebf45f47..1546a3faded 100644
--- a/packages/js/explat/package.json
+++ b/packages/js/explat/package.json
@@ -43,7 +43,7 @@
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@types/qs": "^6.9.7",
- "@types/react": "^17.0.0",
+ "@types/react": "^17.0.2",
"@woocommerce/eslint-plugin": "workspace:*",
"eslint": "^8.12.0",
"jest": "^27.5.1",
diff --git a/packages/js/extend-cart-checkout-block/CHANGELOG.md b/packages/js/extend-cart-checkout-block/CHANGELOG.md
index e69de29bb2d..ade90a4603f 100644
--- a/packages/js/extend-cart-checkout-block/CHANGELOG.md
+++ b/packages/js/extend-cart-checkout-block/CHANGELOG.md
@@ -0,0 +1,15 @@
+# Changelog
+
+This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.1.0](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block/v/1.1.0) - 2022-11-21
+
+- Patch - Include an example of adding an inner block to the WooCommerce Blocks Checkout Block [#35609]
+- Minor - Fix node and pnpm versions via engines [#35609]
+- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35609]
+
+## [1.0.0](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block/v/1.0.0) - 2022-06-01
+
+- Patch - Initial release
+
+[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/extend-cart-checkout-block/CHANGELOG.md).
diff --git a/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example b/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example
deleted file mode 100644
index 1a22ae4197f..00000000000
--- a/packages/js/extend-cart-checkout-block/changelog/add-inner-block-example
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Include an example of adding an inner block to the WooCommerce Blocks Checkout Block
diff --git a/packages/js/extend-cart-checkout-block/changelog/dev-bump-pnpm-version-restraint b/packages/js/extend-cart-checkout-block/changelog/dev-bump-pnpm-version-restraint
deleted file mode 100644
index f7511cb6974..00000000000
--- a/packages/js/extend-cart-checkout-block/changelog/dev-bump-pnpm-version-restraint
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues
diff --git a/packages/js/extend-cart-checkout-block/changelog/dev-fix-pnpm-version-engines b/packages/js/extend-cart-checkout-block/changelog/dev-fix-pnpm-version-engines
deleted file mode 100644
index a1804a282f0..00000000000
--- a/packages/js/extend-cart-checkout-block/changelog/dev-fix-pnpm-version-engines
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Fix node and pnpm versions via engines
diff --git a/packages/js/extend-cart-checkout-block/changelog/dev-simplify-turbo b/packages/js/extend-cart-checkout-block/changelog/dev-simplify-turbo
deleted file mode 100644
index 0d230384010..00000000000
--- a/packages/js/extend-cart-checkout-block/changelog/dev-simplify-turbo
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Package scripts were modified to support simplified running of turbo commands in the monorepo.
-
-
diff --git a/packages/js/extend-cart-checkout-block/package.json b/packages/js/extend-cart-checkout-block/package.json
index c60e8220d88..0d7ff7db5ef 100644
--- a/packages/js/extend-cart-checkout-block/package.json
+++ b/packages/js/extend-cart-checkout-block/package.json
@@ -1,6 +1,6 @@
{
"name": "@woocommerce/extend-cart-checkout-block",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "",
"main": "index.js",
"engines": {
diff --git a/packages/js/internal-style-build/abstracts/_mixins.scss b/packages/js/internal-style-build/abstracts/_mixins.scss
index aec22045af3..43ab642052d 100644
--- a/packages/js/internal-style-build/abstracts/_mixins.scss
+++ b/packages/js/internal-style-build/abstracts/_mixins.scss
@@ -1,7 +1,7 @@
// Rem output with px fallback
@mixin font-size( $sizeValue: 16, $lineHeight: false ) {
font-size: $sizeValue + px;
- font-size: math.div($sizeValue, 16) + rem;
+ font-size: math.div( $sizeValue, 16 ) + rem;
@if ( $lineHeight ) {
line-height: $lineHeight;
}
@@ -106,7 +106,7 @@
background-color: $studio-white;
color: $gray-900;
box-shadow: inset 0 0 0 1px $gray-400, inset 0 0 0 2px $studio-white,
- 0 1px 1px rgba($gray-900, 0.2);
+ 0 1px 1px rgba( $gray-900, 0.2 );
}
@mixin button-style__active() {
@@ -145,7 +145,6 @@
}
/* stylelint-enable */
-
// Gutenberg Switch.
@mixin switch-style__focus-active() {
box-shadow: 0 0 0 2px $studio-white, 0 0 0 3px $gray-700;
@@ -158,19 +157,20 @@
// Sets positions for children of grid elements
@mixin set-grid-item-position( $wrap-after, $number-of-items ) {
@for $i from 1 through $number-of-items {
- &:nth-child(#{$i}) {
- grid-column-start: #{($i - 1) % $wrap-after + 1};
- grid-column-end: #{($i - 1) % $wrap-after + 2};
- grid-row-start: #{floor(math.div($i - 1, $wrap-after)) + 1};
- grid-row-end: #{floor(math.div($i - 1, $wrap-after)) + 2};
+ &:nth-child( #{$i} ) {
+ grid-column-start: #{( $i - 1 ) % $wrap-after + 1};
+ grid-column-end: #{( $i - 1 ) % $wrap-after + 2};
+ grid-row-start: #{floor( math.div( $i - 1, $wrap-after ) ) + 1};
+ grid-row-end: #{floor( math.div( $i - 1, $wrap-after ) ) + 2};
}
}
}
// Hide an element from sighted users, but availble to screen reader users.
+// @deprecated in favor of screen-reader-only
@mixin visually-hidden() {
- clip: rect(1px, 1px, 1px, 1px);
- clip-path: inset(50%);
+ clip: rect( 1px, 1px, 1px, 1px );
+ clip-path: inset( 50% );
height: 1px;
width: 1px;
margin: -1px;
@@ -180,7 +180,20 @@
word-wrap: normal !important;
}
+@mixin screen-reader-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect( 0, 0, 0, 0 );
+ white-space: nowrap;
+ border-width: 0;
+}
+
// Unhide a visually hidden element
+// @deprecated in favor of not-screen-reader-only
@mixin visually-shown() {
clip: auto;
clip-path: none;
@@ -190,6 +203,17 @@
overflow: hidden;
}
+@mixin not-screen-reader-only {
+ position: static;
+ width: auto;
+ height: auto;
+ padding: 0;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+}
+
// Create a string-repeat function
@function str-repeat( $character, $n ) {
@if $n == 0 {
diff --git a/packages/js/internal-style-build/abstracts/_variables.scss b/packages/js/internal-style-build/abstracts/_variables.scss
index 4b9d4ee625a..73155c89baf 100644
--- a/packages/js/internal-style-build/abstracts/_variables.scss
+++ b/packages/js/internal-style-build/abstracts/_variables.scss
@@ -44,6 +44,7 @@ $alert-green: $valid-green;
// WordPress defaults
$adminbar-height: 32px;
$adminbar-height-mobile: 46px;
+$admin-menu-width: 160px;
// wp-admin colors
$wp-admin-background: #f1f1f1;
diff --git a/packages/js/internal-style-build/changelog/enhancement-35565 b/packages/js/internal-style-build/changelog/enhancement-35565
new file mode 100644
index 00000000000..c69b2129169
--- /dev/null
+++ b/packages/js/internal-style-build/changelog/enhancement-35565
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Add updated versions of screen-reader-only and not-screen-reader-only mixins
diff --git a/plugins/woocommerce/changelog/fix-skip-failing-api-test b/packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding
similarity index 51%
rename from plugins/woocommerce/changelog/fix-skip-failing-api-test
rename to packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding
index 86578e5f08d..77755055d34 100644
--- a/plugins/woocommerce/changelog/fix-skip-failing-api-test
+++ b/packages/js/onboarding/changelog/fix-35704-wcpay-benefit-padding
@@ -1,4 +1,4 @@
Significance: patch
Type: fix
-Skip flaky settings API test
+Fix wcpay benefits padding
diff --git a/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx b/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx
index 29c96fbf261..8ec7a90d4f5 100644
--- a/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx
+++ b/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx
@@ -17,7 +17,7 @@ import {
export const WCPayBenefits: React.VFC = () => {
return (
-
+
diff --git a/plugins/woocommerce-admin/babel.config.js b/plugins/woocommerce-admin/babel.config.js
index 70215466892..d1987422307 100644
--- a/plugins/woocommerce-admin/babel.config.js
+++ b/plugins/woocommerce-admin/babel.config.js
@@ -23,28 +23,6 @@ module.exports = function ( api ) {
ignore: [ 'packages/**/node_modules' ],
env: {
production: {},
-
- storybook: {
- plugins: [
- /**
- * We need to set loose mode here because the storybook's default babel config enables the loose mode.
- * The 'loose' mode configuration must be the same for those babel plugins.
- *
- */
- [
- '@babel/plugin-proposal-class-properties',
- { loose: true },
- ],
- [
- '@babel/plugin-proposal-private-methods',
- { loose: true },
- ],
- [
- '@babel/plugin-proposal-private-property-in-object',
- { loose: true },
- ],
- ],
- },
},
};
};
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/constants.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/constants.ts
new file mode 100644
index 00000000000..49a469270be
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/constants.ts
@@ -0,0 +1,5 @@
+export const SHOWN_FOR_ACTIONS_OPTION_NAME =
+ 'woocommerce_ces_shown_for_actions';
+export const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
+ 'woocommerce_admin_install_timestamp';
+export const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-exit-page.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-exit-page.ts
new file mode 100644
index 00000000000..9b261d3de62
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-exit-page.ts
@@ -0,0 +1,232 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+import { dispatch, resolveSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { ALLOW_TRACKING_OPTION_NAME } from './constants';
+
+const CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY = 'customer-effort-score-exit-page';
+
+let allowTracking = false;
+resolveSelect( OPTIONS_STORE_NAME )
+ .getOption( ALLOW_TRACKING_OPTION_NAME )
+ .then( ( trackingOption ) => {
+ allowTracking = trackingOption === 'yes';
+ } );
+
+/**
+ * Gets the list of exited pages from Localstorage.
+ */
+export const getExitPageData = () => {
+ if ( ! window.localStorage ) {
+ return [];
+ }
+
+ const items = window.localStorage.getItem(
+ CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY
+ );
+ const parsedJSONItems = items ? JSON.parse( items ) : [];
+ const arrayItems = Array.isArray( parsedJSONItems ) ? parsedJSONItems : [];
+
+ return arrayItems;
+};
+
+/**
+ * Adds the page to the exit page list in Localstorage.
+ *
+ * @param {string} pageId of page exited early.
+ */
+export const addExitPage = ( pageId: string ) => {
+ if ( ! window.localStorage ) {
+ return;
+ }
+
+ let items = getExitPageData();
+
+ if ( ! items.find( ( pageExitedId ) => pageExitedId === pageId ) ) {
+ items.push( pageId );
+ }
+ items = items.slice( -10 ); // Upper limit.
+
+ window.localStorage.setItem(
+ CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY,
+ JSON.stringify( items )
+ );
+};
+
+/**
+ * Removes the passed in page id from the list in Localstorage.
+ *
+ * @param {string} pageId of page to be removed.
+ */
+export const removeExitPage = ( pageId: string ) => {
+ if ( ! window.localStorage ) {
+ return;
+ }
+
+ let items = getExitPageData();
+
+ items = items.filter( ( pageExitedId ) => pageExitedId !== pageId );
+ items = items.slice( -10 ); // Upper limit.
+
+ window.localStorage.setItem(
+ CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY,
+ JSON.stringify( items )
+ );
+};
+
+const eventListeners: Record< string, ( event: BeforeUnloadEvent ) => void > =
+ {};
+
+/**
+ * Adds unload event listener to add pageId to exit page list incase there were unsaved changes.
+ *
+ * @param {string} pageId the page id of the page being exited early.
+ * @param {Function} hasUnsavedChanges callback to check if the page had unsaved changes.
+ */
+export const addCustomerEffortScoreExitPageListener = (
+ pageId: string,
+ hasUnsavedChanges: () => boolean
+) => {
+ eventListeners[ pageId ] = ( event ) => {
+ if ( hasUnsavedChanges() && allowTracking ) {
+ addExitPage( pageId );
+ }
+ };
+ window.addEventListener( 'unload', eventListeners[ pageId ] );
+};
+
+/**
+ * Removes the unload exit page listener.
+ *
+ * @param {string} pageId the page id to remove the listener from.
+ */
+export const removeCustomerEffortScoreExitPageListener = ( pageId: string ) => {
+ if ( eventListeners[ pageId ] ) {
+ window.removeEventListener( 'unload', eventListeners[ pageId ], {
+ capture: true,
+ } );
+ }
+};
+
+/**
+ * Returns the exit page copy of the passed in pageId.
+ *
+ * @param {string} pageId page id.
+ */
+function getExitPageCESCopy( pageId: string ): {
+ action: string;
+ title: string;
+ firstQuestion: string;
+ secondQuestion: string;
+ noticeLabel?: string;
+ description?: string;
+ icon?: string;
+} | null {
+ switch ( pageId ) {
+ case 'product_edit_view':
+ case 'editing_new_product':
+ return {
+ action:
+ pageId === 'editing_new_product' ? 'new_product' : pageId,
+ noticeLabel: __(
+ 'How is your experience with editing products?',
+ 'woocommerce'
+ ),
+ title: __(
+ "How's your experience with editing products?",
+ 'woocommerce'
+ ),
+ description: __(
+ 'We noticed you started editing a product, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.',
+ 'woocommerce'
+ ),
+ firstQuestion: __(
+ 'The product editing screen is easy to use',
+ 'woocommerce'
+ ),
+ secondQuestion: __(
+ "The product editing screen's functionality meets my needs",
+ 'woocommerce'
+ ),
+ };
+ case 'product_add_view':
+ case 'new_product':
+ return {
+ action: pageId,
+ noticeLabel: __(
+ 'How is your experience with creating products?',
+ 'woocommerce'
+ ),
+ title: __(
+ 'How is your experience with creating products?',
+ 'woocommerce'
+ ),
+ description: __(
+ 'We noticed you started creating a product, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.',
+ 'woocommerce'
+ ),
+ firstQuestion: __(
+ 'The product creation screen is easy to use',
+ 'woocommerce'
+ ),
+ secondQuestion: __(
+ "The product creation screen's functionality meets my needs",
+ 'woocommerce'
+ ),
+ };
+ case 'settings_change':
+ return {
+ action: pageId,
+ icon: '⚙️',
+ noticeLabel: __(
+ 'Did you find the right setting?',
+ 'woocommerce'
+ ),
+ title: __(
+ 'How’s your experience with settings?',
+ 'woocommerce'
+ ),
+ description: __(
+ 'We noticed you started changing store settings, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.',
+ 'woocommerce'
+ ),
+ firstQuestion: __(
+ 'The settings screen is easy to use',
+ 'woocommerce'
+ ),
+ secondQuestion: __(
+ "The settings screen's functionality meets my needs",
+ 'woocommerce'
+ ),
+ };
+ default:
+ return null;
+ }
+}
+
+/**
+ * Checks the exit page list and triggers a CES survey for the first item in the list.
+ */
+export function triggerExitPageCesSurvey() {
+ const exitPageItems: string[] = getExitPageData();
+ if ( exitPageItems && exitPageItems.length > 0 ) {
+ const copy = getExitPageCESCopy( exitPageItems[ 0 ] );
+ if ( copy && copy.title.length > 0 ) {
+ dispatch( 'wc/customer-effort-score' ).addCesSurvey( {
+ ...copy,
+ pageNow: window.pagenow,
+ adminPage: window.adminpage,
+ props: {
+ ces_location: 'outside',
+ },
+ } );
+ }
+ removeExitPage( exitPageItems[ 0 ] );
+ }
+}
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx
new file mode 100644
index 00000000000..038f166ac07
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx
@@ -0,0 +1,88 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { CustomerFeedbackModal } from '@woocommerce/customer-effort-score';
+import { recordEvent } from '@woocommerce/tracks';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import { getStoreAgeInWeeks } from './utils';
+import { ADMIN_INSTALL_TIMESTAMP_OPTION_NAME } from './constants';
+import { STORE_KEY } from './data/constants';
+
+export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
+ 'woocommerce_ces_product_mvp_ces_action';
+
+export const CustomerEffortScoreModalContainer: React.FC = () => {
+ const { createSuccessNotice } = useDispatch( 'core/notices' );
+ const { hideCesModal } = useDispatch( STORE_KEY );
+ const {
+ storeAgeInWeeks,
+ resolving: isLoading,
+ visibleCESModalData,
+ } = useSelect( ( select ) => {
+ const { getOption, hasFinishedResolution } =
+ select( OPTIONS_STORE_NAME );
+ const { getVisibleCESModalData } = select( STORE_KEY );
+
+ const adminInstallTimestamp =
+ ( getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) as number ) || 0;
+
+ const resolving =
+ adminInstallTimestamp === null ||
+ ! hasFinishedResolution( 'getOption', [
+ ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
+ ] );
+
+ return {
+ storeAgeInWeeks: getStoreAgeInWeeks( adminInstallTimestamp ),
+ visibleCESModalData: getVisibleCESModalData(),
+ resolving,
+ };
+ } );
+
+ const recordScore = (
+ score: number,
+ secondScore: number,
+ comments: string
+ ) => {
+ recordEvent( 'ces_feedback', {
+ action: visibleCESModalData.action,
+ score,
+ score_second_question: secondScore ?? null,
+ score_combined: score + ( secondScore ?? 0 ),
+ comments: comments || '',
+ store_age: storeAgeInWeeks,
+ } );
+ createSuccessNotice(
+ visibleCESModalData.onSubmitLabel ||
+ __(
+ "Thanks for the feedback. We'll put it to good use!",
+ 'woocommerce'
+ ),
+ visibleCESModalData.onSubmitNoticeProps || {}
+ );
+ };
+
+ if ( ! visibleCESModalData || isLoading ) {
+ return null;
+ }
+
+ return (
+ {
+ recordScore( ...args );
+ hideCesModal();
+ } }
+ onCloseModal={ () => hideCesModal() }
+ />
+ );
+};
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js
index 32fba5c39e6..4b55614aa21 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js
@@ -49,7 +49,12 @@ function CustomerEffortScoreTracksContainer( {
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js
index b55b39548e3..0b1f7960fbb 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js
@@ -7,13 +7,18 @@ import { recordEvent } from '@woocommerce/tracks';
import { CustomerEffortScore } from '@woocommerce/customer-effort-score';
import { compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';
-import { OPTIONS_STORE_NAME, WEEK } from '@woocommerce/data';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { __ } from '@wordpress/i18n';
-const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';
-const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
- 'woocommerce_admin_install_timestamp';
-const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
+/**
+ * Internal dependencies
+ */
+import {
+ SHOWN_FOR_ACTIONS_OPTION_NAME,
+ ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
+ ALLOW_TRACKING_OPTION_NAME,
+} from './constants';
+import { getStoreAgeInWeeks } from './utils';
/**
* A CustomerEffortScore wrapper that uses tracks to track the selected
@@ -22,7 +27,12 @@ const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
* @param {Object} props Component props.
* @param {string} props.action The action name sent to Tracks.
* @param {Object} props.trackProps Additional props sent to Tracks.
- * @param {string} props.label The label displayed in the modal.
+ * @param {string} props.title The title displayed in the modal.
+ * @param {string} props.noticeLabel Label for notice, defaults to title.
+ * @param {string} props.description Description shown in CES modal.
+ * @param {string} props.firstQuestion The first survey question.
+ * @param {string} props.secondQuestion The second survey question.
+ * @param {string} props.icon Optional icon to show in notice.
* @param {string} props.onSubmitLabel The label displayed upon survey submission.
* @param {Array} props.cesShownForActions The array of actions that the CES modal has been shown for.
* @param {boolean} props.allowTracking Whether tracking is allowed or not.
@@ -34,7 +44,12 @@ const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
function CustomerEffortScoreTracks( {
action,
trackProps,
- label,
+ title,
+ description,
+ noticeLabel,
+ firstQuestion,
+ secondQuestion,
+ icon,
onSubmitLabel = __( 'Thank you for your feedback!', 'woocommerce' ),
cesShownForActions,
allowTracking,
@@ -61,7 +76,11 @@ function CustomerEffortScoreTracks( {
// (we don't want to return null early), if the modal was shown for this
// instantiation, so that the component doesn't go away while we are
// still showing it.
- if ( cesShownForActions.indexOf( action ) !== -1 && ! modalShown ) {
+ if (
+ cesShownForActions &&
+ cesShownForActions.indexOf( action ) !== -1 &&
+ ! modalShown
+ ) {
return null;
}
@@ -69,6 +88,7 @@ function CustomerEffortScoreTracks( {
recordEvent( 'ces_snackbar_view', {
action,
store_age: storeAgeInWeeks,
+ ces_location: 'inside',
...trackProps,
} );
};
@@ -77,7 +97,7 @@ function CustomerEffortScoreTracks( {
updateOptions( {
[ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [
action,
- ...cesShownForActions,
+ ...( cesShownForActions || [] ),
],
} );
};
@@ -86,30 +106,44 @@ function CustomerEffortScoreTracks( {
recordEvent( 'ces_snackbar_dismiss', {
action,
store_age: storeAgeInWeeks,
+ ces_location: 'inside',
...trackProps,
} );
addActionToShownOption();
};
+ const onModalDismissed = () => {
+ recordEvent( 'ces_view_dismiss', {
+ action,
+ store_age: storeAgeInWeeks,
+ ces_location: 'inside',
+ ...trackProps,
+ } );
+ };
+
const onModalShown = () => {
setModalShown( true );
recordEvent( 'ces_view', {
action,
store_age: storeAgeInWeeks,
+ ces_location: 'inside',
...trackProps,
} );
addActionToShownOption();
};
- const recordScore = ( score, comments ) => {
+ const recordScore = ( score, secondScore, comments ) => {
recordEvent( 'ces_feedback', {
action,
score,
+ score_second_question: secondScore,
+ score_combined: score + secondScore,
comments: comments || '',
store_age: storeAgeInWeeks,
+ ces_location: 'inside',
...trackProps,
} );
createNotice( 'success', onSubmitLabel );
@@ -118,17 +152,22 @@ function CustomerEffortScoreTracks( {
return (
- ✏️
+ { icon || '✏' }
}
/>
@@ -147,7 +186,7 @@ CustomerEffortScoreTracks.propTypes = {
/**
* The label displayed in the modal.
*/
- label: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
/**
* The label for the snackbar that appears upon survey submission.
*/
@@ -155,7 +194,7 @@ CustomerEffortScoreTracks.propTypes = {
/**
* The array of actions that the CES modal has been shown for.
*/
- cesShownForActions: PropTypes.arrayOf( PropTypes.string ).isRequired,
+ cesShownForActions: PropTypes.arrayOf( PropTypes.string ),
/**
* Whether tracking is allowed or not.
*/
@@ -178,25 +217,12 @@ CustomerEffortScoreTracks.propTypes = {
createNotice: PropTypes.func,
};
-function getStoreAgeInWeeks( adminInstallTimestamp ) {
- if ( adminInstallTimestamp === 0 ) {
- return null;
- }
-
- // Date.now() is ms since Unix epoch, adminInstallTimestamp is in
- // seconds since Unix epoch.
- const storeAgeInMs = Date.now() - adminInstallTimestamp * 1000;
- const storeAgeInWeeks = Math.round( storeAgeInMs / WEEK );
-
- return storeAgeInWeeks;
-}
-
export default compose(
withSelect( ( select ) => {
- const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
+ const { getOption, hasFinishedResolution } =
+ select( OPTIONS_STORE_NAME );
- const cesShownForActions =
- getOption( SHOWN_FOR_ACTIONS_OPTION_NAME ) || [];
+ const cesShownForActions = getOption( SHOWN_FOR_ACTIONS_OPTION_NAME );
const adminInstallTimestamp =
getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0;
@@ -207,12 +233,16 @@ export default compose(
const allowTracking = allowTrackingOption === 'yes';
const resolving =
- isResolving( 'getOption', [ SHOWN_FOR_ACTIONS_OPTION_NAME ] ) ||
+ ! hasFinishedResolution( 'getOption', [
+ SHOWN_FOR_ACTIONS_OPTION_NAME,
+ ] ) ||
storeAgeInWeeks === null ||
- isResolving( 'getOption', [
+ ! hasFinishedResolution( 'getOption', [
ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
] ) ||
- isResolving( 'getOption', [ ALLOW_TRACKING_OPTION_NAME ] );
+ ! hasFinishedResolution( 'getOption', [
+ ALLOW_TRACKING_OPTION_NAME,
+ ] );
return {
cesShownForActions,
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/action-types.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/action-types.js
index d3960abc423..74e552b9faf 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/action-types.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/action-types.js
@@ -1,6 +1,8 @@
const TYPES = {
SET_CES_SURVEY_QUEUE: 'SET_CES_SURVEY_QUEUE',
ADD_CES_SURVEY: 'ADD_CES_SURVEY',
+ SHOW_CES_MODAL: 'SHOW_CES_MODAL',
+ HIDE_CES_MODAL: 'HIDE_CES_MODAL',
};
export default TYPES;
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js
index f2ef83ea83e..101e599c50f 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js
@@ -23,25 +23,41 @@ export function setCesSurveyQueue( queue ) {
/**
* Add a new CES track to the state.
*
- * @param {string} action action name for the survey
- * @param {string} label label for the snackback
- * @param {string} pageNow value of window.pagenow
- * @param {string} adminPage value of window.adminpage
- * @param {string} onsubmitLabel label for the snackback onsubmit
- * @param {Object} props object for optional props
+ * @param {Object} args All arguments.
+ * @param {string} args.action action name for the survey
+ * @param {string} args.title title for the snackback
+ * @param {string} args.description description for feedback modal.
+ * @param {string} args.noticeLabel noticeLabel for notice.
+ * @param {string} args.firstQuestion first question for modal survey
+ * @param {string} args.secondQuestion second question for modal survey
+ * @param {string} args.icon optional icon for notice.
+ * @param {string} args.pageNow value of window.pagenow
+ * @param {string} args.adminPage value of window.adminpage
+ * @param {string} args.onsubmitLabel label for the snackback onsubmit
+ * @param {Object} args.props object for optional props
*/
-export function addCesSurvey(
+export function addCesSurvey( {
action,
- label,
+ title,
+ description,
+ noticeLabel,
+ firstQuestion,
+ secondQuestion,
+ icon,
pageNow = window.pagenow,
adminPage = window.adminpage,
onsubmitLabel = undefined,
- props = {}
-) {
+ props = {},
+} ) {
return {
type: TYPES.ADD_CES_SURVEY,
action,
- label,
+ title,
+ description,
+ noticeLabel,
+ firstQuestion,
+ secondQuestion,
+ icon,
pageNow,
adminPage,
onsubmit_label: onsubmitLabel,
@@ -49,30 +65,79 @@ export function addCesSurvey(
};
}
+/**
+ * Add show CES modal.
+ *
+ * @param {Object} surveyProps props for CES survey, similar to addCesSurvey.
+ * @param {Object} props object for optional props
+ * @param {Object} onSubmitNoticeProps object for on submit notice props.
+ */
+export function showCesModal(
+ surveyProps = {},
+ props = {},
+ onSubmitNoticeProps = {}
+) {
+ return {
+ type: TYPES.SHOW_CES_MODAL,
+ surveyProps,
+ onsubmit_label: surveyProps.onsubmitLabel || '',
+ props,
+ onSubmitNoticeProps,
+ };
+}
+
+/**
+ * Hide CES Modal.
+ */
+export function hideCesModal() {
+ return {
+ type: TYPES.HIDE_CES_MODAL,
+ };
+}
+
/**
* Add a new CES survey track for the pages in Analytics menu
*/
export function addCesSurveyForAnalytics() {
- return addCesSurvey(
- 'analytics_filtered',
- __( 'How easy was it to filter your store analytics?', 'woocommerce' ),
- 'woocommerce_page_wc-admin',
- 'woocommerce_page_wc-admin'
- );
+ return addCesSurvey( {
+ action: 'analytics_filtered',
+ title: __(
+ 'How easy was it to filter your store analytics?',
+ 'woocommerce'
+ ),
+ firstQuestion: __(
+ 'The filters in the analytics screen are easy to use.',
+ 'woocommerce'
+ ),
+ secondQuestion: __(
+ `The filters' functionality meets my needs.`,
+ 'woocommerce'
+ ),
+ pageNow: 'woocommerce_page_wc-admin',
+ adminPage: 'woocommerce_page_wc-admin',
+ } );
}
/**
* Add a new CES survey track on searching customers.
*/
export function addCesSurveyForCustomerSearch() {
- return addCesSurvey(
- 'ces_search',
- __( 'How easy was it to use search?', 'woocommerce' ),
- 'woocommerce_page_wc-admin',
- 'woocommerce_page_wc-admin',
- undefined,
- {
+ return addCesSurvey( {
+ action: 'ces_search',
+ title: __( 'How easy was it to use search?', 'woocommerce' ),
+ firstQuestion: __(
+ 'The search feature in WooCommerce is easy to use.',
+ 'woocommerce'
+ ),
+ secondQuestion: __(
+ `The search's functionality meets my needs.`,
+ 'woocommerce'
+ ),
+ pageNow: 'woocommerce_page_wc-admin',
+ adminPage: 'woocommerce_page_wc-admin',
+ onsubmit_label: undefined,
+ props: {
search_area: 'customer',
- }
- );
+ },
+ } );
}
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js
index 3d30a67696e..9cf2d75e330 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js
@@ -5,6 +5,8 @@ import TYPES from './action-types';
const DEFAULT_STATE = {
queue: [],
+ cesModalData: undefined,
+ showCESModal: false,
};
const reducer = ( state = DEFAULT_STATE, action ) => {
@@ -12,7 +14,28 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
case TYPES.SET_CES_SURVEY_QUEUE:
return {
...state,
- queue: action.queue,
+ queue: [ ...state.queue, ...action.queue ],
+ };
+ case TYPES.HIDE_CES_MODAL:
+ return {
+ ...state,
+ showCESModal: false,
+ cesModalData: undefined,
+ };
+ case TYPES.SHOW_CES_MODAL:
+ const cesModalData = {
+ action: action.surveyProps.action,
+ label: action.surveyProps.label,
+ onSubmitLabel: action.onSubmitLabel,
+ firstQuestion: action.surveyProps.firstQuestion,
+ secondQuestion: action.surveyProps.secondQuestion,
+ onSubmitNoticeProps: action.onSubmitNoticeProps || {},
+ props: action.props,
+ };
+ return {
+ ...state,
+ showCESModal: true,
+ cesModalData,
};
case TYPES.ADD_CES_SURVEY:
// Prevent duplicate
@@ -24,7 +47,12 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
}
const newTrack = {
action: action.action,
- label: action.label,
+ title: action.title,
+ description: action.description,
+ noticeLabel: action.noticeLabel,
+ firstQuestion: action.firstQuestion,
+ secondQuestion: action.secondQuestion,
+ icon: action.icon,
pagenow: action.pageNow,
adminpage: action.adminPage,
onSubmitLabel: action.onSubmitLabel,
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/selectors.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/selectors.js
index ab18e6631af..e4823d8e126 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/selectors.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/selectors.js
@@ -1,3 +1,7 @@
export function getCesSurveyQueue( state ) {
return state.queue;
}
+
+export function getVisibleCESModalData( state ) {
+ return state.showCESModal ? state.cesModalData : undefined;
+}
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js
index 58b35851d2e..bd9b84cbbe4 100644
--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js
@@ -1,2 +1,3 @@
export { default as CustomerEffortScoreTracks } from './customer-effort-score-tracks';
export { default as CustomerEffortScoreTracksContainer } from './customer-effort-score-tracks-container';
+export * from './customer-effort-score-modal-container.tsx';
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/product-mvp-ces-footer.scss b/plugins/woocommerce-admin/client/customer-effort-score-tracks/product-mvp-ces-footer.scss
new file mode 100644
index 00000000000..f2449b78321
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/product-mvp-ces-footer.scss
@@ -0,0 +1,27 @@
+.woocommerce-product-mvp-ces-footer {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ padding: $gap-smaller $gap;
+
+ .woocommerce-pill {
+ margin-right: $gap-smaller;
+ }
+
+ .components-button {
+ margin-left: $gap;
+ }
+
+ &__close-button {
+ position: absolute;
+ right: $gap;
+ }
+
+ &__container {
+ margin-right: $gap-larger;
+ flex-wrap: nowrap;
+ display: flex;
+ align-items: center;
+ }
+}
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/product-mvp-ces-footer.tsx b/plugins/woocommerce-admin/client/customer-effort-score-tracks/product-mvp-ces-footer.tsx
new file mode 100644
index 00000000000..3c1ea3809e3
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/product-mvp-ces-footer.tsx
@@ -0,0 +1,148 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Button } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { closeSmall } from '@wordpress/icons';
+import { Pill } from '@woocommerce/components';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import './product-mvp-ces-footer.scss';
+import {
+ ALLOW_TRACKING_OPTION_NAME,
+ SHOWN_FOR_ACTIONS_OPTION_NAME,
+} from './constants';
+import { WooFooterItem } from '~/layout/footer';
+import { STORE_KEY } from './data/constants';
+
+export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
+ 'woocommerce_ces_product_mvp_ces_action';
+
+export const ProductMVPCESFooter: React.FC = () => {
+ const { showCesModal } = useDispatch( STORE_KEY );
+ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
+ const {
+ cesAction,
+ allowTracking,
+ cesShownForActions,
+ resolving: isLoading,
+ } = useSelect( ( select ) => {
+ const { getOption, hasFinishedResolution } =
+ select( OPTIONS_STORE_NAME );
+
+ const action = getOption(
+ PRODUCT_MVP_CES_ACTION_OPTION_NAME
+ ) as string;
+
+ const shownForActions =
+ ( getOption( SHOWN_FOR_ACTIONS_OPTION_NAME ) as string[] ) || [];
+
+ const allowTrackingOption =
+ getOption( ALLOW_TRACKING_OPTION_NAME ) || 'no';
+
+ const resolving =
+ ! hasFinishedResolution( 'getOption', [
+ SHOWN_FOR_ACTIONS_OPTION_NAME,
+ ] ) ||
+ ! hasFinishedResolution( 'getOption', [
+ PRODUCT_MVP_CES_ACTION_OPTION_NAME,
+ ] ) ||
+ ! hasFinishedResolution( 'getOption', [
+ ALLOW_TRACKING_OPTION_NAME,
+ ] );
+
+ return {
+ cesShownForActions: shownForActions,
+ allowTracking: allowTrackingOption === 'yes',
+ cesAction: action,
+ resolving,
+ };
+ } );
+
+ const shareFeedback = () => {
+ showCesModal(
+ {
+ action: cesAction,
+ label: __(
+ "How's your experience with the product editor?",
+ 'woocommerce'
+ ),
+ firstQuestion: __(
+ 'The product editing screen is easy to use',
+ 'woocommerce'
+ ),
+ secondQuestion: __(
+ "The product editing screen's functionality meets my needs",
+ 'woocommerce'
+ ),
+ onsubmitLabel: __(
+ "Thanks for the feedback. We'll put it to good use!",
+ 'woocommerce'
+ ),
+ },
+ {},
+ {
+ type: 'snackbar',
+ icon: 🌟,
+ }
+ );
+ updateOptions( {
+ [ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [
+ cesAction,
+ ...cesShownForActions,
+ ],
+ } );
+ };
+
+ const onDisablingCES = () => {
+ updateOptions( {
+ [ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'hide',
+ } );
+ };
+
+ const showCESFooter =
+ ! isLoading && allowTracking && cesAction && cesAction !== 'hide';
+
+ return (
+ <>
+ { showCESFooter && (
+
+
+
+ { __( 'BETA', 'woocommerce' ) }
+ { __(
+ "You're using the new product editor (currently in development). How is your experience so far?",
+ 'woocommerce'
+ ) }
+
+
+
+
+
+
+ ) }
+ >
+ );
+};
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts
new file mode 100644
index 00000000000..5693325bb1e
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import { dispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { triggerExitPageCesSurvey } from '../customer-effort-score-exit-page';
+
+jest.mock( '@woocommerce/data', () => ( {
+ OPTIONS_STORE_NAME: 'options',
+} ) );
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ useSelect: jest.fn(),
+ dispatch: jest.fn(),
+ resolveSelect: jest.fn().mockReturnValue( {
+ getOption: jest.fn().mockResolvedValue( 'yes' ),
+ } ),
+} ) );
+
+describe( 'triggerExitPageCesSurvey', () => {
+ const addCESSurveyMock = jest.fn();
+ beforeEach( () => {
+ jest.clearAllMocks();
+ ( dispatch as jest.Mock ).mockReturnValue( {
+ addCesSurvey: addCESSurveyMock,
+ } );
+ } );
+
+ it( 'should not trigger addCESSurvey if local storage is empty', () => {
+ triggerExitPageCesSurvey();
+ expect( addCESSurveyMock ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should not trigger addCESSurvey if copy does not exist for item, but clear localStorage still', () => {
+ window.localStorage.setItem(
+ 'customer-effort-score-exit-page',
+ JSON.stringify( [ 'random-id' ] )
+ );
+ triggerExitPageCesSurvey();
+ expect( addCESSurveyMock ).not.toHaveBeenCalled();
+ const list = window.localStorage.getItem(
+ 'customer-effort-score-exit-page'
+ );
+ expect( list ).toEqual( '[]' );
+ } );
+
+ it( 'should trigger addCESSurvey if copy does exist for item, and clear localStorage still', () => {
+ window.localStorage.setItem(
+ 'customer-effort-score-exit-page',
+ JSON.stringify( [ 'new_product' ] )
+ );
+ triggerExitPageCesSurvey();
+ expect( addCESSurveyMock ).toHaveBeenCalled();
+ const list = window.localStorage.getItem(
+ 'customer-effort-score-exit-page'
+ );
+ expect( list ).toEqual( '[]' );
+ } );
+} );
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts
new file mode 100644
index 00000000000..60121766783
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import {
+ addCustomerEffortScoreExitPageListener,
+ addExitPage,
+ removeCustomerEffortScoreExitPageListener,
+} from './customer-effort-score-exit-page';
+
+export const useCustomerEffortScoreExitPageTracker = (
+ pageId: string,
+ hasUnsavedChanges: boolean
+) => {
+ const hasUnsavedChangesRef = useRef( hasUnsavedChanges );
+
+ // Using unmounting as a way to see when the react router changes.
+ useEffect( () => {
+ hasUnsavedChangesRef.current = hasUnsavedChanges;
+ }, [ hasUnsavedChanges ] );
+
+ useEffect( () => {
+ return () => {
+ if ( hasUnsavedChangesRef.current ) {
+ // unmounted.
+ addExitPage( pageId );
+ }
+ };
+ }, [] );
+
+ // This effect listen to the native beforeunload event to show
+ // a confirmation message
+ useEffect( () => {
+ addCustomerEffortScoreExitPageListener(
+ pageId,
+ () => hasUnsavedChanges
+ );
+
+ return () => {
+ removeCustomerEffortScoreExitPageListener( pageId );
+ };
+ }, [ hasUnsavedChanges ] );
+};
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-product-mvp-ces-footer.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-product-mvp-ces-footer.ts
new file mode 100644
index 00000000000..dd072eb93ef
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-product-mvp-ces-footer.ts
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { resolveSelect, useDispatch } from '@wordpress/data';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import { PRODUCT_MVP_CES_ACTION_OPTION_NAME } from './product-mvp-ces-footer';
+
+async function isProductMVPCESHidden(): Promise< boolean > {
+ const productCESAction: string = await resolveSelect(
+ OPTIONS_STORE_NAME
+ ).getOption( PRODUCT_MVP_CES_ACTION_OPTION_NAME );
+ return productCESAction === 'hide';
+}
+
+export const useProductMVPCESFooter = () => {
+ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
+
+ const onSaveDraft = async () => {
+ if ( ( await isProductMVPCESHidden() ) === false ) {
+ updateOptions( {
+ [ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'new_product',
+ } );
+ }
+ };
+
+ const onPublish = async () => {
+ if ( ( await isProductMVPCESHidden() ) === false ) {
+ updateOptions( {
+ [ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'new_product',
+ } );
+ }
+ };
+
+ return { onSaveDraft, onPublish };
+};
diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/utils.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/utils.ts
new file mode 100644
index 00000000000..9e583b76644
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/utils.ts
@@ -0,0 +1,17 @@
+/**
+ * External dependencies
+ */
+import { WEEK } from '@woocommerce/data';
+
+export function getStoreAgeInWeeks( adminInstallTimestamp: number ) {
+ if ( adminInstallTimestamp === 0 ) {
+ return null;
+ }
+
+ // Date.now() is ms since Unix epoch, adminInstallTimestamp is in
+ // seconds since Unix epoch.
+ const storeAgeInMs = Date.now() - adminInstallTimestamp * 1000;
+ const storeAgeInWeeks = Math.round( storeAgeInMs / WEEK );
+
+ return storeAgeInWeeks;
+}
diff --git a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx
index 166041c6d61..de8c9c40ddd 100644
--- a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx
+++ b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx
@@ -357,6 +357,9 @@ export function StoreAddress( {
label={ __( 'Country / Region', 'woocommerce' ) + ' *' }
required
autoComplete="new-password" // disable autocomplete and autofill
+ getSearchExpression={ ( query: string ) => {
+ return new RegExp( '^' + query, 'i' );
+ } }
options={ countryStateOptions }
excludeSelectedOptions={ false }
showAllOnFocus
diff --git a/plugins/woocommerce-admin/client/dashboard/style.scss b/plugins/woocommerce-admin/client/dashboard/style.scss
index 8018c82e548..04c551fa392 100644
--- a/plugins/woocommerce-admin/client/dashboard/style.scss
+++ b/plugins/woocommerce-admin/client/dashboard/style.scss
@@ -90,3 +90,11 @@
.components-card .woocommerce-ellipsis-menu__toggle {
padding: 0;
}
+
+.components-modal__frame.woocommerce-cart-modal .components-modal__content {
+ margin-top: 6rem;
+
+ @include breakpoint( '<600px' ) {
+ margin-top: 7rem;
+ }
+}
diff --git a/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx b/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx
index ec31ae5fc9c..98e7aa7c676 100644
--- a/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx
+++ b/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import { applyFilters } from '@wordpress/hooks';
+import { useEffect } from '@wordpress/element';
import QueryString, { parse } from 'qs';
/**
@@ -12,6 +13,7 @@ import { ShippingRecommendations } from '../shipping';
import { EmbeddedBodyProps } from './embedded-body-props';
import { StoreAddressTour } from '../guided-tours/store-address-tour';
import './style.scss';
+import { triggerExitPageCesSurvey } from '~/customer-effort-score-tracks/customer-effort-score-exit-page';
type QueryParams = EmbeddedBodyProps;
@@ -34,6 +36,10 @@ const EMBEDDED_BODY_COMPONENT_LIST: React.ElementType[] = [
* Each Fill component receives QueryParams, consisting of a page, tab, and section string.
*/
export const EmbeddedBodyLayout = () => {
+ useEffect( () => {
+ triggerExitPageCesSurvey();
+ }, [] );
+
const query = parse( location.search.substring( 1 ) );
let queryParams: QueryParams = { page: '', tab: '' };
if ( isWPPage( query ) ) {
diff --git a/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx b/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx
index 1561e2077a7..954d90492e1 100644
--- a/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx
+++ b/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx
@@ -9,6 +9,18 @@ import { addFilter } from '@wordpress/hooks';
*/
import { EmbeddedBodyLayout } from '../embedded-body-layout';
+jest.mock(
+ '~/customer-effort-score-tracks/customer-effort-score-exit-page',
+ () => ( {
+ triggerExitPageCesSurvey: jest.fn(),
+ } )
+);
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ resolveSelect: jest.fn().mockReturnValue( {
+ getOption: jest.fn(),
+ } ),
+} ) );
jest.mock( '@woocommerce/data', () => ( {
useUser: () => ( {
currentUserCan: jest.fn(),
diff --git a/plugins/woocommerce-admin/client/guided-tours/add-product-tour/index.tsx b/plugins/woocommerce-admin/client/guided-tours/add-product-tour/index.tsx
index c2b45f8f4ed..d487cb93141 100644
--- a/plugins/woocommerce-admin/client/guided-tours/add-product-tour/index.tsx
+++ b/plugins/woocommerce-admin/client/guided-tours/add-product-tour/index.tsx
@@ -15,7 +15,7 @@ import { useActiveEditorType } from './use-active-editor-type';
import {
bindEnableGuideModeClickEvent,
waitUntilElementTopNotChange,
-} from './utils';
+} from '../utils';
import {
ProductTourStepName,
useProductStepChange,
diff --git a/plugins/woocommerce-admin/client/guided-tours/add-product-tour/use-track-publish-button.ts b/plugins/woocommerce-admin/client/guided-tours/add-product-tour/use-track-publish-button.ts
index f05eaae5a65..0628f2983ea 100644
--- a/plugins/woocommerce-admin/client/guided-tours/add-product-tour/use-track-publish-button.ts
+++ b/plugins/woocommerce-admin/client/guided-tours/add-product-tour/use-track-publish-button.ts
@@ -7,7 +7,7 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
-import { bindPublishClickEvent } from './utils';
+import { bindPublishClickEvent } from '../utils';
export const useTrackPublishButton = ( showTour: boolean ) => {
const unbindPublishClickEvent = useRef< () => void >( () => {} );
diff --git a/plugins/woocommerce-admin/client/guided-tours/add-product-tour/utils.ts b/plugins/woocommerce-admin/client/guided-tours/utils.ts
similarity index 72%
rename from plugins/woocommerce-admin/client/guided-tours/add-product-tour/utils.ts
rename to plugins/woocommerce-admin/client/guided-tours/utils.ts
index 079146b1a8d..315a1bc4d1e 100644
--- a/plugins/woocommerce-admin/client/guided-tours/add-product-tour/utils.ts
+++ b/plugins/woocommerce-admin/client/guided-tours/utils.ts
@@ -19,6 +19,26 @@ export const waitUntilElementTopNotChange = (
return intervalId;
};
+// Observer position changes of an element
+export const observePositionChange = (
+ selector: string,
+ callback: () => void,
+ pollMs: number
+) => {
+ const initialElement = document.querySelector(
+ selector
+ ) as HTMLElement | null;
+ let lastInitialElementTop = initialElement?.offsetTop;
+
+ return setInterval( () => {
+ const top = initialElement?.offsetTop;
+ if ( lastInitialElementTop !== top ) {
+ callback();
+ }
+ lastInitialElementTop = top;
+ }, pollMs );
+};
+
// Overwrite the default behavior of click event for the "Enable guided mode" button
export const bindEnableGuideModeClickEvent = (
onClick: EventListenerOrEventListenerObject
diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts
new file mode 100644
index 00000000000..3b167616353
--- /dev/null
+++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-config.ts
@@ -0,0 +1,125 @@
+/**
+ * External dependencies
+ */
+import { TourKitTypes } from '@woocommerce/components';
+
+/**
+ * Internal dependencies
+ */
+import { scrollPopperToVisibleAreaIfNeeded } from './utils';
+
+export const getTourConfig = ( {
+ closeHandler,
+ onNextStepHandler,
+ autoScrollBlock,
+ steps,
+}: {
+ closeHandler: TourKitTypes.CloseHandler;
+ onNextStepHandler: ( currentStepIndex: number ) => void;
+ autoScrollBlock: ScrollLogicalPosition;
+ steps: TourKitTypes.WooStep[];
+} ): TourKitTypes.WooConfig => {
+ let previousPopperTopPosition: number | null = null;
+ let perviousPopperRef: unknown = null;
+ const defaultPlacement = 'top-start';
+
+ return {
+ placement: defaultPlacement,
+ options: {
+ effects: {
+ spotlight: {
+ interactivity: {
+ enabled: true,
+ rootElementSelector: '.woocommerce.wc-addons-wrap',
+ },
+ },
+ autoScroll: {
+ behavior: 'auto',
+ block: autoScrollBlock,
+ },
+ },
+ popperModifiers: [
+ {
+ name: 'arrow',
+ options: {
+ padding: ( {
+ popper,
+ }: {
+ popper: { width: number };
+ } ) => {
+ return {
+ // Align the arrow to the left of the popper.
+ right: popper.width - 34,
+ };
+ },
+ },
+ },
+ {
+ name: 'offset',
+ options: {
+ offset: [ 20, 20 ],
+ },
+ },
+ {
+ name: 'flip',
+ options: {
+ allowedAutoPlacements: [ 'right', 'bottom', 'top' ],
+ fallbackPlacements: [ 'bottom-start', 'right' ],
+ flipVariations: false,
+ boundry: 'clippingParents',
+ },
+ },
+ {
+ name: 'inAppTourPopperModifications',
+ enabled: true,
+ phase: 'read',
+ fn( { state, instance } ) {
+ // 1. First modification - force `right` placement for items in admin menu.
+ if ( perviousPopperRef !== state.elements.reference ) {
+ const isAdminMenuItem = (
+ state.elements.reference as HTMLElement
+ ).closest( '#adminmenu' );
+ const desiredPlacement = isAdminMenuItem
+ ? 'right'
+ : defaultPlacement;
+ if ( state.placement !== desiredPlacement ) {
+ instance.setOptions( {
+ placement: desiredPlacement,
+ } );
+ }
+ }
+
+ // 2. Second modification - Try to make sure that the popper is visible once when
+ // the next step is displayed.
+ const popperBoundingRect =
+ state.elements.popper.getBoundingClientRect();
+ const arrowBoundingRect =
+ state.elements.arrow?.getBoundingClientRect();
+ const arrowHeight = arrowBoundingRect?.height || 0;
+
+ // Try to make sure that the popper is visible if poppers' reference (step) changed and
+ // if arrowHeight is not 0 (it means that popper's position hasn't been updated yet).
+ // Also, change if popper's top position changed - the modifier can be called
+ // multiple times for the same position.
+ if (
+ perviousPopperRef !== state.elements.reference &&
+ arrowHeight !== 0 &&
+ previousPopperTopPosition !== popperBoundingRect.top
+ ) {
+ scrollPopperToVisibleAreaIfNeeded(
+ popperBoundingRect
+ );
+ previousPopperTopPosition = popperBoundingRect.top;
+ perviousPopperRef = state.elements.reference;
+ }
+ },
+ },
+ ],
+ callbacks: {
+ onNextStep: onNextStepHandler,
+ },
+ },
+ steps,
+ closeHandler,
+ };
+};
diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts
new file mode 100644
index 00000000000..5eb00fc2ba5
--- /dev/null
+++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts
@@ -0,0 +1,153 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { createElement, createInterpolateElement } from '@wordpress/element';
+import { TourKitTypes } from '@woocommerce/components';
+
+export const getSteps = (): TourKitTypes.WooStep[] => {
+ const lineBreak = createElement( 'br' );
+ return [
+ {
+ referenceElements: {
+ desktop: '#adminmenu a[href="admin.php?page=wc-addons"]',
+ },
+ focusElement: {
+ desktop: '#adminmenu a[href="admin.php?page=wc-addons"]',
+ },
+ meta: {
+ name: 'wc-addons-menu-item',
+ heading: __(
+ 'Welcome to the WooCommerce Marketplace',
+ 'woocommerce'
+ ),
+ descriptions: {
+ desktop: createInterpolateElement(
+ __(
+ 'Power up your store by adding extra functionality using extensions, find a fresh new look with themes, or integrate your store with other software and services.
The WooCommerce Marketplace is your go-to for all of the above, and the only place you’ll find products that have been reviewed and approved by the WooCommerce team.
Whether you’re looking to improve your store or grow your business, you can find a solution here. There are hundreds of options available, and new products are added regularly.
The WooCommerce Marketplace is also available at WooCommerce.com.',
+ 'woocommerce'
+ ),
+ {
+ br: lineBreak,
+ }
+ ),
+ },
+ },
+ },
+ {
+ referenceElements: {
+ desktop: '.marketplace-header__search-form',
+ },
+ focusElement: {
+ desktop: '.marketplace-header__search-form',
+ },
+ meta: {
+ name: 'wc-addons-search',
+ heading: __( 'Find exactly what you need', 'woocommerce' ),
+ descriptions: {
+ desktop: __(
+ 'Use the search box to find specific products or solutions.',
+ 'woocommerce'
+ ),
+ },
+ },
+ },
+ {
+ referenceElements: {
+ desktop: '#marketplace-current-section-dropdown',
+ },
+ focusElement: {
+ desktop: '#marketplace-current-section-dropdown',
+ },
+ meta: {
+ name: 'wc-addons-categories',
+ heading: __( 'Browse for new ideas', 'woocommerce' ),
+ descriptions: {
+ desktop: createInterpolateElement(
+ __(
+ 'Or browse all available products by category.',
+ 'woocommerce'
+ ),
+ {
+ br: lineBreak,
+ }
+ ),
+ },
+ },
+ },
+ {
+ referenceElements: {
+ desktop: '.addon-product-group:first-child',
+ },
+ focusElement: {
+ desktop: '.addon-product-group:first-child',
+ },
+ meta: {
+ name: 'wc-addons-featured',
+ heading: __( 'Learn more about products', 'woocommerce' ),
+ descriptions: {
+ desktop: createInterpolateElement(
+ __(
+ 'Scroll down to see all available products for a search or selected category.
Click on any product to see more information about it, including features, requirements, and available documentation.',
+ 'woocommerce'
+ ),
+ {
+ br: lineBreak,
+ }
+ ),
+ },
+ },
+ },
+ {
+ referenceElements: {
+ desktop: '.marketplace-header__tab-link_helper',
+ },
+ focusElement: {
+ desktop: '.marketplace-header__tab-link_helper',
+ },
+ meta: {
+ name: 'wc-addons-my-subscriptions',
+ heading: __( 'Manage your purchases', 'woocommerce' ),
+ descriptions: {
+ desktop: createInterpolateElement(
+ __(
+ "Products purchased from the WooCommerce Marketplace can be managed in My Subscriptions, either here or on WooCommerce.com.
Every purchase is backed by our 30-day money-back guarantee, and includes email and live chat support.
That's it! We hope the WooCommerce Marketplace helps you build the business of your dreams.",
+ 'woocommerce'
+ ),
+ {
+ a1: createElement(
+ 'a',
+ {
+ href: 'https://woocommerce.com/refund-policy/',
+ 'aria-label': __(
+ 'Refund policy',
+ 'woocommerce'
+ ),
+ },
+ __(
+ '30-day money-back guarantee',
+ 'woocommerce'
+ )
+ ),
+ a2: createElement(
+ 'a',
+ {
+ href: 'https://woocommerce.com/my-account/create-a-ticket/',
+ 'aria-label': __(
+ 'Contact support',
+ 'woocommerce'
+ ),
+ },
+ __(
+ 'email and live chat support',
+ 'woocommerce'
+ )
+ ),
+ br: lineBreak,
+ }
+ ),
+ },
+ },
+ },
+ ];
+};
diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx
new file mode 100644
index 00000000000..fec151af83f
--- /dev/null
+++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/index.tsx
@@ -0,0 +1,122 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { TourKit, TourKitTypes } from '@woocommerce/components';
+import { recordEvent } from '@woocommerce/tracks';
+import { useDispatch } from '@wordpress/data';
+import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+import qs from 'qs';
+
+/**
+ * Internal dependencies
+ */
+import { observePositionChange, waitUntilElementTopNotChange } from '../utils';
+import { getTourConfig } from './get-config';
+import { scrollPopperToVisibleAreaIfNeeded } from './utils';
+import { getSteps } from './get-steps';
+
+const WCAddonsTour = () => {
+ const [ showTour, setShowTour ] = useState( false );
+
+ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
+
+ const steps = getSteps();
+ const defaultAutoScrollBlock: ScrollLogicalPosition = 'center';
+
+ useEffect( () => {
+ const query = qs.parse( window.location.search.slice( 1 ) );
+ if ( query?.tutorial === 'true' ) {
+ const intervalId = waitUntilElementTopNotChange(
+ steps[ 0 ].referenceElements?.desktop || '',
+ () => {
+ const stepName = steps[ 0 ]?.meta?.name;
+ setShowTour( true );
+ recordEvent( 'in_app_marketplace_tour_started', {
+ step: stepName,
+ } );
+ },
+ 500
+ );
+ return () => clearInterval( intervalId );
+ }
+ // only run once
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [] );
+
+ useEffect( () => {
+ if ( showTour ) {
+ function showPopper() {
+ const tourKitElement = document.querySelector(
+ '.tour-kit-frame__container'
+ );
+ if ( tourKitElement ) {
+ scrollPopperToVisibleAreaIfNeeded(
+ tourKitElement.getBoundingClientRect()
+ );
+ }
+ }
+
+ // In a rare case, admin notices might load before observe is added below (moving `.wc-addons-wrap`).
+ // In such a case, if Tour is shown before this effect is called, it might not be position correctly.
+ // Updating popper's position here, ensures it's always visible.
+ const timeoutId = setTimeout( showPopper, 500 );
+
+ const intervalId = observePositionChange(
+ '.wc-addons-wrap',
+ showPopper,
+ 150
+ );
+ return () => {
+ clearTimeout( timeoutId );
+ clearInterval( intervalId );
+ };
+ }
+ }, [ showTour ] );
+
+ if ( ! showTour ) {
+ return null;
+ }
+
+ const closeHandler: TourKitTypes.CloseHandler = (
+ tourSteps,
+ currentStepIndex
+ ) => {
+ setShowTour( false );
+ // mark tour as completed
+ updateOptions( {
+ woocommerce_admin_dismissed_in_app_marketplace_tour: 'yes',
+ } );
+ // remove `tutorial` from search query, so it's not shown on page refresh
+ const url = new URL( window.location.href );
+ url.searchParams.delete( 'tutorial' );
+ window.history.replaceState( null, '', url );
+
+ if ( steps.length - 1 === currentStepIndex ) {
+ recordEvent( 'in_app_marketplace_tour_completed' );
+ } else {
+ const stepName = tourSteps[ currentStepIndex ]?.meta?.name;
+ recordEvent( 'in_app_marketplace_tour_dismissed', {
+ step: stepName,
+ } );
+ }
+ };
+
+ const onNextStepHandler = ( previousStepIndex: number ) => {
+ const stepName = steps[ previousStepIndex + 1 ]?.meta?.name || '';
+ recordEvent( 'in_app_marketplace_tour_step_viewed', {
+ step: stepName,
+ } );
+ };
+
+ const tourConfig = getTourConfig( {
+ closeHandler,
+ onNextStepHandler,
+ autoScrollBlock: defaultAutoScrollBlock,
+ steps,
+ } );
+
+ return ;
+};
+
+export default WCAddonsTour;
diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/utils.ts b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/utils.ts
new file mode 100644
index 00000000000..5f05a103750
--- /dev/null
+++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/utils.ts
@@ -0,0 +1,18 @@
+// Try to make popper element visible on the screen
+export const scrollPopperToVisibleAreaIfNeeded = (
+ popperBoundingRect: DOMRect
+) => {
+ // 8px is added for some extra spacing from the top admin bar
+ const adminBarHeight =
+ ( document.getElementById( 'wpadminbar' )?.offsetHeight || 0 ) + 8;
+
+ // check if element is cut from the top
+ if ( popperBoundingRect.top < adminBarHeight ) {
+ window.scrollBy( 0, popperBoundingRect.top - adminBarHeight );
+ } else if (
+ // check if element is cut from the bottom
+ popperBoundingRect.bottom > window.innerHeight
+ ) {
+ window.scrollBy( 0, popperBoundingRect.bottom - window.innerHeight );
+ }
+};
diff --git a/plugins/woocommerce-admin/client/hooks/usePreventLeavingPage.ts b/plugins/woocommerce-admin/client/hooks/usePreventLeavingPage.ts
new file mode 100644
index 00000000000..2a7d938eec8
--- /dev/null
+++ b/plugins/woocommerce-admin/client/hooks/usePreventLeavingPage.ts
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+import { useContext, useEffect, useMemo } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
+
+export default function usePreventLeavingPage(
+ hasUnsavedChanges: boolean,
+ /**
+ * Some browsers ignore this message currently on before unload event.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes
+ */
+ message?: string
+) {
+ const confirmMessage = useMemo(
+ () =>
+ message ??
+ __( 'Changes you made may not be saved.', 'woocommerce' ),
+ [ message ]
+ );
+ const { navigator } = useContext( NavigationContext );
+
+ // This effect prevent react router from navigate and show
+ // a confirmation message. It's a work around to beforeunload
+ // because react router does not triggers that event.
+ useEffect( () => {
+ if ( hasUnsavedChanges ) {
+ const push = navigator.push;
+
+ navigator.push = ( ...args: Parameters< typeof push > ) => {
+ /* eslint-disable-next-line no-alert */
+ const result = window.confirm( confirmMessage );
+ if ( result !== false ) {
+ push( ...args );
+ }
+ };
+
+ return () => {
+ navigator.push = push;
+ };
+ }
+ }, [ navigator, hasUnsavedChanges, confirmMessage ] );
+
+ // This effect listen to the native beforeunload event to show
+ // a confirmation message
+ useEffect( () => {
+ if ( hasUnsavedChanges ) {
+ function onBeforeUnload( event: BeforeUnloadEvent ) {
+ event.preventDefault();
+ return ( event.returnValue = confirmMessage );
+ }
+
+ window.addEventListener( 'beforeunload', onBeforeUnload, {
+ capture: true,
+ } );
+
+ return () => {
+ window.removeEventListener( 'beforeunload', onBeforeUnload, {
+ capture: true,
+ } );
+ };
+ }
+ }, [ hasUnsavedChanges, confirmMessage ] );
+}
diff --git a/plugins/woocommerce-admin/client/inbox-panel/index.js b/plugins/woocommerce-admin/client/inbox-panel/index.js
index 6917abeb6c6..87184556951 100644
--- a/plugins/woocommerce-admin/client/inbox-panel/index.js
+++ b/plugins/woocommerce-admin/client/inbox-panel/index.js
@@ -93,11 +93,13 @@ const renderNotes = ( {
notes,
onDismiss,
onNoteActionClick,
+ onNoteVisible,
setShowDismissAllModal: onDismissAll,
showHeader = true,
loadMoreNotes,
allNotesFetched,
notesHaveResolved,
+ unreadNotesCount,
} ) => {
if ( isBatchUpdating ) {
return;
@@ -114,17 +116,6 @@ const renderNotes = ( {
hasFiredPanelViewTrack = true;
}
- const screen = getScreenName();
- const onNoteVisible = ( note ) => {
- recordEvent( 'inbox_note_view', {
- note_content: note.content,
- note_name: note.name,
- note_title: note.title,
- note_type: note.type,
- screen,
- } );
- };
-
const notesArray = Object.keys( notes ).map( ( key ) => notes[ key ] );
return (
@@ -135,7 +126,7 @@ const renderNotes = ( {
{ __( 'Inbox', 'woocommerce' ) }
-
+