Merge branch 'trunk' into dev/fix-k6-hpos-env-var-parsing

This commit is contained in:
Rodel Calasagsag 2023-10-26 00:33:25 +08:00
commit a4108b3b67
149 changed files with 8732 additions and 731 deletions

View File

@ -11,3 +11,4 @@ Various code snippets you can add to your site to enable custom functionality:
- [Change number of related products output](./number-of-products-per-row.md)
- [Rename a country](./rename-a-country.md)
- [Unhook and remove WooCommerce emails](./unhook--remove-woocommerce-emails.md)
- [Useful Functions](./useful-functions.md)

View File

@ -0,0 +1,398 @@
# Customizing checkout fields using actions and filters
If you are unfamiliar with code and resolving potential conflicts, we have an extension that can help: [WooCommerce Checkout Field Editor](https://woocommerce.com/products/woocommerce-checkout-field-editor/). Installing and activating this extension overrides any code below that you try to implement; and you cannot have custom checkout field code in your functions.php file when the extension is activated.
Custom code should be copied into your child themes **functions.php** file.
## How Are Checkout Fields Loaded to WooCommerce?
The billing and shipping fields for checkout pull from the countries class `class-wc-countries.php` and the **`get_address_fields`** function. This allows WooCommerce to enable/disable fields based on the users location.
Before returning these fields, WooCommerce puts the fields through a *filter*. This allows them to be edited by third-party plugins, themes and your own custom code.
Billing:
```php
$address_fields = apply_filters( 'woocommerce_billing_fields', $address_fields );
```
Shipping:
```php
$address_fields = apply_filters( 'woocommerce_shipping_fields', $address_fields );
```
The checkout class adds the loaded fields to its `checkout_fields` array, as well as adding a few other fields like “order notes”.
```php
$this->checkout_fields['billing'] = $woocommerce->countries->get_address_fields( $this->get_value( 'billing_country' ), 'billing_' );
$this->checkout_fields['shipping'] = $woocommerce->countries->get_address_fields( $this->get_value( 'shipping_country' ), 'shipping_' );
$this->checkout_fields['account'] = array(
'account_username' => array(
'type' => 'text',
'label' => __( 'Account username', 'woocommerce' ),
'placeholder' => _x( 'Username', 'placeholder', 'woocommerce' ),
),
'account_password' => array(
'type' => 'password',
'label' => __( 'Account password', 'woocommerce' ),
'placeholder' => _x( 'Password', 'placeholder', 'woocommerce' ),
'class' => array( 'form-row-first' )
),
'account_password-2' => array(
'type' => 'password',
'label' => __( 'Account password', 'woocommerce' ),
'placeholder' => _x( 'Password', 'placeholder', 'woocommerce' ),
'class' => array( 'form-row-last' ),
'label_class' => array( 'hidden' )
),
);
$this->checkout_fields['order'] = array(
'order_comments' => array(
'type' => 'textarea',
'class' => array( 'notes' ),
'label' => __( 'Order Notes', 'woocommerce' ),
'placeholder' => _x( 'Notes about your order, e.g. special notes for delivery.', 'placeholder', 'woocommerce' )
)
);
```
This array is also passed through a filter:
```php
$this->checkout_fields = apply_filters( 'woocommerce_checkout_fields', $this->checkout_fields );
```
That means you have **full control** over checkout fields you only need to know how to access them.
## Overriding Core Fields
Hooking into the  **`woocommerce_checkout_fields`** filter lets you override any field. As an example, lets change the placeholder on the order_comments fields. Currently, its set to:
```php
_x( 'Notes about your order, e.g. special notes for delivery.', 'placeholder', 'woocommerce' );
```
We can change this by adding a function to our theme functions.php file:
```php
// Hook in
add_filter( 'woocommerce_checkout_fields' , 'custom_override_checkout_fields' );
// Our hooked in function - $fields is passed via the filter!
function custom_override_checkout_fields( $fields ) {
$fields['order']['order_comments']['placeholder'] = 'My new placeholder';
return $fields;
}
```
You can override other parts, such as labels:
```php
// Hook in
add_filter( 'woocommerce_checkout_fields' , 'custom_override_checkout_fields' );
// Our hooked in function - $fields is passed via the filter!
function custom_override_checkout_fields( $fields ) {
$fields['order']['order_comments']['placeholder'] = 'My new placeholder';
$fields['order']['order_comments']['label'] = 'My new label';
return $fields;
}
```
Or remove fields:
```php
// Hook in
add_filter( 'woocommerce_checkout_fields' , 'custom_override_checkout_fields' );
// Our hooked in function - $fields is passed via the filter!
function custom_override_checkout_fields( $fields ) {
unset( $fields['order']['order_comments'] );
return $fields;
}
```
Heres a full list of fields in the array passed to `woocommerce_checkout_fields`:
- Billing
- `billing_first_name`
- `billing_last_name`
- `billing_company`
- `billing_address_1`
- `billing_address_2`
- `billing_city`
- `billing_postcode`
- `billing_country`
- `billing_state`
- `billing_email`
- `billing_phone`
- Shipping
- `shipping_first_name`
- `shipping_last_name`
- `shipping_company`
- `shipping_address_1`
- `shipping_address_2`
- `shipping_city`
- `shipping_postcode`
- `shipping_country`
- `shipping_state`
- Account
- `account_username`
- `account_password`
- `account_password-2`
- Order
- `order_comments`
Each field contains an array of properties:
- `type` type of field (text, textarea, password, select)
- `label` label for the input field
- `placeholder` placeholder for the input
- `class` class for the input
- `required` true or false, whether or not the field is require
- `clear` true or false, applies a clear fix to the field/label
- `label_class` class for the label element
- `options` for select boxes, array of options (key => value pairs)
In specific cases you need to use the **`woocommerce_default_address_fields`** filter. This filter is applied to all billing and shipping default fields:
- `country`
- `first_name`
- `last_name`
- `company`
- `address_1`
- `address_2`
- `city`
- `state`
- `postcode`
For example, to make the `address_1` field optional:
```php
// Hook in
add_filter( 'woocommerce_default_address_fields' , 'custom_override_default_address_fields' );
// Our hooked in function - $address_fields is passed via the filter!
function custom_override_default_address_fields( $address_fields ) {
$address_fields['address_1']['required'] = false;
return $address_fields;
}
```
### Defining select options
If you are adding a field with type select, as stated above you would define key/value pairs. For example:
```php
$fields['billing']['your_field']['options'] = array(
'option_1' => 'Option 1 text',
'option_2' => 'Option 2 text'
);
```
## Priority
Priority in regards to PHP code helps establish when a bit of code — called a function — runs in relation to a page load. It is set inside of each function and is useful when overriding existing code for custom display.
Code with a higher number set as the priority will run after code with a lower number, meaning code with a priority of 20 will run after code with 10 priority.
The priority argument is set during the [add_action](https://developer.wordpress.org/reference/functions/add_action/) function, after you establish which hook youre connecting to and what the name of your custom function will be.
In the example below, blue text is the name of the hook were modifying, green text is the name of our custom function, and red is the priority we set.
![Setting priority for the hooked function](https://woocommerce.com/wp-content/uploads/2012/04/priority-markup.png)
## Examples
### Change Return to Shop button redirect URL
In this example, the code is set to redirect the “Return to Shop” button found in the cart to a category that lists products for sale at `http://example.url/category/specials/`.
```php
/**
* Changes the redirect URL for the Return To Shop button in the cart.
*/
function wc_empty_cart_redirect_url() {
return 'http://example.url/category/specials/';
}
add_filter( 'woocommerce_return_to_shop_redirect', 'wc_empty_cart_redirect_url', 10 );
```
There, we can see the priority is set to 10. This is the typical default for WooCommerce functions and scripts, so that may not be sufficient to override that buttons functionality.
Instead, we can change the priority to any number greater than 10. While 11 would work, best practice dictates we use increments of ten, so 20, 30, and so on.
```php
/**
* Changes the redirect URL for the Return To Shop button in the cart.
*/
function wc_empty_cart_redirect_url() {
return 'http://example.com/category/specials/';
}
add_filter( 'woocommerce_return_to_shop_redirect', 'wc_empty_cart_redirect_url', 20 );
```
With priority, we can have two functions that are acting on the same hook. Normally this would cause a variety of problems, but since weve established one has a higher priority than the other, our site will only load the appropriate function, and we will be taken to the Specials page as intended with the code below.
```php
/**
* Changes the redirect URL for the Return To Shop button in the cart.
* BECAUSE THIS FUNCTION HAS THE PRIORITY OF 20, IT WILL RUN AFTER THE FUNCTION BELOW (HIGHER NUMBERS RUN LATER)
*/
function wc_empty_cart_redirect_url() {
return 'http://example.com/category/specials/';
}
add_filter( 'woocommerce_return_to_shop_redirect', 'wc_empty_cart_redirect_url', 20 );
/**
* Changes the redirect URL for the Return To Shop button in the cart.
* EVEN THOUGH THIS FUNCTION WOULD NORMALLY RUN LATER BECAUSE IT'S CODED AFTERWARDS, THE 10 PRIORITY IS LOWER THAN 20 ABOVE
*/
function wc_empty_cart_redirect_url() {
return 'http://example.com/shop/';
}
add_filter( 'woocommerce_return_to_shop_redirect', 'wc_empty_cart_redirect_url', 10 );
```
### Adding Custom Shipping And Billing Fields
Adding fields is done in a similar way to overriding fields. For example, lets add a new field to shipping fields `shipping_phone`:
```php
// Hook in
add_filter( 'woocommerce_checkout_fields' , 'custom_override_checkout_fields' );
// Our hooked in function - $fields is passed via the filter!
function custom_override_checkout_fields( $fields ) {
$fields['shipping']['shipping_phone'] = array(
'label' => __( 'Phone', 'woocommerce' ),
'placeholder' => _x( 'Phone', 'placeholder', 'woocommerce' ),
'required' => false,
'class' => array( 'form-row-wide' ),
'clear' => true
);
return $fields;
}
/**
* Display field value on the order edit page
*/
add_action( 'woocommerce_admin_order_data_after_shipping_address', 'my_custom_checkout_field_display_admin_order_meta', 10, 1 );
function my_custom_checkout_field_display_admin_order_meta($order){
echo '<p><strong>'. esc_html__( 'Phone From Checkout Form' ) . ':</strong> ' . esc_html( get_post_meta( $order->get_id(), '_shipping_phone', true ) ) . '</p>';
}
```
![It's alive!](https://woocommerce.com/wp-content/uploads/2012/04/WooCommerce-Codex-Shipping-Field-Hook.png)
Its alive!
What do we do with the new field? Nothing. Because we defined the field in the `checkout_fields` array, the field is automatically processed and saved to the order post meta (in this case, \_shipping_phone). If you want to add validation rules, see the checkout class where there are additional hooks you can use.
### Adding a Custom Special Field
To add a custom field is similar. Lets add a new field to checkout, after the order notes, by hooking into the following:
```php
/**
* Add the field to the checkout
*/
add_action( 'woocommerce_after_order_notes', 'my_custom_checkout_field' );
function my_custom_checkout_field( $checkout ) {
echo '<div id="my_custom_checkout_field"><h2>' . esc_html__( 'My Field' ) . '</h2>';
woocommerce_form_field(
'my_field_name',
array(
'type' => 'text',
'class' => array( 'my-field-class form-row-wide' ),
'label' => __( 'Fill in this field' ),
'placeholder' => __( 'Enter something' ),
),
$checkout->get_value( 'my_field_name' )
);
echo '</div>';
}
```
This gives us:
![WooCommerce Codex - Checkout Field Hook](https://woocommerce.com/wp-content/uploads/2012/04/WooCommerce-Codex-Checkout-Field-Hook.png)
Next we need to validate the field when the checkout form is posted. For this example the field is required and not optional:
```php
/**
* Process the checkout
*/
add_action( 'woocommerce_checkout_process', 'my_custom_checkout_field_process' );
function my_custom_checkout_field_process() {
// Check if set, if its not set add an error.
if ( ! $_POST['my_field_name'] ) {
wc_add_notice( esc_html__( 'Please enter something into this new shiny field.' ), 'error' );
}
}
```
A checkout error is displayed if the field is blank:
![WooCommerce Codex - Checkout Field Notice](https://woocommerce.com/wp-content/uploads/2012/04/WooCommerce-Codex-Checkout-Field-Notice.png)
Finally, lets save the new field to order custom fields using the following code:
```php
/**
* Update the order meta with field value
*/
add_action( 'woocommerce_checkout_update_order_meta', 'my_custom_checkout_field_update_order_meta' );
function my_custom_checkout_field_update_order_meta( $order_id ) {
if ( ! empty( $_POST['my_field_name'] ) ) {
update_post_meta( $order_id, 'My Field', sanitize_text_field( $_POST['my_field_name'] ) );
}
}
```
The field is now saved to the order.
If you wish to display the custom field value on the admin order edition page, you can add this code:
```php
/**
* Display field value on the order edit page
*/
add_action( 'woocommerce_admin_order_data_after_billing_address', 'my_custom_checkout_field_display_admin_order_meta', 10, 1 );
function my_custom_checkout_field_display_admin_order_meta( $order ){
echo '<p><strong>' . esc_html__( 'My Field' ) . ':</strong> ' . esc_html( get_post_meta( $order->id, 'My Field', true ) ) . '</p>';
}
```
This is the result:
[![checkout_field_custom_field_admin](https://woocommerce.com/wp-content/uploads/2012/04/checkout_field_custom_field_admin.png)](https://woocommerce.com/wp-content/uploads/2012/04/checkout_field_custom_field_admin.png)
### Make phone number not required
```php
add_filter( 'woocommerce_billing_fields', 'wc_npr_filter_phone', 10, 1 );
function wc_npr_filter_phone( $address_fields ) {
$address_fields['billing_phone']['required'] = false;
return $address_fields;
}
```

View File

@ -0,0 +1,309 @@
# Useful Core Functions
WooCommerce core functions are available on both front-end and admin. They can be found in `includes/wc-core-functions.php` and can be used by themes in plugins.
## Conditional Functions
WooCommerce conditional functions help determine the current query/page.
### is_woocommerce
Returns true if on a page which uses WooCommerce templates (cart and checkout are standard pages with shortcodes and thus are not included).
```php
is_woocommerce()
```
### is_shop
Returns true when viewing the product type archive (shop).
```php
is_shop()
```
### is_product
Returns true when viewing a single product.
```php
is_product()
```
## Coupon Functions
### wc_get_coupon_code_by_id
Get coupon code by coupon ID.
```php
wc_get_coupon_code_by_id( $id )
```
The argument `$id` is the coupon ID.
### wc_get_coupon_id_by_code
Gets the coupon ID by code.
```php
wc_get_coupon_id_by_code( $code, $exclude = 0 )
```
`$code` is the coupon code and `$exclude` is to exclude an ID from the check if you're checking existence.
## User Functions
### wc_customer_bought_product
Checks if a customer has bought an item. The check can be done by email or user ID or both.
```php
wc_customer_bought_product( $customer_email, $user_id, $product_id )
```
### wc_get_customer_total_spent
Gets the total spent for a customer.
```php
wc_get_customer_total_spent( $user_id )
```
`$user_id` is the user ID of the customer.
### wc_get_customer_order_count
Gets the total orders for a customer.
```php
wc_get_customer_order_count( $user_id )
```
`$user_id` is the user ID of the customer.
## Formatting Functions
### wc_get_dimension
Takes a measurement `$dimension` measured in WooCommerce's dimension unit and converts it to the target unit `$to_unit`.
```php
wc_get_dimension( $dimension, $to_unit, $from_unit = '' )
```
Example usages:
```php
wc_get_dimension( 55, 'in' );
wc_get_dimension( 55, 'in', 'm' );
```
### wc_get_weight
Takes a weight `$weight` weighed in WooCommerce's weight unit and converts it to the target weight unit `$to_unit`.
```php
wc_get_weight( $weight, $to_unit, $from_unit = '' )
```
Example usages:
```php
wc_get_weight( 55, 'kg' );
wc_get_weight( 55, 'kg', 'lbs' );
```
### wc_clean
Clean variables using `sanitize_text_field`. Arrays are cleaned recursively. Non-scalar values are ignored.
```php
wc_clean( $var )
```
### wc_price
Formats a passed price with the correct number of decimals and currency symbol.
```php
wc_price( $price, $args = array() )
```
The ` $args` array has an option called ` ex_tax_label` if true then an `excluding tax` message will be appended.
## Order Functions
### wc_get_orders
This function is the standard way of retrieving orders based on certain parameters. This function should be used for order retrieval so that when we move to custom tables, functions still work.
```php
wc_get_orders( $args )
```
[Arguments and usage](https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query)
### wc_get_order
This is the main function for returning orders, uses the `WC_Order_Factory` class.
```php
wc_get_order( $the_order = false )
```
The `the_order` parameter can be a post object or post ID of the order.
### wc_orders_count
Returns the orders count of a specific order status.
```php
wc_orders_count( $status, string $type = '' )
```
### wc_order_search
Searches orders based on the given `$term`.
```php
wc_order_search( $term )
```
## Page Functions
### wc_get_page_id
Gets a WooCommerce page ID by name, e.g. thankyou
```php
wc_get_page_id( $page )
```
### wc_get_endpoint_url
Gets the URL for an `$endpoint`, which varies depending on permalink settings.
```php
wc_get_endpoint_url( $endpoint, $value = '', $permalink = '' )
```
## Product Functions
### wc_get_products
This function is the standard way of retrieving products based on certain parameters.
```php
wc_get_products( $args )
```
[Arguments and usage](https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query)
### wc_get_product
This is the main function for returning products. It uses the `WC_Product_Factory` class.
```php
wc_get_product( $the_product = false )
```
The argument `$the_product` can be a post object or post ID of the product.
### wc_get_product_ids_on_sale
Returns an array containing the IDs of the products that are on sale.
```php
wc_get_product_ids_on_sale()
```
### wc_get_featured_product_ids
Returns an array containing the IDs of the featured products.
```php
wc_get_featured_product_ids()
```
### wc_get_related_products
Gets the related products for product based on product category and tags.
```php
wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() )
```
## Account Functions
### wc_get_account_endpoint_url
Gets the account endpoint URL.
```php
wc_get_account_endpoint_url( $endpoint )
```
## Attribute Functions
### wc_get_attribute_taxonomies
Gets the taxonomies of product attributes.
```php
wc_get_attribute_taxonomies()
```
### wc_attribute_taxonomy_name
Gets the taxonomy name for a given product attribute.
```php
wc_attribute_taxonomy_name( $attribute_name )
```
### wc_attribute_taxonomy_id_by_name
Gets a product attribute ID by name.
```php
wc_attribute_taxonomy_id_by_name( $name )
```
## REST Functions
### wc_rest_prepare_date_response
Parses and formats a date for ISO8601/RFC3339.
```php
wc_rest_prepare_date_response( $date, $utc = true )
```
Pass `$utc` as `false` to get local/offset time.
### wc_rest_upload_image_from_url
Uploads an image from a given URL.
```php
wc_rest_upload_image_from_url( $image_url )
```
### wc_rest_urlencode_rfc3986
Encodes a `$value` according to RFC 3986.
```php
wc_rest_urlencode_rfc3986( $value )
```
### wc_rest_check_post_permissions
Checks permissions of posts on REST API.
```php
wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 )
```
The available values for `$context` which is the request context are `read`, `create`, `edit`, `delete` and `batch`.

View File

@ -0,0 +1,40 @@
# CSS SASS coding guidelines and naming convetions
Our guidelines are based on those used in [Calypso](https://github.com/Automattic/wp-calypso) which itself follows the BEM methodology. Refer to [this doc](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md?term=css) for full details. There are a few differences in WooCommerce however which are outlined below;
## Prefixing
As a WordPress plugin WooCommerce has to play nicely with WordPress core and other plugins / themes. To minimise conflict potential all classes should be prefixed with `.woocommerce-`.
## Class names
Calypso is built in React and uses component names to formulate CSS class names. WooCommerce Core has none of these components so uses a more traditional [BEM](http://getbem.com/) approach to [naming classes](http://cssguidelin.es/#bem-like-naming).
When adding classes just remember;
* **Block** - Standalone entity that is meaningful on its own.
* **Element** - Parts of a block and have no standalone meaning. They are semantically tied to its block.
* **Modifier** - Flags on blocks or elements. Use them to change appearance or behaviour.
### Example
* `.woocommerce-loop {}` (block).
* `.woocommerce-loop-product {}` (nested block).
* `.woocommerce-loop-product--sale {}` (modifier).
* `.woocommerce-loop-product__link {}` (element).
* `.woocommerce-loop-product__title {}` (element).
* `.woocommerce-loop-product__price {}` (element).
* `.woocommerce-loop-product__rating {}` (element).
* `.woocommerce-loop-product__button-add-to-cart {}` (element).
* `.woocommerce-loop-product__button-add-to-cart--added {}` (modifier).
**Note:** `.woocommerce-loop-product` is not the chosen classname _because_ the block is nested within `.woocommerce-loop`. It's to be specific so that we can have separate classes for single products, cart products etc. _Nested blocks do not need to inherit their parents full name_.
You can read more about BEM key concepts [here](https://en.bem.info/methodology/key-concepts/).
#### TL;DR
* Follow the [WP Coding standards for CSS](https://make.wordpress.org/core/handbook/best-practices/coding-standards/css/) unless it contradicts anything here.
* Follow [Calypso guidelines](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md?term=css).
* Use BEM for [class names](https://en.bem.info/methodology/naming-convention/).
* Prefix all the things.

View File

@ -0,0 +1,57 @@
# API Critical Flows
In our documentation, we've pinpointed the essential user flows within the WooCommerce Core API. These flows serve as
the compass for our testing initiatives, aiding us in concentrating our efforts where they matter most. They also
provide invaluable insights into assessing the ramifications of modifications and determining issue priorities.
It's important to note that these flows remain dynamic, evolving in lockstep with the platform. They regularly undergo
updates, additions, and re-prioritization to stay aligned with the evolving needs of our system.
## Products 🛒
| Route | Flow name | Endpoint | Test File |
|----------|----------------------------|--------------------------------|-------------------------------------------------------------|
| Products | Can view all products | `/wp-json/wc/v3/products` | `tests/api-core-tests/tests/products/product-list.test.js` |
| Products | Can search products | `/wp-json/wc/v3/products` | `tests/api-core-tests/tests/products/product-list.test.js` |
| Products | Can add a simple product | `/wp-json/wc/v3/products` | `tests/api-core-tests/tests/products/products-crud.test.js` |
| Products | Can add a variable product | `/wp-json/wc/v3/products` | `tests/api-core-tests/tests/products/products-crud.test.js` |
| Products | Can add a virtual product | `/wp-json/wc/v3/products` | `tests/api-core-tests/tests/products/products-crud.test.js` |
| Products | Can view a single product | `/wp-json/wc/v3/products/{id}` | `tests/api-core-tests/tests/products/products-crud.test.js` |
| Products | Can update a product | `/wp-json/wc/v3/products/{id}` | `tests/api-core-tests/tests/products/products-crud.test.js` |
| Products | Can delete a product | `/wp-json/wc/v3/products/{id}` | `tests/api-core-tests/tests/products/products-crud.test.js` |
## Orders 📃
| Route | Flow name | Endpoints | Test File |
|--------|------------------------------------------------------------------|------------------------------|-----------------------------------------------------------|
| Orders | Can create an order | `/wp-json/wc/v3/orders` | `tests/api-core-tests/tests/orders/orders-crud.test.js` |
| Orders | Can view a single order | `/wp-json/wc/v3/orders/{id}` | `tests/api-core-tests/tests/orders/orders-crud.test.js` |
| Orders | Can update an order | `/wp-json/wc/v3/orders/{id}` | `tests/api-core-tests/tests/orders/orders-crud.test.js` |
| Orders | Can delete an order | `/wp-json/wc/v3/orders/{id}` | `tests/api-core-tests/tests/orders/orders-crud.test.js` |
| Orders | Can view all orders | `/wp-json/wc/v3/orders` | `tests/api-core-tests/tests/orders/orders.test.js` |
| Orders | Can search orders | `/wp-json/wc/v3/orders` | `tests/api-core-tests/tests/orders/order-search.test.js` |
| Orders | Can add new Order complex multiple product types & tax classes | `/wp-json/wc/v3/orders` | `tests/api-core-tests/tests/orders/order-complex.test.js` |
## Refunds 💸
| Route | Flow name | Endpoints | Test File |
|---------|---------------------|--------------------------------------|-----------------------------------------------------|
| Refunds | Can refund an order | `/wp-json/wc/v3/orders/{id}/refunds` | `tests/api-core-tests/tests/refunds/refund.test.js` |
## Coupons 🤑
| Route | Flow name | Endpoints | Test File |
|---------|---------------------------|--------------------------------------|------------------------------------------------------|
| Coupons | Can create a coupon | `/wp-json/wc/v3/coupons` | `tests/api-core-tests/tests/coupons/coupons.test.js` |
| Coupons | Can update a coupon | `/wp-json/wc/v3/coupons/{id}` | `tests/api-core-tests/tests/coupons/coupons.test.js` |
| Coupons | Can delete a coupon | `/wp-json/wc/v3/coupons/{id}` | `tests/api-core-tests/tests/coupons/coupons.test.js` |
| Coupons | Can add a coupon to order | `/wp-json/wc/v3/orders/{id}/coupons` | `tests/api-core-tests/tests/coupons/coupons.test.js` |
## Shipping 🚚
| Route | Flow name | Endpoints | Test File |
|------------------|-----------------------------------------------|----------------------------------------------|--------------------------------------------------------------|
| Shipping zones | Can create shipping zones | `/wp-json/wc/v3/shipping/zones` | `tests/api-core-tests/tests/shipping/shipping-zones.test.js` |
| Shipping methods | Can create shipping method to a shipping zone | `/wp-json/wc/v3/shipping/zones/{id}/methods` | n/a |
| Shipping classes | Can create a product shipping class | `/wp-json/wc/v3/products/shipping_classes` | `tests/api-core-tests/tests/products/products-crud.test.js` |

View File

@ -0,0 +1,121 @@
# Common issues
This page aims to document a comprehensive list of known issues, commonly encountered problems, and their solutions or workarounds. If you have encountered an issue that is not mentioned here and should be, please don't hesitate to add to the list.
## Composer error on `Automattic\Jetpack\Autoloader\AutoloadGenerator`
```bash
[ErrorException]
Declaration of Automattic\Jetpack\Autoloader\AutoloadGenerator::dump(Composer\Config $config, Composer\Repository\Inst
alledRepositoryInterface $localRepo, Composer\Package\PackageInterface $mainPackage, Composer\Installer\InstallationMa
nager $installationManager, $targetDir, $scanPsrPackages = false, $suffix = NULL) should be compatible with Composer\A
utoload\AutoloadGenerator::dump(Composer\Config $config, Composer\Repository\InstalledRepositoryInterface $localRepo,
Composer\Package\RootPackageInterface $rootPackage, Composer\Installer\InstallationManager $installationManager, $targ
etDir, $scanPsrPackages = false, $suffix = '')
```
A recent [change](https://github.com/composer/composer/commit/b574f10d9d68acfeb8e36cad0b0b25a090140a3b#diff-67d1dfefa9c7b1c7e0b04b07274628d812f82cd82fae635c0aeba643c02e8cd8) in composer 2.0.7 made our autoloader incompatible with the new `AutoloadGenerator` signature. Try to downgrading to composer 2.0.6 by using `composer self-update 2.0.6`.
## VVV: HostsUpdater vagrant plugin error
```bash
...vagrant-hostsupdater/HostsUpdater.rb:126:in ``digest': no implicit conversion of nil into String (TypeError)
```
You might be running an unsupported version of Vagrant. At the time of writing, VVV works with Vagrant 2.2.7. Please check VVV's [requirements](https://github.com/Varying-Vagrant-Vagrants/VVV#minimum-system-requirements).
## VVV: `install-wp-tests.sh` error
```bash
mysqladmin: CREATE DATABASE failed; error: 'Access denied for user 'wp'@'localhost' to database 'wordpress-one-tests''
```
To fix:
- Open MySQL with `sudo mysql`.
- Run `GRANT ALL PRIVILEGES ON * . * TO 'wp'@'localhost';`. Exit by typing `exit;`.
- Run the `install-wp-tests.sh` script again.
## Timeout / 404 errors while running e2e tests
```bash
Store owner can complete onboarding wizard can complete the product types section
TimeoutError: waiting for function failed: timeout 30000ms exceeded
1 | export const waitForElementCount = function ( page, domSelector, count ) {
> 2 | return page.waitForFunction(
| ^
3 | ( domSelector, count ) => {
4 | return document.querySelectorAll( domSelector ).length === count;
5 | },
```
Timeouts or 404 errors in the e2e tests signal that the existing build might be broken. Run `npm install && npm run clean && npm run build` to generate a fresh build. It should also be noted that some of our npm scripts also remove the current build, so it's a good practice to always run a build before running e2e tests.
## Docker container couldn't be built when attempting e2e test
```bash
Thu Dec 3 11:55:56 +08 2020 - Docker container is still being built
Thu Dec 3 11:56:06 +08 2020 - Docker container is still being built
Thu Dec 3 11:56:16 +08 2020 - Docker container is still being built
Thu Dec 3 11:56:26 +08 2020 - Docker container couldn't be built
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! @woocommerce/e2e-environment@0.1.6 test:e2e: `bash ./bin/wait-for-build.sh && ./bin/e2e-test-integration.js`
npm ERR! Exit status 1
```
Ensure that Docker is running. While the script says `Docker container is still being built`, it is not actually responsible for running Docker; it's just waiting for an existing docker instance to respond. Run `npm run docker:up` if it's not.
## Set up WooCommerce Payments dev mode
Add this to `wp-config.php`:
```php
define( 'WCPAY_DEV_MODE', true );
```
Also see [this document](https://docs.woocommerce.com/document/payments/testing/dev-mode).
## WooCommerce Admin install timestamp
To get the install timestamp (used in `wc_admin_active_for()` in `NoteTraits` for example) try this SQL:
```sql
SELECT * FROM wp_options WHERE option_name = 'woocommerce_admin_install_timestamp'
```
## Reset the onboarding wizard
Delete the `woocommerce_onboarding_profile` option:
```sql
DELETE FROM wp_options WHERE option_name = 'woocommerce_onboarding_profile'
```
## Enable tracks debugging in the console
```javascript
localStorage.setItem( 'debug', 'wc-admin:tracks' );
```
and set Chrome's log level "verbose" to checked.
## Running PHP unit tests using Vagrant (VVV)
1. SSH into Vagrant box (`vagrant ssh`)
2. `cd /srv/www/<WP_INSTANCE>/public_html/wp-content/plugins/woocommerce-admin`
3. Set up: `bin/install-wp-tests.sh wc-admin-tests root root`
4. Fast tests: `./vendor/bin/phpunit --group fast`
5. All tests: `./vendor/bin/phpunit`
You might need to `composer install` if `phpunit` doesn't exist.
## Show the welcome modal again
Delete the option `woocommerce_task_list_welcome_modal_dismissed`:
```sql
DELETE FROM wp_options WHERE option_name = 'woocommerce_task_list_welcome_modal_dismissed'
```

View File

@ -0,0 +1,77 @@
# Deprecation in Core
Deprecation is a method of discouraging usage of a feature or practice in favour of something else without breaking backwards compatibility or totally prohibiting its usage. To quote the Wikipedia article on Deprecation:
> While a deprecated software feature remains in the software, its use may raise warning messages recommending alternative practices; deprecated status may also indicate the feature will be removed in the future. Features are deprecated rather than immediately removed, to provide backward compatibility and give programmers time to bring affected code into compliance with the new standard.
There are various reasons why a function, method, or feature may be deprecated. To name a few:
- We may want to remove a function we do not use any longer.
- We may decide to change or improve how a function works, but due to breaking backwards compatibility we need to deprecate the old function instead.
- We may want to standardise naming conventions.
- We may find opportunities to merge similar functions to avoid reusing the same code in difference places.
Whilst deprecation notices are not ideal or attractive, they are just _warnings_ — not errors.
_*Store owners:* deprecation warnings do not mean your store is broken, it just serves as a reminder that code will need to be updated._
## How do we deprecate functions?
When we deprecate something in WooCommerce, we take a few actions to make it clear to developers and to maintain backwards compatibility.
1. We add a docblock to the function or method showing what version the function was deprecated in, e.g., `@deprecated 2.x.x`.
2. We add a warning notice using our own `wc_deprecated_function` function that shows what version, what function, and what replacement is available. More on that in a bit.
3. We remove usage of the deprecated function throughout the codebase.
The function or method itself is not removed from the codebase. This preserves backwards compatibility until removed — usually over a year or several major releases into the future.
We mentioned `wc_deprecated_function` above this is our own wrapper for the `_deprecated_function` WordPress function. It works very similar except for that it forces a log entry instead of displaying it — regardless of the value of `WP_DEBUG` during AJAX events — so that AJAX requests are not broken by the notice.
## What happens when a deprecated function is called?
If an extension or theme uses a deprecated function, you may see a warning like the following example:
```bash
Notice: woocommerce_show_messages is deprecated since version 2.1! Use wc_print_notices instead. in /srv/www/wordpress-default/wp-includes/functions.php on line 3783
```
This tells you what is deprecated, since when, where, and what replacement is available.
Notices and warnings are usually shown inline, but there are some plugins you can use to collect and show them nicely in the footer of your site. Consider, for example, [Query Monitor](https://wordpress.org/plugins/query-monitor/).
### Warnings in production (store owners — read this!)
Showing PHP notices and warnings (or any error for that matter) is highly discouraged on your production stores. They can reveal information about your setup that a malicious user could exploit to gain access to your store. Make sure they are hidden from public view and optionally logged instead.
In WordPress you can do this by adding or modifying some constants in `wp-config.php`:
```php
define( 'WP_DEBUG', false );
```
On some hosts, errors may still be visible due to the hosts configuration. To force them to not display you might need to add this to `wp-config.php` as well:
```php
@ini_set( 'display_errors', 0 );
```
To log notices instead of displaying them, use:
```php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
```
The default location of the WordPress error log is `wp-content/debug.log`.
Note that this log can be publicly accessible, which could also pose a security risk. To keep it private, you can use a plugin or define a custom path in `wp-config.php`.
```php
<?php
/**
* Plugin Name: Custom Debug Log Path
*/
ini_set( 'error_log', '/path/to/site/logs/debug.log' );
```

View File

@ -0,0 +1,14 @@
# Minification of SCSS and JS
## SCSS
When updating SCSS files in the WooCommerce project, please **commit only your changes to unminified SCSS files**. The minification will be handled as part of the release process.
To get the minified CSS files, run `pnpm -- turbo run build --filter='woocommerce-legacy-assets'` from the repository root directory. To set up the development environment from scratch, see the section on [how to install dependencies and generate assets](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment#install-dependencies-and-generate-assets) in the guide to set up a WooCommerce development environment.
## Javascript
When changing the JS files, please **commit only unminified files** (i.e. the readable JS files). The minification will be handled as part of the release process.
To ensure you can test your changes, run with `SCRIPT_DEBUG` turned on, i.e. add `define( 'SCRIPT_DEBUG', true );` to your wp-config.php file.

View File

@ -0,0 +1,46 @@
# Naming Conventions
## PHP
WooCommerce core generally follows [WordPress PHP naming conventions](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions). On top of that, function, class, and hook names should be prefixed. For functions the prefix is `wc_`, for classes is `WC_` and for hooks is `woocommerce_`.
Function name examples:
- `wc_get_product()`
- `wc_is_active_theme()`
Class name examples:
- `WC_Breadcrumb`
- `WC_Cart`
Hook name examples (actions or filters):
- `woocommerce_after_checkout_validation`
- `woocommerce_get_formatted_order_total`
There are however some exceptions which apply to classes defined inside `src/`. Within this directory:
- We do not use the `WC_` prefix for class names (the prefix is not needed, because all of the classes in this location live within the `Automattic\WooCommerce` namespace)
- Classes are named using the `CamelCase` convention (however, method names should still be `underscore_separated`)
- Class files should match the class name and do not need the `class-` prefix (for example, the filename for the `StringUtil` class is `StringUtil.php`)
## JS
WooCommerce core follows [WordPress JS naming conventions](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/#naming-conventions). As with PHP, function, class, and hook names should be prefixed, but the convention for JS is slightly different, and camelCase is used instead of snake_case. For functions, the prefix is `wc`, for classes is `WC` and for hooks is `woocommerce`.
Function name example:
- `wcSettings()`
Class name example:
- `WCOrdersTable`
Hook name example (actions or filters):
- `woocommerceTracksEventProperties`
## CSS and SASS
See [CSS SASS coding guidelines and naming conventions](https://github.com/woocommerce/woocommerce/wiki/CSS-SASS-coding-guidelines-and-naming-conventions).

View File

@ -0,0 +1,8 @@
# String localization guidelines
1. Use `woocommerce` textdomain in all strings.
2. When using dynamic strings in printf/sprintf, if you are replacing > 1 string use numbered args. e.g. `Test %s string %s.` would be `Test %1$s string %2$s.`
3. Use sentence case. e.g. `Some Thing` should be `Some thing`.
4. Avoid HTML. If needed, insert the HTML using sprintf.
For more information, see WP core document [i18n for WordPress Developers](https://codex.wordpress.org/I18n_for_WordPress_Developers).

View File

@ -0,0 +1,34 @@
# WooCommerce Git Flow
For core development, we use the following structure and flow.
![Git Flow](https://woocommerce.files.wordpress.com/2023/10/flow-1.png)
## Branches
* **Trunk** is the branch for all development and should always be the target of pull requests.
* Each major or minor release has a release branch e.g. `release/3.0` or `release/3.2`. There are no release branches for patch releases.
* Fixes are applied to trunk, and then **cherry picked into the release branch if needed**.
* Tags get created from release branches when ready to deploy.
## Branch naming
Prefixes determine the type of branch, and include:
* fix/
* feature/
* add/
* update/
* release/
When creating a **fix branch**, use the correct prefix and the issue number. Example:
``` text
fix/12345
```
Alternatively you can summarise the change:
``` text
fix/shipping-tax-rate-saving
```

View File

@ -0,0 +1,294 @@
# Implementing Settings for Extensions
If youre customizing WooCommerce or adding your own functionality to it youll probably need a settings page of some sort. One of the easiest ways to create a settings page is by taking advantage of the [`WC_Integration` class](https://woocommerce.github.io/code-reference/classes/WC-Integration.html 'WC_Integration Class'). Using the Integration class will automatically create a new settings page under **WooCommerce > Settings > Integrations** and it will automatically save, and sanitize your data for you. Weve created this tutorial so you can see how to create a new integration.
## Setting up the Integration
Youll need at least two files to create an integration so youll need to create a directory.
### Creating the Main Plugin File
Create your main plugin file to [hook](https://developer.wordpress.org/reference/functions/add_action/ 'WordPress add_action()') into the `plugins_loaded` hook and check if the `WC_Integration` [class exists](https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.extends 'PHP Class Exists'). If it doesnt then the user most likely doesnt have WooCommerce activated. After you do that you need to register the integration. Load the integration file (well get to this file in a minute). Use the `woocommerce_integrations` filter to add a new integration to the [array](http://php.net/manual/en/language.types.array.php 'PHP Array').
### Creating the Integration Class
Now that we have the framework setup lets actually implement this Integration class. There already is a `WC_Integration` class so we want to make a [child class](http://php.net/manual/en/keyword.extends.php 'PHP Child Class'). This way it inherits all of the existing methods and data. Youll need to set an id, a description, and a title for your integration. These will show up on the integration page. Youll also need to load the settings by calling: `$this->init_form_fields();` & `$this->init_settings();` Youll also need to save your options by calling the `woocommerce_update_options_integration_{your method id}` hook. Lastly you have to input some settings to save! Weve included two dummy fields below but well go more into fields in the next section.
> Added to a file named `class-wc-integration-demo-integration.php`
```php
<?php
/**
* Integration Demo Integration.
*
* @package WC_Integration_Demo_Integration
* @category Integration
* @author Patrick Rauland
*/
if ( ! class_exists( 'WC_Integration_Demo_Integration' ) ) :
/**
* Demo Integration class.
*/
class WC_Integration_Demo_Integration extends WC_Integration {
/**
* Init and hook in the integration.
*/
public function __construct() {
global $woocommerce;
$this->id = 'integration-demo';
$this->method_title = __( 'Integration Demo', 'woocommerce-integration-demo' );
$this->method_description = __( 'An integration demo to show you how easy it is to extend WooCommerce.', 'woocommerce-integration-demo' );
// Load the settings.
$this->init_form_fields();
$this->init_settings();
// Define user set variables.
$this->api_key = $this->get_option( 'api_key' );
$this->debug = $this->get_option( 'debug' );
// Actions.
add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) );
}
/**
* Initialize integration settings form fields.
*/
public function init_form_fields() {
$this->form_fields = array(
'api_key' => array(
'title' => __( 'API Key', 'woocommerce-integration-demo' ),
'type' => 'text',
'description' => __( 'Enter with your API Key. You can find this in "User Profile" drop-down (top right corner) > API Keys.', 'woocommerce-integration-demo' ),
'desc_tip' => true,
'default' => '',
),
'debug' => array(
'title' => __( 'Debug Log', 'woocommerce-integration-demo' ),
'type' => 'checkbox',
'label' => __( 'Enable logging', 'woocommerce-integration-demo' ),
'default' => 'no',
'description' => __( 'Log events such as API requests', 'woocommerce-integration-demo' ),
),
);
}
}
endif;
```
> Added to a file named `wc-integration-demo.php`
```php
<?php
/**
* Plugin Name: WooCommerce Integration Demo
* Plugin URI: https://gist.github.com/BFTrick/091d55feaaef0c5341d8
* Description: A plugin demonstrating how to add a new WooCommerce integration.
* Author: Patrick Rauland
* Author URI: http://speakinginbytes.com/
* Version: 1.0
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
if ( ! class_exists( 'WC_Integration_Demo' ) ) :
/**
* Integration demo class.
*/
class WC_Integration_Demo {
/**
* Construct the plugin.
*/
public function __construct() {
add_action( 'plugins_loaded', array( $this, 'init' ) );
}
/**
* Initialize the plugin.
*/
public function init() {
// Checks if WooCommerce is installed.
if ( class_exists( 'WC_Integration' ) ) {
// Include our integration class.
include_once 'class-wc-integration-demo-integration.php';
// Register the integration.
add_filter( 'woocommerce_integrations', array( $this, 'add_integration' ) );
} else {
// throw an admin error if you like
}
}
/**
* Add a new integration to WooCommerce.
*
* @param array Array of integrations.
*/
public function add_integration( $integrations ) {
$integrations[] = 'WC_Integration_Demo_Integration';
return $integrations;
}
}
endif;
$WC_Integration_Demo = new WC_Integration_Demo( __FILE__ );
```
## Creating Settings
If you took a look through the last section youll see that we added two dummy settings using the `init_form_fields()` method.
### Types of Settings
WooCommerce includes support for 8 types of settings.
- text
- price
- decimal
- password
- textarea
- checkbox
- select
- multiselect
And these settings have attributes which you can use. These affect the way the setting looks and behaves on the settings page. It doesnt affect the setting itself. The attributes will manifest slightly differently depending on the setting type. A placeholder for example doesnt work with checkboxes. To see exactly how they work you should look through the [source code](https://github.com/woocommerce/woocommerce/blob/master/includes/abstracts/abstract-wc-settings-api.php#L180 'WC Settings API on GitHub'). Ex.
- title
- class
- css
- placeholder
- description
- default
- desc_tip
### Creating Your Own Settings
The built-in settings are great but you may need extra controls to create your settings page. Thats why we included some methods to do this for you. First, define a setting by adding it to the `$this->form_fields` array, entering the kind of form control you want under `type`. You can override the default HTML for your form inputs by creating a method with a name of the format `generate_{ type }_html` which outputs HTML markup. To specify how buttons are rendered, youd add a method called `generate_button_html`. For textareas, youd add a `generate_textarea_html` method, and so on. (Check out the `generate_settings_html` method of the `WC_Settings_API` class in the WooCommerce source code to see how WooCommerce uses this.) The below example creates a button that goes to woo.com.
```php
/**
* Initialize integration settings form fields.
*
* @return void
*/
public function init_form_fields() {
$this->form_fields = array(
// don't forget to put your other settings here
'customize_button' => array(
'title' => __( 'Customize!', 'woocommerce-integration-demo' ),
'type' => 'button',
'custom_attributes' => array(
'onclick' => "location.href='http://www.woo.com'",
),
'description' => __( 'Customize your settings by going to the integration site directly.', 'woocommerce-integration-demo' ),
'desc_tip' => true,
)
);
}
/**
* Generate Button HTML.
*
* @access public
* @param mixed $key
* @param mixed $data
* @since 1.0.0
* @return string
*/
public function generate_button_html( $key, $data ) {
$field = $this->plugin_id . $this->id . '_' . $key;
$defaults = array(
'class' => 'button-secondary',
'css' => '',
'custom_attributes' => array(),
'desc_tip' => false,
'description' => '',
'title' => '',
);
$data = wp_parse_args( $data, $defaults );
ob_start();
?>
<tr valign="top">
<th scope="row" class="titledesc">
<label for="<?php echo esc_attr( $field ); ?>"><?php echo wp_kses_post( $data['title'] ); ?></label>
<?php echo $this->get_tooltip_html( $data ); ?>
</th>
<td class="forminp">
<fieldset>
<legend class="screen-reader-text"><span><?php echo wp_kses_post( $data['title'] ); ?></span></legend>
<button class="<?php echo esc_attr( $data['class'] ); ?>" type="button" name="<?php echo esc_attr( $field ); ?>" id="<?php echo esc_attr( $field ); ?>" style="<?php echo esc_attr( $data['css'] ); ?>" <?php echo $this->get_custom_attribute_html( $data ); ?>><?php echo wp_kses_post( $data['title'] ); ?></button>
<?php echo $this->get_description_html( $data ); ?>
</fieldset>
</td>
</tr>
<?php
return ob_get_clean();
}
```
## Validating & Sanitizing Data
To create the best user experience youll most likely want to validate and sanitize your data. The integration class already performs basic sanitization so that theres no malicious code present but you could further sanitize by removing unused data. An example of sanitizing data would be integrating with a 3rd party service where all API keys are upper case. You could convert the API key to upper case which will make it a bit more clear for the user.
### Sanitize
We'll demonstrate how to sanitize data first because its a bit easier to understand. But the one thing you should keep in mind is that sanitizing happens _after_ validation. So if something isnt validated it wont get to the sanitization step.
```php
/**
* Init and hook in the integration.
*/
public function __construct() {
// do other constructor stuff first
// Filters.
add_filter( 'woocommerce_settings_api_sanitized_fields_' . $this->id, array( $this, 'sanitize_settings' ) );
}
/**
* Sanitize our settings
*/
public function sanitize_settings( $settings ) {
// We're just going to make the api key all upper case characters since that's how our imaginary API works
if ( isset( $settings ) &&
isset( $settings['api_key'] ) ) {
$settings['api_key'] = strtoupper( $settings['api_key'] );
}
return $settings;
}
```
### Validation
Validation isnt always necessary but its nice to do. If your API keys are always 10 characters long and someone enters one thats not 10 then you can print out an error message and prevent the user a lot of headache when they assumed they put it in correctly. First set up a `validate_{setting key}_field` method for each field you want to validate. For example, with the `api_key` field you need a `validate_api_key_field()` method.
```php
public function validate_api_key_field( $key, $value ) {
if ( isset( $value ) && 20 < strlen( $value ) ) {
WC_Admin_Settings::add_error( esc_html__( 'Looks like you made a mistake with the API Key field. Make sure it isn&apos;t longer than 20 characters', 'woocommerce-integration-demo' ) );
}
return $value;
}
```
## A complete example
If youve been following along you should have a complete integration example. If you have any problems see our [full integration demo](https://github.com/woogists/woocommerce-integration-demo 'Integration Demo').

View File

@ -30,7 +30,7 @@ If you've ever wanted to contribute to the WooCommerce platform as a developer p
Visit the WooCommerce home repository on GitHub to learn the first steps to environment set up and platform contribution expectations.
### [Developer tools](/getting-started/developer-tools.md)
### [Developer tools](docs/getting-started/developer-tools.md)
Check out our guide to learn more about developer tools, libraries, and utilities.

View File

@ -0,0 +1,106 @@
# WooCommerce Endpoints
**Note:** We are unable to provide support for customizations under our **[Support Policy](http://www.woocommerce.com/support-policy/)**. If you need to further customize a snippet, or extend its functionality, we highly recommend [**Codeable**](https://codeable.io/?ref=z4Hnp), or a [**Certified WooExpert**](https://woocommerce.com/experts/).
Endpoints are an extra part in the website URL that is detected to show different content when present.
For example: You may have a my account page shown at URL **yoursite.com/my-account**. When the endpoint edit-account is appended to this URL, making it **yoursite.com/my-account/edit-account** then the **Edit account page** is shown instead of the **My account page**.
This allows us to show different content without the need for multiple pages and shortcodes, and reduces the amount of content that needs to be installed.
Endpoints are located at **WooCommerce > Settings > Advanced**.
## Checkout Endpoints
The following endpoints are used for checkout-related functionality and are appended to the URL of the /checkout page:
- Pay page `/order-pay/{ORDER_ID}`
- Order received (thanks) `/order-received/`
- Add payment method `/add-payment-method/`
- Delete payment method `/delete-payment-method/`
- Set default payment method `/set-default-payment-method/`
## Account Endpoints
The following endpoints are used for account-related functionality and are appended to the URL of the /my-account page:
- Orders `/orders/`
- View order `/view-order/{ORDER_ID}`
- Downloads `/downloads/`
- Edit account (and change password) `/edit-account/`
- Addresses `/edit-address/`
- Payment methods `/payment-methods/`
- Lost password `/lost-password/`
- Logout `/customer-logout/`
## Customizing endpoint URLs
The URL for each endpoint can be customized in **WooCommerce > Settings > Advanced** in the Page setup section.
![Endpoints](https://woocommerce.com/wp-content/uploads/2014/02/endpoints.png)
Ensure that they are unique to avoid conflicts. If you encounter issues with 404s, go to **Settings > Permalinks** and save to flush the rewrite rules.
## Using endpoints in menus
If you want to include an endpoint in your menus, you need to use the Links section:
![2014-02-26 at 14.26](https://woocommerce.com/wp-content/uploads/2014/02/2014-02-26-at-14.26.png)
Enter the full URL to the endpoint and then insert that into your menu.
Remember that some endpoints, such as view-order, require an order ID to work. In general, we dont recommend adding these endpoints to your menus. These pages can instead be accessed via the my-account page.
## Using endpoints in Payment Gateway Plugins
WooCommerce provides helper functions in the order class for getting these URLs. They are:
`$order->get_checkout_payment_url( $on_checkout = false );`
and:
`$order->get_checkout_order_received_url();`
Gateways need to use these methods for full 2.1+ compatibility.
## Troubleshooting
### Endpoints showing 404
- If you see a 404 error, go to **WordPress Admin** > **Settings > Permalinks** and Save. This ensures that rewrite rules for endpoints exist and are ready to be used.
- If using an endpoint such as view-order, ensure that it specifies an order number. /view-order/ is invalid. /view-order/10/ is valid. These types of endpoints should not be in your navigation menus.
### Endpoints are not working
On Windows servers, the **web.config** file may not be set correctly to allow for the endpoints to work correctly. In this case, clicking on endpoint links (e.g. /edit-account/ or /customer-logout/) may appear to do nothing except refresh the page. In order to resolve this, try simplifying the **web.config** file on your Windows server. Heres a sample file configuration:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers accessPolicy="Read, Execute, Script" />
<rewrite>
<rules>
<rule name="wordpress" patternSyntax="Wildcard">
<match url="*" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
```
### Pages direct to wrong place
Landing on the wrong page when clicking an endpoint URL is typically caused by incorrect settings. For example, clicking Edit address on your account page takes you to the Shop page instead of the edit address form means you selected the wrong page in settings. Confirm that your pages are correctly configured and that a different page is used for each section.
### How to Remove “Downloads” from My Account
Sometimes the “Downloads” endpoint on the “My account” page does not need to be displayed. This can be removed by going to **WooCommerce → Settings → Advanced → Account endpoints** and clearing the Downloads endpoint field.
![Account endpoints](https://woocommerce.com/wp-content/uploads/2023/04/Screenshot-2023-04-09-at-11.45.58-PM.png?w=650)

View File

@ -0,0 +1,252 @@
# Payment Gateway API
Payment gateways in WooCommerce are class based and can be added through traditional plugins. This guide provides an intro to gateway development.
**Note:** We are unable to provide support for customizations under our **[Support Policy](http://www.woocommerce.com/support-policy/)**. If you need to further customize a snippet, or extend its functionality, we highly recommend [**Codeable**](https://codeable.io/?ref=z4Hnp), or a [**Certified WooExpert**](https://woocommerce.com/experts/).
## Types of payment gateway
Payment gateways come in several varieties:
1. **Form based** This is where the user must click a button on a form that then redirects them to the payment processor on the gateways own website. _Example_: PayPal standard, Authorize.net DPM
2. **iFrame based** This is when the gateway payment system is loaded inside an iframe on your store. _Example_: SagePay Form, PayPal Advanced
3. **Direct** This is when the payment fields are shown directly on the checkout page and the payment is made when place order is pressed. _Example_: PayPal Pro, Authorize.net AIM
4. **Offline** No online payment is made. _Example_: Cheque, Bank Transfer
Form and iFrame based gateways post data offsite, meaning there are less security issues for you to think about. Direct gateways, however, require server security to be implemented ([SSL certificates](https://woocommerce.com/document/ssl-and-https/), etc.) and may also require a level of [PCI compliance](https://woocommerce.com/document/pci-dss-compliance-and-woocommerce/).
## Creating a basic payment gateway
**Note:** We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/). If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/)  for assistance.
**Note:** The instructions below are for the default Checkout page. If youre looking to add a custom payment method for the new Checkout block, check out [this documentation.](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/checkout-payment-methods/payment-method-integration.md)
Payment gateways should be created as additional plugins that hook into WooCommerce. Inside the plugin, you need to create a class after plugins are loaded. Example:
``` php
add_action( 'plugins_loaded', 'init_your_gateway_class' );
```
It is also important that your gateway class extends the WooCommerce base gateway class, so you have access to important methods and the [settings API](https://woocommerce.com/document/settings-api/ "https://woocommerce.com/document/settings-api/"):
``` php
function init_your_gateway_class() {
class WC_Gateway_Your_Gateway extends WC_Payment_Gateway {}
}
```
You can view the [WC_Payment_Gateway class in the API Docs](https://woocommerce.github.io/code-reference/classes/WC-Payment-Gateway.html).
As well as defining your class, you need to also tell WooCommerce (WC) that it exists. Do this by filtering _woocommerce_payment_gateways_:
``` php
function add_your_gateway_class( $methods ) {
$methods\[\] = 'WC_Gateway_Your_Gateway';
return $methods;
}
```
``` php
add_filter( 'woocommerce_payment_gateways', 'add_your_gateway_class' );
```
### Required Methods
Most methods are inherited from the WC_Payment_Gateway class, but some are required in your custom gateway.
#### \_\_construct()
Within your constructor, you should define the following variables:
- `$this->id` Unique ID for your gateway, e.g., your_gateway
- `$this->icon` If you want to show an image next to the gateways name on the frontend, enter a URL to an image.
- `$this->has_fields` Bool. Can be set to true if you want payment fields to show on the checkout (if doing a direct integration).
- `$this->method_title` Title of the payment method shown on the admin page.
- `$this->method_description` Description for the payment method shown on the admin page.
Your constructor should also define and load settings fields:
``` php
$this->init\_form\_fields();
$this->init_settings();
```
Well cover `init_form_fields()` later, but this basically defines your settings that are then loaded with `init_settings()`.
After `init_settings()` is called, you can get the settings and load them into variables, meaning:
``` php
$this->title = $this->get_option( 'title' );
```
Finally, you need to add a save hook for your settings:
``` php
add_action( 'woocommerce_update_options_payment_gateways\_' . $this->id, array( $this, 'process_admin_options' ) );
```
#### init_form_fields()
Use this method to set `$this->form_fields` these are options youll show in admin on your gateway settings page and make use of the [WC Settings API](https://woocommerce.com/document/settings-api/ "https://woocommerce.com/document/settings-api/").
A basic set of settings for your gateway would consist of _enabled_, _title_ and _description_:
``` php
$this->form_fields = array(
'enabled' => array(
'title' => \_\_( 'Enable/Disable', 'woocommerce' ),
'type' => 'checkbox',
'label' => \_\_( 'Enable Cheque Payment', 'woocommerce' ),
'default' => 'yes'
),
'title' => array(
'title' => \_\_( 'Title', 'woocommerce' ),
'type' => 'text',
'description' => \_\_( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
'default' => \_\_( 'Cheque Payment', 'woocommerce' ),
'desc_tip' => true,
),
'description' => array(
'title' => \_\_( 'Customer Message', 'woocommerce' ),
'type' => 'textarea',
'default' => ''
)
);
```
#### process_payment( $order_id )
Now for the most important part of the gateway — handling payment and processing the order. Process_payment also tells WC where to redirect the user, and this is done with a returned array.
Here is an example of a process_payment function from the Cheque gateway:
``` php
function process_payment( $order_id ) {
global $woocommerce;
$order = new WC_Order( $order_id );
// Mark as on-hold (we're awaiting the cheque)
$order->update\_status('on-hold', \_\_( 'Awaiting cheque payment', 'woocommerce' ));
// Remove cart
$woocommerce->cart->empty\_cart();
// Return thankyou redirect
return array(
'result' => 'success',
'redirect' => $this->get\_return\_url( $order )
);
}
```
As you can see, its job is to:
- Get and update the order being processed
- Return success and redirect URL (in this case the thanks page)
Cheque gives the order On-Hold status since the payment cannot be verified automatically. If, however, you are building a direct gateway, then you can complete the order here instead. Rather than using update_status when an order is paid, you should use payment_complete:
``` php
$order->payment_complete();
```
This ensures stock reductions are made, and the status is changed to the correct value.
If payment fails, you should throw an error and return null:
``` php
wc_add_notice( \_\_('Payment error:', 'woothemes') . $error_message, 'error' );
return;
```
WooCommerce will catch this error and show it on the checkout page.
Stock levels are updated via actions (`woocommerce_payment_complete` and in transitions between order statuses), so its no longer needed to manually call the methods reducing stock levels while processing the payment.
### Updating Order Status and Adding Notes
Updating the order status can be done using functions in the order class. You should only do this if the order status is not processing (in which case you should use payment_complete()). An example of updating to a custom status would be:
``` php
$order = new WC\_Order( $order\_id );
$order->update_status('on-hold', \_\_('Awaiting cheque payment', 'woothemes'));
```
The above example updates the status to On-Hold and adds a note informing the owner that it is awaiting a Cheque. You can add notes without updating the order status; this is used for adding a debug message:
``` php
$order->add_order_note( \_\_('IPN payment completed', 'woothemes') );
```
### Order Status Best Practice
- If the order has completed but the admin needs to manually verify payment, use **On-Hold**
- If the order fails and has already been created, set to **Failed**
- If payment is complete, let WooCommerce handle the status and use `$order->payment_complete()`. WooCommerce will use either **Completed** or **Processing** status and handle stock.
## Notes on Direct Gateways
If you are creating an advanced, direct gateway (i.e., one that takes payment on the actual checkout page), there are additional steps involved. First, you need to set has_fields to true in the gateway constructor:
``` php
$this->has_fields = true;
```
This tells the checkout to output a payment_box containing your direct payment form that you define next.
Create a method called `payment_fields()` this contains your form, most likely to have credit card details.
The next but optional method to add is `validate_fields()`. Return true if the form passes validation or false if it fails. You can use the `wc_add_notice()` function if you want to add an error and display it to the user.
Finally, you need to add payment code inside your `process_payment( $order_id )` method. This takes the posted form data and attempts payment directly via the payment provider.
If payment fails, you should output an error and return nothing:
``` php
wc_add_notice( \_\_('Payment error:', 'woothemes') . $error_message, 'error' );
return;
```
If payment is successful, you should set the order as paid and return the success array:
``` php
// Payment complete
$order->payment_complete();
```
``` php
// Return thank you page redirect
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order )
);
```
## Working with Payment Gateway Callbacks (such as PayPal IPN)
If you are building a gateway that makes a callback to your store to tell you about the status of an order, you need to add code to handle this inside your gateway.
The best way to add a callback and callback handler is to use WC-API hooks. An example would be as PayPal Standard does. It sets the callback/IPN URL as:
``` php
str_replace( 'https:', 'http:', add_query_arg( 'wc-api', 'WC_Gateway_Paypal', home_url( '/' ) ) );
```
Then hooks in its handler to the hook:
``` php
add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_ipn_response' ) );
```
WooCommerce will call your gateway and run the action when the URL is called.
For more information, see [WC_API — The WooCommerce API Callback](https://woocommerce.com/document/wc_api-the-woocommerce-api-callback/).
## Hooks in Gateways
Its important to note that adding hooks inside gateway classes may not trigger. Gateways are only loaded when needed, such as during checkout and on the settings page in admin.
You should keep hooks outside of the class or use WC-API if you need to hook into WordPress events from your class.

View File

@ -0,0 +1,45 @@
# WooCommerce payment gateway plugin base
This code can be used as a base to create your own simple custom payment gateway for WooCommerce. If not used in a custom plugin, you need to add this code to your child themes functions.php file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Please dont add custom code directly to your parent themes functions.php file as this will be wiped entirely when you update the theme.
``` php
<?php
/*
Plugin Name: WooCommerce <enter name> Gateway
Plugin URI: http://woothemes.com/woocommerce
Description: Extends WooCommerce with an <enter name> gateway.
Version: 1.0
Author: WooThemes
Author URI: http://woothemes.com/
Copyright: © 2009-2011 WooThemes.
License: GNU General Public License v3.0
License URI: http://www.gnu.org/licenses/gpl-3.0.html
*/
add_action('plugins_loaded', 'woocommerce_gateway_name_init', 0);
function woocommerce_gateway_name_init() {
if ( !class_exists( 'WC_Payment_Gateway' ) ) return;
/**
* Localisation
*/
load_plugin_textdomain('wc-gateway-name', false, dirname( plugin_basename( __FILE__ ) ) . '/languages');
/**
* Gateway class
*/
class WC_Gateway_Name extends WC_Payment_Gateway {
// Go wild in here
}
/**
* Add the Gateway to WooCommerce
**/
function woocommerce_add_gateway_name_gateway($methods) {
$methods[] = 'WC_Gateway_Name';
return $methods;
}
add_filter('woocommerce_payment_gateways', 'woocommerce_add_gateway_name_gateway' );
}
```

View File

@ -0,0 +1,599 @@
# Payment Token API
WooCommerce 2.6 introduced an API for storing and managing payment tokens for gateways. Users can also manage these tokens from their account settings and choose from saved payment tokens on checkout.
This guide offers a few useful tutorials for using the new API as well as all the various methods available to you.
## Table Of Contents
* [Tutorials](#tutorials)
* [Adding Payment Token API Support To Your Gateway](#adding-payment-token-api-support-to-your-gateway)
* [Creating A New Token Type](#creating-a-new-token-type)
* [Classes](#classes)
* [WC_Payment_Tokens](#wc_payment_tokens)
* [WC_Payment_Token_CC](#wc_payment_token_cc)
* [WC_Payment_Token_eCheck](#wc_payment_token_echeck)
* [WC_Payment_Token](#wc_payment_token)
## Tutorials
### Adding Payment Token API Support To Your Gateway
We'll use the Simplify Commerce gateway in some of these examples.
#### Step 0: Extending The Correct Gateway Base
WooCommerce ships with two base classes for gateways. These classes were introduced along with the Token API in 2.6. They are `WC_Payment_Gateway_CC` (for credit card based tokens) and `WC_Payment_Gateway_eCheck` (for eCheck based tokens). They contain some useful code for generating payment forms on checkout and should hopefully cover most cases.
You can also implement your own gateway base by extending the abstract `WC_Payment_Gateway` class, if neither of those classes work for you.
Since Simplify deals with credit cards, we extend the credit card gateway.
`class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway_CC`
#### Step 1: 'Supports' Array
We need to tell WooCommerce our gateway supports tokenization. Like other gateways features, this is defined in a gateway's `__construct` in an array called `supports`.
Here is the Simplify array:
``` php
$this->supports = array(
'subscriptions',
'products',
...
'refunds',
'pre-orders',
);
```
Add `tokenization` to this array.
#### Step 2: Define A Method For Adding/Saving New Payment Methods From "My Account"
The form handler that is run when adding a new payment method from the "my accounts" section will call your gateway's `add_payment_method` method.
After any validation (i.e. making sure the token and data you need is present from the payment provider), you can start building a new token by creating an instance of one of the following classes: `WC_Payment_Token_CC` or `WC_Payment_Token_eCheck`. Like gateways, you can also extend the abstract `WC_Payment_Token` class and define your own token type type if necessary. For more information on all three of these classes and their methods, see further down below in this doc.
Since Simplify uses credit cards, we will use the credit card class.
`$token = new WC_Payment_Token_CC();`
We will use various `set_` methods to pass in information about our token. To start with we will pass the token string and the gateway ID so the token can be associated with Simplify.
``` php
$token->set_token( $token_string );
$token->set_gateway_id( $this->id ); // `$this->id` references the gateway ID set in `__construct`
```
At this point we can set any other necessary information we want to store with the token. Credit cards require a card type (visa, mastercard, etc), last four digits of the card number, an expiration month, and an expiration year.
``` php
$token->set_card_type( 'visa' );
$token->set_last4( '1234' );
$token->set_expiry_month( '12' );
$token->set_expiry_year( '2018' );
```
In most cases, you will also want to associate a token with a specific user:
`$token->set_user_id( get_current_user_id() );`
Finally, we can save our token to the database once the token object is built.
`$token->save();`
Save will return `true` if the token was successfully saved, and `false` if an error occurred (like a missing field).
#### Step 3: Save Methods On Checkout
WooCommerce also allows customers to save a new payment token during the checkout process in addition to "my account". You'll need to add some code to your gateways `process_payment` function to make this work correctly.
To figure out if you need to save a new payment method you can check the following POST field which should return `true` if the "Save to Account" checkbox was selected.
`wc-{$gateway_id}-new-payment-method`
If you have previously saved tokens being offered to the user, you can also look at `wc-{$gateway_id}-payment-token` for the value `new` to make sure the "Use a new card" / "Use new payment method" radio button was selected.
Once you have found out that a token should be saved you can save a token in the same way you did in Step 2, using the `set_` and `save` methods.
#### Step 4: Retrieve The Token When Processing Payments
You will need to retrieve a saved token when processing a payment in your gateway if a user selects one. This should also be done in your `process_payment` method.
You can check if an existing token should be used with a conditional like the following:
`if ( isset( $_POST['wc-simplify_commerce-payment-token'] ) && 'new' !== $_POST['wc-simplify_commerce-payment-token'] ) {`
`wc-{$gateway_id}}-payment-token` will return the ID of the selected token.
You can then load a token from ta ID (more on the WC_Payment_Tokens class later in this doc):
``` php
$token_id = wc_clean( $_POST['wc-simplify_commerce-payment-token'] );
$token = WC_Payment_Tokens::get( $token_id );
```
This does **not** check if the loaded token belongs to the current user. You can do that with a simple check:
``` php
// Token user ID does not match the current user... bail out of payment processing.
if ( $token->get_user_id() !== get_current_user_id() ) {
// Optionally display a notice with `wc_add_notice`
return;
}
```
Once you have loaded the token and done any necessary checks, you can get the actual token string (to pass to your payment provider) by using
`$token->get_token()`.
### Creating A New Token Type
You can extend the abstract WC_Payment_Token class and create a new token type If the provided eCheck and CC token types do not satisfy your requirements. There are a few things you need to include if you do this.
#### Step 0: Extend WC_Payment_Token And Name Your Type
Start by extending WC_Payment_Token and providing a name for the new type. We'll look at how the eCheck token class is built since it is the most basic token type shipped in WooCommerce core.
A barebones token file should look like this:
``` php
class WC_Payment_Token_eCheck extends WC_Payment_Token {
/** @protected string Token Type String */
protected $type = 'eCheck';
}
```
The name for this token type is 'eCheck'. The value provided in `$type` needs to match the class name (i.e: `WC_Payment_Token_$type`).
#### Step 1: Provide A Validate Method
Some basic validation is performed on a token before it is saved to the database. `WC_Payment_Token` checks to make sure the actual token value is set, as well as the `$type` defined above. If you want to validate the existence of other data (eChecks require the last 4 digits for example) or length (an expiry month should be 2 characters), you can provide your own `validate()` method.
Validate should return `true` if everything looks OK, and false if something doesn't.
Always make sure to call `WC_Payment_Token`'s validate method before adding in your own logic.
``` php
public function validate() {
if ( false === parent::validate() ) {
return false;
}
```
Now we can add some logic in for the "last 4" digits.
``` php
if ( ! $this->get_last4() ) {
return false;
}
```
Finally, return true if we make it to the end of the `validate()` method.
``` php
return true;
}
```
#### Step 2: Provide get\_ And set\_ Methods For Extra Data
You can now add your own methods for each piece of data you would like to expose. Handy functions are provided to you to make storing and retrieving data easy. All data is stored in a meta table so you do not need to make your own table or add new fields to an existing one.
Provide a `get_` and `set_` method for each piece of data you want to capture. For eChecks, this is "last4" for the last 4 digits of a check.
``` php
public function get_last4() {
return $this->get_meta( 'last4' );
}
public function set_last4( $last4 ) {
$this->add_meta_data( 'last4', $last4, true );
}
```
That's it! These meta functions are provided by [WC_Data](https://github.com/woothemes/woocommerce/blob/trunk/includes/abstracts/abstract-wc-data.php).
#### Step 3: Use Your New Token Type
You can now use your new token type, either directly when building a new token
``` php
`$token = new WC_Payment_Token_eCheck();`
// set token properties
$token->save()
```
or it will be returned when using `WC_Payment_Tokens::get( $token_id )`.
## Classes
### WC_Payment_Tokens
This class provides a set of helpful methods for interacting with payment tokens. All methods are static and can be called without creating an instance of the class.
#### get_customer_tokens( $customer_id, $gateway_id = '' )
Returns an array of token objects for the customer specified in `$customer_id`. You can filter by gateway by providing a gateway ID as well.
``` php
// Get all tokens for the current user
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id() );
// Get all tokens for user 42
$tokens = WC_Payment_Tokens::get_customer_tokens( 42 );
// Get all Simplify tokens for the current user
$tokens = WC_Payment_Tokens::get_customer_tokens( get_current_user_id(), 'simplify_commerce' );
```
#### get_customer_default_token( $customer_id )
Returns a token object for the token that is marked as 'default' (the token that will be automatically selected on checkout). If a user does not have a default token/has no tokens, this function will return null.
``` php
// Get default token for the current user
$token = WC_Payment_Tokens::get_customer_default_token( get_current_user_id() );
// Get default token for user 520
$token = WC_Payment_Tokens::get_customer_default_token( 520 );
```
#### get_order_tokens( $order_id )
Orders can have payment tokens associated with them (useful for subscription products and renewing, for example). You can get a list of tokens associated with this function. Alternatively you can use `WC_Order`'s '`get_payment_tokens()` function to get the same result.
``` php
// Get tokens associated with order 25
$tokens = WC_Payment_Tokens::get_order_tokens( 25 );
// Get tokens associated with order 25, via WC_Order
$order = wc_get_order( 25 );
$tokens = $order->get_payment_tokens();
```
#### get( $token_id )
Returns a single payment token object for the provided `$token_id`.
``` php
// Get payment token 52
$token = WC_Payment_Tokens::get( 52 );
```
#### delete( $token_id )
Deletes the provided token.
``` php
// Delete payment token 52
WC_Payment_Tokens::delete( 52 );
```
#### set_users_default( $user_id, $token_id )
Makes the provided token (`$token_id`) the provided user (`$user_id`)'s default token. It makes sure that whatever token is currently set is default is removed and sets the new one.
``` php
// Set user 17's default token to token 82
WC_Payment_Tokens::set_users_default( 17, 82 );
```
#### get_token_type_by_id( $token_id )
You can use this function If you have a token's ID but you don't know what type of token it is (credit card, eCheck, ...).
``` php
// Find out that payment token 23 is a cc/credit card token
$type = WC_Payment_Tokens::get_token_type_by_id( 23 );
```
### WC_Payment_Token_CC
`set_` methods **do not** update the token in the database. You must call `save()`, `create()` (new tokens only), or `update()` (existing tokens only).
#### validate()
Makes sure the credit card token has the last 4 digits stored, an expiration year in the format YYYY, an expiration month with the format MM, the card type, and the actual token.
``` php
$token = new WC_Payment_Token_CC();
$token->set_token( 'token here' );
$token->set_last4( '4124' );
$token->set_expiry_year( '2017' );
$token->set_expiry_month( '1' ); // incorrect length
$token->set_card_type( 'visa' );
var_dump( $token->validate() ); // bool(false)
$token->set_expiry_month( '01' );
var_dump( $token->validate() ); // bool(true)
```
#### get_card_type()
Get the card type (visa, mastercard, etc).
``` php
$token = WC_Payment_Tokens::get( 42 );
echo $token->get_card_type();
```
#### set_card_type( $type )
Set the credit card type. This is a freeform text field, but the following values can be used and WooCommerce will show a formatted label New labels can be added with the `wocommerce_credit_card_type_labels` filter.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->set_last4( 'visa' );
echo $token->get_card_type(); // returns visa
```
Supported types/labels:
``` php
array(
'mastercard' => __( 'MasterCard', 'woocommerce' ),
'visa' => __( 'Visa', 'woocommerce' ),
'discover' => __( 'Discover', 'woocommerce' ),
'american express' => __( 'American Express', 'woocommerce' ),
'diners' => __( 'Diners', 'woocommerce' ),
'jcb' => __( 'JCB', 'woocommerce' ),
) );
```
#### get_expiry_year()
Get the card's expiration year.
``` php
$token = WC_Payment_Tokens::get( 42 );
echo $token->get_expiry_year;
```
#### set_expiry_year( $year )
Set the card's expiration year. YYYY format.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->set_expiry_year( '2018' );
echo $token->get_expiry_year(); // returns 2018
```
#### get_expiry_month()
Get the card's expiration month.
``` php
$token = WC_Payment_Tokens::get( 42 );
echo $token->get_expiry_month();
```
#### set_expiry_month( $month )
Set the card's expiration month. MM format.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->set_expiry_year( '12' );
echo $token->get_expiry_month(); // returns 12
```
#### get_last4()
Get the last 4 digits of the stored credit card number.
``` php
$token = WC_Payment_Tokens::get( 42 );
echo $token->get_last4();
```
#### set_last4( $last4 )
Set the last 4 digits of the stored credit card number.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->set_last4( '2929' );
echo $token->get_last4(); // returns 2929
```
### WC_Payment_Token_eCheck
`set_` methods **do not** update the token in the database. You must call `save()`, `create()` (new tokens only), or `update()` (existing tokens only).
#### validate()
Makes sure the eCheck token has the last 4 digits stored as well as the actual token.
``` php
$token = new WC_Payment_Token_eCheck();
$token->set_token( 'token here' );
var_dump( $token->validate() ); // bool(false)
$token->set_last4( '4123' );
var_dump( $token->validate() ); // bool(true)
```
#### get_last4()
Get the last 4 digits of the stored account number.
``` php
$token = WC_Payment_Tokens::get( 42 );
echo $token->get_last4();
```
#### set_last4( $last4 )
Set the last 4 digits of the stored credit card number.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->set_last4( '2929' );
echo $token->get_last4(); // returns 2929
```
### WC_Payment_Token
You should not use `WC_Payment_Token` directly. Use one of the bundled token classes (`WC_Payment_Token_CC` for credit cards and `WC_Payment_Token_eCheck`). You can extend this class if neither of those work for you. All the methods defined in this section are available to those classes.
`set_` methods **do not** update the token in the database. You must call `save()`, `create()` (new tokens only), or `update()` (existing tokens only).
#### get_id()
Get the token's ID.
``` php
// Get the token ID for user ID 26's default token
$token = WC_Payment_Tokens::get_customer_default_token( 26 );
echo $token->get_id();
```
#### get_token()
Get the actual token string (used to communicate with payment processors).
``` php
$token = WC_Payment_Tokens::get( 49 );
echo $token->get_token();
```
#### set_token( $token )
Set the token string.
``` php
// $api_token comes from an API request to a payment processor.
$token = WC_Payment_Tokens::get( 42 );
$token->set_token( $api_token );
echo $token->get_token(); // returns our token
```
#### get_type()
Get the type of token. CC or eCheck. This will also return any new types introduced.
``` php
$token = WC_Payment_Tokens::get( 49 );
echo $token->get_type();
```
#### get_user_id()
Get the user ID associated with the token.
``` php
$token = WC_Payment_Tokens::get( 49 );
if ( $token->get_user_id() === get_current_user_id() ) {
// This token belongs to the current user.
}
```
#### set_user_id( $user_id )
Associate a token with a user.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->set_user_id( '21' ); // This token now belongs to user 21.
echo $token->get_last4(); // returns 2929
```
#### get_gateway_id
Get the gateway associated with the token.
``` php
$token = WC_Payment_Tokens::get( 49 );
$token->get_gateway_id();
```
#### set_gateway_id( $gateway_id )
Set the gateway associated with the token. This should match the "ID" defined in your gateway. For example, 'simplify_commerce' is the ID for core's implementation of Simplify.
``` php
$token->set_gateway_id( 'simplify_commerce' );
echo $token->get_gateway_id();
```
#### is_default()
Returns true if the token is marked as a user's default. Default tokens are auto-selected on checkout.
``` php
$token = WC_Payment_Tokens::get( 42 ); // Token 42 is a default token for user 3
var_dump( $token->is_default() ); // returns true
$token = WC_Payment_Tokens::get( 43 ); // Token 43 is user 3's token, but not default
var_dump( $token->is_default() ); // returns false
```
#### set_default( $is_default )
Toggle a tokens 'default' flag. Pass true to set it as default, false if its just another token. This **does not** unset any other tokens that may be set as default. You can use `WC_Payment_Tokens::set_users_default()` to handle that instead.
``` php
$token = WC_Payment_Tokens::get( 42 ); // Token 42 is a default token for user 3
var_dump( $token->is_default() ); // returns true
$token->set_default( false );
var_dump( $token->is_default() ); // returns false
```
#### validate()
Does a check to make sure both the token and token type (CC, eCheck, ...) are present. See `WC_Payment_Token_CC::validate()` or `WC_Payment_Token_eCheck::validate()` for usage.
#### read( $token_id )
Load an existing token object from the database. See `WC_Payment_Tokens::get()` which is an alias of this function.
``` php
// Load a credit card toke, ID 55, user ID 5
$token = WC_Payment_Token_CC();
$token->read( 55 );
echo $token->get_id(); // returns 55
echo $token->get_user_id(); // returns 5
```
#### update()
Update an existing token. This will take any changed fields (`set_` functions) and actually save them to the database. Returns true or false depending on success.
``` php
$token = WC_Payment_Tokens::get( 42 ); // credit card token
$token->set_expiry_year( '2020' );
$token->set_expiry_month( '06 ');
$token->update();
```
#### create()
This will create a new token in the database. So once you build it, create() will create a new token in the database with the details. Returns true or false depending on success.
``` php
$token = new WC_Payment_Token_CC();
// set last4, expiry year, month, and card type
$token->create(); // save to database
```
#### save()
`save()` can be used in place of `update()` and `create()`. If you are working with an existing token, `save()` will call `update()`. A new token will call `create()`. Returns true or false depending on success.
``` php
// calls update
$token = WC_Payment_Tokens::get( 42 ); // credit card token
$token->set_expiry_year( '2020' );
$token->set_expiry_month( '06 ');
$token->save();
// calls create
$token = new WC_Payment_Token_CC();
// set last4, expiry year, month, and card type
$token->save();
```
#### delete()
Deletes a token from the database.
``` php
$token = WC_Payment_Tokens::get( 42 );
$token->delete();
```

View File

@ -0,0 +1,190 @@
# Shipping Method API
WooCommerce has a shipping method API which plugins can use to add their own rates. This article will take you through the steps to creating a new shipping method and interacting with the API.
## Create a plugin
First off, create a regular WordPress/WooCommerce plugin see our [Extension Developer Handbook](/docs/extension-development/extension-developer-handbook.md) to get started. Youll define your shipping method class in this plugin file and maintain it outside of WooCommerce.
## Create a function to house your class
Create a function to house your class
To ensure the classes you need to extend exist, you should wrap your class in a function which is called after all plugins are loaded:
``` php
function your_shipping_method_init() {
// Your class will go here
}
add_action( 'woocommerce_shipping_init', 'your_shipping_method_init' );
```
## Create your class
Create your class and place it inside the function you just created. Make sure it extends the shipping method class so that you have access to the API. Youll see below we also init our shipping method options.
``` php
if ( ! class_exists( 'WC_Your_Shipping_Method' ) ) {
class WC_Your_Shipping_Method extends WC_Shipping_Method {
/**
* Constructor for your shipping class
*
* @access public
* @return void
*/
public function __construct() {
$this->id = 'your_shipping_method';
$this->title = __( 'Your Shipping Method' );
$this->method_description = __( 'Description of your shipping method' ); //
$this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
$this->init();
}
/**
* Init your settings
*
* @access public
* @return void
*/
function init() {
// Load the settings API
$this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings
$this->init_settings(); // This is part of the settings API. Loads settings you previously init.
// Save settings in admin if you have any defined
add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
}
/**
* calculate_shipping function.
*
* @access public
* @param mixed $package
* @return void
*/
public function calculate_shipping( $package ) {
// This is where you'll add your rates
}
}
}
```
## Defining settings/options
You can then define your options using the settings API. In the snippets above youll notice we init_form_fields and init_settings. These load up the settings API. To see how to add settings, see [WooCommerce settings API](https://woocommerce.com/document/settings-api/).
## The calculate_shipping() method
`calculate_shipping()`` is a method which you use to add your rates WooCommerce will call this when doing shipping calculations. Do your plugin specific calculations here and then add the rates via the API. How do you do that? Like so:
``` php
$rate = array(
'label' => "Label for the rate",
'cost' => '10.99',
'calc_tax' => 'per_item'
);
// Register the rate
$this->add_rate( $rate );
```
Add_rate takes an array of options. The defaults/possible values for the array are as follows:
``` php
$defaults = array(
'label' => '', // Label for the rate
'cost' => '0', // Amount for shipping or an array of costs (for per item shipping)
'taxes' => '', // Pass an array of taxes, or pass nothing to have it calculated for you, or pass 'false' to calculate no tax for this method
'calc_tax' => 'per_order' // Calc tax per_order or per_item. Per item needs an array of costs passed via 'cost'
);
```
Your shipping method can pass as many rates as you want just ensure that the id for each is different. The user will get to choose rate during checkout.
## Piecing it all together
The skeleton shipping method code all put together looks like this:
``` php
<?php
/*
Plugin Name: Your Shipping plugin
Plugin URI: https://woocommerce.com/
Description: Your shipping method plugin
Version: 1.0.0
Author: WooThemes
Author URI: https://woocommerce.com/
*/
/**
* Check if WooCommerce is active
*/
if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
function your_shipping_method_init() {
if ( ! class_exists( 'WC_Your_Shipping_Method' ) ) {
class WC_Your_Shipping_Method extends WC_Shipping_Method {
/**
* Constructor for your shipping class
*
* @access public
* @return void
*/
public function __construct() {
$this->id = 'your_shipping_method'; // Id for your shipping method. Should be uunique.
$this->method_title = __( 'Your Shipping Method' ); // Title shown in admin
$this->method_description = __( 'Description of your shipping method' ); // Description shown in admin
$this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
$this->title = "My Shipping Method"; // This can be added as an setting but for this example its forced.
$this->init();
}
/**
* Init your settings
*
* @access public
* @return void
*/
function init() {
// Load the settings API
$this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings
$this->init_settings(); // This is part of the settings API. Loads settings you previously init.
// Save settings in admin if you have any defined
add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
}
/**
* calculate_shipping function.
*
* @access public
* @param array $package
* @return void
*/
public function calculate_shipping( $package = array() ) {
$rate = array(
'label' => $this->title,
'cost' => '10.99',
'calc_tax' => 'per_item'
);
// Register the rate
$this->add_rate( $rate );
}
}
}
}
add_action( 'woocommerce_shipping_init', 'your_shipping_method_init' );
function add_your_shipping_method( $methods ) {
$methods['your_shipping_method'] = 'WC_Your_Shipping_Method';
return $methods;
}
add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );
}
```

View File

@ -0,0 +1,7 @@
# User Experience Guidelines: Accessibility
## Accessibility
Your extensions must meet the [Web Content Accessibility Guidelines](https://www.google.com/url?q=https://www.w3.org/WAI/standards-guidelines/wcag/&sa=D&source=editors&ust=1692895324247620&usg=AOvVaw3zuZP9mII_1wB0hF2DHvqz) (WCAG). Meeting 100% conformance with WCAG 2.0 is hard work; meet the AA level of conformance at a minimum.
For more information on accessibility, check out the [WordPress accessibility quick start guide](https://www.google.com/url?q=https://make.wordpress.org/accessibility/handbook/best-practices/quick-start-guide/&sa=D&source=editors&ust=1692895324247995&usg=AOvVaw1FOL7wC9TwyiIxLUiQZ34k).

View File

@ -0,0 +1,34 @@
# User Experience Guidelines: Best Practices
## Best practices
**Plugin name should simply state the feature of the plugin and not use an existing core feature or extension in its title**. The plugin name should appear at all times in the UI as a functional and original name. e.g “Appointments” instead of “VendorXYZ Bookings Plugin for WooCommerce.”
**Avoid creating new UI**. Before considering a new UI, review the WordPress interface to see if a component can be repurposed. Follow existing UI navigation patterns so merchants have context on where they are when navigating to a new experience.
**Be considerate of mobile for the merchant (and shopper-facing if applicable) experience**. Stores operate 24/7. Merchants shouldnt be limited to checking their store on a desktop. Extensions need to be built responsively so they work on all device sizes.
**Its all about the merchant**. Dont distract with unrelated content. Keep the product experience front and center to help the user achieve the tasks they purchased your product for.
**Present a review request at the right time**. Presenting users with a request for review is a great way to get feedback on your extension. Think about best placement and timing to show these prompts.
Here are some best practices:
- Avoid showing the user a review request upon first launching the extension. Once the user has had a chance to set up, connect, and use the plugin theyll have a better idea of how to rate it.
- Try to present the review request at a time thats least disruptive, such as after successful completion of a task or event.
**Dont alter the core interface**. Dont express your brand by changing the shape of containers in the Woo admin.
**Focus on the experience**. After the customer installs your product, the experience should be the primary focus. Keep things simple and guide the user to successful setup. Do not convolute the experience or distract the user with branding, self promotion, large banners, or anything obtrusive.
**Keep copy short and simple**. Limit instructions within the interface to 120-140 characters. Anything longer should be placed in the product documentation.
**Maintain a consistent tone when communicating with a user**. Maintain the same communication style and terminology across an extension, and avoid abbreviations and acronyms.
In extensions:
- Use sentences for descriptions, feedback, and headlines. Avoid all-caps text.
- Use standard punctuation and avoid excessive exclamation marks.
- Use American English.
For more, read our [Grammar, Punctuation, and Capitalization guide](https://www.google.com/url?q=https://woocommerce.com/document/grammar-punctuation-style-guide/&sa=D&source=editors&ust=1692895324244468&usg=AOvVaw2FWh4SUBI0dLsCqUZtXGFt).

View File

@ -0,0 +1,13 @@
# User Experience Guidelines: Colors
## Colors
When creating extensions for the WordPress wp-admin, use the core colors, respect the users WordPress admin color scheme selection, and ensure your designs pass AA level guidelines.
When using components with text, such as buttons, cards, or navigation, the background-to-text contrast ratio should be at least 4.5:1 to be [WCAG AA compliant](https://www.google.com/url?q=https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html&sa=D&source=editors&ust=1692895324245359&usg=AOvVaw04OufEgaTguaV-k6wMtlMU). Be sure to [test your color contrast ratios](https://www.google.com/url?q=https://webaim.org/resources/contrastchecker/&sa=D&source=editors&ust=1692895324245608&usg=AOvVaw1aGcU7vUM05t3bxPA2qrIX) to abide by WCAG standards.
- [Accessibility handbook on uses of color and contrast](https://www.google.com/url?q=https://make.wordpress.org/accessibility/handbook/current-projects/use-of-color/&sa=D&source=editors&ust=1692895324245960&usg=AOvVaw3DDtjcP5MkNoQgX3VgPKXr)
- [Color contrast ratio checker](https://www.google.com/url?q=http://webaim.org/resources/contrastchecker/&sa=D&source=editors&ust=1692895324246320&usg=AOvVaw1RTR_DT4liFu_SiBOF8RxK)
- [More resources regarding accessibility and color testing](https://www.google.com/url?q=http://webaim.org/resources/contrastchecker/&sa=D&source=editors&ust=1692895324246679&usg=AOvVaw316-gDJXDzTH8gOjibWeRm)
For WooCommerce-specific color use, review our [Style Guide](https://www.google.com/url?q=https://woocommerce.com/brand-and-logo-guidelines/&sa=D&source=editors&ust=1692895324247100&usg=AOvVaw2cgvb_mHoClPzhtW57QooS).

View File

@ -0,0 +1,33 @@
# User Experience Guidelines: Notices
## Notices
Use notices primarily to provide user feedback in response to an action. Avoid using notices to communicate offers or announcements. Dont apply brand colors, fonts, or illustrations to your notices.
If a post-activation notice is required, keep it within the WordPress plugin area—do not display it on the dashboard, or any other parts of the platform.
Use the standard WordPress notice format and WooCommerce admin notices API.
### Language
Providing timely feedback like success and error messages is essential for ensuring that the user understands whether changes have been made.
Use short but meaningful messages that communicate what is happening. Ensure that the message provides instructions on what the user needs to do to continue. Proper punctuation should be used if the message contains multiple sentences. Avoid abbreviations.
### Design
The placement of feedback is vital so the user notices it. For example, when validation messages are needed to prompt the user to enter data, get the users attention by displaying a message close to the inputs where data needs to be revised.
![visualization of four different notice designs next to one another](https://woocommerce.files.wordpress.com/2023/10/notices1.png)
**Success** message: When the user performs an action that is executed successfully.
**Error Message**: When the user performs an action that could not be completed. (This can include validation messages.) When requiring the user to input data, make sure you verify whether each field meets the requirements, such as format, ranges, and if the field is required. Provide validation messages that are adjacent to each field so that the user can act on each in context. Avoid technical jargon.
**Warning Message**: When the user performs an action that may have completed successfully, but the user should review it and proceed with caution.
**Informational Message**: When its necessary to provide information before the user executes any action on the screen. Examples can be limitations within a time period or when a global setting limits actions on the current screen.
### Examples
![an example of an informational message as a notice](https://woocommerce.files.wordpress.com/2023/10/informational-notice.png)

View File

@ -0,0 +1,27 @@
# User Experience Guidelines: Onboarding
## Onboarding
The first experience your users have with your extension is crucial. A user activating your extension for the first time provides an opportunity to onboard new and reorient returning users the right way. Is it clear to the user how to get started? Keep in mind that the more difficult the setup, the more likely a user will abandon the product altogether so keep it simple and direct.
**Use primary buttons as calls to action and keep secondary information deprioritized for clarity**. Guide merchants towards successful setup with a clear next step and/or step-by-step process with progress indicator if the extension isnt configured or if setup is not complete.
**If necessary, provide a dismissible notification in the plugin area**. Add a notification to communicate next steps if setup or connection is required to successfully enable the plugin.
- Use the standard WordPress notice format and WooCommerce admin notices API.
- Notices should be dismissible. Users should always have a clear way to close the notice.
- Keep the post-activation notice with the WordPress plugin area in context of the plugin listing—do not display it on the dashboard, or any other parts of the platform.
- Dont display more than one notice.
- Try to keep the notice copy between 125 to 200 characters.
If no action is required for setup its best to rely on other onboarding aids such as the Task List (link to component) and Inbox (link to component) to help users discover features and use your plugin.
**Get to the point and keep it instructional**. This is not a time to promote your brand or pitch the product. The user has bought your product and is ready to use it. Keep the information instructional and precise and avoid the use of branded colors, fonts, and illustrations in empty states and other onboarding aids. Help users with context on next steps.
**Show helpful empty states**. Rely on the existing plugin UI, if any, to guide users towards successful setup and use of the plugin. Avoid onboarding emails, push notifications, and welcome tours.
**Plugins should not redirect on activation from WordPress plugins area**. This can break bulk activation of plugins. Following the [dotorg plugin guideline 11](https://www.google.com/url?q=https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/%2311-plugins-should-not-hijack-the-admin-dashboard&sa=D&source=editors&ust=1693330746653223&usg=AOvVaw3YwkUVGDikvvG4jHmZ4Yej), the extension shouldnt hijack the dashboard or hide functionality of core or other extensions.
**Avoid dead end links and pages**. There should always be a way forward or back.
**Error Handling and Messaging**. If users encounter an error during setup, provide a clear and useful notification with clear and easily understood information on what went wrong and how to fix it.

View File

@ -0,0 +1,48 @@
# User Experience Guidelines: Task list and Inbox
## Task List & Inbox
Plugins should choose between implementing a Task or Inbox note based on the following guidelines. Avoid implementing both Task and Inbox note for the same message, which adds clutter and reduces the impact of the message.
Use the Task List and Inbox sparingly. Messages should be clear, concise, and maintain a consistent tone. Follow the [Grammar, Punctuation, and Capitalization guide](https://www.google.com/url?q=https://woocommerce.com/document/grammar-punctuation-style-guide/&sa=D&source=editors&ust=1693330746656102&usg=AOvVaw3bYX5mFADFqIMpsW8-owen).
### Task List
![an example of a task in the task list](https://woocommerce.files.wordpress.com/2023/10/task-list1.png)
Anything that **requires** action should go in the task list.
- *What appears in the Things to Do Task List:*
- Tasks that will enable, connect, or configure an extension.
- Tasks that are critical to the business, such as capturing payment or responding to disputes.
- *What doesnt appear in the Things to do Task List:*
- Any critical update that would impact or prevent the functioning of the store should appear as a top level notice using the standard WordPress component.
- Informational notices such as feature announcements or tips for using the plugin should appear in the Inbox as they are not critical and do not require action.
- Notifications from user activity should result in regular feedback notices (success, info, error, warning).
Examples:
![three tasks in the task list under the heading "Things to do next" with the option to expand at the bottom to "show 3 more tasks" ](https://woocommerce.files.wordpress.com/2023/10/task-list-example.png)
### Inbox
The Inbox provides informational, useful, and supplemental content to the user, while important notices and setup tasks have their separate and relevant locations.
![an example of an inbox notification](https://woocommerce.files.wordpress.com/2023/10/inbox1.png)
- *What appears in the Inbox*:
- Informational notices such as non-critical reminders.
- Requests for plugin reviews and feedback.
- Tips for using the plugin and introducing features.
- Insights such as inspirational messages or milestones.
- *What doesnt appear in the Inbox*:
- Notices that require action, extension setup tasks, or regular feedback notices.
Examples:
![an example of two inbox notifications listed under the "Inbox" section of the admin](https://woocommerce.files.wordpress.com/2023/10/inbox-examples.png)

View File

@ -0,0 +1,27 @@
# User Experience Guidelines
This guide covers general guidelines, and best practices to follow in order to ensure your product experience aligns with WooCommerce for ease of use, seamless integration, and strong adoption.
We strongly recommend you review the current [WooCommerce setup experience](https://www.google.com/url?q=https://woocommerce.com/documentation/plugins/woocommerce/getting-started/&sa=D&source=editors&ust=1692895324238396&usg=AOvVaw2TmgGeQmH4N_DZY6QS9Bve) to get familiar with the user experience and taxonomy.
We also recommend you review the [WordPress core guidelines](https://www.google.com/url?q=https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/&sa=D&source=editors&ust=1692895324239052&usg=AOvVaw1E61gu1LlpT1F6yqYdMrcl) to ensure your product isnt breaking any rules, and review [this helpful resource](https://www.google.com/url?q=https://woocommerce.com/document/grammar-punctuation-style-guide/&sa=D&source=editors&ust=1692895324239337&usg=AOvVaw0tMP_9YsdpSjtiAOQSw_D-) on content style.
## General
Use existing WordPress/WooCommerce UI, built in components (text fields, checkboxes, etc) and existing menu structures.
Plugins which draw on WordPress core design aesthetic will benefit from future updates to this design as WordPress continues to evolve. If you need to make an exception for your product, be prepared to provide a valid use case.
- [WordPress Components library](https://www.google.com/url?q=https://wordpress.github.io/gutenberg/?path%3D/story/docs-introduction--page&sa=D&source=editors&ust=1692895324240206&usg=AOvVaw12wUm2BSmyxcEjcAQxlwaU)
- [Figma for WordPress](https://www.google.com/url?q=https://make.wordpress.org/design/2018/11/19/figma-for-wordpress/&sa=D&source=editors&ust=1692895324240568&usg=AOvVaw1iTxXh4YpA9AZlAACquK3g) | ([WordPress Design Library Figma](https://www.google.com/url?q=https://www.figma.com/file/e4tLacmlPuZV47l7901FEs/WordPress-Design-Library?type%3Ddesign%26node-id%3D7-42%26t%3Dm8IgUWrqfZX0GNCh-0&sa=D&source=editors&ust=1692895324240869&usg=AOvVaw0N2Y5nktcq9dypK8N68nMD))
- [WooCommerce Component Library](https://www.google.com/url?q=https://woocommerce.github.io/woocommerce-admin/%23/&sa=D&source=editors&ust=1692895324241224&usg=AOvVaw0rXxnruNoF8alalpaev9yD)
## Explore User Experience Guidlines
- [Best Practices](/docs/user-experience/best-practices.md)
- [Colors](/docs/user-experience/colors.md)
- [Accessibility](/docs/user-experience/accessibility.md)
- [Onboarding](/docs/user-experience/onboarding.md)
- [Notices](/docs/user-experience/notices.md)
- [Task list & Inbox](/docs/user-experience/task-list-and-inbox.md)

3845
docs/wc-cli/commands.md Normal file

File diff suppressed because it is too large Load Diff

375
docs/wc-cli/overview.md Normal file
View File

@ -0,0 +1,375 @@
# WC CLI: Overview
WooCommerce CLI (WC-CLI) offers the ability to manage WooCommerce (WC) via the command-line, using WP CLI. The documentation here covers the version of WC CLI that started shipping in WC 3.0.0 and later.
WC CLI is powered by the [WC REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/), meaning most of what is possible with the REST API can also be achieved via the command-line.
_If you're looking for documentation on the [WC 2.5 and 2.6's CLI go here](https://github.com/woocommerce/woocommerce/wiki/Legacy-CLI-commands-(v2.6-and-below))._
## What is WP-CLI?
For those who have never heard before WP-CLI, here's a brief description extracted from the [official website](http://wp-cli.org/).
> **WP-CLI** is a set of command-line tools for managing WordPress installations. You can update plugins, set up multisite installs and much more, without using a web browser.
## WooCommerce Commands
A full listing of WC-CLI commands and their accepted arguments can be found on the [commands page](https://github.com/woocommerce/woocommerce/wiki/WC-CLI-Commands).
All WooCommerce-related commands are grouped into `wp wc` command. The available commands (as of WC 3.0) are:
```bash
$ wp wc
usage: wp wc customer <command>
or: wp wc customer_download <command>
or: wp wc order_note <command>
or: wp wc payment_gateway <command>
or: wp wc product <command>
or: wp wc product_attribute <command>
or: wp wc product_attribute_term <command>
or: wp wc product_cat <command>
or: wp wc product_review <command>
or: wp wc product_shipping_class <command>
or: wp wc product_tag <command>
or: wp wc product_variation <command>
or: wp wc shipping_method <command>
or: wp wc shipping_zone <command>
or: wp wc shipping_zone_location <command>
or: wp wc shipping_zone_method <command>
or: wp wc shop_coupon <command>
or: wp wc shop_order <command>
or: wp wc shop_order_refund <command>
or: wp wc tax <command>
or: wp wc tax_class <command>
or: wp wc tool <command>
or: wp wc webhook <command>
or: wp wc webhook_delivery <command>
See 'wp help wc <command>' for more information on a specific command.
```
**Note**: When using the commands, you must specify your username or user ID using the `--user` argument. This is to let the REST API know which user should be used.
You can see more details about the commands using `wp help wc` or with the `--help` flag, which explains arguments and subcommands.
Example:
`wp wc customer --help`
```bash
NAME
wp wc customer
SYNOPSIS
wp wc customer <command>
SUBCOMMANDS
create Create a new item.
delete Delete an existing item.
get Get a single item.
list List all items.
update Update an existing item.
```
`wp wc customer list --help`
```bash
NAME
wp wc customer list
DESCRIPTION
List all items.
SYNOPSIS
wp wc customer list [--context=<context>] [--page=<page>]
[--per_page=<per_page>] [--search=<search>] [--exclude=<exclude>]
[--include=<include>] [--offset=<offset>] [--order=<order>]
[--orderby=<orderby>] [--email=<email>] [--role=<role>] [--fields=<fields>]
[--field=<field>] [--format=<format>]
OPTIONS
[--context=<context>]
Scope under which the request is made; determines fields present in
response.
[--page=<page>]
Current page of the collection.
[--per_page=<per_page>]
Maximum number of items to be returned in result set.
[--search=<search>]
Limit results to those matching a string.
[--exclude=<exclude>]
Ensure result set excludes specific IDs.
[--include=<include>]
Limit result set to specific IDs.
[--offset=<offset>]
Offset the result set by a specific number of items.
[--order=<order>]
Order sort attribute ascending or descending.
[--orderby=<orderby>]
Sort collection by object attribute.
[--email=<email>]
Limit result set to resources with a specific email.
[--role=<role>]
Limit result set to resources with a specific role.
[--fields=<fields>]
Limit response to specific fields. Defaults to all fields.
[--field=<field>]
Get the value of an individual field.
[--format=<format>]
Render response in a particular format.
---
default: table
options:
- table
- json
- csv
- ids
- yaml
- count
- headers
- body
- envelope
---
```
Arguments like `--context`, `--fields`, `--field`, `--format` can be used on any `get` or `list` WC CLI command.
The `--porcelain` argument can be used on any `create` or `update` command to just get back the ID of the object, instead of a response.
Updating or creating some fields will require passing JSON. These are fields that contain arrays of information — for example, setting [https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties](billing information) using the customer command. This is just passing key/value pairs.
Example:
`$ wp wc customer create --email='me@woo.local' --user=1 --billing='{"first_name":"Justin","last_name":"S","company":"Automattic"}' --password='he
llo'`
`Success: Created customer 16.`
`$ wp wc customer get 16 --user=1`
```bash
+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | 16 |
| date_created | 2016-12-09T20:07:35 |
| date_modified | 2016-12-09T20:07:35 |
| email | me@woo.local |
| first_name | |
| last_name | |
| role | customer |
| username | me |
| billing | {"first_name":"Justin","last_name":"S","company":"Automattic","address_1":"","address_2":"","city":"","state":"","postcode":"","country":"","email":"","phone" |
| | :""} |
| shipping | {"first_name":"","last_name":"","company":"","address_1":"","address_2":"","city":"","state":"","postcode":"","country":""} |
| is_paying_customer | false |
| meta_data | |
| orders_count | 0 |
| total_spent | 0.00 |
| avatar_url | http://2.gravatar.com/avatar/81a56b00c3b9952d6d2c107a8907e71f?s=96 |
+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
```
## Examples
Full documentation for every command is available using `--help`. Below are some example commands to show what the CLI can do.
All the examples below use user ID 1 (usually an admin account), but you should replace that with your own user account.
You can also find other examples (without output) by looking at [the testing files for our CLI tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/cli/features).
Each command will have a `.feature` file. For example, [these some payment gateway commands](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/cli/features/payment_gateway.feature).
### Clearing the product/shop transients cache
Command:
`$ wp wc tool run clear_transients --user=1`
Response:
`Success: Updated system_status_tool clear_transients.`
### Listing all system tools
Command:
`$ wp wc tool list --user=1`
Response:
```bash
+----------------------------+----------------------------------+-------------------------------+-----------------------------------------------------------------------------------+
| id | name | action | description |
+----------------------------+----------------------------------+-------------------------------+-----------------------------------------------------------------------------------+
| clear_transients | WC transients | Clear transients | This tool will clear the product/shop transients cache. |
| clear_expired_transients | Expired transients | Clear expired transients | This tool will clear ALL expired transients from WordPress. |
| delete_orphaned_variations | Orphaned variations | Delete orphaned variations | This tool will delete all variations which have no parent. |
| recount_terms | Term counts | Recount terms | This tool will recount product terms - useful when changing your settings in a wa |
| | | | y which hides products from the catalog. |
| reset_roles | Capabilities | Reset capabilities | This tool will reset the admin, customer and shop_manager roles to default. Use t |
| | | | his if your users cannot access all of the WooCommerce admin pages. |
| clear_sessions | Customer sessions | Clear all sessions | <strong class="red">Note:</strong> This tool will delete all customer session dat |
| | | | a from the database, including any current live carts. |
| install_pages | Install WooCommerce pages | Install pages | <strong class="red">Note:</strong> This tool will install all the missing WooComm |
| | | | erce pages. Pages already defined and set up will not be replaced. |
| delete_taxes | Delete all WooCommerce tax rates | Delete ALL tax rates | <strong class="red">Note:</strong> This option will delete ALL of your tax rates, |
| | | | use with caution. |
| reset_tracking | Reset usage tracking settings | Reset usage tracking settings | This will reset your usage tracking settings, causing it to show the opt-in banne |
| | | | r again and not sending any data. |
+----------------------------+----------------------------------+-------------------------------+-----------------------------------------------------------------------------------+
````
### Creating a customer
Command:
`$ wp wc customer create --email='woo@woo.local' --user=1 --billing='{"first_name":"Bob","last_name":"Tester","company":"Woo", "address_1": "123 Main St.", "city":"New York", "state:": "NY", "country":"USA"}' --shipping='{"first_name":"Bob","last_name":"Tester","company":"Woo", "address_1": "123 Main St.", "city":"New York", "state:": "NY", "country":"USA"}' --password='hunter2' --username='mrbob' --first_name='Bob' --last_name='Tester'`
Response:
`Success: Created customer 17.`
### Getting a customer in CSV format
Command:
`$ wp wc customer get 17 --user=1 --format=csv`
Response:
```bash
Field,Value
id,17
date_created,2016-12-09T20:22:10
date_modified,2016-12-09T20:22:10
email,woo@woo.local
first_name,Bob
last_name,Tester
role,customer
username,mrbob
billing,"{""first_name"":""Bob"",""last_name"":""Tester"",""company"":""Woo"",""address_1"":""123 Main St."",""address_2"":"""",""city"":""New York"",""state"":"""",""postcode"":"""","
"country"":""USA"",""email"":"""",""phone"":""""}"
shipping,"{""first_name"":""Bob"",""last_name"":""Tester"",""company"":""Woo"",""address_1"":""123 Main St."",""address_2"":"""",""city"":""New York"",""state"":"""",""postcode"":"""",
""country"":""USA""}"
is_paying_customer,false
meta_data,"[{""id"":825,""key"":""shipping_company"",""value"":""Woo""},{""id"":829,""key"":""_order_count"",""value"":""0""},{""id"":830,""key"":""_money_spent"",""value"":""0""}]"
orders_count,0
total_spent,0.00
avatar_url,http://2.gravatar.com/avatar/5791d33f7d6472478c0b5fa69133f09a?s=96
```
### Adding a customer note on order 355
Command:
`$ wp wc order_note create 355 --note="Great repeat customer" --customer_note=true --user=1`
Response:
`Success: Created order_note 286.`
### Getting an order note
Command:
`$ wp wc order_note get 355 286 --user=1`
Response:
```bash
+---------------+-----------------------+
| Field | Value |
+---------------+-----------------------+
| id | 286 |
| date_created | 2016-12-09T20:27:26 |
| note | Great repeat customer |
| customer_note | true |
+---------------+-----------------------+
```
### Updating a coupon
Command:
`$ wp wc shop_coupon update 45 --amount='10' --discount_type='percent' --free_shipping=true --user=1`
Response:
`Success: Updated shop_coupon 45.`
### Getting a coupon
Command:
`$ wp wc shop_coupon get 45 --user=1`
Response:
```bash
+-----------------------------+---------------------+
| Field | Value |
+-----------------------------+---------------------+
| id | 45 |
| code | hello |
| amount | 10.00 |
| date_created | 2016-08-09T17:37:28 |
| date_modified | 2016-12-09T20:30:32 |
| discount_type | percent |
| description | Yay |
| date_expires | 2016-10-22T00:00:00 |
| usage_count | 2 |
| individual_use | false |
| product_ids | [] |
| excluded_product_ids | [] |
| usage_limit | null |
| usage_limit_per_user | null |
| limit_usage_to_x_items | null |
| free_shipping | true |
| product_categories | [] |
| excluded_product_categories | [] |
| exclude_sale_items | false |
| minimum_amount | 0.00 |
| maximum_amount | 0.00 |
| email_restrictions | [] |
| used_by | ["1","1"] |
| meta_data | [] |
+-----------------------------+---------------------+
```
## Frequently Asked Questions
### I get a 401 error when using commands, what do I do?
If you are getting a 401 error like `Error: Sorry, you cannot list resources. {"status":401}`, you are trying to use the command unauthenticated. The WooCommerce CLI as of 3.0 requires you to provide a proper user to run the action as. Pass in your user ID using the `--user` flag.
### I am trying to update a list of X, but it's not saving
Some 'lists' are actually objects. For example, if you want to set categories for a product, [the REST API expects an _array of objects_](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-properties).
To set this you would use JSON like this:
```bash
wp wc product create --name='Product Name' --categories='[ { "id" : 21 } ]' --user=admin
```

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove dependency on Jetpack from WooCommerce Shipping & Tax onboarding tasks

View File

@ -37,7 +37,7 @@ export const Plugins = ( {
onAbort,
onComplete,
onError = () => null,
pluginSlugs = [ 'jetpack', 'woocommerce-services' ],
pluginSlugs = [ 'woocommerce-services' ],
onSkip,
installText = __( 'Install & enable', 'woocommerce' ),
skipText = __( 'No thanks', 'woocommerce' ),

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Export WCUser type for consumption in wcadmin

View File

@ -108,6 +108,7 @@ export {
} from './product-categories/types';
export { TaxClass } from './tax-classes/types';
export { ProductTag, Query } from './product-tags/types';
export { WCUser } from './user/types';
/**
* Internal dependencies

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Update default visibility settings for variation options

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Create product page skeleton

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Create TableEmptyState component

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Show error message when single variation price is empty #40885

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Fix checkbox not working when checkedValue is provided

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update the variation name, by using the wc_get_formatted_variation.

View File

@ -32,7 +32,7 @@ export function Edit( {
return (
<div { ...blockProps }>
<Checkbox
value={ Boolean( value ) }
value={ value || null }
onChange={ setValue }
label={ label || '' }
title={ title }

View File

@ -92,7 +92,7 @@ export function Edit( {
'name',
async function nameValidator() {
if ( ! name || name === AUTO_DRAFT_NAME ) {
return __( 'This field is required.', 'woocommerce' );
return __( 'Name field is required.', 'woocommerce' );
}
if ( name.length > 120 ) {

View File

@ -22,7 +22,7 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data';
*/
import { sanitizeHTML } from '../../../utils/sanitize-html';
import { VariationsBlockAttributes } from './types';
import { EmptyVariationsImage } from './empty-variations-image';
import { EmptyVariationsImage } from '../../../images/empty-variations-image';
import { NewAttributeModal } from '../../../components/attribute-control/new-attribute-modal';
import {
EnhancedProductAttribute,

View File

@ -38,7 +38,9 @@
&-icon {
color: $gray-600;
cursor: help;
}
&-help-icon {
position: absolute;
right: -2px;

View File

@ -13,7 +13,7 @@ import { createElement } from '@wordpress/element';
* Internal dependencies
*/
import NotFilterableIcon from './not-filterable-icon';
import HiddenIcon from '../../icons/hidden-icon';
import SeenIcon from '../../icons/seen-icon';
type AttributeListItemProps = {
attribute: ProductAttribute;
@ -25,7 +25,7 @@ type AttributeListItemProps = {
onRemoveClick?: ( attribute: ProductAttribute ) => void;
};
const NOT_VISIBLE_TEXT = __( 'Not visible', 'woocommerce' );
const VISIBLE_TEXT = __( 'Visible in product details', 'woocommerce' );
const NOT_FILTERABLE_CUSTOM_ATTR_TEXT = __(
'Custom attribute. Customers cant filter or search by it to find this product',
'woocommerce'
@ -75,15 +75,15 @@ export const AttributeListItem: React.FC< AttributeListItemProps > = ( {
</div>
</Tooltip>
) }
{ ! attribute.visible && (
{ attribute.visible && (
<Tooltip
// @ts-expect-error className is missing in TS, should remove this when it is included.
className="woocommerce-attribute-list-item__actions-tooltip"
position="top center"
text={ NOT_VISIBLE_TEXT }
text={ VISIBLE_TEXT }
>
<div className="woocommerce-attribute-list-item__actions-icon-wrapper">
<HiddenIcon className="woocommerce-attribute-list-item__actions-icon-wrapper-icon" />
<SeenIcon className="woocommerce-attribute-list-item__actions-icon-wrapper-icon" />
</div>
</Tooltip>
) }

View File

@ -1,4 +1,6 @@
.woocommerce-product-block-editor {
padding-top: 106px;
h1,
h2,
h3,

View File

@ -101,6 +101,16 @@ export function usePublish( {
wpError.message = (
error as Record< string, string >
).variations;
} else {
const errorMessage = Object.values(
error as Record< string, string >
).find( ( value ) => value !== undefined ) as
| string
| undefined;
if ( errorMessage !== undefined ) {
wpError.code = 'product_form_field_error';
wpError.message = errorMessage;
}
}
}
onPublishError( wpError );

View File

@ -42,3 +42,5 @@ export {
export { Checkbox as __experimentalCheckboxControl } from './checkbox-control';
export { NumberControl as __experimentalNumberControl } from './number-control';
export * from './product-page-skeleton';

View File

@ -0,0 +1 @@
export * from './product-page-skeleton';

View File

@ -0,0 +1,63 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function ProductPageSkeleton() {
return (
<div className="woocommerce-product-page-skeleton" aria-hidden="true">
<div className="woocommerce-product-page-skeleton__header">
<div className="woocommerce-product-page-skeleton__header-row">
<div />
<div className="woocommerce-product-page-skeleton__header-title" />
<div className="woocommerce-product-page-skeleton__header-actions">
<div className="woocommerce-product-page-skeleton__header-actions-other" />
<div className="woocommerce-product-page-skeleton__header-actions-main" />
<div className="woocommerce-product-page-skeleton__header-actions-config" />
</div>
</div>
<div className="woocommerce-product-page-skeleton__header-row">
<div className="woocommerce-product-page-skeleton__tabs">
{ Array( 7 )
.fill( 0 )
.map( ( _, index ) => (
<div
key={ index }
className="woocommerce-product-page-skeleton__tab-item"
/>
) ) }
</div>
</div>
</div>
<div className="woocommerce-product-page-skeleton__body">
<div className="woocommerce-product-page-skeleton__body-tabs-content">
<div className="woocommerce-product-page-skeleton__block-title" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-input" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
<div className="woocommerce-product-page-skeleton__block-separator" />
<div className="woocommerce-product-page-skeleton__block-title" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-input" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,156 @@
@mixin skeleton {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
min-width: $grid-unit-20;
min-height: $grid-unit-20;
}
.woocommerce-product-page-skeleton {
height: calc(100vh - 46px);
overflow: hidden;
@include breakpoint(">782px") {
height: calc(100vh - $grid-unit-40);
}
&__header {
border-bottom: 1px solid $gray-300;
}
&__header-row {
&:first-child {
display: flex;
align-items: center;
height: 60px;
padding: 0 $grid-unit-40;
@include breakpoint(">782px") {
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-gap: $grid-unit-20;
padding: 0 $grid-unit-20;
}
}
&:last-child {
overflow: hidden;
@include breakpoint(">782px") {
display: flex;
justify-content: center;
}
}
}
&__header-title {
@include skeleton();
width: 100%;
height: $grid-unit-30;
@include breakpoint(">782px") {
width: 450px;
}
}
&__header-actions {
display: none;
@include breakpoint(">782px") {
display: flex;
align-items: center;
justify-content: flex-end;
gap: $grid-unit;
}
}
&__header-actions-other,
&__header-actions-main,
&__header-actions-config {
@include skeleton();
height: $grid-unit-30;
}
&__header-actions-other {
width: $grid-unit-60;
}
&__header-actions-main {
width: $grid-unit-80;
}
&__header-actions-config {
width: $grid-unit-30;
}
&__tabs {
height: 46px;
width: fit-content;
display: flex;
align-items: flex-start;
justify-content: center;
gap: $grid-unit-30;
overflow-x: auto;
margin: 0 $grid-unit-40;
}
&__tab-item {
@include skeleton();
width: $grid-unit-80;
height: $grid-unit-20;
margin-top: $grid-unit;
flex-shrink: 0;
}
&__body-tabs-content {
padding-top: $grid-unit-80;
padding-left: $grid-unit-40;
padding-right: $grid-unit-40;
@include breakpoint(">782px") {
padding-left: 0;
padding-right: 0;
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
}
&__block-title {
@include skeleton();
width: 100%;
height: 28px;
margin-bottom: $grid-unit-40;
@include breakpoint(">782px") {
width: 450px;
}
}
&__block-label {
@include skeleton();
width: $grid-unit-80;
margin-bottom: $grid-unit;
}
&__block-input {
@include skeleton();
width: 100%;
height: 36px;
margin-bottom: $grid-unit-30;
}
&__block-textarea {
@include skeleton();
width: 100%;
height: 126px;
margin-bottom: $grid-unit-30;
}
&__block-separator {
border-bottom: 1px solid $gray-300;
width: 100%;
height: 0;
margin: $grid-unit-80 0;
}
}

View File

@ -1,6 +1,7 @@
@import "./variations-actions-menu/styles.scss";
@import "./downloads-menu-item/styles.scss";
@import "./pagination/styles.scss";
@import "./table-empty-state/styles.scss";
$table-row-height: calc($grid-unit * 9);

View File

@ -0,0 +1,2 @@
export * from './table-empty-state';
export * from './types';

View File

@ -0,0 +1,27 @@
.woocommerce-variations-table-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 2px;
border: 1px solid $gray-300;
padding: $grid-unit-40;
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
&__message {
color: $gray-900;
font-size: 13px;
font-weight: 600;
line-height: 16px;
margin: $grid-unit-50 + 2 0 $grid-unit-15 + 2 0;
}
&__actions {
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { TableEmptyStateProps } from './types';
import { EmptyVariationsImage } from '../../../images/empty-variations-image';
export function EmptyTableState( { onActionClick }: TableEmptyStateProps ) {
return (
<div className="woocommerce-variations-table-empty-state">
<EmptyVariationsImage
className="woocommerce-variations-table-empty-state__image"
aria-hidden="true"
/>
<p className="woocommerce-variations-table-empty-state__message">
{ __( 'No variations yet', 'woocommerce' ) }
</p>
<div className="woocommerce-variations-table-empty-state__actions">
<Button variant="link" onClick={ onActionClick }>
{ __( 'Generate from options', 'woocommerce' ) }
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,8 @@
/**
* External dependencies
*/
import { MouseEvent } from 'react';
export type TableEmptyStateProps = {
onActionClick( event: MouseEvent< HTMLButtonElement > ): void;
};

View File

@ -12,6 +12,7 @@ import {
} from '@wordpress/components';
import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
Product,
ProductVariation,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
@ -33,7 +34,7 @@ import { CurrencyContext } from '@woocommerce/currency';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityId } from '@wordpress/core-data';
import { useEntityId, useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
@ -49,6 +50,8 @@ import { useSelection } from '../../hooks/use-selection';
import { VariationsActionsMenu } from './variations-actions-menu';
import HiddenIcon from '../../icons/hidden-icon';
import { Pagination } from './pagination';
import { EmptyTableState } from './table-empty-state';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
@ -128,40 +131,35 @@ export const VariationsTable = forwardRef<
const context = useContext( CurrencyContext );
const { formatAmount } = context;
const { isLoading, latestVariations, isGeneratingVariations } = useSelect(
( select ) => {
const {
getProductVariations,
hasFinishedResolution,
isGeneratingVariations: getIsGeneratingVariations,
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
return {
isLoading: ! hasFinishedResolution( 'getProductVariations', [
requestParams,
] ),
isGeneratingVariations: getIsGeneratingVariations( {
product_id: requestParams.product_id,
} ),
latestVariations:
getProductVariations< ProductVariation[] >( requestParams ),
};
},
[ currentPage, perPage, productId ]
);
const { totalCount } = useSelect(
( select ) => {
const { getProductVariationsTotalCount } = select(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
);
return {
totalCount:
getProductVariationsTotalCount< number >( requestParams ),
};
},
[ productId ]
);
const { isLoading, latestVariations, isGeneratingVariations, totalCount } =
useSelect(
( select ) => {
const {
getProductVariations,
getProductVariationsTotalCount,
hasFinishedResolution,
isGeneratingVariations: getIsGeneratingVariations,
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
return {
isLoading: ! hasFinishedResolution(
'getProductVariations',
[ requestParams ]
),
isGeneratingVariations: getIsGeneratingVariations( {
product_id: requestParams.product_id,
} ),
latestVariations:
getProductVariations< ProductVariation[] >(
requestParams
),
totalCount:
getProductVariationsTotalCount< number >(
requestParams
),
};
},
[ requestParams ]
);
const {
updateProductVariation,
@ -169,6 +167,16 @@ export const VariationsTable = forwardRef<
batchUpdateProductVariations,
invalidateResolution,
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const { invalidateResolution: coreInvalidateResolution } =
useDispatch( 'core' );
const { generateProductVariations } = useProductVariationsHelper();
const [ productAttributes ] = useEntityProp< Product[ 'attributes' ] >(
'postType',
'product',
'attributes'
);
const { createSuccessNotice, createErrorNotice } =
useDispatch( 'core/notices' );
@ -189,6 +197,19 @@ export const VariationsTable = forwardRef<
</div>
);
}
function handleEmptyTableStateActionClick() {
generateProductVariations( productAttributes );
}
if ( ! ( isLoading || isGeneratingVariations ) && totalCount === 0 ) {
return (
<EmptyTableState
onActionClick={ handleEmptyTableStateActionClick }
/>
);
}
// this prevents a weird jump from happening while changing pages.
const variations = latestVariations || lastVariations.current;
@ -246,6 +267,16 @@ export const VariationsTable = forwardRef<
invalidateResolution( 'getProductVariations', [
requestParams,
] );
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product',
productId,
] );
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product_variation',
variationId,
] );
} )
.finally( () => {
setIsUpdating( ( prevState ) => ( {
@ -314,11 +345,24 @@ export const VariationsTable = forwardRef<
delete: values.map( ( { id } ) => id ),
}
)
.then( ( response: VariationResponseProps ) =>
.then( ( response: VariationResponseProps ) => {
invalidateResolution( 'getProductVariations', [
requestParams,
] ).then( () => response )
)
] );
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product',
productId,
] );
values.forEach( ( { id: variationId } ) => {
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product_variation',
variationId,
] );
} );
return response;
} )
.then( ( response: VariationResponseProps ) => {
createSuccessNotice( getSnackbarText( response ) );
onVariationTableChange( 'delete' );
@ -369,50 +413,54 @@ export const VariationsTable = forwardRef<
{ noticeText }
</Notice>
) }
<div className="woocommerce-product-variations__header">
<div className="woocommerce-product-variations__selection">
<CheckboxControl
value="all"
checked={ areAllSelected( variationIds ) }
// @ts-expect-error Property 'indeterminate' does not exist
indeterminate={
! areAllSelected( variationIds ) &&
hasSelection( variationIds )
}
onChange={ onSelectAll( variationIds ) }
/>
</div>
<div className="woocommerce-product-variations__filters">
{ hasSelection( variationIds ) && (
<>
<Button
variant="tertiary"
onClick={ () =>
onSelectAll( variationIds )( true )
}
>
{ __( 'Select all', 'woocommerce' ) }
</Button>
<Button
variant="tertiary"
onClick={ onClearSelection }
>
{ __( 'Clear selection', 'woocommerce' ) }
</Button>
</>
) }
</div>
<div>
<VariationsActionsMenu
selection={ variations.filter( ( variation ) =>
isSelected( variation.id )
{ totalCount > 0 && (
<div className="woocommerce-product-variations__header">
<div className="woocommerce-product-variations__selection">
<CheckboxControl
value="all"
checked={ areAllSelected( variationIds ) }
// @ts-expect-error Property 'indeterminate' does not exist
indeterminate={
! areAllSelected( variationIds ) &&
hasSelection( variationIds )
}
onChange={ onSelectAll( variationIds ) }
/>
</div>
<div className="woocommerce-product-variations__filters">
{ hasSelection( variationIds ) && (
<>
<Button
variant="tertiary"
onClick={ () =>
onSelectAll( variationIds )( true )
}
>
{ __( 'Select all', 'woocommerce' ) }
</Button>
<Button
variant="tertiary"
onClick={ onClearSelection }
>
{ __( 'Clear selection', 'woocommerce' ) }
</Button>
</>
) }
disabled={ ! hasSelection( variationIds ) }
onChange={ handleUpdateAll }
onDelete={ handleDeleteAll }
/>
</div>
<div>
<VariationsActionsMenu
selection={ variations.filter( ( variation ) =>
isSelected( variation.id )
) }
disabled={ ! hasSelection( variationIds ) }
onChange={ handleUpdateAll }
onDelete={ handleDeleteAll }
/>
</div>
</div>
</div>
) }
<Sortable className="woocommerce-product-variations__table">
{ variations.map( ( variation ) => (
<ListItem key={ `${ variation.id }` }>

View File

@ -65,6 +65,8 @@ export function useProductVariationsHelper() {
generateProductVariations: _generateProductVariations,
invalidateResolutionForStoreSelector,
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const { invalidateResolution: coreInvalidateResolution } =
useDispatch( 'core' );
const [ isGenerating, setIsGenerating ] = useState( false );
@ -75,7 +77,7 @@ export function useProductVariationsHelper() {
) => {
setIsGenerating( true );
const { status: lastStatus } = await resolveSelect(
const { status: lastStatus, variations } = await resolveSelect(
'core'
).getEditedEntityRecord< Product >(
'postType',
@ -111,6 +113,20 @@ export function useProductVariationsHelper() {
invalidateResolutionForStoreSelector(
'getProductVariations'
);
if ( variations && variations.length > 0 ) {
for ( const variationId of variations ) {
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product_variation',
variationId,
] );
}
}
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product',
productId,
] );
return invalidateResolutionForStoreSelector(
'getProductVariationsTotalCount'
);

View File

@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import classNames from 'classnames';
export default function SeenIcon( {
width = 24,
height = 24,
className,
...props
}: React.SVGProps< SVGSVGElement > ) {
return (
<svg
{ ...props }
width={ width }
height={ height }
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={ classNames( className, 'woocommerce-hidden-icon' ) }
>
<path
d="M3.99863 13C4.66945 13.3354 4.66932 13.3357 4.66919 13.3359L4.672 13.3305C4.67523 13.3242 4.68086 13.3135 4.6889 13.2985C4.70497 13.2686 4.73062 13.2218 4.76597 13.1608C4.83672 13.0385 4.94594 12.8592 5.09443 12.6419C5.39214 12.2062 5.84338 11.624 6.45337 11.0431C7.6721 9.88241 9.49621 8.75 11.9986 8.75C14.501 8.75 16.3251 9.88241 17.5439 11.0431C18.1539 11.624 18.6051 12.2062 18.9028 12.6419C19.0513 12.8592 19.1605 13.0385 19.2313 13.1608C19.2666 13.2218 19.2923 13.2686 19.3083 13.2985C19.3164 13.3135 19.322 13.3242 19.3252 13.3305L19.3281 13.3359C19.3279 13.3357 19.3278 13.3354 19.9986 13C20.6694 12.6646 20.6693 12.6643 20.6691 12.664L20.6678 12.6614L20.6652 12.6563L20.6573 12.6408C20.6507 12.6282 20.6417 12.6108 20.63 12.5892C20.6068 12.5459 20.5734 12.4852 20.5296 12.4096C20.4422 12.2584 20.3131 12.0471 20.1413 11.7956C19.7984 11.2938 19.2809 10.626 18.5784 9.9569C17.1721 8.61759 14.9962 7.25 11.9986 7.25C9.00105 7.25 6.82516 8.61759 5.41889 9.9569C4.71638 10.626 4.19886 11.2938 3.85596 11.7956C3.68413 12.0471 3.55507 12.2584 3.46762 12.4096C3.42386 12.4852 3.39044 12.5459 3.3672 12.5892C3.35558 12.6108 3.3465 12.6282 3.33994 12.6408L3.33199 12.6563L3.32943 12.6614L3.3285 12.6632C3.32833 12.6635 3.32781 12.6646 3.99863 13ZM11.9986 16C13.9316 16 15.4986 14.433 15.4986 12.5C15.4986 10.567 13.9316 9 11.9986 9C10.0656 9 8.49863 10.567 8.49863 12.5C8.49863 14.433 10.0656 16 11.9986 16Z"
fill="#949494"
/>
</svg>
);
}

View File

@ -33,6 +33,7 @@
@import "components/variation-switcher-footer/variation-switcher-footer.scss";
@import "components/remove-confirmation-modal/style.scss";
@import "components/manage-download-limits-modal/style.scss";
@import "components/product-page-skeleton/styles.scss";
/* Field Blocks */

View File

@ -5,6 +5,7 @@ import { __ } from '@wordpress/i18n';
export type WPErrorCode =
| 'variable_product_no_variation_prices'
| 'product_form_field_error'
| 'product_invalid_sku'
| 'product_create_error'
| 'product_publish_error'
@ -22,6 +23,8 @@ export function getProductErrorMessage( error: WPError ) {
switch ( error.code ) {
case 'variable_product_no_variation_prices':
return error.message;
case 'product_form_field_error':
return error.message;
case 'product_invalid_sku':
return __( 'Invalid or duplicated SKU.', 'woocommerce' );
case 'product_create_error':

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Only initializing background removal when JP connection present.

View File

@ -1,19 +1,16 @@
/**
* External dependencies
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore createRoot included for future compatibility
// eslint-disable-next-line @woocommerce/dependency-group
import { render, createRoot } from '@wordpress/element';
/**
* Internal dependencies
*/
import { BackgroundRemovalLink } from './background-removal-link';
import { getCurrentAttachmentDetails } from './image_utils';
import { FILENAME_APPEND, LINK_CONTAINER_ID } from './constants';
import { renderWrappedComponent } from '../utils';
( () => {
if ( ! window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
return;
}
export const init = () => {
const _previous = wp.media.view.Attachment.Details.prototype;
wp.media.view.Attachment.Details = wp.media.view.Attachment.Details.extend(
@ -22,17 +19,10 @@ export const init = () => {
_previous.initialize.call( this );
setTimeout( () => {
const root = document.body.querySelector(
`#${ LINK_CONTAINER_ID }`
renderWrappedComponent(
BackgroundRemovalLink,
document.body.querySelector( `#${ LINK_CONTAINER_ID }` )
);
if ( ! root ) {
return;
}
if ( createRoot ) {
createRoot( root ).render( <BackgroundRemovalLink /> );
} else {
render( <BackgroundRemovalLink />, root );
}
}, 0 );
},
template( view: { id: number } ) {
@ -54,4 +44,4 @@ export const init = () => {
},
}
);
};
} )();

View File

@ -1,73 +0,0 @@
/**
* External dependencies
*/
import { render, createRoot } from '@wordpress/element';
import { QueryClient, QueryClientProvider } from 'react-query';
/**
* Internal dependencies
*/
import { WriteItForMeButtonContainer } from './product-description';
import { ProductNameSuggestions } from './product-name';
import { ProductCategorySuggestions } from './product-category';
import { WriteShortDescriptionButtonContainer } from './product-short-description';
import setPreferencesPersistence from './utils/preferencesPersistence';
import { init as initBackgroundRemoval } from './image-background-removal';
import './index.scss';
// This sets up loading and saving the plugin's preferences.
setPreferencesPersistence();
initBackgroundRemoval();
const queryClient = new QueryClient();
const renderComponent = ( Component, rootElement ) => {
if ( ! rootElement ) {
return;
}
const WrappedComponent = () => (
<QueryClientProvider client={ queryClient }>
<Component />
</QueryClientProvider>
);
if ( createRoot ) {
createRoot( rootElement ).render( <WrappedComponent /> );
} else {
render( <WrappedComponent />, rootElement );
}
};
const renderProductCategorySuggestions = () => {
const root = document.createElement( 'div' );
root.id = 'woocommerce-ai-app-product-category-suggestions';
renderComponent( ProductCategorySuggestions, root );
// Insert the category suggestions node in the product category meta box.
document.getElementById( 'taxonomy-product_cat' ).append( root );
};
const descriptionButtonRoot = document.getElementById(
'woocommerce-ai-app-product-gpt-button'
);
const nameSuggestionsRoot = document.getElementById(
'woocommerce-ai-app-product-name-suggestions'
);
const shortDescriptionButtonRoot = document.getElementById(
'woocommerce-ai-app-product-short-description-gpt-button'
);
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
renderComponent( WriteItForMeButtonContainer, descriptionButtonRoot );
renderComponent( ProductNameSuggestions, nameSuggestionsRoot );
renderProductCategorySuggestions();
renderComponent(
WriteShortDescriptionButtonContainer,
shortDescriptionButtonRoot
);
}

View File

@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import setPreferencesPersistence from './utils/preferencesPersistence';
import './image-background-removal';
import './product-description';
import './product-short-description';
import './product-name';
import './product-category';
import './index.scss';
// This sets up loading and saving the plugin's preferences.
setPreferencesPersistence();

View File

@ -1 +1,15 @@
export * from './product-category-suggestions';
/**
* Internal dependencies
*/
import { renderWrappedComponent } from '../utils';
import { ProductCategorySuggestions } from './product-category-suggestions';
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
const root = document.createElement( 'div' );
root.id = 'woocommerce-ai-app-product-category-suggestions';
renderWrappedComponent( ProductCategorySuggestions, root );
// Insert the category suggestions node in the product category meta box.
document.getElementById( 'taxonomy-product_cat' )?.append( root );
}

View File

@ -1 +1,12 @@
export * from './product-description-button-container';
/**
* Internal dependencies
*/
import { renderWrappedComponent } from '../utils';
import { WriteItForMeButtonContainer } from './product-description-button-container';
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
renderWrappedComponent(
WriteItForMeButtonContainer,
document.getElementById( 'woocommerce-ai-app-product-gpt-button' )
);
}

View File

@ -1,3 +1,16 @@
/**
* Internal dependencies
*/
import { renderWrappedComponent } from '../utils';
import { ProductNameSuggestions } from './product-name-suggestions';
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
renderWrappedComponent(
ProductNameSuggestions,
document.getElementById( 'woocommerce-ai-app-product-name-suggestions' )
);
}
export * from './product-name-suggestions';
export * from './powered-by-link';
export * from './suggestion-item';

View File

@ -1 +1,14 @@
export * from './product-short-description-button-container';
/**
* Internal dependencies
*/
import { renderWrappedComponent } from '../utils';
import { WriteShortDescriptionButtonContainer } from './product-short-description-button-container';
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
renderWrappedComponent(
WriteShortDescriptionButtonContainer,
document.getElementById(
'woocommerce-ai-app-product-short-description-gpt-button'
)
);
}

View File

@ -6,3 +6,4 @@ export * from './tiny-tools';
export * from './productDataInstructionsGenerator';
export * from './categorySelector';
export * from './htmlEntities';
export * from './renderWrappedComponent';

View File

@ -0,0 +1,32 @@
/**
* External dependencies
*/
import * as elementExports from '@wordpress/element';
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Incorrect type definition for createRoot
const { createRoot, render } = elementExports;
export const renderWrappedComponent = (
Component: React.ComponentType,
rootElement: HTMLElement | null
) => {
if ( ! rootElement ) {
return;
}
const WrappedComponent = () => (
<QueryClientProvider client={ queryClient }>
<Component />
</QueryClientProvider>
);
if ( createRoot ) {
createRoot( rootElement ).render( <WrappedComponent /> );
} else {
render( <WrappedComponent />, rootElement );
}
};

View File

@ -76,12 +76,58 @@ const recordTracksSkipBusinessLocationCompleted = () => {
} );
};
// Temporarily expand the step viewed track for BusinessInfo so that we can include the experiment assignment
// Remove this and change the action back to recordTracksStepViewed when the experiment is over
const recordTracksStepViewedBusinessInfo = (
context: CoreProfilerStateMachineContext,
_event: unknown,
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( 'coreprofiler_step_view', {
step,
email_marketing_experiment_assignment:
context.emailMarketingExperimentAssignment,
wc_version: getSetting( 'wcVersion' ),
} );
};
const recordTracksIsEmailChanged = (
context: CoreProfilerStateMachineContext,
event: BusinessInfoEvent
) => {
if ( context.emailMarketingExperimentAssignment === 'treatment' ) {
let emailSource, isEmailChanged;
if ( context.onboardingProfile.store_email ) {
emailSource = 'onboarding_profile_store_email'; // from previous entry
isEmailChanged =
event.payload.storeEmailAddress !==
context.onboardingProfile.store_email;
} else if ( context.currentUserEmail ) {
emailSource = 'current_user_email'; // from currentUser
isEmailChanged =
event.payload.storeEmailAddress !== context.currentUserEmail;
} else {
emailSource = 'was_empty';
isEmailChanged = event.payload.storeEmailAddress?.length > 0;
}
recordEvent( 'coreprofiler_email_marketing', {
opt_in: event.payload.isOptInMarketing,
email_field_prefilled_source: emailSource,
email_field_modified: isEmailChanged,
} );
}
};
const recordTracksBusinessInfoCompleted = (
_context: CoreProfilerStateMachineContext,
context: CoreProfilerStateMachineContext,
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >
) => {
recordEvent( 'coreprofiler_step_complete', {
step: 'business_info',
email_marketing_experiment_assignment:
context.emailMarketingExperimentAssignment,
wc_version: getSetting( 'wcVersion' ),
} );
@ -92,8 +138,8 @@ const recordTracksBusinessInfoCompleted = (
) === -1,
industry: event.payload.industry,
store_location_previously_set:
_context.onboardingProfile.is_store_country_set || false,
geolocation_success: _context.geolocatedLocation !== undefined,
context.onboardingProfile.is_store_country_set || false,
geolocation_success: context.geolocatedLocation !== undefined,
geolocation_overruled: event.payload.geolocationOverruled,
} );
};
@ -180,4 +226,6 @@ export default {
recordFailedPluginInstallations,
recordSuccessfulPluginInstallation,
recordTracksPluginsInstallationRequest,
recordTracksIsEmailChanged,
recordTracksStepViewedBusinessInfo,
};

View File

@ -31,8 +31,13 @@ import {
GeolocationResponse,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME,
USER_STORE_NAME,
WCUser,
} from '@woocommerce/data';
import { initializeExPlat } from '@woocommerce/explat';
import {
initializeExPlat,
loadExperimentAssignment,
} from '@woocommerce/explat';
import { CountryStateOption } from '@woocommerce/onboarding';
import { getAdminLink } from '@woocommerce/settings';
import CurrencyFactory from '@woocommerce/currency';
@ -99,6 +104,8 @@ export type BusinessInfoEvent = {
industry?: IndustryChoice;
storeLocation: CountryStateOption[ 'key' ];
geolocationOverruled: boolean;
isOptInMarketing: boolean;
storeEmailAddress: string;
};
};
@ -139,6 +146,8 @@ export type OnboardingProfile = {
selling_platforms: SellingPlatform[] | null;
skip?: boolean;
is_store_country_set: boolean | null;
store_email?: string;
is_agree_marketing?: boolean;
};
export type PluginsPageSkippedEvent = {
@ -195,6 +204,8 @@ export type CoreProfilerStateMachineContext = {
persistBusinessInfoRef?: ReturnType< typeof spawn >;
spawnUpdateOnboardingProfileOptionRef?: ReturnType< typeof spawn >;
spawnGeolocationRef?: ReturnType< typeof spawn >;
emailMarketingExperimentAssignment: 'treatment' | 'control';
currentUserEmail: string | undefined;
};
const getAllowTrackingOption = async () =>
@ -309,6 +320,35 @@ const handleOnboardingProfileOption = assign( {
},
} );
const getMarketingOptInExperimentAssignment = async () => {
return loadExperimentAssignment(
`woocommerce_core_profiler_email_marketing_opt_in_2023_Q4_V1`
);
};
const getCurrentUserEmail = async () => {
const currentUser: WCUser< 'email' > = await resolveSelect(
USER_STORE_NAME
).getCurrentUser();
return currentUser?.email;
};
const assignCurrentUserEmail = assign( {
currentUserEmail: (
_context,
event: DoneInvokeEvent< string | undefined >
) => {
if (
event.data &&
event.data.length > 0 &&
event.data !== 'wordpress@example.com' // wordpress default prefilled email address
) {
return event.data;
}
return undefined;
},
} );
const assignOnboardingProfile = assign( {
onboardingProfile: (
_context,
@ -316,6 +356,17 @@ const assignOnboardingProfile = assign( {
) => event.data,
} );
const assignMarketingOptInExperimentAssignment = assign( {
emailMarketingExperimentAssignment: (
_context,
event: DoneInvokeEvent<
Awaited<
ReturnType< typeof getMarketingOptInExperimentAssignment >
>
>
) => event.data.variationName ?? 'control',
} );
const getGeolocation = async ( context: CoreProfilerStateMachineContext ) => {
if ( context.optInDataSharing ) {
return resolveSelect( COUNTRIES_STORE_NAME ).geolocate();
@ -499,6 +550,11 @@ const updateBusinessInfo = async (
...refreshedOnboardingProfile,
is_store_country_set: true,
industry: [ event.payload.industry ],
is_agree_marketing: event.payload.isOptInMarketing,
store_email:
event.payload.storeEmailAddress.length > 0
? event.payload.storeEmailAddress
: null,
},
} );
};
@ -644,6 +700,8 @@ const coreProfilerMachineActions = {
handleCountries,
handleOnboardingProfileOption,
assignOnboardingProfile,
assignMarketingOptInExperimentAssignment,
assignCurrentUserEmail,
persistBusinessInfo,
spawnUpdateOnboardingProfileOption,
redirectToWooHome,
@ -657,6 +715,8 @@ const coreProfilerMachineServices = {
getCountries,
getGeolocation,
getOnboardingProfileOption,
getMarketingOptInExperimentAssignment,
getCurrentUserEmail,
getPlugins,
browserPopstateHandler,
updateBusinessInfo,
@ -693,6 +753,8 @@ export const coreProfilerStateMachineDefinition = createMachine( {
loader: {},
onboardingProfile: {} as OnboardingProfile,
jetpackAuthUrl: undefined,
emailMarketingExperimentAssignment: 'control',
currentUserEmail: undefined,
} as CoreProfilerStateMachineContext,
states: {
navigate: {
@ -1026,6 +1088,45 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
},
marketingOptInExperiment: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'getMarketingOptInExperimentAssignment',
onDone: {
target: 'done',
actions: [
'assignMarketingOptInExperimentAssignment',
],
},
},
},
done: { type: 'final' },
},
},
currentUserEmail: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'getCurrentUserEmail',
onDone: {
target: 'done',
actions: [
'assignCurrentUserEmail',
],
},
onError: {
target: 'done',
},
},
},
done: {
type: 'final',
},
},
},
},
// onDone is reached when child parallel states fo fetching are resolved (reached final states)
onDone: {
@ -1039,14 +1140,17 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
entry: [
{
type: 'recordTracksStepViewed',
type: 'recordTracksStepViewedBusinessInfo',
step: 'business_info',
},
],
on: {
BUSINESS_INFO_COMPLETED: {
target: 'postBusinessInfo',
actions: [ 'recordTracksBusinessInfoCompleted' ],
actions: [
'recordTracksBusinessInfoCompleted',
'recordTracksIsEmailChanged',
],
},
},
},

View File

@ -2,7 +2,13 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, TextControl, Notice, Spinner } from '@wordpress/components';
import {
Button,
TextControl,
Notice,
Spinner,
CheckboxControl,
} from '@wordpress/components';
import { SelectControl } from '@woocommerce/components';
import { Icon, chevronDown } from '@wordpress/icons';
import {
@ -83,9 +89,18 @@ export type BusinessInfoContextProps = Pick<
> & {
onboardingProfile: Pick<
CoreProfilerStateMachineContext[ 'onboardingProfile' ],
'industry' | 'business_choice' | 'is_store_country_set'
| 'industry'
| 'business_choice'
| 'is_store_country_set'
| 'is_agree_marketing'
| 'store_email'
>;
} & Partial<
Pick<
CoreProfilerStateMachineContext,
'emailMarketingExperimentAssignment' | 'currentUserEmail'
>
>;
};
export const BusinessInfo = ( {
context,
@ -105,7 +120,11 @@ export const BusinessInfo = ( {
is_store_country_set: isStoreCountrySet,
industry: industryFromOnboardingProfile,
business_choice: businessChoiceFromOnboardingProfile,
is_agree_marketing: isOptInMarketingFromOnboardingProfile,
store_email: storeEmailAddressFromOnboardingProfile,
},
emailMarketingExperimentAssignment,
currentUserEmail,
} = context;
const [ storeName, setStoreName ] = useState(
@ -176,6 +195,14 @@ export const BusinessInfo = ( {
const [ hasSubmitted, setHasSubmitted ] = useState( false );
const [ storeEmailAddress, setEmailAddress ] = useState(
storeEmailAddressFromOnboardingProfile || currentUserEmail || ''
);
const [ isOptInMarketing, setIsOptInMarketing ] = useState< boolean >(
isOptInMarketingFromOnboardingProfile || false
);
return (
<div
className="woocommerce-profiler-business-information"
@ -345,12 +372,55 @@ export const BusinessInfo = ( {
</ul>
</Notice>
) }
{ emailMarketingExperimentAssignment === 'treatment' && (
<>
<TextControl
className="woocommerce-profiler-business-info-email-adddress"
onChange={ ( value ) => {
setEmailAddress( value );
} }
value={ decodeEntities( storeEmailAddress ) }
label={
<>
{ __(
'Your email address',
'woocommerce'
) }
{ isOptInMarketing && (
<span className="woocommerce-profiler-question-required">
{ '*' }
</span>
) }
</>
}
placeholder={ __(
'wordpress@example.com',
'woocommerce'
) }
/>
<CheckboxControl
className="core-profiler__checkbox"
label={ __(
'Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox.',
'woocommerce'
) }
checked={ isOptInMarketing }
onChange={ setIsOptInMarketing }
/>
</>
) }
</form>
<div className="woocommerce-profiler-button-container">
<Button
className="woocommerce-profiler-button"
variant="primary"
disabled={ ! storeCountry.key }
disabled={
! storeCountry.key ||
( emailMarketingExperimentAssignment ===
'treatment' &&
isOptInMarketing &&
storeEmailAddress.length === 0 )
}
onClick={ () => {
sendEvent( {
type: 'BUSINESS_INFO_COMPLETED',
@ -360,6 +430,8 @@ export const BusinessInfo = ( {
storeLocation: storeCountry.key,
geolocationOverruled:
geolocationOverruled || false,
isOptInMarketing,
storeEmailAddress,
},
} );
setHasSubmitted( true );

View File

@ -173,6 +173,8 @@ describe( 'BusinessInfo', () => {
industry: 'other',
storeLocation: 'AU:VIC',
storeName: '',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
@ -224,6 +226,8 @@ describe( 'BusinessInfo', () => {
industry: 'other',
storeLocation: 'AW',
storeName: '',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
@ -273,6 +277,8 @@ describe( 'BusinessInfo', () => {
industry: 'food_and_drink',
storeLocation: 'AU:VIC',
storeName: 'Test Store Name',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
@ -301,8 +307,106 @@ describe( 'BusinessInfo', () => {
industry: 'food_and_drink',
storeLocation: 'AU:VIC',
storeName: 'Test Store Name',
isOptInMarketing: false,
storeEmailAddress: '',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
} );
describe( 'business info page, email marketing variant', () => {
beforeEach( () => {
props.context.emailMarketingExperimentAssignment = 'treatment';
} );
it( 'should correctly render the experiment variant with the email field', () => {
render( <BusinessInfo { ...props } /> );
expect(
screen.getByText( /Your email address/i )
).toBeInTheDocument();
} );
it( 'should not disable the continue field when experiment variant is shown, opt in checkbox is not checked and email field is empty', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
expect( continueButton ).not.toBeDisabled();
} );
it( 'should disable the continue field when experiment variant is shown, opt in checkbox is checked and email field is empty', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const checkbox = screen.getByRole( 'checkbox', {
name: /Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox./i,
} );
userEvent.click( checkbox );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
expect( continueButton ).toBeDisabled();
} );
it( 'should correctly send event with opt-in true when experiment variant is shown, opt in checkbox is checked and email field is filled', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const checkbox = screen.getByRole( 'checkbox', {
name: /Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox./i,
} );
userEvent.click( checkbox );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
userEvent.type( emailInput, 'wordpress@automattic.com' );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
userEvent.click( continueButton );
expect( props.sendEvent ).toHaveBeenCalledWith( {
payload: {
geolocationOverruled: false,
industry: 'other',
storeLocation: 'AW',
storeName: '',
isOptInMarketing: true,
storeEmailAddress: 'wordpress@automattic.com',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
} );
it( 'should correctly prepopulate the email field if populated in the onboarding profile', () => {
props.context.onboardingProfile.store_email =
'wordpress@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'wordpress@automattic.com' );
} );
it( 'should correctly prepopulate the email field if populated in the current user', () => {
props.context.currentUserEmail = 'currentUser@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'currentUser@automattic.com' );
} );
it( 'should correctly favor the onboarding profile email over the current user email', () => {
props.context.currentUserEmail = 'currentUser@automattic.com';
props.context.onboardingProfile.store_email =
'wordpress@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'wordpress@automattic.com' );
} );
} );
} );

View File

@ -419,6 +419,8 @@
}
.woocommerce-profiler-question-label,
.woocommerce-profiler-business-info-email-adddress
.components-base-control__label,
.woocommerce-profiler-business-info-store-name
.components-base-control__label {
text-transform: uppercase;
@ -430,11 +432,15 @@
}
.woocommerce-profiler-question-label
.woocommerce-profiler-question-required,
.woocommerce-profiler-business-info-email-adddress
.woocommerce-profiler-question-required {
color: #cc1818;
padding-left: 3px;
}
.woocommerce-profiler-business-info-email-adddress
.components-text-control__input,
.woocommerce-profiler-business-info-store-name
.components-text-control__input {
height: 40px;
@ -448,6 +454,29 @@
}
}
.woocommerce-profiler-select-control__country-spacer + .woocommerce-profiler-business-info-email-adddress {
margin-top: 8px;
}
.woocommerce-profiler-business-info-email-adddress {
margin-top: 20px;
}
.core-profiler__checkbox {
margin-top: 4px;
.components-checkbox-control__input-container {
margin-right: 16px;
}
.components-checkbox-control__label {
color: $gray-700;
font-size: 12px;
line-height: 16px;
font-weight: 400;
}
}
.woocommerce-profiler-select-control__industry {
margin-bottom: 20px;
}

View File

@ -60,7 +60,7 @@ function ScaledBlockPreview( {
isNavigable = false,
isScrollable = true,
}: ScaledBlockPreviewProps ) {
const { setLogoBlock } = useContext( LogoBlockContext );
const { setLogoBlockIds } = useContext( LogoBlockContext );
const [ fontFamilies ] = useGlobalSetting(
'typography.fontFamilies.theme'
) as [ FontFamily[] ];
@ -182,18 +182,18 @@ function ScaledBlockPreview( {
// Get the current logo block client ID from DOM and set it in the logo block context. This is used for the logo settings. See: ./sidebar/sidebar-navigation-screen-logo.tsx
// Ideally, we should be able to get the logo block client ID from the block editor store but it is not available.
// We should update this code once the there is a selector in the block editor store that can be used to get the logo block client ID.
const siteLogo = bodyElement.querySelector(
const siteLogos = bodyElement.querySelectorAll(
'.wp-block-site-logo'
);
const blockClientId = siteLogo
? siteLogo.getAttribute( 'data-block' )
: null;
setLogoBlock( {
clientId: blockClientId,
isLoading: false,
} );
const logoBlockIds = Array.from( siteLogos )
.map( ( siteLogo ) => {
return siteLogo.getAttribute(
'data-block'
);
} )
.filter( Boolean ) as string[];
setLogoBlockIds( logoBlockIds );
if ( isNavigable ) {
enableNavigation();
@ -221,10 +221,7 @@ function ScaledBlockPreview( {
return () => {
observer.disconnect();
possiblyRemoveAllListeners();
setLogoBlock( {
clientId: null,
isLoading: true,
} );
setLogoBlockIds( [] );
};
},
[ isNavigable ]

View File

@ -53,13 +53,7 @@ const { useGlobalStyle } = unlock( blockEditorPrivateApis );
const ANIMATION_DURATION = 0.5;
export const Layout = () => {
const [ logoBlock, setLogoBlock ] = useState< {
clientId: string | null;
isLoading: boolean;
} >( {
clientId: null,
isLoading: true,
} );
const [ logoBlockIds, setLogoBlockIds ] = useState< Array< string > >( [] );
// This ensures the edited entity id and type are initialized properly.
useInitEditedEntityFromURL();
const { shouldTourBeShown, ...onboardingTourProps } = useOnboardingTour();
@ -97,8 +91,8 @@ export const Layout = () => {
return (
<LogoBlockContext.Provider
value={ {
logoBlock,
setLogoBlock,
logoBlockIds,
setLogoBlockIds,
} }
>
<HighlightedBlockContextProvider>

View File

@ -4,18 +4,9 @@
import { createContext } from '@wordpress/element';
export const LogoBlockContext = createContext< {
logoBlock: {
clientId: string | null;
isLoading: boolean;
};
setLogoBlock: ( newBlock: {
clientId: string | null;
isLoading: boolean;
} ) => void;
logoBlockIds: Array< string >;
setLogoBlockIds: ( clientIds: Array< string > ) => void;
} >( {
logoBlock: {
clientId: null,
isLoading: false,
},
setLogoBlock: () => {},
logoBlockIds: [],
setLogoBlockIds: () => {},
} );

View File

@ -248,68 +248,6 @@ export const COLOR_PALETTES = [
},
wpcom_category: 'Neutral',
},
{
title: 'Lemon Myrtle',
version: 2,
settings: {
color: {
palette: {
theme: [
{
color: '#3E7172',
name: 'Primary',
slug: 'primary',
},
{
color: '#FC9B00',
name: 'Secondary',
slug: 'secondary',
},
{
color: '#325C5D',
name: 'Foreground',
slug: 'foreground',
},
{
color: '#ffffff',
name: 'Background',
slug: 'background',
},
{
color: '#E3F2EF',
name: 'Tertiary',
slug: 'tertiary',
},
],
},
},
},
styles: {
color: {
background: 'var(--wp--preset--color--background)',
text: 'var(--wp--preset--color--foreground)',
},
elements: {
button: {
color: {
background: 'var(--wp--preset--color--primary)',
text: 'var(--wp--preset--color--foreground)',
},
},
link: {
color: {
text: 'var(--wp--preset--color--secondary)',
},
':hover': {
color: {
text: 'var(--wp--preset--color--foreground)',
},
},
},
},
},
wpcom_category: 'Neutral',
},
{
title: 'Green Thumb',
version: 2,

View File

@ -367,9 +367,7 @@ const LogoEdit = ( {
export const SidebarNavigationScreenLogo = () => {
// Get the current logo block client ID and attributes. These are used for the logo settings.
const {
logoBlock: { clientId, isLoading: isLogoBlockLoading },
} = useContext( LogoBlockContext );
const { logoBlockIds } = useContext( LogoBlockContext );
const {
attributes,
@ -381,7 +379,7 @@ export const SidebarNavigationScreenLogo = () => {
( select ) => {
const logoBlocks =
// @ts-ignore No types for this exist yet.
select( blockEditorStore ).getBlocksByClientId( clientId );
select( blockEditorStore ).getBlocksByClientId( logoBlockIds );
const _isAttributesLoading =
! logoBlocks.length || logoBlocks[ 0 ] === null;
@ -398,7 +396,7 @@ export const SidebarNavigationScreenLogo = () => {
isAttributesLoading: _isAttributesLoading,
};
},
[ clientId ]
[ logoBlockIds ]
);
const { siteLogoId, canUserEdit, mediaItemData, isRequestingMediaItem } =
@ -441,16 +439,16 @@ export const SidebarNavigationScreenLogo = () => {
// @ts-ignore No types for this exist yet.
const { updateBlockAttributes } = useDispatch( blockEditorStore );
const setAttributes = ( newAttributes: LogoAttributes ) => {
if ( ! clientId ) {
if ( ! logoBlockIds.length ) {
return;
}
updateBlockAttributes( clientId, newAttributes );
logoBlockIds.forEach( ( clientId ) =>
updateBlockAttributes( clientId, newAttributes )
);
};
const isLoading =
siteLogoId === undefined ||
isRequestingMediaItem ||
isLogoBlockLoading ||
isAttributesLoading;
return (

View File

@ -21,7 +21,8 @@ import { decodeEntities } from '@wordpress/html-entities';
import { forwardRef } from '@wordpress/element';
// @ts-ignore No types for this exist yet.
import SiteIcon from '@wordpress/edit-site/build-module/components/site-icon';
import { getNewPath } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
@ -93,7 +94,12 @@ export const SiteHub = forwardRef(
ease: 'easeOut',
} }
>
<SiteIcon className="edit-site-layout__view-mode-toggle-icon" />
<Link
href={ getNewPath( {}, '/', {} ) }
type="wp-admin"
>
<SiteIcon className="edit-site-layout__view-mode-toggle-icon" />
</Link>
</motion.div>
<AnimatePresence>

View File

@ -53,6 +53,12 @@
}
}
.woocommerce-customize-store {
.edit-site-site-hub__view-mode-toggle-container a {
color: unset;
}
}
.woocommerce-customize-store__step-assemblerHub {
a {
text-decoration: none;
@ -84,6 +90,7 @@
padding-bottom: 0;
gap: 0;
width: 348px;
z-index: 2;
}
.edit-site-sidebar-navigation-screen-patterns__group-header {
@ -445,7 +452,7 @@
/* Layout sidebar */
.block-editor-block-patterns-list__item {
.block-editor-block-preview__container {
border-radius: 2px;
border-radius: 4px;
border: 1.5px solid transparent;
}

View File

@ -40,14 +40,6 @@ const colorChoices: ColorPalette[] = [
background: '#ffffff',
lookAndFeel: [ 'Bold' ] as Look[],
},
{
name: 'Lemon Myrtle',
primary: '#3E7172',
secondary: '#FC9B00',
foreground: '#325C5D',
background: '#ffffff',
lookAndFeel: [ 'Contemporary' ] as Look[],
},
{
name: 'Green Thumb',
primary: '#164A41',

View File

@ -7,26 +7,29 @@ import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
import { withDispatch, withSelect } from '@wordpress/data';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
import { ONBOARDING_STORE_NAME } from '@woocommerce/data';
/**
* Button redirecting to Jetpack auth flow.
*
* Only render this component when the user has accepted Jetpack's Terms of Service.
* The API endpoint used by this component sets "jetpack_tos_agreed" to true when
* returning the URL.
*/
export class Connect extends Component {
constructor( props ) {
super( props );
this.state = {
isConnecting: false,
isAwaitingRedirect: false,
isRedirecting: false,
};
this.connectJetpack = this.connectJetpack.bind( this );
props.setIsPending( true );
props.setIsPending( false );
}
componentDidUpdate( prevProps ) {
const { createNotice, error, isRequesting, onError, setIsPending } =
this.props;
if ( prevProps.isRequesting && ! isRequesting ) {
setIsPending( false );
}
const { createNotice, error, onError, isRequesting } = this.props;
if ( error && error !== prevProps.error ) {
if ( onError ) {
@ -34,37 +37,35 @@ export class Connect extends Component {
}
createNotice( 'error', error );
}
if (
this.state.isAwaitingRedirect &&
! this.state.isRedirecting &&
! isRequesting &&
! error
) {
this.setState( { isRedirecting: true }, () => {
window.location = this.props.jetpackAuthUrl;
} );
}
}
async connectJetpack() {
const { jetpackConnectUrl, onConnect } = this.props;
connectJetpack() {
const { onConnect } = this.props;
this.setState(
{
isConnecting: true,
},
() => {
if ( onConnect ) {
onConnect();
}
window.location = jetpackConnectUrl;
}
);
if ( onConnect ) {
onConnect();
}
this.setState( { isAwaitingRedirect: true } );
}
render() {
const {
hasErrors,
isRequesting,
onSkip,
skipText,
onAbort,
abortText,
} = this.props;
const { error, onSkip, skipText, onAbort, abortText } = this.props;
return (
<Fragment>
{ hasErrors ? (
{ error ? (
<Button
isPrimary
onClick={ () => window.location.reload() }
@ -73,8 +74,7 @@ export class Connect extends Component {
</Button>
) : (
<Button
disabled={ isRequesting }
isBusy={ this.state.isConnecting }
isBusy={ this.state.isAwaitingRedirect }
isPrimary
onClick={ this.connectJetpack }
>
@ -103,26 +103,24 @@ Connect.propTypes = {
createNotice: PropTypes.func.isRequired,
/**
* Human readable error message.
*
* Also used to determine if the "Retry" button should be displayed.
*/
error: PropTypes.string,
/**
* Bool to determine if the "Retry" button should be displayed.
*/
hasErrors: PropTypes.bool,
/**
* Bool to check if the connection URL is still being requested.
*/
isRequesting: PropTypes.bool,
/**
* Generated Jetpack connection URL.
* Generated Jetpack authentication URL.
*/
jetpackConnectUrl: PropTypes.string,
jetpackAuthUrl: PropTypes.string,
/**
* Called before the redirect to Jetpack.
*/
onConnect: PropTypes.func,
/**
* Called when the plugin has an error retrieving the jetpackConnectUrl.
* Called when the plugin has an error retrieving the jetpackAuthUrl.
*/
onError: PropTypes.func,
/**
@ -157,20 +155,32 @@ Connect.defaultProps = {
export default compose(
withSelect( ( select, props ) => {
const { getJetpackConnectUrl, isPluginsRequesting, getPluginsError } =
select( PLUGINS_STORE_NAME );
const { getJetpackAuthUrl, isResolving } = select(
ONBOARDING_STORE_NAME
);
const queryArgs = {
redirect_url: props.redirectUrl || window.location.href,
redirectUrl: props.redirectUrl || window.location.href,
from: 'woocommerce-services',
};
const isRequesting = isPluginsRequesting( 'getJetpackConnectUrl' );
const error = getPluginsError( 'getJetpackConnectUrl' ) || '';
const jetpackConnectUrl = getJetpackConnectUrl( queryArgs );
const jetpackAuthUrlResponse = getJetpackAuthUrl( queryArgs );
const isRequesting = isResolving( 'getJetpackAuthUrl', [ queryArgs ] );
let error;
if ( ! isResolving && ! jetpackAuthUrlResponse ) {
error = __( 'Error requesting connection URL.', 'woocommerce' );
}
if ( jetpackAuthUrlResponse?.errors?.length ) {
error = jetpackAuthUrlResponse?.errors[ 0 ];
}
return {
error,
isRequesting,
jetpackConnectUrl,
jetpackAuthUrl: jetpackAuthUrlResponse.url,
};
} ),
withDispatch( ( dispatch ) => {

View File

@ -16,6 +16,7 @@ import {
isWCAdmin,
} from '@woocommerce/navigation';
import { Spinner } from '@woocommerce/components';
import { ProductPageSkeleton } from '@woocommerce/product-editor';
/**
* Internal dependencies
@ -207,6 +208,7 @@ export const getPages = () => {
if ( isFeatureEnabled( 'product_block_editor' ) ) {
const productPage = {
container: ProductPage,
fallback: ProductPageSkeleton,
layout: {
header: false,
},
@ -272,6 +274,10 @@ export const getPages = () => {
if ( window.wcAdminFeatures[ 'product-variation-management' ] ) {
pages.push( {
container: ProductVariationPage,
fallback: ProductPageSkeleton,
layout: {
header: false,
},
path: '/product/:productId/variation/:variationId',
breadcrumbs: [
[ '/edit-product', __( 'Product', 'woocommerce' ) ],
@ -280,9 +286,6 @@ export const getPages = () => {
navArgs: {
id: 'woocommerce-edit-product',
},
layout: {
header: false,
},
wpOpenMenu: 'menu-posts-product',
capability: 'edit_products',
} );
@ -451,8 +454,19 @@ export const Controller = ( { ...props } ) => {
window.wpNavMenuUrlUpdate( query );
window.wpNavMenuClassChange( page, url );
function getFallback() {
return page.fallback ? (
<page.fallback />
) : (
<div className="woocommerce-layout__loading">
<Spinner />
</div>
);
}
return (
<Suspense fallback={ <Spinner /> }>
<Suspense fallback={ getFallback() }>
<page.container
params={ params }
path={ url }

View File

@ -190,6 +190,15 @@ function _Layout( {
const query = getQuery();
useEffect( () => {
const wpbody = document.getElementById( 'wpbody' );
if ( showHeader ) {
wpbody?.classList.remove( 'no-header' );
} else {
wpbody?.classList.add( 'no-header' );
}
}, [ showHeader ] );
return (
<LayoutContextProvider
value={ getLayoutContextValue( [

View File

@ -1,6 +1,17 @@
.woocommerce-layout {
margin: 0;
padding: 0;
&__loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
.woocommerce-layout__no-match {
@ -18,7 +29,7 @@
margin: $gutter-large 0 128px $fallback-gutter-large;
margin: $gutter-large 0 128px $gutter-large;
@include breakpoint( '<782px' ) {
@include breakpoint('<782px') {
margin-top: 20px;
}
}
@ -33,6 +44,18 @@
.update-nag {
display: none;
}
#wpbody {
display: block;
&.no-header {
margin-top: 0;
.woocommerce-layout__primary {
margin-top: 0;
}
}
}
}
.woocommerce-admin-is-loading {
@ -104,7 +127,7 @@
.wp-toolbar .is-wp-toolbar-disabled {
margin-top: -$adminbar-height;
@include breakpoint( '<600px' ) {
@include breakpoint('<600px') {
margin-top: -$adminbar-height-mobile;
}
}

View File

@ -1,14 +1,6 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2483_9556)">
<path d="M15.9776 43.968C11.1189 43.968 5.34584 44.544 2.95838 46.928C2.95838 46.928 2.95838 46.928 2.95439 46.932C2.95439 46.932 2.95439 46.932 2.95039 46.936C0.574906 49.332 -9.13044e-07 55.112 -7.00082e-07 59.984C-4.8712e-07 64.856 0.574907 70.64 2.95039 73.032C2.95039 73.032 2.95039 73.032 2.95439 73.036C2.95439 73.036 2.95438 73.036 2.95838 73.04C5.34584 75.424 11.1189 76 15.9776 76C20.8364 76 26.6094 75.424 28.9969 73.04C28.9969 73.04 28.9969 73.04 29.0009 73.036C29.0049 73.032 29.0009 73.036 29.0049 73.032C31.3804 70.636 31.9553 64.856 31.9553 59.984C31.9553 55.112 31.3804 49.328 29.0049 46.936C29.0049 46.936 29.0049 46.936 29.0009 46.932C28.9969 46.928 29.0009 46.932 28.9969 46.928C26.6094 44.544 20.8364 43.968 15.9776 43.968Z" fill="#757575"/>
<path d="M63.9776 3.968C59.1189 3.968 53.3458 4.544 50.9584 6.928C50.9584 6.928 50.9584 6.928 50.9544 6.932C50.9544 6.932 50.9544 6.932 50.9504 6.936C48.5749 9.332 48 15.112 48 19.984C48 24.856 48.5749 30.64 50.9504 33.032C50.9504 33.032 50.9504 33.032 50.9544 33.036C50.9544 33.036 50.9544 33.036 50.9584 33.04C53.3458 35.424 59.1189 36 63.9776 36C68.8364 36 74.6094 35.424 76.9969 33.04C76.9969 33.04 76.9969 33.04 77.0009 33.036C77.0049 33.032 77.0009 33.036 77.0049 33.032C79.3804 30.636 79.9553 24.856 79.9553 19.984C79.9553 15.112 79.3804 9.328 77.0049 6.936C77.0049 6.936 77.0049 6.936 77.0009 6.932C76.9969 6.928 77.0009 6.932 76.9969 6.92799C74.6094 4.544 68.8364 3.968 63.9776 3.968Z" fill="#757575"/>
<path d="M40 60C40 64.8656 40.7193 70.6467 43.6963 73.0375C43.6963 73.0375 43.6963 73.0375 43.7013 73.0415C43.7013 73.0415 43.7013 73.0415 43.7063 73.0455C46.6983 75.4243 53.9161 76 60 76C66.0839 76 73.3067 75.4243 76.2937 73.0455C76.2937 73.0455 76.2937 73.0455 76.2987 73.0415C76.2987 73.0415 76.2987 73.0415 76.3037 73.0375C79.2807 70.6467 80 64.8656 80 60C80 55.1344 79.2807 49.3533 76.3037 46.9625C76.3037 46.9625 76.3037 46.9625 76.2987 46.9585C76.2937 46.9545 76.2987 46.9585 76.2937 46.9545C73.3017 44.5757 66.0839 44 60 44C53.9161 44 46.6933 44.5757 43.7063 46.9545C43.7063 46.9545 43.7063 46.9545 43.7013 46.9585C43.6963 46.9625 43.7013 46.9585 43.6963 46.9625C40.7193 49.3533 40 55.1344 40 60Z" fill="#E0E0E0"/>
<path d="M-1.39876e-06 20C-9.73403e-07 24.8656 0.719276 30.6467 3.6963 33.0375C3.6963 33.0375 3.6963 33.0375 3.7013 33.0415C3.7013 33.0415 3.7013 33.0415 3.70629 33.0455C6.6983 35.4243 13.9161 36 20 36C26.0839 36 33.3067 35.4243 36.2937 33.0455C36.2937 33.0455 36.2937 33.0455 36.2987 33.0415C36.2987 33.0415 36.2987 33.0415 36.3037 33.0375C39.2807 30.6467 40 24.8656 40 20C40 15.1344 39.2807 9.35332 36.3037 6.96251C36.3037 6.96251 36.3037 6.96251 36.2987 6.95852C36.2937 6.95452 36.2987 6.95852 36.2937 6.95452C33.3017 4.57571 26.0839 4 20 4C13.9161 4 6.6933 4.57571 3.70629 6.95452C3.70629 6.95452 3.70629 6.95452 3.7013 6.95852C3.6963 6.96252 3.70129 6.95852 3.6963 6.96252C0.719274 9.35332 -1.82413e-06 15.1344 -1.39876e-06 20Z" fill="#E0E0E0"/>
<path d="M38.5095 14.0378C40.9729 11.6218 45.2004 11.3695 45.2004 10.4C45.2004 9.43044 40.9729 9.1782 38.5095 6.76221C36.0461 4.34621 35.789 0.199998 34.8004 0.199998C33.8118 0.199998 33.5546 4.34621 31.0913 6.76221C28.6279 9.1782 24.4004 9.43044 24.4004 10.4C24.4004 11.3695 28.6279 11.6218 31.0913 14.0378C33.5546 16.4538 33.8118 20.6 34.8004 20.6C35.789 20.6 36.0461 16.4538 38.5095 14.0378Z" fill="#757575"/>
</g>
<defs>
<clipPath id="clip0_2483_9556">
<rect width="80" height="80" fill="white" transform="translate(0 80) rotate(-90)"/>
</clipPath>
</defs>
<path d="M70.8227 17.1683C73.8744 17.1683 77.2598 18.623 80 20.1745V13.4403C80 9.31419 77.664 7 73.5407 7H6.45562C2.33604 6.99628 0 9.31047 0 13.4366V47.3717C0 51.4978 2.33604 53.812 6.45933 53.812H29.5824C28.1214 56.4871 26.8014 59.7091 26.8014 62.626C26.8014 66.6553 28.1214 69.3676 30.7207 71.3916C32.9455 73.1254 35.9898 73.9662 39.9981 73.9662C44.0065 73.9662 47.0508 73.1254 49.2756 71.3916C51.8749 69.3676 53.1949 66.659 53.1949 62.626C53.1949 59.7053 51.8749 56.4871 50.4139 53.812H73.537C77.6603 53.812 79.9963 51.4978 79.9963 47.3717V40.645C77.2561 42.1964 73.8744 43.6512 70.819 43.6512C66.8032 43.6512 64.1001 42.3266 62.083 39.7185C60.355 37.4862 59.517 34.4316 59.517 30.4097C59.517 26.3878 60.355 23.3332 62.083 21.1009C64.1001 18.4928 66.7995 17.1683 70.819 17.1683H70.8227Z" fill="#F0F0F0"/>
<path d="M39.9865 20.4834H6.6145V23.8321H39.9865V20.4834Z" fill="#DDDDDD"/>
<path d="M39.9865 13.7158H6.6145V17.0645H39.9865V13.7158Z" fill="#DDDDDD"/>
<path d="M26.6377 27.1804H6.6145V30.5291H26.6377V27.1804Z" fill="#DDDDDD"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,25 +1,17 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2584_6481)">
<path d="M79.9999 0H40V39.9999H79.9999V0Z" fill="#757575"/>
<path d="M71.4366 14.3426C74.7128 14.3426 77.3687 11.6867 77.3687 8.41043C77.3687 5.13419 74.7128 2.47827 71.4366 2.47827C68.1603 2.47827 65.5044 5.13419 65.5044 8.41043C65.5044 11.6867 68.1603 14.3426 71.4366 14.3426Z" fill="#E0E0E0"/>
<path d="M70.8753 24.5383C65.3379 24.5383 62.8872 32.6033 59.0083 32.6033C54.9361 32.6033 56.7333 15.4783 50.9219 15.4783C46.9533 15.4783 41.9711 26.1877 40 33.159V40H43.6031C43.626 39.927 45.908 26.8388 50.3351 26.9083C53.8943 26.9639 53.9435 36.1975 57.9296 36.6593C62.1283 37.1472 63.0067 31.0476 68.4492 31.0476C73.2785 31.0476 75.6994 40 75.6994 40H79.9999V35.3189C79.9999 33.4455 76.4126 24.54 70.877 24.54L70.8753 24.5383Z" fill="#E0E0E0"/>
<path d="M25.9218 50H4.07821C1.82588 50 0 51.8259 0 54.0782V75.9218C0 78.1741 1.82588 80 4.07821 80H25.9218C28.1741 80 30 78.1741 30 75.9218V54.0782C30 51.8259 28.1741 50 25.9218 50Z" fill="#E0E0E0"/>
<path d="M14.5919 69.7092L13.4895 66.928H7.28028L6.17781 69.7092H4.40088L9.43142 57.2632H11.3585L16.3711 69.7092H14.5942H14.5919ZM10.3837 58.8505L7.74635 65.5687H13.0391L10.3837 58.8505Z" fill="#757575"/>
<path d="M23.5888 69.7093V68.6831C22.8606 69.5036 21.8119 69.9328 20.5974 69.9328C19.0826 69.9328 17.4558 68.9066 17.4558 66.9482C17.4558 64.9897 19.0647 63.9815 20.5974 63.9815C21.832 63.9815 22.8606 64.3727 23.5888 65.1932V63.5701C23.5888 62.3762 22.6163 61.6854 21.3077 61.6854C20.2232 61.6854 19.3448 62.0588 18.5403 62.9352L17.886 61.9649C18.8585 60.9566 20.017 60.4714 21.4959 60.4714C23.423 60.4714 24.9938 61.3299 24.9938 63.512V69.707H23.5911L23.5888 69.7093ZM23.5888 67.8067V66.0897C23.0465 65.3609 22.092 64.9897 21.1195 64.9897C19.7907 64.9897 18.8742 65.8102 18.8742 66.9482C18.8742 68.0862 19.7907 68.9268 21.1195 68.9268C22.092 68.9268 23.0465 68.5534 23.5888 67.8067Z" fill="#757575"/>
<circle cx="15.7997" cy="9.52504" r="3.52504" transform="rotate(90 15.7997 9.52504)" fill="#E0E0E0"/>
<circle cx="15.7997" cy="21.1237" r="3.52504" transform="rotate(90 15.7997 21.1237)" fill="#E0E0E0"/>
<circle cx="15.7997" cy="32.7223" r="3.52504" transform="rotate(90 15.7997 32.7223)" fill="#757575"/>
<circle cx="15.7994" cy="32.7216" r="5.68555" transform="rotate(90 15.7994 32.7216)" stroke="#271B3D" stroke-width="0.227422"/>
<path d="M75.001 80L75.001 50L70.001 50L70.001 80L75.001 80Z" fill="#757575"/>
<path d="M62.001 80L62.001 50L57.001 50L57.001 80L62.001 80Z" fill="#757575"/>
<path d="M49.0002 80L49.0002 50L44.0002 50L44.0002 80L49.0002 80Z" fill="#757575"/>
<path d="M76.3637 69C74.9082 69 74.4716 70.0689 72.7182 70.0689C70.9647 70.0689 70.5281 69 69.0726 69C67.5125 69 67.0008 70.3509 67.0008 71.9929C67.0008 73.6349 67.5125 74.9857 69.0726 74.9857C70.5281 74.9857 70.9647 73.9169 72.7182 73.9169C74.4716 73.9169 74.9082 74.9857 76.3637 74.9857C77.9238 74.9857 78.4355 73.6349 78.4355 71.9929C78.4355 70.3509 77.9238 69 76.3637 69Z" fill="#E0E0E0"/>
<path d="M63.3637 58C61.9082 58 61.4716 59.0689 59.7182 59.0689C57.9647 59.0689 57.5281 58 56.0726 58C54.5125 58 54.0008 59.3509 54.0008 60.9929C54.0008 62.6349 54.5125 63.9857 56.0726 63.9857C57.5281 63.9857 57.9647 62.9169 59.7182 62.9169C61.4716 62.9169 61.9082 63.9857 63.3637 63.9857C64.9238 63.9857 65.4355 62.6349 65.4355 60.9929C65.4355 59.3509 64.9238 58 63.3637 58Z" fill="#E0E0E0"/>
<path d="M50.363 66C48.9075 66 48.4709 67.0689 46.7174 67.0689C44.964 67.0689 44.5274 66 43.0719 66C41.5117 66 41 67.3509 41 68.9929C41 70.6349 41.5117 71.9857 43.0719 71.9857C44.5274 71.9857 44.964 70.9169 46.7174 70.9169C48.4709 70.9169 48.9075 71.9857 50.363 71.9857C51.9231 71.9857 52.4348 70.6349 52.4348 68.9929C52.4348 67.3509 51.9231 66 50.363 66Z" fill="#E0E0E0"/>
</g>
<defs>
<clipPath id="clip0_2584_6481">
<rect width="80" height="80" fill="white"/>
</clipPath>
</defs>
</svg>
<g clip-path="url(#clip0_2293_37324)">
<path d="M6.45466 7C2.33301 7 0 9.31262 0 13.4396V73.9767H59.8473L59.9917 13.4396C59.9917 9.31262 57.6587 7 53.537 7L6.45466 7Z" fill="#F0F0F0"/>
<path d="M80.0001 27.0774H43.3274V63.8897H80.0001V27.0774Z" fill="#DDDDDD"/>
<path d="M36.6616 27.0774H6.66577V43.8086H36.6616V27.0774Z" fill="#DDDDDD"/>
<path d="M36.6616 47.5229H6.66943V50.4974H36.6616V47.5229Z" fill="#DDDDDD"/>
<path d="M23.3301 54.219H6.66577V57.1934H23.3301V54.219Z" fill="#DDDDDD"/>
<path d="M60.1989 51.7429L59.3768 49.5232H53.785L52.9629 51.7429H49.8152L54.8404 38.6479H58.3214L63.3466 51.7429H60.1989ZM56.5809 41.4365L54.5479 47.073H58.614L56.5809 41.4365Z" fill="white"/>
<path d="M70.179 51.7429V50.7427C69.5347 51.5272 68.42 51.9771 67.1869 51.9771C65.6834 51.9771 63.9207 50.9584 63.9207 48.8354C63.9207 46.5971 65.6834 45.7754 67.1869 45.7754C68.4608 45.7754 69.5532 46.1881 70.179 46.9317V45.7345C70.179 44.7716 69.3569 44.1432 68.1053 44.1432C67.1091 44.1432 66.1722 44.5373 65.3871 45.2623L64.4095 43.5149C65.5649 42.4738 67.0499 42.0239 68.5348 42.0239C70.7049 42.0239 72.6787 42.8865 72.6787 45.6156V51.7429H70.1753H70.179ZM70.179 49.4637V48.2888C69.768 47.7386 68.9866 47.4448 68.1867 47.4448C67.2091 47.4448 66.4055 47.9765 66.4055 48.88C66.4055 49.7835 67.2091 50.2928 68.1867 50.2928C68.9866 50.2928 69.768 50.0177 70.179 49.4674V49.4637Z" fill="white"/>
<path d="M0 13.4396V16.9829H59.9917V13.4396C59.9917 9.31262 57.6587 7 53.537 7H6.45466C2.33301 7 0 9.31262 0 13.4396Z" fill="#DDDDDD"/>
</g>
<defs>
<clipPath id="clip0_2293_37324">
<rect width="80" height="66.9767" fill="white" transform="translate(0 7)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -15,6 +15,10 @@ import Products from '../products/products';
import SearchResults from '../search-results/search-results';
import { MarketplaceContext } from '../../contexts/marketplace-context';
import { fetchSearchResults } from '../../utils/functions';
import {
recordMarketplaceView,
recordLegacyTabView,
} from '../../utils/tracking';
export default function Content(): JSX.Element {
const marketplaceContextValue = useContext( MarketplaceContext );
@ -25,7 +29,19 @@ export default function Content(): JSX.Element {
// Get the content for this screen
useEffect( () => {
const abortController = new AbortController();
if ( [ '', 'discover' ].includes( selectedTab ) ) {
// we are recording both the new and legacy events here for now
// they're separate methods to make it easier to remove the legacy one later
const marketplaceViewProps = {
view: query?.tab,
search_term: query?.term,
product_type: query?.section,
category: query?.category,
};
recordMarketplaceView( marketplaceViewProps );
recordLegacyTabView( marketplaceViewProps );
if ( query.tab && [ '', 'discover' ].includes( query.tab ) ) {
return;
}
@ -43,9 +59,9 @@ export default function Content(): JSX.Element {
'category',
query.category === '_all' ? '' : query.category
);
} else if ( selectedTab === 'themes' ) {
} else if ( query?.tab === 'themes' ) {
params.append( 'category', 'themes' );
} else if ( selectedTab === 'search' ) {
} else if ( query?.tab === 'search' ) {
params.append( 'category', 'extensions-themes' );
}
@ -67,7 +83,13 @@ export default function Content(): JSX.Element {
return () => {
abortController.abort();
};
}, [ query.term, query.category, selectedTab, setIsLoading ] );
}, [
query.term,
query.category,
query?.tab,
setIsLoading,
query?.section,
] );
const renderContent = (): JSX.Element => {
switch ( selectedTab ) {

View File

@ -185,6 +185,7 @@
grid-template-columns: 2fr 1fr;
}
.woocommerce-marketplace__product-card__image {
border-bottom: 1px solid $gray-200;
grid-column-start: span 2;
overflow: hidden;
padding-top: 75%;

View File

@ -11,6 +11,11 @@
.woocommerce-marketplace__no-results__product-group {
margin-top: $grid-unit-60;
.woocommerce-marketplace__product-list-title {
font-size: 16px;
font-weight: 600;
}
}
.woocommerce-marketplace__no-results__icon {

View File

@ -24,10 +24,12 @@
.woocommerce-store-alerts {
margin-left: 16px;
margin-right: 16px;
margin-top: 16px;
@media (min-width: $breakpoint-medium) {
margin-left: 32px;
margin-right: 32px;
margin-top: 32px;
}
}
}

View File

@ -0,0 +1,93 @@
/**
* External dependencies
*/
import { recordEvent } from '@woocommerce/tracks';
interface MarketplaceViewProps {
view?: string;
search_term?: string;
product_type?: string;
category?: string;
}
/**
* Record a marketplace view event.
* This is a new event that is easier to understand and implement consistently
*/
function recordMarketplaceView( props: MarketplaceViewProps ) {
// The category prop changes to a blank string on first viewing all products after a search.
// This is undesirable and causes a duplicate event that will artificially inflate event counts.
if ( props.category === '' ) {
return;
}
const view = props.view || 'discover';
const search_term = props.search_term || null;
const product_type = props.product_type || null;
const category = props.category || null;
const eventProps = {
...( view && { view } ),
...( search_term && { search_term } ),
...( product_type && { product_type } ),
...( category && { category } ),
};
// User sees the default extensions or themes view
if ( view && [ 'extensions', 'themes' ].includes( view ) && ! category ) {
eventProps.category = '_all';
}
// User clicks the `View All` button on search results
if ( view && view === 'search' && product_type && ! category ) {
eventProps.category = '_all';
}
recordEvent( 'marketplace_view', eventProps );
}
/**
* Ensure we still have legacy events in place
* the "view" prop maps to a "section" prop in the event for compatibility with old funnels.
*
* @param props The props object containing view, search_term, section, and category.
*/
function recordLegacyTabView( props: MarketplaceViewProps ) {
// product_type will artificially inflate legacy event counts.
if ( props.product_type ) {
return;
}
let oldEventName = 'extensions_view';
const view = props.view || '_featured';
const search_term = props.search_term || null;
const category = props.category || null;
const oldEventProps = {
// legacy event refers to "section" instead of "view"
...( view && { section: view } ),
...( search_term && { search_term } ),
version: '2',
};
switch ( view ) {
case 'extensions':
oldEventProps.section = category || '_all';
break;
case 'themes':
oldEventProps.section = 'themes';
break;
case 'search':
oldEventName = 'extensions_view_search';
oldEventProps.section = view;
oldEventProps.search_term = search_term || '';
break;
case 'my-subscriptions':
oldEventName = 'subscriptions_view';
oldEventProps.section = 'helper';
break;
}
recordEvent( oldEventName, oldEventProps );
}
export { recordMarketplaceView, recordLegacyTabView };

View File

@ -10,9 +10,9 @@ import {
TRACKS_SOURCE,
__experimentalProductMVPCESFooter as FeedbackBar,
__experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer,
ProductPageSkeleton,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
import { Spinner } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
import { useParams } from 'react-router-dom';
@ -75,7 +75,7 @@ export default function ProductPage() {
);
if ( ! product?.id ) {
return <Spinner />;
return <ProductPageSkeleton />;
}
return (

View File

@ -10,9 +10,9 @@ import {
TRACKS_SOURCE,
__experimentalVariationSwitcherFooter as VariationSwitcherFooter,
__experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer,
ProductPageSkeleton,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
import { Spinner } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
@ -81,7 +81,7 @@ export default function ProductPage() {
);
if ( ! variation?.id ) {
return <Spinner />;
return <ProductPageSkeleton />;
}
return (

View File

@ -31,7 +31,7 @@ const WooCommerceServicesItem: React.FC< {
actions.push( {
url: getAdminLink( 'plugins.php' ),
label: __(
'Finish the setup by connecting your store to Jetpack.',
'Finish the setup by connecting your store to WordPress.com.',
'woocommerce'
),
} );

View File

@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { Text } from '@woocommerce/experimental';
import interpolateComponents from '@automattic/interpolate-components';
import { __, sprintf } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
export const TermsOfService = ( { buttonText } ) => (
<Text
variant="caption"
className="woocommerce-task__caption is-tos"
size="12"
lineHeight="16px"
style={ { display: 'block' } }
>
{ interpolateComponents( {
mixedString: sprintf(
__(
'By clicking "%s," you agree to our {{tosLink}}Terms of Service{{/tosLink}} and have read our {{privacyPolicyLink}}Privacy Policy{{/privacyPolicyLink}}.',
'woocommerce'
),
buttonText
),
components: {
tosLink: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
>
<></>
</Link>
),
privacyPolicyLink: (
<Link
href={ 'https://automattic.com/privacy/' }
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
</Text>
);

View File

@ -9,7 +9,7 @@ import { recordEvent } from '@woocommerce/tracks';
import { default as ConnectForm } from '~/dashboard/components/connect';
type ConnectProps = {
onConnect: () => void;
onConnect?: () => void;
};
export const Connect: React.FC< ConnectProps > = ( { onConnect } ) => {

View File

@ -2,11 +2,9 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components';
import { Link, Plugins as PluginInstaller } from '@woocommerce/components';
import { Plugins as PluginInstaller } from '@woocommerce/components';
import { OPTIONS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
@ -14,6 +12,7 @@ import { useEffect } from '@wordpress/element';
* Internal dependencies
*/
import { createNoticesFromResponse } from '~/lib/notices';
import { TermsOfService } from '~/task-lists/components/terms-of-service';
const isWcConnectOptions = (
wcConnectOptions: unknown
@ -58,15 +57,6 @@ export const Plugins: React.FC< Props > = ( {
nextStep();
}, [ nextStep, pluginsToActivate, tosAccepted ] );
const agreementText = pluginsToActivate.includes( 'woocommerce-services' )
? __(
'By installing Jetpack and WooCommerce Shipping you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
)
: __(
'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
);
if ( isResolving ) {
return null;
@ -74,6 +64,11 @@ export const Plugins: React.FC< Props > = ( {
return (
<>
{ ! tosAccepted && (
<TermsOfService
buttonText={ __( 'Install & enable', 'woocommerce' ) }
/>
) }
<PluginInstaller
onComplete={ (
activatedPlugins: string[],
@ -96,30 +91,6 @@ export const Plugins: React.FC< Props > = ( {
}
pluginSlugs={ pluginsToActivate }
/>
{ ! tosAccepted && (
<Text
variant="caption"
className="woocommerce-task__caption"
size="12"
lineHeight="16px"
style={ { display: 'block' } }
>
{ interpolateComponents( {
mixedString: agreementText,
components: {
link: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
</Text>
) }
</>
);
};

View File

@ -20,7 +20,7 @@ import { redirectToWCSSettings } from './utils';
/**
* Plugins required to automate shipping.
*/
const AUTOMATION_PLUGINS = [ 'jetpack', 'woocommerce-services' ];
const AUTOMATION_PLUGINS = [ 'woocommerce-services' ];
export const ShippingRecommendation: React.FC<
TaskProps & ShippingRecommendationProps
@ -94,12 +94,7 @@ export const ShippingRecommendation: React.FC<
},
{
key: 'plugins',
label: pluginsToActivate.includes( 'woocommerce-services' )
? __(
'Install Jetpack and WooCommerce Shipping',
'woocommerce'
)
: __( 'Install Jetpack', 'woocommerce' ),
label: __( 'Install WooCommerce Shipping', 'woocommerce' ),
description: __(
'Enable shipping label printing and discounted rates',
'woocommerce'
@ -126,7 +121,7 @@ export const ShippingRecommendation: React.FC<
{ __( 'Complete task', 'woocommerce' ) }
</Button>
) : (
<Connect onConnect={ redirect } />
<Connect />
),
},
];

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