Merge branch 'trunk' into #46886

This commit is contained in:
Vedanshu Jain 2024-08-29 20:34:31 +05:30 committed by GitHub
commit 9e5224687f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
684 changed files with 19186 additions and 4281 deletions

View File

@ -59,8 +59,8 @@ jobs:
<${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>
*Labels:* ${{ join(github.event.pull_request.labels.*.name, ', ') }} *Labels:* ${{ join(github.event.pull_request.labels.*.name, ', ') }}
*Monthly Release Milestone:* <${{ github.event.pull_request.milestone.html_url }}|${{ github.event.pull_request.milestone.title }}> (Release Date: ${{ env.MILESTONE_DATE }}) *Monthly Release Milestone:* <${{ github.event.pull_request.milestone.html_url }}|${{ github.event.pull_request.milestone.title }}> (Release Date: ${{ env.MILESTONE_DATE }})
*WooAF (weekly) Timeline: this PR can be tested from:* ${{ env.TEST_DATE_MESSAGE }} Please check the Milestone above and test using the latest <https://github.com/woocommerce/woocommerce/releases|pre-release build>.
Please visit the <#${{ secrets.WOO_CORE_RELEASES_SLACK_CHANNEL }}> to obtain the latest WooAF build for testing. If a pre-release build for the stated Milestone does not exist, please use the Nightly build.
slack-optional-unfurl_links: false slack-optional-unfurl_links: false
slack-optional-unfurl_media: false slack-optional-unfurl_media: false

View File

@ -88,7 +88,8 @@
"webpack*" "webpack*"
], ],
"packages": [ "packages": [
"@woocommerce/block-library" "@woocommerce/block-library",
"@woocommerce/storybook"
], ],
"isIgnored": true "isIgnored": true
}, },
@ -199,7 +200,8 @@
"@wordpress/viewport", "@wordpress/viewport",
"@wordpress/interface", "@wordpress/interface",
"@wordpress/router", "@wordpress/router",
"@wordpress/edit-site" "@wordpress/edit-site",
"@wordpress/private-apis"
], ],
"packages": [ "packages": [
"@woocommerce/block-templates", "@woocommerce/block-templates",

View File

@ -1,5 +1,20 @@
== Changelog == == Changelog ==
= 9.2.3 2024-08-26 =
**WooCommerce**
* Fix - Ensure translation is fully loaded for certain parts of Checkout block. [#50892](https://github.com/woocommerce/woocommerce/pull/50892)
= 9.2.2 2024-08-22 =
**WooCommerce**
* Fix - Revert PR#48731 to address possible issues with plugins using WC's bundled select2 package. [#50854](https://github.com/woocommerce/woocommerce/pull/50854)
* Fix - Partially revert PR#48709 as it could cause issues for some users of the REST API system_status endpoint. [#50881](https://github.com/woocommerce/woocommerce/pull/50881)
= 9.2.1 2024-08-21 = = 9.2.1 2024-08-21 =
**WooCommerce** **WooCommerce**

View File

@ -43,7 +43,7 @@ WooCommerce also comes with two specific [block template parts](https://github.c
- Mini-Cart (`mini-cart.html`): used inside the Mini-Cart block drawer. - Mini-Cart (`mini-cart.html`): used inside the Mini-Cart block drawer.
- Checkout header (`checkout-header.html`): used as the header in the Checkout template. - Checkout header (`checkout-header.html`): used as the header in the Checkout template.
Similarly to the templates, they can be overriden by themes by adding a file with the same file name under the `/parts` folder. Similarly to the templates, they can be overridden by themes by adding a file with the same file name under the `/parts` folder.
### Global styles ### Global styles

View File

@ -1,9 +1,25 @@
---
post_title: Blocks reference
menu_title: Blocks Reference
---
# Woo Blocks Reference # Woo Blocks Reference
This page lists the Woo blocks included in the package. (Incomplete as there are still blocks that are not using block.json definition). This page lists the Woo blocks included in the package. (Incomplete as there are still blocks that are not using block.json definition).
<!-- START Autogenerated - DO NOT EDIT --> <!-- START Autogenerated - DO NOT EDIT -->
## Add to Cart with Options - woocommerce/add-to-cart-form
Display a button so the customer can add a product to their cart. Options will also be displayed depending on product type. e.g. quantity, variation.
- **Name:** woocommerce/add-to-cart-form
- **Category:** woocommerce-product-elements
- **Ancestor:**
- **Parent:**
- **Supports:**
- **Attributes:** isDescendentOfSingleProductBlock
## Product Average Rating (Beta) - woocommerce/product-average-rating ## Product Average Rating (Beta) - woocommerce/product-average-rating
Display the average rating of a product Display the average rating of a product
@ -26,6 +42,17 @@ Display a call to action button which either adds the product to the cart, or li
- **Supports:** align (full, wide), color (link, text, ~~background~~), interactivity, typography (fontSize, lineHeight), ~~html~~ - **Supports:** align (full, wide), color (link, text, ~~background~~), interactivity, typography (fontSize, lineHeight), ~~html~~
- **Attributes:** isDescendentOfQueryLoop, isDescendentOfSingleProductBlock, productId, textAlign, width - **Attributes:** isDescendentOfQueryLoop, isDescendentOfSingleProductBlock, productId, textAlign, width
## Product Image - woocommerce/product-image
Display the main product image.
- **Name:** woocommerce/product-image
- **Category:** woocommerce-product-elements
- **Ancestor:**
- **Parent:**
- **Supports:**
- **Attributes:** aspectRatio, height, imageSizing, isDescendentOfQueryLoop, isDescendentOfSingleProductBlock, productId, saleBadgeAlign, scale, showProductLink, showSaleBadge, width
## Product Details - woocommerce/product-details ## Product Details - woocommerce/product-details
Display a product's description, attributes, and reviews. Display a product's description, attributes, and reviews.
@ -35,7 +62,7 @@ Display a product's description, attributes, and reviews.
- **Ancestor:** - **Ancestor:**
- **Parent:** - **Parent:**
- **Supports:** align, spacing (margin) - **Supports:** align, spacing (margin)
- **Attributes:** - **Attributes:** hideTabTitle
## Product Image Gallery - woocommerce/product-image-gallery ## Product Image Gallery - woocommerce/product-image-gallery
@ -114,6 +141,17 @@ Display related products.
- **Supports:** align, ~~reusable~~ - **Supports:** align, ~~reusable~~
- **Attributes:** - **Attributes:**
## On-Sale Badge - woocommerce/product-sale-badge
Displays an on-sale badge if the product is on-sale.
- **Name:** woocommerce/product-sale-badge
- **Category:** woocommerce-product-elements
- **Ancestor:**
- **Parent:**
- **Supports:**
- **Attributes:** isDescendentOfQueryLoop, isDescendentOfSingleProductTemplate, productId
## Active Filters Controls - woocommerce/active-filters ## Active Filters Controls - woocommerce/active-filters
Display the currently active filters. Display the currently active filters.
@ -650,7 +688,7 @@ Renders classic WooCommerce shortcodes.
- **Category:** woocommerce - **Category:** woocommerce
- **Ancestor:** - **Ancestor:**
- **Parent:** - **Parent:**
- **Supports:** color (text, ~~background~~) - **Supports:** color (text, ~~background~~), ~~inserter~~
- **Attributes:** color, storeOnly - **Attributes:** color, storeOnly
## Customer account - woocommerce/customer-account ## Customer account - woocommerce/customer-account
@ -717,7 +755,7 @@ Display a button for shoppers to quickly view their cart.
- **Ancestor:** - **Ancestor:**
- **Parent:** - **Parent:**
- **Supports:** typography (fontSize), ~~html~~, ~~multiple~~ - **Supports:** typography (fontSize), ~~html~~, ~~multiple~~
- **Attributes:** addToCartBehaviour, cartAndCheckoutRenderStyle, hasHiddenPrice, iconColor, iconColorValue, isPreview, miniCartIcon, priceColor, priceColorValue, productCountColor, productCountColorValue - **Attributes:** addToCartBehaviour, cartAndCheckoutRenderStyle, hasHiddenPrice, iconColor, iconColorValue, isPreview, miniCartIcon, priceColor, priceColorValue, productCountColor, productCountColorValue, productCountVisibility
## Empty Mini-Cart view - woocommerce/empty-mini-cart-contents-block ## Empty Mini-Cart view - woocommerce/empty-mini-cart-contents-block
@ -1060,29 +1098,18 @@ The contents of this block will display when there are no products found.
- **Supports:** align, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Supports:** align, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~
- **Attributes:** - **Attributes:**
## Add to Cart with Options - woocommerce/add-to-cart-form
Display a button so the customer can add a product to their cart. Options will also be displayed depending on product type. e.g. quantity, variation.
- **Name:** woocommerce/add-to-cart-form
- **Category:** woocommerce-product-elements
- **Ancestor:**
- **Parent:**
- **Supports:** interactivity
- **Attributes:** isDescendentOfSingleProductBlock, quantitySelectorStyle
## Product Filter (Experimental) - woocommerce/product-filter ## Product Filter (Experimental) - woocommerce/product-filter
A block that adds product filters to the product collection. A block that adds product filters to the product collection.
- **Name:** woocommerce/product-filter - **Name:** woocommerce/product-filter
- **Category:** woocommerce - **Category:** woocommerce
- **Ancestor:** - **Ancestor:** woocommerce/product-filters
- **Parent:** - **Parent:**
- **Supports:** ~~html~~, ~~inserter~~, ~~reusable~~ - **Supports:** ~~html~~, ~~inserter~~, ~~reusable~~
- **Attributes:** attributeId, filterType, heading, isPreview - **Attributes:** attributeId, filterType, heading, isPreview
## Product Filter: Active Filters (Experimental) - woocommerce/product-filter-active ## Filter Options - woocommerce/product-filter-active
Display the currently active filters. Display the currently active filters.
@ -1093,7 +1120,7 @@ Display the currently active filters.
- **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~ - **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~
- **Attributes:** displayStyle - **Attributes:** displayStyle
## Product Filter: Attribute (Experimental) - woocommerce/product-filter-attribute ## Filter Options - woocommerce/product-filter-attribute
Enable customers to filter the product grid by selecting one or more attributes, such as color. Enable customers to filter the product grid by selecting one or more attributes, such as color.
@ -1101,8 +1128,8 @@ Enable customers to filter the product grid by selecting one or more attributes,
- **Category:** woocommerce - **Category:** woocommerce
- **Ancestor:** woocommerce/product-filter - **Ancestor:** woocommerce/product-filter
- **Parent:** - **Parent:**
- **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~ - **Supports:** color (text, ~~background~~), interactivity, spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~inserter~~
- **Attributes:** attributeId, displayStyle, isPreview, queryType, selectType, showCounts - **Attributes:** attributeId, clearButton, displayStyle, hideEmpty, isPreview, queryType, selectType, showCounts, sortOrder
## Clear (Experimental) - woocommerce/product-filter-clear-button ## Clear (Experimental) - woocommerce/product-filter-clear-button
@ -1115,7 +1142,7 @@ Allows shoppers to reset this filter.
- **Supports:** interactivity, ~~inserter~~ - **Supports:** interactivity, ~~inserter~~
- **Attributes:** - **Attributes:**
## Product Filter: Price (Experimental) - woocommerce/product-filter-price ## Filter Options - woocommerce/product-filter-price
Enable customers to filter the product collection by choosing a price range. Enable customers to filter the product collection by choosing a price range.
@ -1126,7 +1153,7 @@ Enable customers to filter the product collection by choosing a price range.
- **Supports:** interactivity, ~~inserter~~ - **Supports:** interactivity, ~~inserter~~
- **Attributes:** inlineInput, showInputFields - **Attributes:** inlineInput, showInputFields
## Product Filter: Rating (Experimental) - woocommerce/product-filter-rating ## Filter Options - woocommerce/product-filter-rating
Enable customers to filter the product collection by rating. Enable customers to filter the product collection by rating.
@ -1137,7 +1164,7 @@ Enable customers to filter the product collection by rating.
- **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~ - **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~
- **Attributes:** className, displayStyle, isPreview, selectType, showCounts - **Attributes:** className, displayStyle, isPreview, selectType, showCounts
## Product Filter: Stock Status (Experimental) - woocommerce/product-filter-stock-status ## Filter Options - woocommerce/product-filter-stock-status
Enable customers to filter the product collection by stock status. Enable customers to filter the product collection by stock status.
@ -1156,8 +1183,30 @@ Let shoppers filter products displayed on the page.
- **Category:** woocommerce - **Category:** woocommerce
- **Ancestor:** - **Ancestor:**
- **Parent:** - **Parent:**
- **Supports:** align, interactivity, ~~multiple~~ - **Supports:** align, color (background, text), interactivity, layout (allowJustification, allowOrientation, allowVerticalAlignment, default, ~~allowInheriting~~), spacing (blockGap), typography (fontSize, textAlign), ~~inserter~~, ~~multiple~~
- **Attributes:** - **Attributes:** overlay, overlayButtonStyle, overlayIcon, overlayIconSize
## Product Filters Overlay (Experimental) - woocommerce/product-filters-overlay
Display product filters in an overlay on top of a page.
- **Name:** woocommerce/product-filters-overlay
- **Category:** woocommerce
- **Ancestor:**
- **Parent:**
- **Supports:** align, color (background, text), dimensions (), layout (allowCustomContentAndWideSize), spacing (blockGap, padding), typography (), ~~inserter~~, ~~multiple~~
- **Attributes:** overlayPosition, overlayStyle, style
## Overlay Navigation (Experimental) - woocommerce/product-filters-overlay-navigation
Display overlay navigation controls.
- **Name:** woocommerce/product-filters-overlay-navigation
- **Category:** woocommerce
- **Ancestor:** woocommerce/product-filters-overlay,woocommerce/product-filters
- **Parent:**
- **Supports:** align (center, left, right), color (background, text), layout (default, ~~allowEditing~~), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~inserter~~
- **Attributes:** align, buttonStyle, iconSize, navigationStyle, overlayMode, style, triggerType
## Product Gallery (Beta) - woocommerce/product-gallery ## Product Gallery (Beta) - woocommerce/product-gallery

View File

@ -10,41 +10,41 @@ Please note that the actions and filters here run on the server side. The client
## Legacy Filters ## Legacy Filters
- [loop_shop_per_page](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#loop_shop_per_page) - [loop_shop_per_page](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#loop_shop_per_page)
- [wc_session_expiration](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#wc_session_expiration) - [wc_session_expiration](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#wc_session_expiration)
- [woocommerce_add_cart_item](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_cart_item) - [woocommerce_add_cart_item](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_cart_item)
- [woocommerce_add_cart_item_data](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_cart_item_data) - [woocommerce_add_cart_item_data](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_cart_item_data)
- [woocommerce_add_to_cart_quantity](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_quantity) - [woocommerce_add_to_cart_quantity](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_quantity)
- [woocommerce_add_to_cart_sold_individually_quantity](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_sold_individually_quantity) - [woocommerce_add_to_cart_sold_individually_quantity](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_sold_individually_quantity)
- [woocommerce_add_to_cart_validation](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_validation) - [woocommerce_add_to_cart_validation](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_validation)
- [woocommerce_adjust_non_base_location_prices](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_adjust_non_base_location_prices) - [woocommerce_adjust_non_base_location_prices](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_adjust_non_base_location_prices)
- [woocommerce_apply_base_tax_for_local_pickup](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_base_tax_for_local_pickup) - [woocommerce_apply_base_tax_for_local_pickup](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_base_tax_for_local_pickup)
- [woocommerce_apply_individual_use_coupon](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_individual_use_coupon) - [woocommerce_apply_individual_use_coupon](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_individual_use_coupon)
- [woocommerce_apply_with_individual_use_coupon](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_with_individual_use_coupon) - [woocommerce_apply_with_individual_use_coupon](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_with_individual_use_coupon)
- [woocommerce_cart_contents_changed](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_cart_contents_changed) - [woocommerce_cart_contents_changed](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_cart_contents_changed)
- [woocommerce_cart_item_permalink](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_cart_item_permalink) - [woocommerce_cart_item_permalink](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_cart_item_permalink)
- [woocommerce_get_item_data](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_get_item_data) - [woocommerce_get_item_data](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_get_item_data)
- [woocommerce_loop_add_to_cart_args](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_loop_add_to_cart_args) - [woocommerce_loop_add_to_cart_args](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_loop_add_to_cart_args)
- [woocommerce_loop_add_to_cart_link](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_loop_add_to_cart_link) - [woocommerce_loop_add_to_cart_link](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_loop_add_to_cart_link)
- [woocommerce_new_customer_data](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_new_customer_data) - [woocommerce_new_customer_data](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_new_customer_data)
- [woocommerce_pay_order_product_has_enough_stock](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_pay_order_product_has_enough_stock) - [woocommerce_pay_order_product_has_enough_stock](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_pay_order_product_has_enough_stock)
- [woocommerce_pay_order_product_in_stock](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_pay_order_product_in_stock) - [woocommerce_pay_order_product_in_stock](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_pay_order_product_in_stock)
- [woocommerce_registration_errors](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_registration_errors) - [woocommerce_registration_errors](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_registration_errors)
- [woocommerce_shipping_package_name](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_shipping_package_name) - [woocommerce_shipping_package_name](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_shipping_package_name)
- [woocommerce_show_page_title](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_show_page_title) - [woocommerce_show_page_title](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_show_page_title)
- [woocommerce_single_product_image_thumbnail_html](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_single_product_image_thumbnail_html) - [woocommerce_single_product_image_thumbnail_html](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_single_product_image_thumbnail_html)
## Legacy Actions ## Legacy Actions
- [woocommerce_add_to_cart](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_add_to_cart) - [woocommerce_add_to_cart](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_add_to_cart)
- [woocommerce_after_main_content](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_after_main_content) - [woocommerce_after_main_content](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_after_main_content)
- [woocommerce_after_shop_loop](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_after_shop_loop) - [woocommerce_after_shop_loop](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_after_shop_loop)
- [woocommerce_applied_coupon](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_applied_coupon) - [woocommerce_applied_coupon](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_applied_coupon)
- [woocommerce_archive_description](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_archive_description) - [woocommerce_archive_description](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_archive_description)
- [woocommerce_before_main_content](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_before_main_content) - [woocommerce_before_main_content](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_before_main_content)
- [woocommerce_before_shop_loop](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_before_shop_loop) - [woocommerce_before_shop_loop](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_before_shop_loop)
- [woocommerce_check_cart_items](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_check_cart_items) - [woocommerce_check_cart_items](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_check_cart_items)
- [woocommerce_created_customer](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_created_customer) - [woocommerce_created_customer](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_created_customer)
- [woocommerce_no_products_found](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_no_products_found) - [woocommerce_no_products_found](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_no_products_found)
- [woocommerce_register_post](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_register_post) - [woocommerce_register_post](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_register_post)
- [woocommerce_shop_loop](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_shop_loop) - [woocommerce_shop_loop](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_shop_loop)

View File

@ -23,7 +23,7 @@
"menu_title": "Theming for Woo Blocks", "menu_title": "Theming for Woo Blocks",
"tags": "reference", "tags": "reference",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/block-theme-development/theming-woo-blocks.md", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/block-theme-development/theming-woo-blocks.md",
"hash": "1ce763e8afcc7dfdd8c5eca4da799add21dfac48279c08fc7b614071edb67a7d", "hash": "cec80a34a38b7286be676a35624e2e441f5ccbb1aa318def6afe56a5a2bb6558",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/block-theme-development/theming-woo-blocks.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/block-theme-development/theming-woo-blocks.md",
"id": "90b16f4143d6db728d5ed6dce2ee2c60bdcfdbf6" "id": "90b16f4143d6db728d5ed6dce2ee2c60bdcfdbf6"
}, },
@ -70,6 +70,14 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/configuring-caching-plugins.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/configuring-caching-plugins.md",
"id": "9f484f8db1111fa6c1b6108d40939c967eea7f47" "id": "9f484f8db1111fa6c1b6108d40939c967eea7f47"
}, },
{
"post_title": "Blocks reference",
"menu_title": "Blocks Reference",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/block-references.md",
"hash": "a33fe5766283aaa70154077692a180319110e133ad430bf8dda3032455bad45c",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/block-references.md",
"id": "1fbe91d7fa4fafaf35f0297e4cee1e7958756aed"
},
{ {
"post_title": "How to add a custom field to simple and variable products", "post_title": "How to add a custom field to simple and variable products",
"menu_title": "Add Custom Fields to Products", "menu_title": "Add Custom Fields to Products",
@ -259,7 +267,7 @@
"menu_title": "Legacy Hooks", "menu_title": "Legacy Hooks",
"tags": "reference", "tags": "reference",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/hooks/migrated-hooks.md", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/hooks/migrated-hooks.md",
"hash": "025731fc8884e13e09ecc857bee7d669a091f11640e023e40c08ffc521f38964", "hash": "be4cf6862932c6696568b210e5f6ae4bd5313dacfb66724e23fab830baeb94e1",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/hooks/migrated-hooks.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/hooks/migrated-hooks.md",
"id": "5264fa45d393327b2c9cffb038c4d3670879d911" "id": "5264fa45d393327b2c9cffb038c4d3670879d911"
} }
@ -1119,7 +1127,7 @@
] ]
}, },
{ {
"content": "\nEnsuring the quality of your WooCommerce projects is essential. This section will delve into quality exoectations, best practices, coding standards, and other methodologies to ensure your projects stand out in terms of reliability, efficiency, user experience, and more. \n", "content": "\nEnsuring the quality of your WooCommerce projects is essential. This section will delve into quality expectations, best practices, coding standards, and other methodologies to ensure your projects stand out in terms of reliability, efficiency, user experience, and more. \n",
"category_slug": "quality-and-best-practices", "category_slug": "quality-and-best-practices",
"category_title": "Quality And Best Practices", "category_title": "Quality And Best Practices",
"posts": [ "posts": [
@ -1244,7 +1252,72 @@
"id": "b09a572b8a452b6cd795e0985daa85f06e5889fb" "id": "b09a572b8a452b6cd795e0985daa85f06e5889fb"
} }
], ],
"categories": [
{
"content": "\nReviews are an integral part of the online shopping experience, and people installing software pay attention to them. Prospective users of your extensions will likely consider average ratings when making software choices.\n\nMany of today's most popular online review platforms - from Yelp business reviews, to Amazon product reviews - have a range of opinion that can be polarized, with many extremely positive and/or negative reviews, and fewer moderate opinions. This creates a \"J-shaped\" distribution of reviews that isn't as accurate or as helpful as could be.\n\nWooCommerce.com and WordPress.org both feature reviews heavily, and competing extensions having a higher rating likely have the edge in user choice. \n\n## Primary considerations around reviews\n\nRequesting more reviews for a extension with major issues will not generate good reviews, and analyzing existing reviews will help surface areas to address before soliciting reviews.\n\nIt is extremely rare for users of WordPress plugins to leave reviews organically (.2% of users for WordPress.org leave reviews), which means that there's an untapped market of 99.8% of users of the average plugin.\n\nThese plugins are competing with other plugins on the same search terms in the WordPress.org plugin directory, and ratings are a large factor in the ranking algorithm. This is not usually a factor to the same extent on the WooCommerce Marketplace. For instance, WooCommerce's PayPal extension directly competes on all possible keywords with other PayPal extensions on the WordPress.org repository, while it does not compete with other PayPal payments extensions on the Marketplace.\n",
"category_slug": "review-guidelines",
"category_title": "Review Guidelines",
"posts": [
{
"post_title": "When to request WooCommerce extension reviews",
"menu_title": "When to request reviews",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/when-to-request-reviews.md",
"hash": "7a14ceb03ac0a98d84c0d380e44534129198f8ee682e84824cbb6c793da71be8",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/when-to-request-reviews.md",
"id": "d40eb30ff49c89545a5918f5b8c08b82e6f8f45a"
},
{
"post_title": "Utilizing your support team to respond to feedback",
"menu_title": "Utilizing your support team",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/utilizing-your-support-team.md",
"hash": "3cadfdd506c2c279c16d411b4037f6ea6cd2fa2dcfad6ffe895e89e61b0e54c0",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/utilizing-your-support-team.md",
"id": "f7029c8549d5ed86dd8f9cc27d7a62da539b9ba7"
},
{
"post_title": "Utilizing WooCommerce extension feature requests",
"menu_title": "Utilizing feature requests",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/utilizing-feature-requests.md",
"hash": "28af9f05e0c843a932ba1320d16097c029e71bdeae0dbe02ff95a96eb9d8c1c0",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/utilizing-feature-requests.md",
"id": "4efca02b0f14b13a1471f5beec4ef15365f98b17"
},
{
"post_title": "How to respond to negative WooCommerce extension reviews",
"menu_title": "Responding to negative reviews",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/responding-to-negative-reviews.md",
"hash": "306df15c394c6867657e7c68282f16dab5d08e821bbb7e27514abca764262b24",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/responding-to-negative-reviews.md",
"id": "7cb825a3830e392aeae853b441a91237f48c3ae5"
},
{
"post_title": "Notifying users about bug fixes and feature requests",
"menu_title": "Notifying users about bug fixes and feature requests",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/notifying-users-about-important-events.md",
"hash": "30b6241ebb32204910980a5dea2399a8fa2e02fa37ea22449edc549f3f14034b",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/notifying-users-about-important-events.md",
"id": "9ccc4bf579cc624002478fe8706241f4640d92b8"
},
{
"post_title": "Miscellaneous guidelines and advice",
"menu_title": "Miscellaneous guidelines",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/misc-guidelines.md",
"hash": "c5515db05195cebbe1ed0595dbf2a09623c3b85e1b5b6e1c3628a79b957f8884",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/misc-guidelines.md",
"id": "003ece0250c6a0c248019a095f75f3cfedbc290e"
},
{
"post_title": "How to request WooCommerce extension reviews",
"menu_title": "Requesting reviews",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/review-guidelines/how-to-request-reviews.md",
"hash": "dfdf5add075777636eb628d25484e93268251437dec0253766c12d80ac82573b",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/review-guidelines/how-to-request-reviews.md",
"id": "3d0c8bf7339a71198737d19eec7e6d71697b3727"
}
],
"categories": [] "categories": []
}
]
}, },
{ {
"content": "\nUnderstand Woo's reporting capabilities. Learn to generate, understand, and optimize reports to make informed decisions about your WooCommerce projects.\n", "content": "\nUnderstand Woo's reporting capabilities. Learn to generate, understand, and optimize reports to make informed decisions about your WooCommerce projects.\n",
@ -1731,5 +1804,5 @@
"categories": [] "categories": []
} }
], ],
"hash": "b6fab4eae1266824ee3e876c8fa5fd0342f59b4a0e5978f1460afc67d82e6d94" "hash": "0b0ae9b9ed454ab234a5f053f6efb37bafb3e90f1c98f6488263c019f552697b"
} }

View File

@ -4,4 +4,4 @@ category_slug: quality-and-best-practices
post_title: Quality and best practices post_title: Quality and best practices
--- ---
Ensuring the quality of your WooCommerce projects is essential. This section will delve into quality exoectations, best practices, coding standards, and other methodologies to ensure your projects stand out in terms of reliability, efficiency, user experience, and more. Ensuring the quality of your WooCommerce projects is essential. This section will delve into quality expectations, best practices, coding standards, and other methodologies to ensure your projects stand out in terms of reliability, efficiency, user experience, and more.

View File

@ -0,0 +1,19 @@
---
category_title: Review Guidelines
category_slug: review-guidelines
post_title: Review Guidelines
---
Reviews are an integral part of the online shopping experience, and people installing software pay attention to them. Prospective users of your extensions will likely consider average ratings when making software choices.
Many of today's most popular online review platforms - from Yelp business reviews, to Amazon product reviews - have a range of opinion that can be polarized, with many extremely positive and/or negative reviews, and fewer moderate opinions. This creates a "J-shaped" distribution of reviews that isn't as accurate or as helpful as could be.
WooCommerce.com and WordPress.org both feature reviews heavily, and competing extensions having a higher rating likely have the edge in user choice.
## Primary considerations around reviews
Requesting more reviews for a extension with major issues will not generate good reviews, and analyzing existing reviews will help surface areas to address before soliciting reviews.
It is extremely rare for users of WordPress plugins to leave reviews organically (.2% of users for WordPress.org leave reviews), which means that there's an untapped market of 99.8% of users of the average plugin.
These plugins are competing with other plugins on the same search terms in the WordPress.org plugin directory, and ratings are a large factor in the ranking algorithm. This is not usually a factor to the same extent on the WooCommerce Marketplace. For instance, WooCommerce's PayPal extension directly competes on all possible keywords with other PayPal extensions on the WordPress.org repository, while it does not compete with other PayPal payments extensions on the Marketplace.

View File

@ -0,0 +1,39 @@
---
post_title: How to request WooCommerce extension reviews
menu_title: Requesting reviews
---
## Methods of requesting reviews
### Admin notices
Admin notices are an industry standard method of requesting reviews, but bombarding the admin dashboard with admin notices is not effective. We recommend using restraint in the design of a notice, as well as limiting to a single notice at a time. It's very easy to overwhelm merchants with too many notices, or too intrusive notices.
#### Recommendations
* A good place for an admin notice to review an extension would be to show on the `Plugins` page and extension's settings pages.
* Include a snooze option (or multiple) on your notices with a clear expectation of when the notice will reappear.
* Admin notices should always be always be completely dismissable. They cannot only have a snooze option.
* The options presented in the notice must be phrased carefully to avoid manipulative language.
* Use consistently designed notices so the request for reviews feels like a part of your extension, and looks consistent with WooCommerce's design.
### Direct contact
#### Recommendations
* The most direct route to requesting reviews with the highest chance of being positive is to contact the customer when they are the happiest with the product.
* This can be milestone or time based, following the timing guidelines below.
* The best method for this is either an email or other direct exchange (support chat, call, etc.). This has the highest conversion rate, especially when timed properly so that the customer is happiest.
* This is also extremely effective when you are able to request feedback from specifically qualified merchants, such as merchants that have processed a certain amount with your platform, or who have shipped their first 100 orders using your fulfillment extension, or similar.
* Direct outreach is most likely to be successful if you have ways of targeting users for review requests (merchant account / usage info, etc.), as well as ways to gather the reviews, like sales or marketing teams able to email/call/chat with merchants.
## Messaging for requesting reviews
One method of requesting feedback that we recommend is using the NPS style of review solicitation. This can allow for an increase positive reviews as well as providing the opportunity to assist merchants that are struggling.
NPS-style reviews first ask the user how they rate the product (out of 5 stars), then route them based on their response:
* If they click 4 or 5 stars, ask them to leave a review.
* If they click 1, 2 or 3, tell them we're here to help & ask them to submit a support ticket.
Merchants are significantly more likely to leave a review after a positive support interaction with a support rep who explicitly asked for a review. The language "the best way to thank me is to leave a 5 star review that mentions me in it" or similar tends to work very well - people are more willing to help a person than a produc or company.

View File

@ -0,0 +1,14 @@
---
post_title: Miscellaneous guidelines and advice
menu_title: Miscellaneous guidelines
---
Contributors' names matching search terms directly will rank extremely highly on the WordPress.org plugin repo, which means that having a WordPress.org user named after your business (if that's a search term for your plugin) could tilt the scales over a competing plugin.
Constant nags and overwhelming the admin dashboard with unnecessary alerts detract from your user experience.
You can request to have reviews that are not actually feedback removed. Where this might be applicable is if the request is a simple support request, where it's obviously in the wrong spot. 1 star reviews, even if aggressive or angry, are not usually removed.
Reply to reviews! Thank the giver for offering feedback, acknowledge the issue if needed, ask for more specific feedback, or provide an update when that feedback is addressed! Your reviews are a great window into what your extension's users are actually thinking.
Having folks close to the extension's development (think developers and project managers) looking at reviews on a regular basis is a good way to ensure the customers voice is heard. 1 star reviews are among the most useful, as long as the issue is understood and addressed (if needed).

View File

@ -0,0 +1,18 @@
---
post_title: Notifying users about bug fixes and feature requests
menu_title: Notifying users about bug fixes and feature requests
---
A bug or a missing feature can be a showstopper for merchants. Bugs that pile up or popular features that are not implemented can lead to negative reviews and/or merchants churning and looking at a competitive solution.
Bugs are usually reported via support or GitHub, or you can discover them yourselves in testing.
When a critical bug is found, resolve it within a couple of days (most critical bugs should be resolved within 24 hours) and release a new plugin version promptly. Part of your release process should be to notify all stakeholders (the support team and the merchant affected) about the upcoming release.
Even though a critical bug is a great source of stress for merchants, a quick resolution makes merchants feel heard and supported - having a reliable business partner, who is keen to help in the most difficult situation, helps build a stronger relationship. Therefore, we usually ask merchants for a 5* review when we deliver a fast solution.
When you implement a new feature request and ship a new plugin version, you can follow a similar approach to bugs:
* Notify all stakeholders.
* Update the relevant request in the Feature Requests board, by sharing a public update and marking it as 'Completed'.
* For breaking releases: communicating with your marketing/relations teams to publish updates/newsletters before the release.

View File

@ -0,0 +1,33 @@
---
post_title: How to respond to negative WooCommerce extension reviews
menu_title: Responding to negative reviews
---
An unpleasant event in the merchant's journey can lead them to leave a public, negative review. These events usually are:
* a problem with the product,
* a missing product feature,
* an unhelpful reply,
* long wait times to receive a reply, or;
* combinations of the above.
When receiving a negative review, your goal should always be to turn this review around - this sounds tough, but it is really rewarding.
In the majority of cases, merchants who leave a negative review have first tried contacting support for help. This is useful knowledge, as we can read through the conversation history, understand the issue the merchant experienced and share more details with them when we reach out, even from our first reply.
The process we have seen work well is:
* Create a new response (via email, or on a public review) with subject: Regarding your recent review for xxx.
* Start by introducing yourself, for example: "Hey there, This is Andrew from the team that develops xxx".
* Use empathetic language and make it clear that this negative review had an impact on you. For example, "I read your recent review for xxx and I am worried to hear that an issue is preventing you from using this plugin as you had in mind. I'd be happy to help you resolve this!".
Compare the above sentence with: "I am sorry to hear that you experienced an issue with xxx". "I am sorry to" indicates that you are saddened by an event, but don't necessarily plan to do something about it. In comparison, in "I am worried to hear", worry indicates action. Additionally, "That you experienced an issue" can be interpreted as if the problem is mainly the merchant's fault, whereas language like "an issue is preventing you from using this plugin as you had in mind. I'd be happy to help you resolve this!" implies you and the merchant are on the same team.
* Share more details, a solution, an idea, a suggestion or an explanation.
* Urge the merchant to update the review, by highlighting how important reviews are for our team and for other merchants. Example language to do this is "We would appreciate it if you could take a couple of minutes to update your review and describe your experience with our product and support. Honest reviews are extremely helpful for other merchants who try to decide if a plugin is a right fit for them. Thank you for your contribution!".
* Include a direct link to the reviews section, so merchants can easily navigate there and change their review.
* On a follow-up communication, if the merchant has changed the review, consider saying something like: "I shared this with the rest of the team and it made everyone's day".
If the above things are true, sharing some of your procedures with merchants (highlighting how your team emphasizes and thrives on feedback) helps ensure merchants feel like you are part of their team and builds a strong relationship with them.
Even a merchant that doesn't change their review can offer a mutually beneficial discussion by learning more about their setup and offering some suggestions. These conversations help grow merchants' trust.

View File

@ -0,0 +1,31 @@
---
post_title: Utilizing WooCommerce extension feature requests
menu_title: Utilizing feature requests
---
It is important to keep track of all feature requests, and have some sort of system of record where anyone can see what kind of feedback the product is receiving over time.
We recommend a daily or bi-daily check-in, where you:
* triage new feature requests,
* celebrate positive reviews and;
* act upon negative reviews.
Carefully maintaining feature request boards (or similar system) is key, as the average board contains a lot of duplicate/spam content, requests about features that have been implemented and requests about features that will likely never be implemented. Poorly maintained boards make merchants feel unheard/neglected.
This results in more negative reviews on the premise that the product teams were not reading/listening to their feedback.
We've seen good results with the following procedures:
Starting with the most affected products, go through all open requests, reply to most/all of them and categorize them as:
* "Open", for requests that we still want more feedback,
* "Planned", for requests that we plan to implement,
* "Completed", for requests that have already been implemented and;
* "Closed", for requests that we do not plan to implement, as they are not a good fit for the product, for duplicate/spam requests and for requests that were actually support questions.
Replying to all "Open" requests is the goal, but if that's not attainable currently, make sure to reply to 100% of the requests that are closed.
For new open requests that arrive as a feature request, discuss/triage them, reply promptly, and assign a status to avoid having the board become unmanaged, and ensure merchants feel (and are) heard.
In addition to the effect a tidy board has on merchants, it also helps product teams better understand which requests are most wanted and most impactful and then plan work accordingly.

View File

@ -0,0 +1,18 @@
---
post_title: Utilizing your support team to respond to feedback
menu_title: Utilizing your support team
---
Your support team is usually the primary contact point of merchants when they contact you. Tickets and chats are the best tools we have to converse with merchants, understand pain-points about our software, listen to their feedback and analyze their feature requests. Collectively, support teams have a great understanding of the products and how people use them. This information is essential to be transferred over to product and engineering teams.
We recommend that you take the following steps to best utilize your support team:
* Create a strict internal SLA where support team requests are answered by product or engineering teams.
* Ensure you have a way for your support team to effectively report bugs to your product and engineering teams.
* When responding to your support team, avoid super-short answers, and try to explain the answer simply and concisely. This will allow the support agent to copy/paste your answer to the merchant.
* Avoid replying with statements like "no, this is not possible" or "no, this feature will not be implemented" without providing additional context about technical or product limitations.
* Regularly dedicate additional time to implement a short custom code snippets or to provide in-depth technical details about how a custom project would be implemented so that merchants can reach a solution faster if they decide to hire a dedicated WooCommerce developer. A small effort can go a long way to amaze merchants and reveal an opportunity to request a 5 star review.
* Keep support in the loop when they report a bug or request a new feature. When you release a new product version, we always consider the impact it can have on support.
* Work closely with your support team. For example, consider having a feedback hangout call every month where you can discuss product feedback and planned improvements.
With these kinds of practices in place, support teams are more willing to share feedback, issues, concerns, and questions with us. This helps maintain a closer relationship with merchants and identify pain-points early, before they become a reason for them to churn.

View File

@ -0,0 +1,24 @@
---
post_title: When to request WooCommerce extension reviews
menu_title: When to request reviews
---
The best approach to increasing our top-star reviews is to identify key moments in the merchant's journey, when they are more likely to leave a review and actively request for it.
The most distinct moments for most of our use cases are:
* When merchants feel helpless, lost, frustrated and we are able to help,
* When merchants find a bug in our code and we quickly ship a fix,
* When merchants need a feature and we notify them when it is shipped,
* When merchants feel alone and we make them feel heard and;
* When merchants contact with a question and we go out of our way to provide them with top-notch support, even if this means slightly stepping outside the official boundaries of a support policy.
Think about who is seeing the review request, and what they are doing at that time. Showing a request to a fulfillment worker just trying to ship an order isn't likely going to work well.
Outreach after a milestone works really well. Some language we've used before is "Congratulations on your xxth sale! We're delighted that WooPayments facilitated this milestone. Would you consider sharing your experience and encouraging others by reviewing our extension?".
Another way to optimally time a review request would be to setup a prompt that aligns with use patterns. For instance, if you know that most of your merchants use your extension daily, you would likely send a review request sooner than a extension that most merchants interact very sparingly with.
SaaS/Connector extensions need to be particularly careful about requesting ratings correctly, as they are the most likely to be overlooked unless there is an issue, leading to skewed ratings not representative of the actual extension.
Consider requesting feedback at the end of every single support interaction, especially in the WordPress.org support forums. One of the largest barriers to leaving a review is the requirement of a user being logged into WooCommerce.com (or WordPress.org), and the WordPress.org support forums present a good opportunity to gather these reviews. By being highly responsive in the public support forum and solving issues there, users are already logged in and able to immediately leave a review (after being requested to!).

View File

@ -1,4 +1,4 @@
Significance: patch Significance: patch
Type: update Type: update
CYS: Improve tracking survey Comment: Fix some comment typos.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Add optional onclick to Plugins component

View File

@ -19,6 +19,7 @@ type PluginsProps = {
response: InstallPluginsResponse response: InstallPluginsResponse
) => void; ) => void;
onError: ( errors: unknown, response: InstallPluginsResponse ) => void; onError: ( errors: unknown, response: InstallPluginsResponse ) => void;
onClick?: () => void;
onSkip?: () => void; onSkip?: () => void;
skipText?: string; skipText?: string;
autoInstall?: boolean; autoInstall?: boolean;
@ -37,6 +38,7 @@ export const Plugins = ( {
onAbort, onAbort,
onComplete, onComplete,
onError = () => null, onError = () => null,
onClick = () => null,
pluginSlugs = [ 'woocommerce-services' ], pluginSlugs = [ 'woocommerce-services' ],
onSkip, onSkip,
installText = __( 'Install & enable', 'woocommerce' ), installText = __( 'Install & enable', 'woocommerce' ),
@ -159,6 +161,7 @@ export const Plugins = ( {
} }
disabled={ isRequesting && hasBeenClicked } disabled={ isRequesting && hasBeenClicked }
onClick={ () => { onClick={ () => {
onClick();
setHasBeenClicked( true ); setHasBeenClicked( true );
installAndActivate(); installAndActivate();
} } } }

View File

@ -215,7 +215,7 @@ $muriel-box-shadow-8dp: 0 5px 5px -3px rgb(0 0 0 / 20%),
} }
// At the time of this comment, it was discovered that this component has // At the time of this comment, it was discovered that this component has
// the same classnames as the WP Components Checkbox, without it being a code depedency. // the same class names as the WP Components Checkbox, without it being a code dependency.
// This caused some visual breakages when changes happened in WP 6.6, and // This caused some visual breakages when changes happened in WP 6.6, and
// the rules have been copied over from WP Components styles of 6.5.1. // the rules have been copied over from WP Components styles of 6.5.1.
// https://github.com/WordPress/gutenberg/blob/403b4b8d014ef7f6edc15c822e455e109bf49c6d/packages/components/src/checkbox-control/style.scss#L4 // https://github.com/WordPress/gutenberg/blob/403b4b8d014ef7f6edc15c822e455e109bf49c6d/packages/components/src/checkbox-control/style.scss#L4

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix payment store selector type

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Add stripe tax status to task type

View File

@ -29,8 +29,10 @@ export type TaskType = {
badge?: string; badge?: string;
additionalData?: { additionalData?: {
woocommerceTaxCountries?: string[]; woocommerceTaxCountries?: string[];
stripeTaxCountries?: string[];
taxJarActivated?: boolean; taxJarActivated?: boolean;
avalaraActivated?: boolean; avalaraActivated?: boolean;
stripeTaxActivated?: boolean;
woocommerceTaxActivated?: boolean; woocommerceTaxActivated?: boolean;
woocommerceShippingActivated?: boolean; woocommerceShippingActivated?: boolean;
}; };

View File

@ -13,7 +13,7 @@ import * as resolvers from './resolvers';
import * as selectors from './selectors'; import * as selectors from './selectors';
import reducer from './reducer'; import reducer from './reducer';
import { STORE_KEY } from './constants'; import { STORE_KEY } from './constants';
import { WPDataActions } from '../types'; import { WPDataSelectors } from '../types';
import { PromiseifySelectors } from '../types/promiseify-selectors'; import { PromiseifySelectors } from '../types/promiseify-selectors';
export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY; export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY;
@ -33,7 +33,7 @@ declare module '@wordpress/data' {
): DispatchFromMap< typeof actions >; ): DispatchFromMap< typeof actions >;
function select( function select(
key: typeof STORE_KEY key: typeof STORE_KEY
): SelectFromMap< typeof selectors > & WPDataActions; ): SelectFromMap< typeof selectors > & WPDataSelectors;
function resolveSelect( function resolveSelect(
key: typeof STORE_KEY key: typeof STORE_KEY
): PromiseifySelectors< SelectFromMap< typeof selectors > >; ): PromiseifySelectors< SelectFromMap< typeof selectors > >;

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Fix comment typos across various files.

View File

@ -1052,7 +1052,7 @@ describe( 'getStoreTimeZoneMoment', () => {
expect( utcOffset ).not.toHaveBeenCalled(); expect( utcOffset ).not.toHaveBeenCalled();
} ); } );
it( 'should use the utc offest when it is set', () => { it( 'should use the utc offset when it is set', () => {
global.window.wcSettings = { global.window.wcSettings = {
timeZone: '+06:00', timeZone: '+06:00',
}; };

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Fix comment typos across various files.

View File

@ -8,7 +8,7 @@
"wordpress", "wordpress",
"woocommerce", "woocommerce",
"expression", "expression",
"evalution" "evaluation"
], ],
"engines": { "engines": {
"node": "^20.11.1", "node": "^20.11.1",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Fix comment typos across various files.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix typos in parse number tests

View File

@ -50,7 +50,7 @@ describe( 'numberFormat', () => {
} ); } );
describe( 'parseNumber', () => { describe( 'parseNumber', () => {
it( 'should remove thousand seperator before parsing number', () => { it( 'should remove thousand separator before parsing number', () => {
const config = { const config = {
decimalSeparator: ',', decimalSeparator: ',',
thousandSeparator: '.', thousandSeparator: '.',
@ -59,7 +59,7 @@ describe( 'parseNumber', () => {
expect( parseNumber( config, '12.345,679' ) ).toBe( '12345.679' ); expect( parseNumber( config, '12.345,679' ) ).toBe( '12345.679' );
} ); } );
it( 'supports empty string as the thousandSeperator', () => { it( 'supports empty string as the thousandSeparator', () => {
const config = { const config = {
decimalSeparator: ',', decimalSeparator: ',',
thousandSeparator: '', thousandSeparator: '',
@ -68,7 +68,7 @@ describe( 'parseNumber', () => {
expect( parseNumber( config, '12345,679' ) ).toBe( '12345.679' ); expect( parseNumber( config, '12345,679' ) ).toBe( '12345.679' );
} ); } );
it( 'supports empty string as the decimalSeperator', () => { it( 'supports empty string as the decimalSeparator', () => {
const config = { const config = {
decimalSeparator: '', decimalSeparator: '',
thousandSeparator: ',', thousandSeparator: ',',

View File

@ -1,4 +1,4 @@
Significance: patch Significance: patch
Type: update Type: update
CYS: Improve tracking survey Comment: Fix some comment typos.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new product data views component for use on new product data views page.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix typo in AligmentToolbarButton function name

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix typo in handleAdviceCardDissmiss function name

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix typo in noticeDismissed ref

View File

@ -1,4 +1,4 @@
Significance: minor Significance: minor
Type: tweak Type: tweak
Add GTIN in structured data Fix toogle typo in class names

View File

@ -71,6 +71,7 @@
"@wordpress/media-utils": "wp-6.0", "@wordpress/media-utils": "wp-6.0",
"@wordpress/plugins": "wp-6.0", "@wordpress/plugins": "wp-6.0",
"@wordpress/preferences": "wp-6.0", "@wordpress/preferences": "wp-6.0",
"@wordpress/private-apis": "^1.6.0",
"@wordpress/url": "wp-6.0", "@wordpress/url": "wp-6.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"dompurify": "^2.4.7", "dompurify": "^2.4.7",

View File

@ -259,7 +259,7 @@ export function LinkedProductListBlockEdit( {
setLinkedProductIds( newLinkedProducts ); setLinkedProductIds( newLinkedProducts );
} }
function handleAdviceCardDissmiss() { function handleAdviceCardDismiss() {
recordEvent( 'linked_products_placeholder_dismiss', { recordEvent( 'linked_products_placeholder_dismiss', {
source: TRACKS_SOURCE, source: TRACKS_SOURCE,
field: property, field: property,
@ -297,7 +297,7 @@ export function LinkedProductListBlockEdit( {
tip={ emptyState.tip } tip={ emptyState.tip }
dismissPreferenceId={ `woocommerce-product-${ property }-advice-card-dismissed` } dismissPreferenceId={ `woocommerce-product-${ property }-advice-card-dismissed` }
isDismissible={ emptyState.isDismissible } isDismissible={ emptyState.isDismissible }
onDismiss={ handleAdviceCardDissmiss } onDismiss={ handleAdviceCardDismiss }
> >
<EmptyStateImage { ...emptyState } /> <EmptyStateImage { ...emptyState } />
</AdviceCard> </AdviceCard>

View File

@ -18,7 +18,7 @@ import type {
TextAreaBlockEditAttributes, TextAreaBlockEditAttributes,
TextAreaBlockEditProps, TextAreaBlockEditProps,
} from './types'; } from './types';
import AligmentToolbarButton from './toolbar/toolbar-button-alignment'; import AlignmentToolbarButton from './toolbar/toolbar-button-alignment';
import { useClearSelectedBlockOnBlur } from '../../../hooks/use-clear-selected-block-on-blur'; import { useClearSelectedBlockOnBlur } from '../../../hooks/use-clear-selected-block-on-blur';
import useProductEntityProp from '../../../hooks/use-product-entity-prop'; import useProductEntityProp from '../../../hooks/use-product-entity-prop';
import { Label } from '../../../components/label/label'; import { Label } from '../../../components/label/label';
@ -100,7 +100,7 @@ export function TextAreaBlockEdit( {
<div { ...blockProps }> <div { ...blockProps }>
{ isRichTextMode && ( { isRichTextMode && (
<BlockControls { ...blockControlsBlockProps }> <BlockControls { ...blockControlsBlockProps }>
<AligmentToolbarButton <AlignmentToolbarButton
align={ align } align={ align }
setAlignment={ setAlignment } setAlignment={ setAlignment }
/> />

View File

@ -38,7 +38,7 @@ export const ALIGNMENT_CONTROLS = [
}, },
]; ];
export default function AligmentToolbarButton( { export default function AlignmentToolbarButton( {
align, align,
setAlignment, setAlignment,
}: AlignmentControl ) { }: AlignmentControl ) {

View File

@ -33,7 +33,7 @@ export function DownloadsMenu( {
icon={ isOpen ? chevronUp : chevronDown } icon={ isOpen ? chevronUp : chevronDown }
variant="secondary" variant="secondary"
onClick={ onToggle } onClick={ onToggle }
className="woocommerce-downloads-menu__toogle" className="woocommerce-downloads-menu__toggle"
> >
<span>{ __( 'Add new', 'woocommerce' ) }</span> <span>{ __( 'Add new', 'woocommerce' ) }</span>
</Button> </Button>

View File

@ -1,5 +1,5 @@
.woocommerce-downloads-menu { .woocommerce-downloads-menu {
&__toogle { &__toggle {
flex-direction: row-reverse; flex-direction: row-reverse;
> span { > span {

View File

@ -34,7 +34,7 @@ export function Edit( {
attributes, attributes,
context: { isInSelectedTab }, context: { isInSelectedTab },
}: ProductEditorBlockEditProps< VariationOptionsBlockAttributes > ) { }: ProductEditorBlockEditProps< VariationOptionsBlockAttributes > ) {
const noticeDimissed = useRef( false ); const noticeDismissed = useRef( false );
const { invalidateResolution } = useDispatch( const { invalidateResolution } = useDispatch(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
); );
@ -107,7 +107,7 @@ export function Edit( {
*/ */
if ( if (
totalCountWithoutPrice > 0 && totalCountWithoutPrice > 0 &&
! noticeDimissed.current && ! noticeDismissed.current &&
productStatus !== 'publish' && productStatus !== 'publish' &&
// New status. // New status.
newData?.status === 'publish' newData?.status === 'publish'
@ -198,7 +198,7 @@ export function Edit( {
ref={ variationTableRef as React.Ref< HTMLDivElement > } ref={ variationTableRef as React.Ref< HTMLDivElement > }
noticeText={ noticeText } noticeText={ noticeText }
onNoticeDismiss={ () => { onNoticeDismiss={ () => {
noticeDimissed.current = true; noticeDismissed.current = true;
updateUserPreferences( { updateUserPreferences( {
variable_items_without_price_notice_dismissed: { variable_items_without_price_notice_dismissed: {
...( itemsWithoutPriceNoticeDismissed || {} ), ...( itemsWithoutPriceNoticeDismissed || {} ),

View File

@ -26,7 +26,7 @@ import type { CustomFieldNameControlProps } from './types';
* the arbitrary value into an option so it can be selected as * the arbitrary value into an option so it can be selected as
* a valid value * a valid value
* *
* @param search The seraching criteria. * @param search The search criteria.
* @return The list of filtered custom field names as a Promise. * @return The list of filtered custom field names as a Promise.
*/ */
async function searchCustomFieldNames( search?: string ) { async function searchCustomFieldNames( search?: string ) {

View File

@ -162,7 +162,7 @@
gap: $gap-smaller; gap: $gap-smaller;
margin-right: $gap-smallest; margin-right: $gap-smallest;
.variations-actions-menu__toogle:disabled { .variations-actions-menu__toggle:disabled {
cursor: not-allowed; cursor: not-allowed;
} }

View File

@ -35,7 +35,7 @@ export function MultipleUpdateMenu( {
icon={ isOpen ? chevronUp : chevronDown } icon={ isOpen ? chevronUp : chevronDown }
variant="secondary" variant="secondary"
onClick={ onToggle } onClick={ onToggle }
className="variations-actions-menu__toogle" className="variations-actions-menu__toggle"
> >
<span>{ __( 'Quick update', 'woocommerce' ) }</span> <span>{ __( 'Quick update', 'woocommerce' ) }</span>
</Button> </Button>

View File

@ -1,5 +1,5 @@
.variations-actions-menu { .variations-actions-menu {
&__toogle { &__toggle {
flex-direction: row-reverse; flex-direction: row-reverse;
> span { > span {

View File

@ -35,6 +35,11 @@ export * from './contexts/validation-context/types';
export { EditorLoadingContext as __experimentalEditorLoadingContext } from './contexts/editor-loading-context'; export { EditorLoadingContext as __experimentalEditorLoadingContext } from './contexts/editor-loading-context';
export { PostTypeContext } from './contexts/post-type-context'; export { PostTypeContext } from './contexts/post-type-context';
/**
* Product data views page.
*/
export * from './products';
// Init the store // Init the store
registerProductEditorUiStore(); registerProductEditorUiStore();

View File

@ -0,0 +1,10 @@
/**
* External dependencies
*/
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
export const { lock, unlock } =
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
'@wordpress/edit-site'
);

View File

@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import {
UnsavedChangesWarning,
// @ts-expect-error No types for this exist yet.
privateApis as editorPrivateApis,
} from '@wordpress/editor';
/**
* Internal dependencies
*/
import { unlock } from '../lock-unlock';
const { RouterProvider } = unlock( routerPrivateApis );
const { GlobalStylesProvider } = unlock( editorPrivateApis );
function ProductsLayout() {
return <div>Initial Products Layout</div>;
}
export function ProductsApp() {
return (
<GlobalStylesProvider>
<UnsavedChangesWarning />
<RouterProvider>
<ProductsLayout />
</RouterProvider>
</GlobalStylesProvider>
);
}

View File

@ -0,0 +1,40 @@
@include wordpress-admin-schemes();
.woocommerce_page_woocommerce-products-dashboard #wpadminbar,
.woocommerce_page_woocommerce-products-dashboard #adminmenumain {
display: none;
}
.woocommerce_page_woocommerce-products-dashboard #wpcontent {
margin-left: 0;
}
body.woocommerce_page_woocommerce-products-dashboard
#woocommerce-products-dashboard {
@include wp-admin-reset("#woocommerce-products-dashboard");
@include reset;
display: block !important;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
min-height: 100vh;
}
body.js.is-fullscreen-mode {
@include break-medium {
// Reset the html.wp-topbar padding.
// Because this uses negative margins, we have to compensate for the height.
margin-top: -$admin-bar-height;
height: calc(100% + #{$admin-bar-height});
#adminmenumain,
#wpadminbar {
display: none;
}
#wpcontent,
#wpfooter {
margin-left: 0;
}
}
}

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
StrictMode,
Suspense,
createElement,
// @ts-expect-error createRoot is available.
createRoot,
lazy,
} from '@wordpress/element';
/**
* Internal dependencies
*/
import { getGutenbergVersion } from './utils/get-gutenberg-version';
const ProductsApp = lazy( () =>
import( './products-app' ).then( ( module ) => ( {
default: module.ProductsApp,
} ) )
);
/**
* Initializes the "Products Dashboard".
*
* @param {string} id DOM element id.
*/
export function initializeProductsDashboard( id: string ) {
const target = document.getElementById( id );
const root = createRoot( target );
const isGutenbergEnabled = getGutenbergVersion() > 0;
root.render(
<StrictMode>
{ isGutenbergEnabled ? (
<Suspense fallback={ null }>
<ProductsApp />
</Suspense>
) : (
<div>
{ __(
'Please enabled Gutenberg for this feature',
'woocommerce'
) }
</div>
) }
</StrictMode>
);
return root;
}

View File

@ -60,3 +60,5 @@
/* Hooks */ /* Hooks */
@import "hooks/use-draggable/styles.scss"; @import "hooks/use-draggable/styles.scss";
@import "products.scss";

View File

@ -25,9 +25,21 @@ declare module '@wordpress/core-data' {
name: string, name: string,
id: number | string, id: number | string,
options?: { enabled: boolean } options?: { enabled: boolean }
): { record: T, editedRecord: T, isResolving: boolean, hasResolved: boolean }; ): {
record: T;
editedRecord: T;
isResolving: boolean;
hasResolved: boolean;
};
} }
declare module '@wordpress/keyboard-shortcuts' { declare module '@wordpress/keyboard-shortcuts' {
function useShortcut(name: string, callback: (event: KeyboardEvent) => void): void; function useShortcut(
name: string,
callback: ( event: KeyboardEvent ) => void
): void;
const store; const store;
} }
declare module '@wordpress/router' {
const privateApis;
}

View File

@ -2,6 +2,12 @@
A remote logging package for Automattic based projects. This package provides error tracking and logging capabilities, with support for rate limiting, stack trace formatting, and customizable error filtering. A remote logging package for Automattic based projects. This package provides error tracking and logging capabilities, with support for rate limiting, stack trace formatting, and customizable error filtering.
## Installation
```bash
npm install @woocommerce/remote-logging --save
```
## Description ## Description
The WooCommerce Remote Logging package offers the following features: The WooCommerce Remote Logging package offers the following features:
@ -44,16 +50,42 @@ The WooCommerce Remote Logging package offers the following features:
} }
``` ```
## Remote Logging Conditions
Remote logging is subject to the following conditions:
1. **Remote Logging Enabled**: The package checks `window.wcSettings.isRemoteLoggingEnabled` to determine if the feature should be enabled. The value is set via PHP and passed to JS as a boolean. It requires tracks to be enabled and a few other conditions internally. Please see the [RemoteLogger.php](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php) for more details.
2. **Non-Development Environment**: It also checks `process.env.NODE_ENV` to ensure logging only occurs in non-development environments.
If either of these conditions are not met (Tracks is not enabled or the environment is development), no logs will be transmitted to the remote server.
## API Reference
- `init(config: RemoteLoggerConfig): void`: Initializes the remote logger with the given configuration.
- `log(severity: LogSeverity, message: string, extraData?: object): Promise<boolean>`: Logs a message with the specified severity and optional extra data.
- `captureException(error: Error, extraData?: object): void`: Captures an error and sends it to the remote API.
For more detailed information about types and interfaces, refer to the source code and inline documentation.
## Customization ## Customization
You can customize the behavior of the remote logger using WordPress filters:
- `woocommerce_remote_logging_should_send_error`: Control whether an error should be sent to the remote API. You can customize the behavior of the remote logger using WordPress filters. Here are the available filters:
- `woocommerce_remote_logging_error_data`: Modify the error data before sending it to the remote API.
- `woocommerce_remote_logging_log_endpoint`: Customize the endpoint URL for sending log messages.
- `woocommerce_remote_logging_js_error_endpoint`: Customize the endpoint URL for sending JavaScript errors.
### Example ### `woocommerce_remote_logging_should_send_error`
Control whether an error should be sent to the remote API.
**Parameters:**
- `shouldSend` (boolean): The default decision on whether to send the error.
- `error` (Error): The error object.
- `stackFrames` (Array): An array of stack frames from the error.
**Return value:** (boolean) Whether the error should be sent.
**Usage example:**
```js ```js
import { addFilter } from '@wordpress/hooks'; import { addFilter } from '@wordpress/hooks';
@ -63,19 +95,80 @@ addFilter(
'my-plugin', 'my-plugin',
(shouldSend, error, stackFrames) => { (shouldSend, error, stackFrames) => {
const containsPluginFrame = stackFrames.some( const containsPluginFrame = stackFrames.some(
( frame ) => (frame) => frame.url && frame.url.includes( /YOUR_PLUGIN_ASSET_PATH/ )
frame.url && frame.url.includes( '/my-plugin/' );
); );
// Custom logic to determine if the error should be sent // Only send errors that originate from our plugin
return shouldSend && containsPluginFrame; return shouldSend && containsPluginFrame;
} }
); );
``` ```
### API Reference ### `woocommerce_remote_logging_error_data`
- `init(config: RemoteLoggerConfig): void`: Initializes the remote logger with the given configuration. Modify the error data before sending it to the remote API.
- `log(severity: LogSeverity, message: string, extraData?: object): Promise<boolean>`: Logs a message with the specified severity and optional extra data.
- `captureException(error: Error, extraData?: object): void`: Captures an error and sends it to the remote API.
For more detailed information about types and interfaces, refer to the source code and inline documentation. **Parameters:**
- `errorData` (ErrorData): The error data object to be sent.
**Return value:** (ErrorData) The modified error data object.
**Usage example:**
```js
import { addFilter } from '@wordpress/hooks';
addFilter(
'woocommerce_remote_logging_error_data',
'my-plugin',
(errorData) => {
// Custom logic to modify error data
errorData.tags = [ ...errorData.tags, 'my-plugin' ];
return errorData;
}
);
```
### `woocommerce_remote_logging_log_endpoint`
Modify the URL of the remote logging API endpoint.
**Parameters:**
- `endpoint` (string): The default endpoint URL.
**Return value:** (string) The modified endpoint URL.
**Usage example:**
```js
import { addFilter } from '@wordpress/hooks';
addFilter(
'woocommerce_remote_logging_log_endpoint',
'my-plugin',
(endpoint) => 'https://my-custom-endpoint.com/log'
);
```
### `woocommerce_remote_logging_js_error_endpoint`
Modify the URL of the remote logging API endpoint for JavaScript errors.
**Parameters:**
- `endpoint` (string): The default endpoint URL for JavaScript errors.
**Return value:** (string) The modified endpoint URL for JavaScript errors.
**Usage example:**
```js
import { addFilter } from '@wordpress/hooks';
addFilter(
'woocommerce_remote_logging_js_error_endpoint',
'my-plugin',
(endpoint) => 'https://my-custom-endpoint.com/js-error-log'
);
```

View File

@ -1,4 +1,4 @@
Significance: patch Significance: patch
Type: update Type: update
Migrate LYS user meta Comment: Fix some comment typos.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update README.md to document the filters specs & usage

View File

@ -292,7 +292,7 @@ export class RemoteLogger {
.map( this.getFormattedFrame ) .map( this.getFormattedFrame )
.join( '\n\n' ); .join( '\n\n' );
// Set hard limit of 8192 characters for the stack trace so it does not use too much user bandwith and also our computation. // Set hard limit of 8192 characters for the stack trace so it does not use too much user bandwidth and also our computation.
return trace.length > 8192 ? trace.substring( 0, 8192 ) : trace; return trace.length > 8192 ? trace.substring( 0, 8192 ) : trace;
} }

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Fix some comment typos.

View File

@ -22,7 +22,7 @@ const GROUP_PREFIX = 'x_woocommerce-';
* @param {Record<string, string> | string} group - The group of stats or a single stat name. * @param {Record<string, string> | string} group - The group of stats or a single stat name.
* @param {string} [name] - The name of the stat if group is a string. * @param {string} [name] - The name of the stat if group is a string.
* *
* @return {URLSearchParams} The constructed querys. * @return {URLSearchParams} The constructed query.
*/ */
function buildQueryParams( function buildQueryParams(
group: Record< string, string > | string, group: Record< string, string > | string,

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Fix comment typos across various files.

View File

@ -1,5 +1,5 @@
/** /**
* Helper function to select a checkbox if it exists within a element * Helper function to select a checkbox if it exists within an element
* *
* @param element - The DOM element to check for a checkbox * @param element - The DOM element to check for a checkbox
*/ */

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Comment: Fix comment typos across various files.

View File

@ -134,7 +134,7 @@ describe( 'ReportSummary', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
} ); } );
test( 'should display SummaryListPlaceholder when isRequesting is true', () => { test( 'should display SummaryListPlaceholder when summaryData.isRequesting is true', () => {
const { container } = renderChart( 'number', null, null, false, true ); const { container } = renderChart( 'number', null, null, false, true );
expect( expect(

View File

@ -69,7 +69,6 @@ class CategoriesReport extends Component {
<ReportSummary <ReportSummary
charts={ charts } charts={ charts }
endpoint="products" endpoint="products"
isRequesting={ isRequesting }
limitProperties={ limitProperties={
isSingleCategoryView isSingleCategoryView
? [ 'products', 'categories' ] ? [ 'products', 'categories' ]

View File

@ -57,7 +57,6 @@ class CouponsReport extends Component {
<ReportSummary <ReportSummary
charts={ charts } charts={ charts }
endpoint="coupons" endpoint="coupons"
isRequesting={ isRequesting }
query={ chartQuery } query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) } selectedChart={ getSelectedChart( query.chart, charts ) }
filters={ filters } filters={ filters }

View File

@ -167,10 +167,10 @@ export const advancedFilters = applyFilters(
placeholder: __( 'Search order number', 'woocommerce' ), placeholder: __( 'Search order number', 'woocommerce' ),
remove: __( 'Remove order number filter', 'woocommerce' ), remove: __( 'Remove order number filter', 'woocommerce' ),
rule: __( rule: __(
'Select a order number filter match', 'Select an order number filter match',
'woocommerce' 'woocommerce'
), ),
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */ /* translators: A sentence describing an order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( title: __(
'<title>Order #</title> <rule/> <filter/>', '<title>Order #</title> <rule/> <filter/>',
'woocommerce' 'woocommerce'
@ -210,7 +210,7 @@ export const advancedFilters = applyFilters(
'Select an IP address filter match', 'Select an IP address filter match',
'woocommerce' 'woocommerce'
), ),
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */ /* translators: A sentence describing an order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( title: __(
'<title>IP Address</title> <rule/> <filter/>', '<title>IP Address</title> <rule/> <filter/>',
'woocommerce' 'woocommerce'

View File

@ -82,7 +82,6 @@ class ProductsReport extends Component {
mode={ mode } mode={ mode }
charts={ charts } charts={ charts }
endpoint="products" endpoint="products"
isRequesting={ isRequesting }
query={ chartQuery } query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) } selectedChart={ getSelectedChart( query.chart, charts ) }
filters={ filters } filters={ filters }

View File

@ -52,7 +52,6 @@ class TaxesReport extends Component {
<ReportSummary <ReportSummary
charts={ charts } charts={ charts }
endpoint="taxes" endpoint="taxes"
isRequesting={ isRequesting }
query={ chartQuery } query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) } selectedChart={ getSelectedChart( query.chart, charts ) }
filters={ filters } filters={ filters }

View File

@ -59,7 +59,6 @@ const VariationsReport = ( props ) => {
mode={ mode } mode={ mode }
charts={ charts } charts={ charts }
endpoint="variations" endpoint="variations"
isRequesting={ isRequesting }
query={ chartQuery } query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) } selectedChart={ getSelectedChart( query.chart, charts ) }
filters={ filters } filters={ filters }

View File

@ -441,7 +441,7 @@ export const useAddAutoBlockPreviewEventListenersAndObservers = (
unsubscribeCallbacks.push( removeEventListenerHidePopover ); unsubscribeCallbacks.push( removeEventListenerHidePopover );
} }
// Add event listner to the button which will insert a default pattern // Add event listener to the button which will insert a default pattern
// when there are no patterns inserted in the block preview. // when there are no patterns inserted in the block preview.
const removePatternButtonClickListener = addPatternButtonClickListener( const removePatternButtonClickListener = addPatternButtonClickListener(
documentElement, documentElement,

View File

@ -49,7 +49,7 @@ export const OptInSubscribe = () => {
await apiFetch< { await apiFetch< {
success: boolean; success: boolean;
} >( { } >( {
path: `/wc/private/patterns`, path: '/wc-admin/patterns',
method: 'POST', method: 'POST',
} ); } );
}; };

View File

@ -75,7 +75,7 @@
} }
} }
div.block-editor-block-patterns-list > div:nth-last-child(2) { div.block-editor-block-patterns-list > div:nth-last-child(1 of .block-editor-block-patterns-list__list-item) {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -264,7 +264,7 @@ export const SidebarNavigationScreenHomepagePTK = ( {
<Button <Button
onClick={ async () => { onClick={ async () => {
await apiFetch( { await apiFetch( {
path: `/wc/private/patterns`, path: `/wc-admin/patterns`,
method: 'POST', method: 'POST',
} ); } );

View File

@ -168,7 +168,7 @@ export const installPatterns = async () => {
const { success } = await apiFetch< { const { success } = await apiFetch< {
success: boolean; success: boolean;
} >( { } >( {
path: '/wc/private/patterns', path: '/wc-admin/patterns',
method: 'POST', method: 'POST',
} ); } );

View File

@ -106,7 +106,7 @@ const fetchIsFontLibraryAvailable = async () => {
const fetchIsPTKPatternsAPIAvailable = async () => { const fetchIsPTKPatternsAPIAvailable = async () => {
try { try {
await apiFetch( { await apiFetch( {
path: '/wc/private/patterns', path: '/wc-admin/patterns',
method: 'GET', method: 'GET',
} ); } );

View File

@ -203,7 +203,7 @@ describe( 'inbox_note_view event', () => {
notesHaveResolved: true, notesHaveResolved: true,
isBatchUpdating: false, isBatchUpdating: false,
} ) ); } ) );
// The original InboxNotecard has a VisibilityDetector so I prefered to mock it and always call onNoteVisible // The original InboxNotecard has a VisibilityDetector so I preferred to mock it and always call onNoteVisible
InboxNoteCard.mockImplementation( ( { onNoteVisible, note } ) => { InboxNoteCard.mockImplementation( ( { onNoteVisible, note } ) => {
useEffect( () => onNoteVisible( note ), [] ); useEffect( () => onNoteVisible( note ), [] );
return <div>{ note.id }</div>; return <div>{ note.id }</div>;

View File

@ -24,7 +24,7 @@ type UseRegisteredChannels = {
}; };
/** /**
* A object that maps the product listings status in * An object that maps the product listings status in
* plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php backend * plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php backend
* to SyncStatusType frontend. * to SyncStatusType frontend.
*/ */

View File

@ -21,15 +21,11 @@ export const PaymentRecommendations: React.FC< EmbeddedBodyProps > = ( {
tab, tab,
section, section,
} ) => { } ) => {
if ( page === 'wc-settings' && tab === 'checkout' && ! section ) {
if ( if (
window?.wcAdminFeatures?.[ page === 'wc-settings' &&
'reactify-classic-payments-settings' tab === 'checkout' &&
] === true ( ! section || section === 'main' )
) { ) {
return null;
}
return ( return (
<RecommendationsEligibilityWrapper> <RecommendationsEligibilityWrapper>
<Suspense fallback={ null }> <Suspense fallback={ null }>

View File

@ -205,6 +205,10 @@ const PaymentRecommendations: React.FC = () => {
}; };
} ); } );
if ( pluginsList.length === 0 ) {
return null;
}
return ( return (
<Card size="medium" className="woocommerce-recommended-payments-card"> <Card size="medium" className="woocommerce-recommended-payments-card">
<CardHeader> <CardHeader>

View File

@ -0,0 +1,155 @@
/**
* External dependencies
*/
import React, { useMemo } from '@wordpress/element';
import { Button } from '@wordpress/components';
import ExternalIcon from 'gridicons/dist/external';
import { __, _x } from '@wordpress/i18n';
import {
ONBOARDING_STORE_NAME,
PAYMENT_GATEWAYS_STORE_NAME,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { getCountryCode } from '~/dashboard/utils';
import {
getEnrichedPaymentGateways,
getIsGatewayWCPay,
getIsWCPayOrOtherCategoryDoneSetup,
getSplitGateways,
} from '~/task-lists/fills/PaymentGatewaySuggestions/utils';
type PaymentGateway = {
id: string;
image_72x72: string;
title: string;
enabled: boolean;
needsSetup: boolean;
// Add other properties as needed...
};
const usePaymentGatewayData = () => {
return useSelect( ( select ) => {
const { getSettings } = select( SETTINGS_STORE_NAME );
const { general: settings = {} } = getSettings( 'general' );
return {
getPaymentGateway: select( PAYMENT_GATEWAYS_STORE_NAME )
.getPaymentGateway,
installedPaymentGateways: select(
PAYMENT_GATEWAYS_STORE_NAME
).getPaymentGateways(),
isResolving: select( ONBOARDING_STORE_NAME ).isResolving(
'getPaymentGatewaySuggestions'
),
paymentGatewaySuggestions: select(
ONBOARDING_STORE_NAME
).getPaymentGatewaySuggestions(),
countryCode: getCountryCode( settings.woocommerce_default_country ),
};
}, [] );
};
const AdditionalGatewayImages = ( {
additionalGateways,
}: {
additionalGateways: PaymentGateway[];
} ) => (
<>
{ additionalGateways.map( ( gateway ) => (
<img
key={ gateway.id }
src={ gateway.image_72x72 }
alt={ gateway.title }
width="24"
height="24"
className="other-payment-methods__image"
/>
) ) }
{ _x( '& more.', 'More payment providers to discover', 'woocommerce' ) }
</>
);
export const OtherPaymentMethods = () => {
const {
paymentGatewaySuggestions,
installedPaymentGateways,
isResolving,
countryCode,
} = usePaymentGatewayData();
const paymentGateways = useMemo(
() =>
getEnrichedPaymentGateways(
installedPaymentGateways,
paymentGatewaySuggestions
),
[ installedPaymentGateways, paymentGatewaySuggestions ]
);
const isWCPayOrOtherCategoryDoneSetup = useMemo(
() =>
getIsWCPayOrOtherCategoryDoneSetup( paymentGateways, countryCode ),
[ countryCode, paymentGateways ]
);
const isWCPaySupported = Array.from( paymentGateways.values() ).some(
getIsGatewayWCPay
);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const [ wcPayGateway, _offlineGateways, additionalGateways ] = useMemo(
() =>
getSplitGateways(
paymentGateways,
countryCode ?? '',
isWCPaySupported,
isWCPayOrOtherCategoryDoneSetup
),
[
paymentGateways,
countryCode,
isWCPaySupported,
isWCPayOrOtherCategoryDoneSetup,
]
);
if ( isResolving || ! wcPayGateway ) {
return null;
}
const hasWcPaySetup = wcPayGateway.enabled && ! wcPayGateway.needsSetup;
return (
<>
<Button
className="is-tertiary"
href="https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations"
target="_blank"
value="tertiary"
rel="noreferrer"
>
<span className="other-payment-methods__button-text">
{ hasWcPaySetup
? __(
'Discover additional payment providers',
'woocommerce'
)
: __(
'Discover other payment providers',
'woocommerce'
) }
</span>
<ExternalIcon size={ 18 } />
</Button>
{ additionalGateways.length > 0 && (
<AdditionalGatewayImages
additionalGateways={ additionalGateways }
/>
) }
</>
);
};

View File

@ -0,0 +1,183 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { PaymentGateway } from '@woocommerce/data';
import { WooPaymentMethodsLogos } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import { getAdminSetting } from '~/utils/admin-settings';
import sanitizeHTML from '~/lib/sanitize-html';
import { WCPayInstallButton } from './wcpay-install-button';
export const PaymentMethod = ( {
id,
enabled,
title,
method_title,
method_description,
settings_url,
}: PaymentGateway ) => {
const isWooPayEligible = getAdminSetting( 'isWooPayEligible', false );
const [ isEnabled, setIsEnabled ] = useState( enabled );
const [ isLoading, setIsLoading ] = useState( false );
const toggleEnabled = async ( e: React.MouseEvent ) => {
e.preventDefault();
setIsLoading( true );
if ( ! window.woocommerce_admin.nonces?.gateway_toggle ) {
// eslint-disable-next-line no-console
console.warn( 'Unexpected error: Nonce not found' );
// Redirect to payment setting page if nonce is not found. Users should still be able to toggle the payment method from that page.
window.location.href = settings_url;
return;
}
try {
const response = await fetch( window.woocommerce_admin.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams( {
action: 'woocommerce_toggle_gateway_enabled',
security: window.woocommerce_admin.nonces?.gateway_toggle,
gateway_id: id,
} ),
} );
const result = await response.json();
if ( result.success ) {
if ( result.data === true ) {
setIsEnabled( true );
} else if ( result.data === false ) {
setIsEnabled( false );
} else if ( result.data === 'needs_setup' ) {
window.location.href = settings_url;
}
} else {
window.location.href = settings_url;
}
} catch ( error ) {
// eslint-disable-next-line no-console
console.error( 'Error toggling gateway:', error );
} finally {
setIsLoading( false );
}
};
return (
<tr data-gateway_id={ id }>
<td className="sort ui-sortable-handle" width="1%"></td>
<td className="name" width="">
<div className="wc-payment-gateway-method__name">
<a
href={ settings_url }
className="wc-payment-gateway-method-title"
>
{ method_title }
</a>
{ id !== 'pre_install_woocommerce_payments_promotion' &&
method_title !== title && (
<span className="wc-payment-gateway-method-name">
&nbsp;&nbsp;
{ title }
</span>
) }
{ id === 'pre_install_woocommerce_payments_promotion' && (
<div className="pre-install-payment-gateway__subtitle">
<WooPaymentMethodsLogos
isWooPayEligible={ isWooPayEligible }
maxElements={ 5 }
/>
</div>
) }
</div>
</td>
<td className="status" width="1%">
<a
className="wc-payment-gateway-method-toggle-enabled"
href={ settings_url }
onClick={ toggleEnabled }
>
<span
className={ `woocommerce-input-toggle ${
isEnabled
? 'woocommerce-input-toggle--enabled'
: 'woocommerce-input-toggle--disabled'
} ${
isLoading ? 'woocommerce-input-toggle--loading' : ''
}` }
/* translators: %s: payment method title */
aria-label={
isEnabled
? sprintf(
/* translators: %s: payment method title */
__(
'The "%s" payment method is currently enabled',
'woocommerce'
),
method_title
)
: sprintf(
/* translators: %s: payment method title */
__(
'The "%s" payment method is currently disabled',
'woocommerce'
),
method_title
)
}
>
{ isEnabled
? __( 'Yes', 'woocommerce' )
: __( 'No', 'woocommerce' ) }
</span>
</a>
</td>
<td
className="description"
width=""
dangerouslySetInnerHTML={ sanitizeHTML( method_description ) }
/>
<td className="action" width="1%">
{ id === 'pre_install_woocommerce_payments_promotion' ? (
<WCPayInstallButton />
) : (
<a
className="button alignright"
aria-label={
enabled
? sprintf(
/* translators: %s: payment method title */
__(
'Manage the "%s" payment method',
'woocommerce'
),
method_title
)
: sprintf(
/* translators: %s: payment method title */
__(
'Set up the "%s" payment method',
'woocommerce'
),
method_title
)
}
href={ settings_url }
>
{ enabled
? __( 'Manage', 'woocommerce' )
: __( 'Finish setup', 'woocommerce' ) }
</a>
) }
</td>
</tr>
);
};

View File

@ -0,0 +1,60 @@
/**
* External dependencies
*/
import React, { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
PAYMENT_GATEWAYS_STORE_NAME,
PLUGINS_STORE_NAME,
} from '@woocommerce/data';
import { Button } from '@wordpress/components';
import { resolveSelect, useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
const slug = 'woocommerce-payments';
export const WCPayInstallButton = () => {
const [ installing, setInstalling ] = useState( false );
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { createNotice } = useDispatch( 'core/notices' );
const redirectToSettings = async () => {
const paymentGateway = await resolveSelect(
PAYMENT_GATEWAYS_STORE_NAME
).getPaymentGateway( slug.replace( /-/g, '_' ) );
if ( paymentGateway?.settings_url ) {
window.location.href = paymentGateway.settings_url;
}
};
const installWooCommercePayments = async () => {
if ( installing ) return;
setInstalling( true );
recordEvent( 'settings_payments_recommendations_setup', {
extension_selected: slug,
} );
try {
await installAndActivatePlugins( [ slug ] );
redirectToSettings();
} catch ( error ) {
if ( error instanceof Error ) {
createNotice( 'error', error.message );
}
setInstalling( false );
}
};
return (
<Button
className="button alignright"
onClick={ installWooCommercePayments }
variant="secondary"
isBusy={ installing }
aria-disabled={ installing }
>
{ __( 'Install', 'woocommerce' ) }
</Button>
);
};

View File

@ -1,9 +1,29 @@
@import "~/wp-admin-scripts/payment-method-promotions/payment-promotion-row.scss";
.settings-payments-main__container { .settings-payments-main__container {
h1 { .settings-payments-main__spinner {
color: #fff; display: flex;
} justify-content: center;
background: #000; align-items: center;
text-align: center; position: absolute;
padding: 50px 0;
width: 100%; width: 100%;
} }
table.wc_gateways {
.other-payment-methods__button-text {
margin-right: 4px;
}
td.other-payment-methods-row {
border-top: 1px solid #c3c4c7;
background-color: #fff;
}
.other-payment-methods__image {
vertical-align: middle;
margin-right: 8px;
}
}
}

View File

@ -1,17 +1,108 @@
/** /**
* External dependencies * External dependencies
*/ */
import '@wordpress/element'; import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { PaymentGateway } from '@woocommerce/data';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './settings-payments-main.scss'; import './settings-payments-main.scss';
import { PaymentMethod } from './components/payment-method';
import { OtherPaymentMethods } from './components/other-payment-methods';
import { PaymentsBannerWrapper } from '~/payments/payment-settings-banner';
export const SettingsPaymentsMain: React.FC = () => { export const SettingsPaymentsMain: React.FC = () => {
const [ paymentGateways, error ] = useMemo( () => {
const script = document.getElementById(
'experimental_wc_settings_payments_gateways'
);
try {
if ( script && script.textContent ) {
return [
JSON.parse( script.textContent ) as PaymentGateway[],
null,
];
}
throw new Error( 'Could not find payment gateways data' );
} catch ( e ) {
return [ [], e as Error ];
}
}, [] );
if ( error ) {
// This is a temporary error message to be replaced by error boundary.
return (
<div>
<h1>
{ __( 'Error loading payment gateways', 'woocommerce' ) }
</h1>
<p>{ error.message }</p>
</div>
);
}
return ( return (
<div className="settings-payments-main__container"> <div className="settings-payments-main__container">
<h1>Main payments screen</h1> <div id="wc_payments_settings_slotfill">
<PaymentsBannerWrapper />
</div>
<table className="form-table">
<tbody>
<tr>
<td
className="wc_payment_gateways_wrapper"
colSpan={ 2 }
>
<table
className="wc_gateways widefat"
cellSpacing="0"
aria-describedby="payment_gateways_options-description"
>
<thead>
<tr>
<th className="sort"></th>
<th className="name">
{ __( 'Method', 'woocommerce' ) }
</th>
<th className="status">
{ __( 'Enabled', 'woocommerce' ) }
</th>
<th className="description">
{ __(
'Description',
'woocommerce'
) }
</th>
<th className="action"></th>
</tr>
</thead>
<tbody className="ui-sortable">
{ paymentGateways.map(
( gateway: PaymentGateway ) => (
<PaymentMethod
key={ gateway.id }
{ ...gateway }
/>
)
) }
<tr>
<td
className="other-payment-methods-row"
colSpan={ 5 }
>
<OtherPaymentMethods />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div> </div>
); );
}; };

View File

@ -1,73 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { getAdminLink } from '@woocommerce/settings';
import interpolateComponents from '@automattic/interpolate-components';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { PartnerCard } from '../components/partner-card';
import { TaxChildProps } from '../utils';
import logo from './logo.png';
export const Card: React.FC< TaxChildProps > = ( { task } ) => {
const { additionalData: { avalaraActivated } = {} } = task;
return (
<PartnerCard
name={ __( 'Avalara', 'woocommerce' ) }
logo={ logo }
description={ __( 'Powerful all-in-one tax tool', 'woocommerce' ) }
benefits={ [
__( 'Real-time sales tax calculation', 'woocommerce' ),
interpolateComponents( {
mixedString: __(
'{{strong}}Multi{{/strong}}-economic nexus compliance',
'woocommerce'
),
components: {
strong: <strong />,
},
} ),
__(
'Cross-border and multi-channel compliance',
'woocommerce'
),
__( 'Automate filing & remittance', 'woocommerce' ),
__(
'Return-ready, jurisdiction-level reporting.',
'woocommerce'
),
] }
terms={ '' }
actionText={
avalaraActivated
? __( 'Continue setup', 'woocommerce' )
: __( 'Download', 'woocommerce' )
}
onClick={ () => {
recordEvent( 'tasklist_tax_select_option', {
selected_option: 'avalara',
} );
if ( avalaraActivated ) {
window.location.href = getAdminLink(
'/admin.php?page=wc-settings&tab=tax&section=avatax'
);
return;
}
window.open(
new URL(
'https://woocommerce.com/products/woocommerce-avatax/'
).toString(),
'_blank'
);
} }
/>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -41,7 +41,7 @@
.woocommerce-tax-partner-card__terms { .woocommerce-tax-partner-card__terms {
color: $gray-600; color: $gray-600;
font-size: 9px; font-size: 12px;
margin-bottom: $gap-smaller; margin-bottom: $gap-smaller;
} }

View File

@ -15,7 +15,8 @@ export const PartnerCard: React.FC< {
description: string; description: string;
benefits: ( string | JSX.Element )[]; benefits: ( string | JSX.Element )[];
terms: string | JSX.Element; terms: string | JSX.Element;
actionText: string; children?: React.ReactNode;
actionText?: string;
onClick: () => void; onClick: () => void;
isBusy?: boolean; isBusy?: boolean;
} > = ( { } > = ( {
@ -27,6 +28,7 @@ export const PartnerCard: React.FC< {
actionText, actionText,
onClick, onClick,
isBusy, isBusy,
children,
} ) => { } ) => {
return ( return (
<div className="woocommerce-tax-partner-card"> <div className="woocommerce-tax-partner-card">
@ -59,6 +61,9 @@ export const PartnerCard: React.FC< {
<div className="woocommerce-tax-partner-card__terms"> <div className="woocommerce-tax-partner-card__terms">
{ terms } { terms }
</div> </div>
{ children ? (
children
) : (
<Button <Button
isSecondary isSecondary
onClick={ onClick } onClick={ onClick }
@ -67,6 +72,7 @@ export const PartnerCard: React.FC< {
> >
{ actionText } { actionText }
</Button> </Button>
) }
</div> </div>
</div> </div>
); );

View File

@ -17,6 +17,7 @@ import {
useEffect, useEffect,
useState, useState,
createElement, createElement,
useMemo,
} from '@wordpress/element'; } from '@wordpress/element';
import { WooOnboardingTask } from '@woocommerce/onboarding'; import { WooOnboardingTask } from '@woocommerce/onboarding';
@ -25,6 +26,7 @@ import { WooOnboardingTask } from '@woocommerce/onboarding';
*/ */
import { redirectToTaxSettings } from './utils'; import { redirectToTaxSettings } from './utils';
import { Card as WooCommerceTaxCard } from './woocommerce-tax/card'; import { Card as WooCommerceTaxCard } from './woocommerce-tax/card';
import { Card as StripeTaxCard } from './stripe-tax/card';
import { createNoticesFromResponse } from '../../../lib/notices'; import { createNoticesFromResponse } from '../../../lib/notices';
import { getCountryCode } from '~/dashboard/utils'; import { getCountryCode } from '~/dashboard/utils';
import { ManualConfiguration } from './manual-configuration'; import { ManualConfiguration } from './manual-configuration';
@ -150,20 +152,21 @@ export const Tax: React.FC< TaxProps > = ( { onComplete, query, task } ) => {
} ); } );
}, [ updateOptions ] ); }, [ updateOptions ] );
const getVisiblePartners = () => { const partners = useMemo( () => {
const countryCode = const countryCode =
getCountryCode( generalSettings?.woocommerce_default_country ) || getCountryCode( generalSettings?.woocommerce_default_country ) ||
''; '';
const { const {
additionalData: { additionalData: {
woocommerceTaxCountries = [], woocommerceTaxCountries = [],
stripeTaxCountries = [],
taxJarActivated, taxJarActivated,
woocommerceTaxActivated, woocommerceTaxActivated,
woocommerceShippingActivated, woocommerceShippingActivated,
} = {}, } = {},
} = task; } = task;
const partners = [ const allPartners = [
{ {
id: 'woocommerce-tax', id: 'woocommerce-tax',
card: WooCommerceTaxCard, card: WooCommerceTaxCard,
@ -174,31 +177,35 @@ export const Tax: React.FC< TaxProps > = ( { onComplete, query, task } ) => {
! woocommerceShippingActivated && ! woocommerceShippingActivated &&
woocommerceTaxCountries.includes( countryCode ), woocommerceTaxCountries.includes( countryCode ),
}, },
{
id: 'stripe-tax',
card: StripeTaxCard,
isVisible: stripeTaxCountries.includes( countryCode ),
},
]; ];
return partners.filter( ( partner ) => partner.isVisible ); return allPartners.filter( ( partner ) => partner.isVisible );
}; // eslint-disable-next-line react-hooks/exhaustive-deps -- the partner list shouldn't be changing in the middle of interaction. for some reason the country is becoming null in a re-render and causing unexpected behaviour
}, [] );
const partners = getVisiblePartners();
useEffect( () => {
const { auto } = query; const { auto } = query;
useEffect( () => {
if ( auto === 'true' ) { if ( auto === 'true' ) {
onAutomate(); onAutomate();
return;
} }
}, [ auto, onAutomate ] );
useEffect( () => {
if ( query.partner ) { if ( query.partner ) {
return; return;
} }
recordEvent( 'tasklist_tax_view_options', { recordEvent( 'tasklist_tax_view_options', {
options: partners.map( ( partner ) => partner.id ), options: partners.map( ( partner ) => partner.id ),
} ); } );
}, [ onAutomate, partners, query ] ); }, [ partners, query.partner ] );
const getCurrentPartner = () => { const currentPartner = useMemo( () => {
if ( ! query.partner ) { if ( ! query.partner ) {
return null; return null;
} }
@ -206,7 +213,7 @@ export const Tax: React.FC< TaxProps > = ( { onComplete, query, task } ) => {
return ( return (
partners.find( ( partner ) => partner.id === query.partner ) || null partners.find( ( partner ) => partner.id === query.partner ) || null
); );
}; }, [ partners, query.partner ] );
const childProps = { const childProps = {
isPending, isPending,
@ -220,8 +227,6 @@ export const Tax: React.FC< TaxProps > = ( { onComplete, query, task } ) => {
return <Spinner />; return <Spinner />;
} }
const currentPartner = getCurrentPartner();
if ( ! partners.length ) { if ( ! partners.length ) {
return ( return (
<TaskCard> <TaskCard>

View File

@ -0,0 +1,107 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
import { Plugins } from '@woocommerce/components';
import { dispatch, useDispatch } from '@wordpress/data';
import { SETTINGS_STORE_NAME } from '@woocommerce/data';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import { PartnerCard } from '../components/partner-card';
import { TaxChildProps } from '../utils';
import StripeTaxLogo from './stripe-tax-logo.svg';
import { createNoticesFromResponse } from '~/lib/notices';
const STRIPE_TAX_PLUGIN_SLUG = 'stripe-tax-for-woocommerce';
const redirectToStripeTaxSettings = () => {
window.location.href = getAdminLink(
'/admin.php?page=wc-settings&tab=stripe_tax_for_woocommerce'
);
};
export const Card: React.FC< TaxChildProps > = ( {
task: {
additionalData: { stripeTaxActivated } = {
stripeTaxActivated: false,
},
},
} ) => {
const { createSuccessNotice } = useDispatch( 'core/notices' );
return (
<PartnerCard
name={ __( 'Stripe Tax', 'woocommerce' ) }
logo={ StripeTaxLogo }
description={ __( 'Powerful global tax tool', 'woocommerce' ) }
benefits={ [
__( 'Real-time sales tax calculation', 'woocommerce' ),
__( 'Multi-economic nexus compliance', 'woocommerce' ),
__( 'Detailed tax transaction reports', 'woocommerce' ),
__( 'Coverage in over 55 countries', 'woocommerce' ),
] }
terms={ __(
'Free to install, then pay as you go.',
'woocommerce'
) }
onClick={ () => {} }
>
{ stripeTaxActivated ? (
<Button
variant="secondary"
onClick={ () => {
recordEvent(
'tasklist_tax_setup_stripe_tax_to_settings'
);
redirectToStripeTaxSettings();
} }
>
{ __( 'Continue to setttings', 'woocommerce' ) }
</Button>
) : (
<Plugins
installText={ __( 'Install for free', 'woocommerce' ) }
onClick={ () => {
recordEvent( 'tasklist_tax_select_option', {
selected_option: STRIPE_TAX_PLUGIN_SLUG,
} );
} }
onComplete={ () => {
recordEvent( 'tasklist_tax_install_plugin_success', {
selected_option: STRIPE_TAX_PLUGIN_SLUG,
} );
const { updateAndPersistSettingsForGroup } =
dispatch( SETTINGS_STORE_NAME );
updateAndPersistSettingsForGroup( 'general', {
general: {
woocommerce_calc_taxes: 'yes', // Stripe tax requires tax calculation to be enabled so let's do it here to save the user from doing it manually
},
} ).then( () => {
createSuccessNotice(
__(
"Stripe Tax for Woocommerce has been successfully installed. Let's configure it now.",
'woocommerce'
)
);
redirectToStripeTaxSettings();
} );
} }
onError={ ( errors, response ) => {
recordEvent( 'tasklist_tax_install_plugin_error', {
selected_option: STRIPE_TAX_PLUGIN_SLUG,
errors,
} );
createNoticesFromResponse( response );
} }
installButtonVariant="secondary"
pluginSlugs={ [ STRIPE_TAX_PLUGIN_SLUG ] }
/>
) }
</PartnerCard>
);
};

View File

@ -0,0 +1 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 468 222.5" style="enable-background:new 0 0 468 222.5" xml:space="preserve"><style>.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#0a2540}</style><path class="st0" d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -88,6 +88,12 @@ declare global {
getUserSetting?: ( name: string ) => string | undefined; getUserSetting?: ( name: string ) => string | undefined;
setUserSetting?: ( name: string, value: string ) => void; setUserSetting?: ( name: string, value: string ) => void;
deleteUserSetting?: ( name: string ) => void; deleteUserSetting?: ( name: string ) => void;
woocommerce_admin: {
ajax_url: string;
nonces: {
gateway_toggle?: string;
}
}
} }
} }

View File

@ -59,4 +59,15 @@ class Fake_WCPayments extends WC_Payment_Gateway_WCPay {
public function is_available() { public function is_available() {
return true; return true;
} }
/**
* Checks if the account has not completed onboarding due to users abandoning the process half way.
* Also used by WC Core to complete the task "Set up WooPayments".
* Called directly by WooCommerce Core.
*
* @return bool
*/
public function is_account_partially_onboarded(): bool {
return false;
}
} }

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update Fake_WCPayments is_account_partially_onboarded to return false

View File

@ -104,22 +104,20 @@ const { state } = store< Store >( 'woocommerce/product-button', {
}, },
get addToCartText(): string { get addToCartText(): string {
const context = getContext(); const context = getContext();
const inTheCartText = state.inTheCartText || '';
// We use the temporary number of items when there's no animation, or the // We use the temporary number of items when there's no animation, or the
// second part of the animation hasn't started. // second part of the animation hasn't started.
if ( const showTemporaryNumber =
context.animationStatus === AnimationStatus.IDLE || context.animationStatus === AnimationStatus.IDLE ||
context.animationStatus === AnimationStatus.SLIDE_OUT context.animationStatus === AnimationStatus.SLIDE_OUT;
) { const numberOfItems = showTemporaryNumber
? context.temporaryNumberOfItems
: state.numberOfItemsInTheCart;
return getButtonText( return getButtonText(
context.addToCartText, context.addToCartText,
state.inTheCartText!, inTheCartText,
context.temporaryNumberOfItems numberOfItems
);
}
return getButtonText(
context.addToCartText,
state.inTheCartText!,
state.numberOfItemsInTheCart
); );
}, },
get displayViewCart(): boolean { get displayViewCart(): boolean {

View File

@ -1,3 +1,4 @@
/* eslint-disable @wordpress/no-unsafe-wp-apis */
/** /**
* External dependencies * External dependencies
*/ */

View File

@ -1,55 +0,0 @@
/**
* External dependencies
*/
import type { BlockAttributes } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { ImageSizing } from './types';
export const blockAttributes: BlockAttributes = {
showProductLink: {
type: 'boolean',
default: true,
},
showSaleBadge: {
type: 'boolean',
default: true,
},
saleBadgeAlign: {
type: 'string',
default: 'right',
},
imageSizing: {
type: 'string',
default: ImageSizing.SINGLE,
},
productId: {
type: 'number',
default: 0,
},
isDescendentOfQueryLoop: {
type: 'boolean',
default: false,
},
isDescendentOfSingleProductBlock: {
type: 'boolean',
default: false,
},
width: {
type: 'string',
},
height: {
type: 'string',
},
scale: {
type: 'string',
default: 'cover',
},
aspectRatio: {
type: 'string',
},
};
export default blockAttributes;

View File

@ -0,0 +1,28 @@
{
"name": "woocommerce/product-image",
"version": "1.0.0",
"title": "Product Image",
"description": "Display the main product image.",
"category": "woocommerce-product-elements",
"attributes": {
"showProductLink": { "type": "boolean", "default": true },
"showSaleBadge": { "type": "boolean", "default": true },
"saleBadgeAlign": { "type": "string", "default": "right" },
"imageSizing": { "type": "string", "default": "single" },
"productId": { "type": "number", "default": 0 },
"isDescendentOfQueryLoop": { "type": "boolean", "default": false },
"isDescendentOfSingleProductBlock": {
"type": "boolean",
"default": false
},
"width": { "type": "string" },
"height": { "type": "string" },
"scale": { "type": "string", "default": "cover" },
"aspectRatio": { "type": "string" }
},
"usesContext": [ "query", "queryId", "postId" ],
"keywords": [ "WooCommerce" ],
"textdomain": "woocommerce",
"apiVersion": 3,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

Some files were not shown because too many files have changed in this diff Show More