Merge branch 'trunk' into docs/add-or-modify-states

This commit is contained in:
Niels Lange 2023-08-23 15:21:19 +02:00 committed by GitHub
commit 1b85b89bc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 7641 additions and 284 deletions

View File

@ -0,0 +1,30 @@
---
name: "\U0001F4C4 Request for New Document"
about: Suggest the creation of a new documentation topic that doesn't exist yet.
title: "[DOC-REQ]"
labels: 'type: documentation'
assignees: ''
---
## Description of the Document Requested
> Provide a detailed description of the topic you'd like to see documented.
## Why is this Document Important?
> Explain why this topic is crucial and how it can benefit the WooCommerce community.
## Potential Content
> If you have an idea about what the new document should cover, list the points or sub-topics here.
## Additional Context
> Add any other context, references, or information that can help in the creation of this new document.

View File

@ -0,0 +1,30 @@
---
name: "\U0001F4DD Suggestion for Documentation Improvement/Correction"
about: Propose a specific improvement or correction for an existing document.
title: "[DOC-BUG]"
labels: 'type: documentation'
assignees: ''
---
## Link to the Page/Section
> Provide a link to the specific page or section that you're referring to.
## Description of the Suggestion
> Describe the changes you suggest to improve or correct the documentation. Be specific about any errors or areas of confusion you've identified.
## Reason for the Suggestion
> Why do you believe this change will make the documentation clearer or more accurate?
## Additional Context
> Add any other context, references, or screenshots that support your suggestion.

View File

@ -48,7 +48,7 @@ jobs:
- name: Annotate Code Linting Results - name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@a1bf7cb320a18aa53cb848a267ce9b7417221526 uses: ataylorme/eslint-annotate-action@a1bf7cb320a18aa53cb848a267ce9b7417221526
if: github.event.pull_request.head.repo.fork != true
with: with:
repo-token: '${{ secrets.GITHUB_TOKEN }}' repo-token: '${{ secrets.GITHUB_TOKEN }}'
report-json: 'combined_eslint_report.json' report-json: 'combined_eslint_report.json'

View File

@ -1,59 +1,90 @@
# WooCommerce internal documentation # WooCommerce Developer Documentation
This directory contains documentation about implementation details specific parts of the WooCommerce code base. This documentation is intended for developers. > ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
## Available documents This is your go-to place to find everything you need to know to get started with WooCommerce development, including implementation details for specific parts of the WooCommerce code base.
* [HPOS](HPOS.md): Details of how the High Performance Order Storage works. ## Getting started
## Other documents WooCommerce is a customizable, open-source eCommerce platform built on WordPress. It empowers businesses worldwide to sell anything from physical products and digital downloads to subscriptions, content, and even appointments.
Get familiar with [WordPress Plugin Development](https://developer.wordpress.org/plugins/).
Take a moment to familiarize yourself with our [Developer Resources](https://developer.wordpress.org/plugins/plugin-basics/).
Once you're ready to move forward, consider one of the following:
- [Tools for low code development](getting-started/tools-for-low-code-development.md)
- [Building your first extension](extension-development/building-your-first-extension.md)
- [How to design a simple extension](extension-development/how-to-design-a-simple-extension.md)
## Contributions
The WooCommerce ecosystem thrives on community contributions. Whether it's improving documentation, reporting bugs, or contributing code, we greatly appreciate every contribution from our community.
- To contribute to **the core WooCommerce project**, check out our [Contributing guide](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md).
- To contribute to **documentation** please refer to the [documentation style guide](style-guide.md).
## Support
- To request a **new document, correction, or improvement**, [create an issue](https://github.com/woocommerce/woodocs/issues/new/choose).
- For development help, start with the [WooCommerce Community Forum](https://wordpress.org/support/plugin/woocommerce/), to see if someone else has already asked the same question. You can also pose your question in the `#developers` channel of our [Community Slack](https://woocommerce.com/community-slack/). If you're not sure where to ask your question, you can always [contact us](https://woocommerce.com/contact-us/), and our Happiness Engineers will be glad to point you in the right direction.
- For additional support with customizations, you might consider hiring from [WooExperts](https://woocommerce.com/experts/) or [Codeable](https://codeable.io/).
### Additional Resources
- [WooCommerce Official Website](https://woocommerce.com/)
- [Woo Marketplace](https://woocommerce.com/marketplace)
- All [WooCommerce Repositories on GitHub](https://woocommerce.github.io/)
### Other documentation
Some directories contain documentation about their own contents, in the form of README file. The available files are listed below, **if you create a new README file please add it to the corresponding list.** Some directories contain documentation about their own contents, in the form of README file. The available files are listed below, **if you create a new README file please add it to the corresponding list.**
Available READMe files for the WooCommerce plugin: Available READMe files for the WooCommerce plugin:
* [`Root README`](../plugins/woocommerce/README.md) - [`Root README`](../plugins/woocommerce/README.md)
* [`i18n/languages`](../plugins/woocommerce/i18n/languages/README.md) - [`i18n/languages`](../plugins/woocommerce/i18n/languages/README.md)
* [`includes`](../plugins/woocommerce/includes/README.md) - [`includes`](../plugins/woocommerce/includes/README.md)
* [`lib`](../plugins/woocommerce/lib/README.md) - [`lib`](../plugins/woocommerce/lib/README.md)
* [`packages`](../plugins/woocommerce/packages/README.md) - [`packages`](../plugins/woocommerce/packages/README.md)
* [`src`](../plugins/woocommerce/src/README.md) - [`src`](../plugins/woocommerce/src/README.md)
* [`src/Admin/RemoteInboxNotifications`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md) - [`src/Admin/RemoteInboxNotifications`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md)
* [`src/Admin/RemoteInboxNotifications/Transformers`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md) - [`src/Admin/RemoteInboxNotifications/Transformers`](../plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md)
* [`src/Blocks`](../plugins/woocommerce/src/Blocks/README.md) - [`src/Blocks`](../plugins/woocommerce/src/Blocks/README.md)
* [`src/Internal`](../plugins/woocommerce/src/Internal/README.md) - [`src/Internal`](../plugins/woocommerce/src/Internal/README.md)
* [`src/Internal/Admin/ProductForm`](../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md) - [`src/Internal/Admin/ProductForm`](../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md)
* [`tests`](../plugins/woocommerce/tests/README.md) - [`tests`](../plugins/woocommerce/tests/README.md)
* [`tests/api-core-tests`](../plugins/woocommerce/tests/api-core-tests/README.md) - [`tests/api-core-tests`](../plugins/woocommerce/tests/api-core-tests/README.md)
* [`tests/e2e`](../plugins/woocommerce/tests/e2e/README.md) - [`tests/e2e`](../plugins/woocommerce/tests/e2e/README.md)
* [`tests/e2e-pw`](../plugins/woocommerce/tests/e2e-pw/README.md) - [`tests/e2e-pw`](../plugins/woocommerce/tests/e2e-pw/README.md)
* [`tests/performance`](../plugins/woocommerce/tests/performance/README.md) - [`tests/performance`](../plugins/woocommerce/tests/performance/README.md)
* [`tests/Tools/CodeHacking`](../plugins/woocommerce/tests/Tools/CodeHacking/README.md) - [`tests/Tools/CodeHacking`](../plugins/woocommerce/tests/Tools/CodeHacking/README.md)
Available READMe files for the WooCommerce Admin plugin: Available READMe files for the WooCommerce Admin plugin:
* [`Root README`](../plugins/woocommerce-admin/README.md) - [`Root README`](../plugins/woocommerce-admin/README.md)
* [`client/activity-panel`](../plugins/woocommerce-admin/client/activity-panel/README.md) - [`client/activity-panel`](../plugins/woocommerce-admin/client/activity-panel/README.md)
* [`client/activity-panel/activity-card`](../plugins/woocommerce-admin/client/activity-panel/activity-card/README.md) - [`client/activity-panel/activity-card`](../plugins/woocommerce-admin/client/activity-panel/activity-card/README.md)
* [`client/activity-panel/activity-header`](../plugins/woocommerce-admin/client/activity-panel/activity-header/README.md) - [`client/activity-panel/activity-header`](../plugins/woocommerce-admin/client/activity-panel/activity-header/README.md)
* [`client/analytics/report`](../plugins/woocommerce-admin/client/analytics/report/README.md) - [`client/analytics/report`](../plugins/woocommerce-admin/client/analytics/report/README.md)
* [`client/analytics/settings`](../plugins/woocommerce-admin/client/analytics/settings/README.md) - [`client/analytics/settings`](../plugins/woocommerce-admin/client/analytics/settings/README.md)
* [`client/dashboard`](../plugins/woocommerce-admin/client/dashboard/README.md) - [`client/dashboard`](../plugins/woocommerce-admin/client/dashboard/README.md)
* [`client/header`](../plugins/woocommerce-admin/client/header/README.md) - [`client/header`](../plugins/woocommerce-admin/client/header/README.md)
* [`client/marketing`](../plugins/woocommerce-admin/client/marketing/README.md) - [`client/marketing`](../plugins/woocommerce-admin/client/marketing/README.md)
* [`client/marketing/components/button`](../plugins/woocommerce-admin/client/marketing/components/button/README.md) - [`client/marketing/components/button`](../plugins/woocommerce-admin/client/marketing/components/button/README.md)
* [`client/marketing/components/card`](../plugins/woocommerce-admin/client/marketing/components/card/README.md) - [`client/marketing/components/card`](../plugins/woocommerce-admin/client/marketing/components/card/README.md)
* [`client/marketing/components/product-icon`](../plugins/woocommerce-admin/client/marketing/components/product-icon/README.md) - [`client/marketing/components/product-icon`](../plugins/woocommerce-admin/client/marketing/components/product-icon/README.md)
* [`client/utils`](../plugins/woocommerce-admin/client/utils/README.md) - [`client/utils`](../plugins/woocommerce-admin/client/utils/README.md)
* [`client/wp-admin-scripts`](../plugins/woocommerce-admin/client/wp-admin-scripts/README.md) - [`client/wp-admin-scripts`](../plugins/woocommerce-admin/client/wp-admin-scripts/README.md)
* [`docs`](../plugins/woocommerce-admin/docs/README.md) - [`docs`](../plugins/woocommerce-admin/docs/README.md)
* [`docs/examples`](../plugins/woocommerce-admin/docs/examples/README.md) - [`docs/examples`](../plugins/woocommerce-admin/docs/examples/README.md)
* [`docs/examples/extensions`](../plugins/woocommerce-admin/docs/examples/extensions/README.md) - [`docs/examples/extensions`](../plugins/woocommerce-admin/docs/examples/extensions/README.md)
* [`docs/features`](../plugins/woocommerce-admin/docs/features/README.md) - [`docs/features`](../plugins/woocommerce-admin/docs/features/README.md)
* [`docs/woocommerce.com`](../plugins/woocommerce-admin/docs/woocommerce.com/README.md) - [`docs/woocommerce.com`](../plugins/woocommerce-admin/docs/woocommerce.com/README.md)
Available READMe files for the WooCommerce Beta Tested plugin: Available READMe files for the WooCommerce Beta Tested plugin:
* [`Root README`](../plugins/woocommerce-beta-tester/README.md) - [`Root README`](../plugins/woocommerce-beta-tester/README.md)
* [`src/tools`](../plugins/woocommerce-beta-tester/src/tools/README.md) - [`src/tools`](../plugins/woocommerce-beta-tester/src/tools/README.md)
* [`userscripts`](../plugins/woocommerce-beta-tester/userscripts/README.md) - [`userscripts`](../plugins/woocommerce-beta-tester/userscripts/README.md)

211
docs/data/crud-objects.md Normal file
View File

@ -0,0 +1,211 @@
# Developing using WooCommerce CRUD objects
CRUD is an abbreviation of the four basic operations you can do to a database or resource Create, Read, Update, Delete.
[WooCommerce 3.0 introduced CRUD objects](https://woocommerce.wordpress.com/2016/10/27/the-new-crud-classes-in-woocommerce-2-7/) for working with WooCommerce data. **Whenever possible these objects should be used in your code instead of directly updating metadata or using WordPress post objects.**
Each of these objects contains a schema for the data it controls (properties), a getter and setter for each property, and a save/delete method which talks to a data store. The data store handles the actual saving/reading from the database. The object itself does not need to be aware of where the data is stored.
## The benefits of CRUD
* Structure Each object has a pre-defined structure and keeps its own data valid.
* Control We control the flow of data, and any validation needed, so we know when changes occur.
* Ease of development As a developer, you dont need to know the internals of the data youre working with, just the names.
* Abstraction The data can be moved elsewhere, e.g. custom tables, without affecting existing code.
* Unification We can use the same code for updating things in admin as we do in the REST API and CLIs. Everything is unified.
* Simplified code Less procedural code to update objects which reduces likelihood of malfunction and adds more unit test coverage.
## CRUD object structure
The [`WC_Data`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/abstracts/abstract-wc-data.php) class is the basic implementation for CRUD objects, and all CRUD objects extend it. The most important properties to note are `$data`, which is an array of props supported in each object, and `$id`, which is the objects ID.
The [coupon object class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/class-wc-coupon.php) is a good example of extending `WC_Data` and adding CRUD functions to all properties.
### Data
`$data` stores the property names, and default values:
```php
/**
* Data array, with defaults.
* @since 3.0.0
* @var array
*/
protected $data = array(
'code' => '',
'amount' => 0,
'date_created' => '',
'date_modified' => '',
'discount_type' => 'fixed_cart',
'description' => '',
'date_expires' => '',
'usage_count' => 0,
'individual_use' => false,
'product_ids' => array(),
'excluded_product_ids' => array(),
'usage_limit' => 0,
'usage_limit_per_user' => 0,
'limit_usage_to_x_items' => 0,
'free_shipping' => false,
'product_categories' => array(),
'excluded_product_categories' => array(),
'exclude_sale_items' => false,
'minimum_amount' => '',
'maximum_amount' => '',
'email_restrictions' => array(),
'used_by' => array(),
);
```
### Getters and setters
Each one of the keys in this array (property) has a getter and setter, e.g. `set_used_by()` and `get_used_by()`. `$data` itself is private, so the getters and setters must be used to access the data.
Example getter:
```php
/**
* Get records of all users who have used the current coupon.
* @since 3.0.0
* @param string $context
* @return array
*/
public function get_used_by( $context = 'view' ) {
return $this->get_prop( 'used_by', $context );
}
```
Example setter:
```php
/**
* Set which users have used this coupon.
* @since 3.0.0
* @param array $used_by
* @throws WC_Data_Exception
*/
public function set_used_by( $used_by ) {
$this->set_prop( 'used_by', array_filter( $used_by ) );
}
```
`set_prop` and `get_prop` are part of `WC_Data`. These apply various filters (based on context) and handle changes, so we can efficiently save only the props that have changed rather than all props.
A note on `$context`: when getting data for use on the front end or display, `view` context is used. This applies filters to the data so extensions can change the values dynamically. `edit` context should be used when showing values to edit in the backend, and for saving to the database. Using `edit` context does not apply any filters to the data.
### The constructor
The constructor of the CRUD objects facilitates the read from the database. The actual read is not done by the CRUD class, but by its data store.
Example:
```php
/**
* Coupon constructor. Loads coupon data.
* @param mixed $data Coupon data, object, ID or code.
*/
public function __construct( $data = '' ) {
parent::__construct( $data );
if ( $data instanceof WC_Coupon ) {
$this->set_id( absint( $data->get_id() ) );
} elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) {
$this->set_id( $data );
} elseif ( ! empty( $data ) ) {
$this->set_id( wc_get_coupon_id_by_code( $data ) );
$this->set_code( $data );
} else {
$this->set_object_read( true );
}
$this->data_store = WC_Data_Store::load( 'coupon' );
if ( $this->get_id() > 0 ) {
$this->data_store->read( $this );
}
}
```
Note how it sets the ID based on the data passed to the object, then calls the data store to retrieve the data from the database. Once the data is read via the data store, or if no ID is set, `$this->set_object_read( true );` is set so the data store and CRUD object knows its read. Once this is set, changes are tracked.
### Saving and deleting
Save and delete methods are optional on CRUD child classes because the `WC_Data` parent class can handle it. When `save` is called, the data store is used to store data to the database. Delete removes the object from the database. `save` must be called for changes to persist, otherwise they will be discarded.
The save method in `WC_Data` looks like this:
```php
/**
* Save should create or update based on object existence.
*
* @since 2.6.0
* @return int
*/
public function save() {
if ( $this->data_store ) {
// Trigger action before saving to the DB. Allows you to adjust object props before save.
do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );
if ( $this->get_id() ) {
$this->data_store->update( $this );
} else {
$this->data_store->create( $this );
}
return $this->get_id();
}
}
```
Update/create is used depending on whether the object has an ID yet. The ID will be set after creation.
The delete method is like this:
```php
/**
* Delete an object, set the ID to 0, and return result.
*
* @since 2.6.0
* @param bool $force_delete
* @return bool result
*/
public function delete( $force_delete = false ) {
if ( $this->data_store ) {
$this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
$this->set_id( 0 );
return true;
}
return false;
}
```
## CRUD usage examples
### Creating a new simple product
```php
$product = new WC_Product_Simple();
$product->set_name( 'My Product' );
$product->set_slug( 'myproduct' );
$product->set_description( 'A new simple product' );
$product->set_regular_price( '9.50' );
$product->save();
$product_id = $product->get_id();
```
### Updating an existing coupon
```php
$coupon = new WC_Coupon( $coupon_id );
$coupon->set_discount_type( 'percent' );
$coupon->set_amount( 25.00 );
$coupon->save();
```
### Retrieving a customer
```php
$customer = new WC_Customer( $user_id );
$email = $customer->get_email();
$address = $customer->get_billing_address();
$name = $customer->get_first_name() . ' ' . $customer->get_last_name();
```

317
docs/data/data-stores.md Normal file
View File

@ -0,0 +1,317 @@
# Data Stores
## Introduction
Data store classes act as a bridge between WooCommerce's data CRUD classes (`WC_Product`, `WC_Order`, `WC_Customer`, etc) and the database layer. With the database logic separate from data, WooCommerce becomes more flexible. The data stores shipped with WooCommerce core (powered by WordPress' custom posts system and some custom tables) can be swapped out for a different database structure, type, or even be powered by an external API.
This guide will walk through the structure of a data store class, how to create a new data store, how to replace a core data store, and how to call a data store from your own code.
The examples in this guide will look at the [`WC_Coupon`](https://github.com/woocommerce/woocommerce/blob/dcecf0f22890f3cd92fbea13a98c11b2537df2a8/includes/class-wc-coupon.php#L19) CRUD data class and [`WC_Coupon_Data_Store_CPT`](https://github.com/woocommerce/woocommerce/blob/dcecf0f22890f3cd92fbea13a98c11b2537df2a8/includes/data-stores/class-wc-coupon-data-store-cpt.php), an implementation of a coupon data store using WordPress custom post types. This is how coupons are currently stored in WooCommerce.
The important thing to know about `WC_Coupon` or any other CRUD data class when working with data stores is which props (properties) they contain. This is defined in the [`data`](https://github.com/woocommerce/woocommerce/blob/dcecf0f22890f3cd92fbea13a98c11b2537df2a8/includes/class-wc-coupon.php#L26) array of each class.
## Structure
Every data store for a CRUD object should implement the `WC_Object_Data_Store_Interface` interface.
`WC_Object_Data_Store_Interface` includes the following methods:
* `create`
* `read`
* `update`
* `delete`
* `read_meta`
* `delete_meta`
* `add_meta`
* `update_meta`
The `create`, `read`, `update`, and `delete` methods should handle the CRUD logic for your props:
* `create` should create a new entry in the database. Example: Create a coupon.
* `read` should query a single entry from the database and set properties based on the response. Example: Read a coupon.
* `update` should make changes to an existing entry. Example: Update or edit a coupon.
* `delete` should remove an entry from the database. Example: Delete a coupon.
All data stores must implement handling for these methods.
In addition to handling your props, other custom data can be passed. This is considered `meta`. For example, coupons can have custom data provided by plugins.
The `read_meta`, `delete_meta`, `add_meta`, and `update_meta` methods should be defined so meta can be read and managed from the correct source. In the case of our WooCommerce core classes, we define them in `WC_Data_Store_WP` and then use the same code for all of our data stores. They all use the WordPress meta system. You can redefine these if meta should come from a different source.
Your data store can also implement other methods to replace direct queries. For example, the coupons data store has a public `get_usage_by_user_id` method. Data stores should always define and implement an interface for the methods they expect, so other developers know what methods they need to write. Put another way, in addition to the `WC_Object_Data_Store_Interface` interface, `WC_Coupon_Data_Store_CPT` also implements `WC_Coupon_Data_Store_Interface`.
## Replacing a data store
Let's look at how we would replace the `WC_Coupon_Data_Store_CPT` class with a `WC_Coupon_Data_Store_Custom_Table` class. Our examples will just provide stub functions, instead of a full working solution. Imagine that we would like to store coupons in a table named `wc_coupons` with the following columns:
```text
id, code, amount, date_created, date_modified, discount_type, description, date_expires, usage_count,individual_use, product_ids, excluded_product_ids, usage_limit, usage_limit_per_user, limit_usage_to_x_items, free_shipping, product_categories, excluded_product_categories, exclude_sale_items, minimum_amount, maximum_amount, email_restrictions, used_by
```
These column names match 1 to 1 with prop names.
First we would need to create a new data store class to contain our logic:
```php
/**
* WC Coupon Data Store: Custom Table.
*/
class WC_Coupon_Data_Store_Custom_Table extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface {
}
```
Note that we implement the main `WC_Object_Data_Store_Interface` interface as well as the ` WC_Coupon_Data_Store_Interface` interface. Together, these represent all the methods we need to provide logic for.
We would then define the CRUD handling for these properties:
```php
/**
* Method to create a new coupon in the database.
*
* @param WC_Coupon
*/
public function create( &$coupon ) {
$coupon->set_date_created( current_time( 'timestamp' ) );
/**
* This is where code for inserting a new coupon would go.
* A query would be built using getters: $coupon->get_code(), $coupon->get_description(), etc.
* After the INSERT operation, we want to pass the new ID to the coupon object.
*/
$coupon->set_id( $coupon_id );
// After creating or updating an entry, we need to also cause our 'meta' to save.
$coupon->save_meta_data();
// Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two.
$coupon->apply_changes();
// It is helpful to provide the same hooks when an action is completed, so that plugins can interact with your data store.
do_action( 'woocommerce_new_coupon', $coupon_id );
}
/**
* Method to read a coupon.
*
* @param WC_Coupon
*/
public function read( &$coupon ) {
$coupon->set_defaults();
// Read should do a check to see if this is a valid coupon
// and otherwise throw an 'Invalid coupon.' exception.
// For valid coupons, set $data to contain our database result.
// All props should be set using set_props with output from the database. This "hydates" the CRUD data object.
$coupon_id = $coupon->get_id();
$coupon->set_props( array(
'code' => $data->code,
'description' => $data->description,
// ..
) );
// We also need to read our meta data into the object.
$coupon->read_meta_data();
// This flag reports if an object has been hydrated or not. If this ends up false, the database hasn't correctly set the object.
$coupon->set_object_read( true );
do_action( 'woocommerce_coupon_loaded', $coupon );
}
/**
* Updates a coupon in the database.
*
* @param WC_Coupon
*/
public function update( &$coupon ) {
// Update coupon query, using the getters.
$coupon->save_meta_data();
$coupon->apply_changes();
do_action( 'woocommerce_update_coupon', $coupon->get_id() );
}
/**
* Deletes a coupon from the database.
*
* @param WC_Coupon
* @param array $args Array of args to pass to the delete method.
*/
public function delete( &$coupon, $args = array() ) {
// A lot of objects in WordPress and WooCommerce support
// the concept of trashing. This usually is a flag to move the object
// to a "recycling bin". Since coupons support trashing, your layer should too.
// If an actual delete occurs, set the coupon ID to 0.
$args = wp_parse_args( $args, array(
'force_delete' => false,
) );
$id = $coupon->get_id();
if ( $args['force_delete'] ) {
// Delete Query
$coupon->set_id( 0 );
do_action( 'woocommerce_delete_coupon', $id );
} else {
// Trash Query
do_action( 'woocommerce_trash_coupon', $id );
}
}
```
We are extending `WC_Data_Store_WP` so our classes will continue to use WordPress' meta system.
As mentioned in the structure section, we are responsible for implementing the methods defined by `WC_Coupon_Data_Store_Interface`. Each interface describes the methods and parameters it accepts, and what your function should do.
A coupons replacement would look like the following:
```php
/**
* Increase usage count for current coupon.
*
* @param WC_Coupon
* @param string $used_by Either user ID or billing email
*/
public function increase_usage_count( &$coupon, $used_by = '' ) {
}
/**
* Decrease usage count for current coupon.
*
* @param WC_Coupon
* @param string $used_by Either user ID or billing email
*/
public function decrease_usage_count( &$coupon, $used_by = '' ) {
}
/**
* Get the number of uses for a coupon by user ID.
*
* @param WC_Coupon
* @param id $user_id
* @return int
*/
public function get_usage_by_user_id( &$coupon, $user_id ) {
}
/**
* Return a coupon code for a specific ID.
* @param int $id
* @return string Coupon Code
*/
public function get_code_by_id( $id ) {
}
/**
* Return an array of IDs for for a specific coupon code.
* Can return multiple to check for existence.
* @param string $code
* @return array Array of IDs.
*/
public function get_ids_by_code( $code ) {
}
```
Once all the data store methods are defined and logic written, we need to tell WooCommerce to load our new class instead of the built-in class. This is done using the `woocommerce_data_stores` filter. An array of data store slugs is mapped to default WooCommerce classes. Example:
```php
'coupon' => 'WC_Coupon_Data_Store_CPT',
'customer' => 'WC_Customer_Data_Store',
'customer-download' => 'WC_Customer_Download_Data_Store',
'customer-session' => 'WC_Customer_Data_Store_Session',
'order' => 'WC_Order_Data_Store_CPT',
'order-refund' => 'WC_Order_Refund_Data_Store_CPT',
'order-item' => 'WC_Order_Item_Data_Store',
'order-item-coupon' => 'WC_Order_Item_Coupon_Data_Store',
'order-item-fee' => 'WC_Order_Item_Fee_Data_Store',
'order-item-product' => 'WC_Order_Item_Product_Data_Store',
'order-item-shipping' => 'WC_Order_Item_Shipping_Data_Store',
'order-item-tax' => 'WC_Order_Item_Tax_Data_Store',
'payment-token' => 'WC_Payment_Token_Data_Store',
'product' => 'WC_Product_Data_Store_CPT',
'product-grouped' => 'WC_Product_Grouped_Data_Store_CPT',
'product-variable' => 'WC_Product_Variable_Data_Store_CPT',
'product-variation' => 'WC_Product_Variation_Data_Store_CPT',
'shipping-zone' => 'WC_Shipping_Zone_Data_Store',
```
We specifically want to target the coupon data store, so we would do something like this:
```php
function myplugin_set_wc_coupon_data_store( $stores ) {
$stores['coupon'] = 'WC_Coupon_Data_Store_Custom_Table';
return $stores;
}
add_filter( 'woocommerce_data_stores', 'myplugin_set_wc_coupon_data_store' );
```
Our class would then be loaded by WooCommerce core, instead of `WC_Coupon_Data_Store_CPT`.
## Creating a new data store
### Defining a new product type
Does your extension create a new product type? Each product type has a data store in addition to a parent product data store. The parent store handles shared properties like name or description and the child handles more specific data.
For example, the external product data store handles "button text" and "external URL". The variable data store handles the relationship between parent products and their variations.
Check out [this walkthrough](https://developer.woocommerce.com/2017/02/06/wc-2-7-extension-compatibility-examples-3-bookings/) for more information on this process.
### Data store for custom data
If your extension introduces a new database table, new custom post type, or some new form of data not related to products, orders, etc, then you should implement your own data store.
Your data store should still implement `WC_Object_Data_Store_Interface` and provide the normal CRUD functions. Your data store should be the main point of entry for interacting with your data, so any other queries or operations should also have methods.
The [shipping zone data store](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php) serves as a good example for a "simple" data store using a custom table. The coupons code is a good example for a data store using a custom post type.
All you need to do to register your data store is add it to the `woocommerce_data_stores` filter:
```php
function myplugin_set_my_custom_data_store( $stores ) {
$stores['mycustomdata'] = 'WC_My_Custom_Data_Store';
return $stores;
}
add_filter( 'woocommerce_data_stores', 'myplugin_set_my_custom_data_store' );
```
You can then load your data store like any other WooCommerce data store.
## Calling a data store
Calling a data store is as simple as using the static `WC_Data_Store::load()` method:
```php
// Load the shipping zone data store.
$data_store = WC_Data_Store::load( 'shipping-zone' );
// Get the number of shipping methods for zone ID 4.
$num_of_methods = $data_store->get_method_count( 4 );
```
You can also chain methods:
```php
// Get the number of shipping methods for zone ID 4.
$num_of_methods = WC_Data_Store::load( 'shipping-zone' )->get_method_count( 4 );
```
The `::load()` method works for any data store registered to `woocommerce_data_stores`, so you could load your custom data store:
```php
$data_store = WC_Data_Store::load( 'mycustomdata' );
```
## Data store limitations and WP Admin
Currently, several WooCommerce screens still rely on WordPress to list objects. Examples of this include coupons and products.
If you replace data via a data store, some parts of the existing UI may fail. An example of this may be lists of coupons when using the `type` filter. This filter uses meta data, and is in turn passed to WordPress which runs a query using the `WP_Query` class. This cannot handle data outside of the regular meta tables (Ref #19937). To get around this, usage of `WP_Query` would need to be deprecated and replaced with custom query classes and functions.

View File

@ -0,0 +1,117 @@
# Building your first extension
The easiest way to get started building an extension is to use the built-in extension generator that is included alongside WooCommerce Admin. This utility is maintained as part of the codebase for WooCommerce Admin, so it includes up-to-date tools and many preconfigured settings for building modern extensions that take advantage of the [React-powered](https://react.dev/) user experience available in current versions of WordPress and WooCommerce.
## Using the extension generator
Browse to your local WooCommerce Admin repository
```sh
cd /your/server/wp-content/plugins/woocommerce-admin
```
Run the extension generator command
```sh
npm run create-wc-extension
```
The extension generator will scaffold out a basic extension and place it in its own plugin directory alongside WooCommerce on your local server.
The extension that the generator creates contains a simple [boilerplate](https://stackoverflow.com/questions/3992199/what-is-boilerplate-code) that handles much of the configuration needed for setting up a React-powered extension, which you can modify to fit your needs.
## The architecture of a basic WooCommerce extension
WooCommerce extensions use a combination of PHP and modern JavaScript to create a seamless user experience for merchants and shoppers that takes advantage of the features and functionality available in the [NodeJS](https://nodejs.org/en) ecosystem while still being a good neighbor within the underlying WordPress application environment.
WordPress plugins (of which WooCommerce extensions are a specialized subset), tend to follow a few common patterns. You can read more about common WordPress plugin architecture in the [Best Practices chapter of the WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#architecture-patterns).
In addition to the main PHP file that all WordPress plugins must contain, a WooCommerce extension will typically contain additional PHP files with classes that assist in server-side functionality.
It will also contain files that are JavaScript and CSS assets which shape the client-side behavior and appearance.
## File structure generated by the `create-wc-extension script`
When you run the built-in extension generator, it will output something that looks similar to the structure below.
```sh
.
├── README.md
├── my-great-extension.php
├── package.json
├── src
│ ├── index.js
│ └── index.scss
└── webpack.config.js
```
Heres a breakdown of what these files are and what purpose they serve:
`README.md`
This file is meant to have a high-level overview of your extension to make it easier for people to use and extend your project. The generator outputs a basic file with some minimal instructions in it to get you started, but you should replace the contents of the file with information specific to your project. Its important to keep in mind that this file is not the same as the readme.txt file required by WordPress.org plugin directory, which must adhere to specific file standads.
`[your-extension-name].php`
This is your extensions main PHP file. It functions as the entry point for your extension and is where youll likely include code that hooks your extension into WordPress and WooCommerce. You can read more about the purpose of this file in the Getting Started section of the WordPress Plugin Developer Handbook.
`package.json`
This is a manifest file that Node uses for a number of different purposes. It can store configuration settings for tools, lists of dependencies, aliases for common scripts, and even metadata about your extension. The WooCommerce extension generator outputs a package.json file that will bundle many helpful dependencies with your extension, as well as a variety of scripts you can use in conjunction with these dependencies to streamline your workflow and make sure your extension conforms to the same standards as other WordPress plugins and WooCommerce extensions. Heres an example of what your package.json file might look like initially:
```json
{
"name": "my-great-extension",
"title": "my-great-extension",
"license": "GPL-3.0-or-later",
"version": "0.1.0",
"description": "my-great-extension",
"scripts": {
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses",
"format:js": "wp-scripts format-js",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"lint:md:docs": "wp-scripts lint-md-docs",
"lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:unit": "wp-scripts test-unit-js"
},
"devDependencies": {
"@wordpress/scripts": "^12.2.1",
"@woocommerce/eslint-plugin": "1.1.0",
"@woocommerce/dependency-extraction-webpack-plugin": "1.1.0"
}
}
```
The settings in this autogenerated file tell Webpack to use the default configuration included with the `@wordpress/scripts` package (listed in your `package.json` as a development dependency) and to override the plugin it uses for dependency extraction with one that is tailor-made for WooCommerce extensions.
## Try out your extension
If you used the extension generator to create your extension, youll need to complete a few final steps to see it in action.
First, navigate to your extensions root directory on your development server:
```sh
cd /your/server/wc-content/plugins/your-extension/
```
Then install the projects dependencies.
```sh
npm install
```
Finally, run the start script to generate an initial build of your extension. This script will also continuously watch your local files for changes.
```sh
npm start
```
Once your initial build is complete, you can browse to the administrative area of your local WordPress environment and activate your extension. If everything worked as it should, you should see a message in your browsers JavaScript console:
```sh
hello world
```

View File

@ -0,0 +1,205 @@
# Setting up your development environment
## Introduction
Building an extension for WooCommerce is a straightforward process, but there are a several moving parts and a few supporting software tools youll want to familiarize yourself with. This guide will walk you through the steps of getting a basic development environment set up for building WooCommerce extensions.
If you would like to contribute to the WooCommerce core platform; please read our [contributor documentation and guidelines](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment).
## Prerequisites
### Recommended reading
WooCommerce extensions are a specialized type of WordPress plugin. If you are new to WordPress plugin development, take a look at a few of these articles in the [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/).
### Required software
[Git](https://git-scm.com/)
[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
[NodeJS](https://nodejs.org/en)
[PNpm](https://pnpm.io/)
[Composer](https://getcomposer.org/download/)
Note: If youre working on a Windows machine, you may want to take a look at the Building Extensions in Windows Environments section of this guide before proceeding.
### Setting up your reusable WordPress development environment
In addition to the software listed above, youll also want to have some way of setting up a local development server stack. There are a number of different tools available for this, each with a certain set of functionality and limitations. We recommend choosing an option below that fits your preferred workflow best.
### WordPress-specific tools
[vvv](https://varyingvagrantvagrants.org/) A highly configurable, cross-platform, and robust environment management tool powered by VirtualBox and Vagrant. This is one the tool that the WooCommerce Core team recommends to contributors.
[wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) A command-line utility maintained by the WordPress community that allows you to set up and run custom WordPress environments with Docker and JSON manifests.
[LocalWP](https://localwp.com/) A cross-platform app that bills itself as a one-click WordPress installation.
### General PHP-based web stack tools
[MAMP](https://www.mamp.info/en/mac/) A local server environment that can be installed on Mac or Windows.
[WAMP](https://www.wampserver.com/en/) A Windows web development environment that lets you create applications with Apache2, PHP, and MySQL.
[XAMPP](https://www.apachefriends.org/index.html) An easy-to-install Apache distribution containing MariaDB, PHP, and Perl. Its available for Windows, Linux, and OS X.
### Minimum server requirements
Regardless of the tool you choose for managing your development environment, you should make sure it [meets the server recommendations](https://woocommerce.com/document/server-requirements/?utm_source=wooextdevguide) for WooCommerce as well as the [requirements for running WordPress](https://wordpress.org/about/requirements/).
## Anatomy of a WordPress development environment (public_html/)
While development environments can vary, the basic file structure for a WordPress environment should be consistent.
When developing a WooCommerce extension, youll usually be doing most of your work within the public_html directory of your local server. For now, take some time to familiarize yourself with a few key paths:
`wp-content/debug.log` This is the file where WordPress writes the important output such as errors and other messages useful for debugging.
`wp-content/plugins/` This is the directory on the server where WordPress plugin folders live.
`wp-content/themes/` This is the directory on the server where WordPress theme folders live.
## Adding WooCommerce Core to your environment
When developing an extension for WooCommerce, its helpful to install a development version of WooCommerce core.
### Clone the WC Core repo into `wp-content/plugins/`
```sh
cd /your/server/wp-content/plugins
git clone https://github.com/woocommerce/woocommerce.git
cd woocommerce
```
### Activate the required Node version
```sh
nvm use
Found '/path/to/woocommerce/.nvmrc' with version <v12>
Now using node v12.21.0 (npm v6.14.11)
```
Note: if you dont have the required version of Node installed, NVM will alert you so you can install it:
```sh
Found '/path/to/woocommerce/.nvmrc' with version <v12>
N/A: version "v12 -> N/A" is not yet installed.
You need to run "nvm install v12" to install it before using it.
```
### Install dependencies
```sh
pnpm install && composer install
```
### Build WooCommerce
```sh
pnpm run build
```
Running this script will compile the JavaScript and CSS that WooCommerce needs to operate. If you try to run WooCommerce on your server without generating the compiled assets, you may experience errors and other unwanted side-effects.
Note: In some environments, you may see an out-of-memory error when you try to build WooCommerce. If this happens, you simply need to adjust the memory_limit setting in your environments php.ini configuration to a higher value. The process for changing this value varies depending on the environment management tooling you use, so its best to consult your tools documentation before making any changes.
## Adding WooCommerce Admin to your environment
Installing a development version of WooCommerce Admin will give you access to some helpful utilities such as a built-in script for generating React-powered WooCommerce extensions.
### Clone the WC Admin repo into `wp-content/plugins/`
```sh
cd /your/server/wp-content/plugins
git clone https://github.com/woocommerce/woocommerce-admin.git
cd woocommerce-admin
```
### Activate the required Node version
```sh
nvm use
Found '/path/to/woocommerce-admin/.nvmrc' with version <lts/*>
Now using node v14.16.0 (npm v6.14.11)
```
Note: if you dont have the required version of Node installed, NVM will alert you so you can install it.
```sh
Found '/path/to/woocommerce-admin/.nvmrc' with version <v12>
N/A: version "lts/* -> N/A" is not yet installed.
You need to run "nvm install lts/*" to install it before using it.
```
Pro-tip: WooCommerce Admin may require a different version of Node than WooCommerce Core requires. Keep this in mind when navigating between directories using the same shell session. As a best practice, always make sure to activate the correct version of Node using nvm use before running any commands inside a cloned repository.
### Install dependencies
```sh
npm install && composer install
```
## Build a development version of WooCommerce Admin
Building a development version will compile unminified versions of asset files, which is useful when debugging extensions that interact with WooCommerce Admin features.
```sh
npm run dev
```
If you run into trouble when building WooCommerce Admin, take a look at this wiki article for troubleshooting help.
## Adding WooCommerce Blocks to your environment
Installing a development version of WooCommerce Blocks is not required in every case, but having a standalone installation of the feature-plugin version of this extension allows you to work with the latest features, which can be helpful for compatibility testing and future-proofing your extension.
### Clone the WC Blocks repo into `wp-content/plugins/`
```sh
cd /your/server/wp-content/plugins
git clone https://github.com/woocommerce/woocommerce-gutenberg-products-block.git
cd woocommerce-gutenberg-products-block
```
### Activate the required Node version
```sh
nvm use
Found '/path/to/woocommerce-gutenberg-products-block/.nvmrc' with version <lts/*>
Now using node v14.16.0 (npm v6.14.11)
```
Note: if you dont have the required version of Node installed, NVM will alert you so you can install it.
```sh
Found '/path/to/woocommerce-gutenberg-products-block/.nvmrc' with version <v12>
N/A: version "lts/* -> N/A" is not yet installed.
You need to run "nvm install lts/*" to install it before using it.
```
Pro-tip: WooCommerce Blocks may require a different version of Node than WooCommerce Core requires. Keep this in mind when navigating between directories using the same shell session. As a best practice, always make sure to activate the correct version of Node using nvm use before running any commands inside a cloned repository.
### Install dependencies
```sh
npm install && composer install
Build the assets
npm run build
```
This will compile and minify the JavaScript and CSS from the /assets directory to be served.
## Finishing up
Once you have WooCommerce and its sibling extensions installed in your WordPress environment, start up your server, browse to your site and handle any initial setup steps or importing youd like to do. This is a good time to load sample data and activate themes and plugins.
Depending on which extensions you installed in your environment you should have one or more of the following directories in your `public_html` directory:
- `wp-content/plugins/woocommerce`
- `wp-content/plugins/woocommerce-admin`
- `wp-content/plugins/woocommerce-gutenberg-products-block`
- `wp-content/themes/storefront`

View File

@ -0,0 +1,231 @@
# WooCommerce Extension Developer Handbook
Want to create a plugin to extend WooCommerce? WooCommerce extensions are the same as regular WordPress plugins. For more information, visit [Writing a plugin](https://www.google.com/url?q=https://developer.wordpress.org/plugins/&sa=D&source=editors&ust=1692724061394513&usg=AOvVaw1PmatucFlJ3lI0z15KYBFq).
Your WooCommerce extension should:
- Adhere to all WordPress plugin coding standards, as well as [best practice guidelines](https://www.google.com/url?q=https://developer.wordpress.org/plugins/plugin-basics/best-practices/&sa=D&source=editors&ust=1692724061394795&usg=AOvVaw1vZcSq6JuW0VNm3HhUSb9s) for harmonious existence within WordPress and alongside other WordPress plugins.
- Have a single core purpose and use WooCommerce features as much as possible.
- Not do anything malicious, illegal, or dishonest — for example, inserting spam links or executable code via third-party systems if not part of the service or  explicitly permitted in the services terms of use.
- Adhere to WooCommerce [compatibility and interoperability guidelines](https://www.google.com/url?q=https://woocommerce.com/document/marketplace-overview/%23section-9&sa=D&source=editors&ust=1692724061395243&usg=AOvVaw2qsdAnXBb2o2dmrTg_QKaa).
Merchants make use of WooCommerce extensions daily, and should have a unified and pleasant experience while doing so without advertising invading their WP Admin or store.
Note: We provide this page as a best practice for developers.
## [Check if WooCommerce is active](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-1&sa=D&source=editors&ust=1692724061395542&usg=AOvVaw2bTUi1Q7fivFhe-Gc3VULl)
Most WooCommerce plugins do not need to run unless WooCommerce is already active. You can wrap your plugin in a check to see if WooCommerce is installed:
```
// Test to see if WooCommerce is active (including network activated).
$plugin_path = trailingslashit( WP_PLUGIN_DIR ) . 'woocommerce/woocommerce.php';
if (
in_array( $plugin_path, wp_get_active_and_valid_plugins() )
|| in_array( $plugin_path, wp_get_active_network_plugins() )
) {
// Custom code here. WooCommerce is active, however it has not
// necessarily initialized (when that is important, consider
// using the \`woocommerce_init\` action).
}
```
Note that this check will fail if the WC plugin folder is named anything other than woocommerce.
## [Main file naming](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-2&sa=D&source=editors&ust=1692724061396656&usg=AOvVaw0bg5CY1zbmVRBUpvcbFoWc)
The main plugin file should adopt the name of the plugin, e.g., A plugin with the directory name plugin-name would have its main file named plugin-name.php.
## [Text domains](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-3&sa=D&source=editors&ust=1692724061397135&usg=AOvVaw2mG8ZyvrV7HLq35afWjwcw)
Follow guidelines for [Internationalization for WordPress Developers](https://www.google.com/url?q=https://codex.wordpress.org/I18n_for_WordPress_Developers&sa=D&source=editors&ust=1692724061397498&usg=AOvVaw3sWMtUFCwi2CM4BnCL9T3w), the text domain should match your plugin directory name, e.g., A plugin with a directory name of plugin-name would have the text domain plugin-name. Do not use underscores.
## [Localization](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-4&sa=D&source=editors&ust=1692724061397875&usg=AOvVaw0aN3CAxkWHDXAaOEAt_XLg)
All text strings within the plugin code should be in English. This is the WordPress default locale, and English should always be the first language. If your plugin is intended for a specific market (e.g., Spain or Italy), include appropriate translation files for those languages within your plugin package. Learn more at [Using Makepot to translate your plugin](https://www.google.com/url?q=https://codex.wordpress.org/I18n_for_WordPress_Developers%23Translating_Plugins_and_Themes&sa=D&source=editors&ust=1692724061398312&usg=AOvVaw1KI1tPNBz1PhXghD6EPeFX).
## [Follow WordPress PHP Guidelines](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-5&sa=D&source=editors&ust=1692724061398556&usg=AOvVaw3r1rBtiLQqnd09_uqcbgN1)
WordPress has a [set of guidelines](https://www.google.com/url?q=http://make.wordpress.org/core/handbook/coding-standards/php/&sa=D&source=editors&ust=1692724061398880&usg=AOvVaw32UFCkh2lVnQ1P11WK5917) to keep all WordPress code consistent and easy to read. This includes quotes, indentation, brace style, shorthand php tags, yoda conditions, naming conventions, and more. Please review the guidelines.
Code conventions also prevent basic mistakes, as [Apple made with iOS 7.0.6](https://www.google.com/url?q=https://www.imperialviolet.org/2014/02/22/applebug.html&sa=D&source=editors&ust=1692724061399164&usg=AOvVaw0U7fB5ITS8uXELL3MgR3zx).
## [Custom Database Tables & Data Storage](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-6&sa=D&source=editors&ust=1692724061399464&usg=AOvVaw2baqakEqCZi76lbxB3zjh9)
Avoid creating custom database tables. Whenever possible, use WordPress [post types](https://www.google.com/url?q=http://codex.wordpress.org/Post_Types%23Custom_Post_Types&sa=D&source=editors&ust=1692724061399803&usg=AOvVaw0flq0h728aDmJWR23oNv0V), [taxonomies](https://www.google.com/url?q=http://codex.wordpress.org/Taxonomies&sa=D&source=editors&ust=1692724061399949&usg=AOvVaw1qbvRfl8wcPI35lvSboCwi), and [options](https://www.google.com/url?q=http://codex.wordpress.org/Creating_Options_Pages&sa=D&source=editors&ust=1692724061400101&usg=AOvVaw3H8WjoRljUHd6q5s8X_Pdi).
Consider the permanence of your data. Heres a quick primer:
- If the data may not always be present (i.e., it expires), use a transient.
- If the data is persistent but not always present, consider using the WP Cache.
- If the data is persistent and always present, consider the wp_options table.
- If the data type is an entity with n units, consider a post type.
- If the data is a means or sorting/categorizing an entity, consider a taxonomy.
Logs should be written to a file using the [WC_Logger](https://www.google.com/url?q=https://woocommerce.com/wc-apidocs/class-WC_Logger.html&sa=D&source=editors&ust=1692724061401335&usg=AOvVaw3mxPgYSD7oL2sCoQNcN1BO) class.
## [Prevent Data Leaks](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-7&sa=D&source=editors&ust=1692724061401572&usg=AOvVaw3xUKNB9qgJDqnd9RwlY8iT)
Try to prevent direct access data leaks. Add this line of code after the opening PHP tag in each PHP file:
```
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
```
## [Readme](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-8&sa=D&source=editors&ust=1692724061402226&usg=AOvVaw0phoD93bjkbxKs01VSxbm_)
All plugins need a [standard WordPress readme](https://www.google.com/url?q=http://wordpress.org/plugins/about/readme.txt&sa=D&source=editors&ust=1692724061402537&usg=AOvVaw0CxV8gQGI6n0FztcJ_yxwr).
Your readme might look something like this:
```
=== Plugin Name ===
Contributors: (this should be a list of wordpress.org userid's)
Tags: comments, spam
Requires at least: 4.0.1
Tested up to: 4.3
Requires PHP: 5.6
Stable tag: 4.3
License: GPLv3 or later License
URI: http://www.gnu.org/licenses/gpl-3.0.html
```
## [Plugin Author Name](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-9&sa=D&source=editors&ust=1692724061403627&usg=AOvVaw0C49AD_KHbRbjBvuZif55T)
To ensure a consistent experience for all WooCommerce users,including finding information on who to contact with queries, the following plugin headers should be in place:
- The Plugin Author isYourName/YourCompany
- The Developer header is YourName/YourCompany, with the Developer URI field listed as http://yourdomain.com/
For example:
```
/**
* Plugin Name: WooCommerce Extension
* Plugin URI: http://woocommerce.com/products/woocommerce-extension/
* Description: Your extension's description text.
* Version: 1.0.0
* Author: Your Name
* Author URI: http://yourdomain.com/
* Developer: Your Name
* Developer URI: http://yourdomain.com/
* Text Domain: woocommerce-extension
* Domain Path: /languages
*
* Woo: 12345:342928dfsfhsf8429842374wdf4234sfd
* WC requires at least: 2.2
* WC tested up to: 2.3
*
* License: GNU General Public License v3.0
* License URI: http://www.gnu.org/licenses/gpl-3.0.html
*/
```
## [Declaring required and supported WooCommerce version](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-10&sa=D&source=editors&ust=1692724061406115&usg=AOvVaw17Ag30ypAdPnc0BXtUdyUo)
Use the follow headers to declare “required” and “tested up to” versions:
- WC requires at least
- WC tested up to
## [Plugin URI](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-11&sa=D&source=editors&ust=1692724061406678&usg=AOvVaw2A80jh9ZfkI6nLGIa93Hpm)
Ensure that the Plugin URI line of the above plugin header is provided. This line should contain the URL of the plugins product/sale page or to a dedicated page for the plugin on your website.
## [Make it Extensible](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-13&sa=D&source=editors&ust=1692724061407164&usg=AOvVaw3UxTnE6_-W2mM5rVyBk6BY)
Developers should use WordPress actions and filters to allow for modification/customization without requiring users to touch the plugins core code base.
If your plugin creates a front-end output, we recommend to having a templating engine in place so users can create custom template files in their themes WooCommerce folder to overwrite the plugins template files.
For more information, check out Pippins post on [Writing Extensible Plugins with Actions and Filters](https://www.google.com/url?q=http://code.tutsplus.com/tutorials/writing-extensible-plugins-with-actions-and-filters--wp-26759&sa=D&source=editors&ust=1692724061407755&usg=AOvVaw1RO30KUvw73kAb73j2Mjxs).
## [Use of External Libraries](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-14&sa=D&source=editors&ust=1692724061408050&usg=AOvVaw064nKRX-btaU6-rP2nDvPR)
The use of entire external libraries is typically not suggested as this can open up the product to security vulnerabilities. If an external library is absolutely necessary, developers should be thoughtful about the code used and assume ownership as well as of responsibility for it. Try to  only include the strictly necessary part of the library, or use a WordPress-friendly version or opt to build your own version. For example, if needing to use a text editor such as TinyMCE, we recommend using the WordPress-friendly version, TinyMCE Advanced.
## [Remove Unused Code](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-15&sa=D&source=editors&ust=1692724061408520&usg=AOvVaw1xpjcmMrZLm46Jgpa_VJdb)
With version control, theres no reason to leave commented-out code; its annoying to scroll through and read. Remove it and add it back later if needed.
## [Comment](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-16&sa=D&source=editors&ust=1692724061408902&usg=AOvVaw1ZMYYAAWMyPDO1S4YMKRLL)
If you have a function, what does the function do? There should be comments for most if not all functions in your code. Someone/You may want to modify the plugin, and comments are helpful for that. We recommend using [PHP Doc Blocks](https://www.google.com/url?q=http://en.wikipedia.org/wiki/PHPDoc&sa=D&source=editors&ust=1692724061409214&usg=AOvVaw0pK1khHhpHhP1aU6Wfgg7l)  similar to [WooCommerce](https://www.google.com/url?q=https://github.com/woocommerce/woocommerce/&sa=D&source=editors&ust=1692724061409366&usg=AOvVaw3UOb2ML3qmjH-MUwEYxwZN).
## [Avoid God Objects](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-17&sa=D&source=editors&ust=1692724061409596&usg=AOvVaw1jhRY0Ozls9pwiMi12lNkG)
[God Objects](https://www.google.com/url?q=http://en.wikipedia.org/wiki/God_object&sa=D&source=editors&ust=1692724061409851&usg=AOvVaw0XP2zyCLmDVwGMwNj0XxF8) are objects that know or do too much. The point of object-oriented programming is to take a large problem and break it into smaller parts. When functions do too much, its hard to follow their logic, making bugs harder to fix. Instead of having massive functions, break them down into smaller pieces.
## [Test Extension Quality & Security with Quality Insights Tool](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-18&sa=D&source=editors&ust=1692724061410124&usg=AOvVaw3WxIdaWb-loeDgk5idgYh7)
Integrate the [Quality Insights Toolkit (QIT)](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/&sa=D&source=editors&ust=1692724061410435&usg=AOvVaw3-rM1B3-aofWDpJB5y-9Qs) into your development workflow to ensure your extension adheres to WordPress / WooCommerce quality and security standards. The QIT allows the ability to test your extensions against new releases of PHP, WooCommerce, and WordPress, as well as other active extensions, at the same time. The following tests are available today:
- [End-to-End](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/e2e&sa=D&source=editors&ust=1692724061410720&usg=AOvVaw2-K7A2Jp9eEE3I7yg5GtLw)
- [Activation](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/activation&sa=D&source=editors&ust=1692724061410980&usg=AOvVaw3EGJl6KSaQL1ygcvoDFFvR)
- [Security](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/security&sa=D&source=editors&ust=1692724061411228&usg=AOvVaw3t5gYK8Md1UQWZORTmKSSx)
- [PHPStan](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/phpstan&sa=D&source=editors&ust=1692724061411473&usg=AOvVaw0cYoAE7ScAXqMU7aw5gAOB)
- [API](https://www.google.com/url?q=https://href.li/?https://woocommerce.github.io/qit-documentation/%23/test-types/api&sa=D&source=editors&ust=1692724061411713&usg=AOvVaw0dXv3dyfNaAwe6wiwqApHn)
## [Test Your Code with WP_DEBUG](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-19&sa=D&source=editors&ust=1692724061411947&usg=AOvVaw3UsBUZeYvFu9v4itS839zy)
Always develop with [WP_DEBUG](https://www.google.com/url?q=http://codex.wordpress.org/Debugging_in_WordPress&sa=D&source=editors&ust=1692724061412254&usg=AOvVaw1412x2vFGfPDqCXxohD-JF) mode on, so you can see all PHP warnings sent to the screen. This will flag things like making sure a variable is set before checking the value.
## [Separate Business Logic & Presentation Logic](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-20&sa=D&source=editors&ust=1692724061412504&usg=AOvVaw1-plFhPYmTkkU99E9IO3VN)
Its a good practice to separate business logic (i.e., how the plugin works) from [presentation logic](https://www.google.com/url?q=http://en.wikipedia.org/wiki/Presentation_logic&sa=D&source=editors&ust=1692724061412782&usg=AOvVaw3graTlf6ciHm0E3N25NOMQ) (i.e., how it looks). Two separate pieces of logic are more easily maintained and swapped if necessary. An example is to have two different classes — one for displaying the end results, and one for the admin settings page.
## [Use Transients to Store Offsite Information](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-21&sa=D&source=editors&ust=1692724061413039&usg=AOvVaw0_S-ooFW3W6n-k4yV3Gbmo)
If you provide a service via an API, its best to store that information so future queries can be done faster and the load on your service is lessened. [WordPress transients](https://www.google.com/url?q=http://codex.wordpress.org/Transients_API&sa=D&source=editors&ust=1692724061413333&usg=AOvVaw2SqfyKOl4wa52wmN_B0iJw) can be used to store data for a certain amount of time.
## [Logging Data](https://www.google.com/url?q=https://woocommerce.com/document/create-a-plugin/%23section-22&sa=D&source=editors&ust=1692724061413569&usg=AOvVaw1Rz8wUNYXdGr4LnOCiOpQM)
You may want to log data that can be useful for debugging purposes. This is great with two conditions:
- Allow any logging as an opt in.
- Use the [WC_Logger](https://www.google.com/url?q=https://woocommerce.com/wc-apidocs/class-WC_Logger.html&sa=D&source=editors&ust=1692724061414103&usg=AOvVaw1Xl7lewASbQMGaV8Frgq-U) class. A user can then view logs on their system status page.
If adding logging to your extension, heres a snippet for presenting a link to the logs, in a way the extension user can easily make use of.
```
$label = \_\_( 'Enable Logging', 'your-textdomain-here' );
$description = \_\_( 'Enable the logging of errors.', 'your-textdomain-here' );
if ( defined( 'WC_LOG_DIR' ) ) {
$log_url = add_query_arg( 'tab', 'logs', add_query_arg( 'page', 'wc-status', admin_url( 'admin.php' ) ) );
$log_key = 'your-plugin-slug-here-' . sanitize_file_name( wp_hash( 'your-plugin-slug-here' ) ) . '-log';
$log_url = add_query_arg( 'log_file', $log_key, $log_url );
$label .= ' | ' . sprintf( \_\_( '%1$sView Log%2$s', 'your-textdomain-here' ), '<a href\="' . esc_url( $log_url ) . '">', '</a\>' );
}
$form_fields\['wc_yourpluginslug_debug'\] = array(
'title' => \_\_( 'Debug Log', 'your-textdomain-here' ),
'label' => $label,
'description' => $description,
'type' => 'checkbox',
'default' => 'no'
);
```

View File

@ -0,0 +1,50 @@
# Handling deactivation and uninstallation
## Introduction
There are a number of cleanup tasks youll need to handle when a merchant deactivates or uninstalls your extension. This guide provides a brief overview of WooCommerce-specific items youll want to make sure you account for when defining your extensions deactivation and uninstallation logic.
## Removing Scheduled Actions
If your extension uses Action Scheduler to queue any background jobs, its important to unschedule those actions when your extension is uninstalled or deactivated.
`as_unschedule_all_actions( $hook, $args, $group );`
You can read more about using Action Scheduler for managing background processing in the [Action Scheduler API Reference](https://actionscheduler.org/api/).
## Removing Admin Notes
If you have created any Notes for merchants, you should delete those notes when your extension is deactivated or, at the very least, when it is uninstalled.
```php
function my_great_extension_deactivate() {
ExampleNote::possibly_delete_note();
}
register_deactivation_hook( __FILE__, 'my_great_extension_deactivate' );
```
The example above assumes that you have followed the pattern this guide recommends for creating Notes as dedicated classes that include the `NoteTraits` trait included with WooCommerce Admin. This approach provides your Note with some baked in functionality that streamlines note operations such as creation and deletion.
## Removing Admin Tasks
When your extension is deactivated or uninstalled, you should take care to unregister any tasks that your extension created for merchants.
```php
// Unregister task.
function my_extension_deactivate_task() {
remove_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 );
}
register_deactivation_hook( __FILE__, 'my_extension_deactivate_task' );
```
Keep in mind that merchant tasks are managed via a hybrid approach that involves both PHP and JavaScript, so the client-side registration only happens when your extensions JavaScript runs.
## Unregistering navigation
When your extension deactivates and uninstalls, any registration youve done with the WooCommerce Navigation will be handled automatically.
## WordPress cleanup tasks
There are additional measures you may need to consider when your extension is deactivated or uninstalled, depending on the types of modifications it makes to the underlying WordPress environment when it activates and runs. You can read more about handling deactivation and uninstallation in the [WordPress Plugin Developer Handbook](https://developer.wordpress.org/plugins/intro/).

View File

@ -0,0 +1,655 @@
# Handling merchant onboarding
## Introduction
Onboarding is a critical part of the merchants user experience. It helps set them up for success and ensures theyre not only using your extension correctly but also getting the most out of it. There are a few especially useful features that you can take advantage of as a developer to help onboard merchants who are using your extension:
- Setup tasks
- Store management links
- Admin notes
---
## Using setup tasks
Setup tasks appear on the WooCommerce Admin home screen and prompt a merchant to complete certain steps in order to set up your extension. Adding tasks is a two-step process that requires:
- Registering the task (and its JavaScript) using PHP
- Using JavaScript to build the task, set its configuration, and add it to the task list
### Registering the task with PHP
To register your task as an extended task list item, youll need to hook in to the `woocommerce_get_registered_extended_tasks` filter with a function that appends your task to the array the filter provides.
```php
// Task registration
function my_extension_register_the_task( $registered_tasks_list_items ) {
$new_task_name = 'your_task_name';
if ( ! in_array( $new_task_name, $registered_tasks_list_items, true ) ) {
array_push( $registered_tasks_list_items, $new_task_name );
}
return $registered_tasks_list_items;
}
add_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 );
```
### Registering the tasks JavaScript
In addition to registering the task name, youll also need to register and enqueue the transpiled JavaScript file containing your task component, its configuration, and its event-handlers. A common way to do this is to create a dedicated registration function that hooks into the `admin_enqueue_scripts` action in WordPress. If you do things this way, you can nest the `add_filter` call for `woocommerce_get_registered_extended_tasks` in this function as well. Below is an annotated example of how this registration might look:
```php
// Register the task list item and the JS.
function add_task_register_script() {
// Check to make sure that this is a request for an Admin page.
if (
! class_exists( 'Automattic\WooCommerce\Admin\Loader' ) ||
! \Automattic\WooCommerce\Admin\Loader::is_admin_page() ||
! Onboarding::should_show_tasks()
) {
return;
}
// Register a handle for your extension's transpiled JavaScript file.
wp_register_script(
'add-task',
plugins_url( '/dist/index.js', __FILE__ ),
array(
'wp-hooks',
'wp-element',
'wp-i18n',
'wc-components',
),
filemtime( dirname( __FILE__ ) . '/dist/index.js' ),
true
);
// Get server-side data via PHP and send it to the JavaScript using wp_localize_script
$client_data = array(
'isComplete' => get_option( 'woocommerce_admin_add_task_example_complete', false ),
);
wp_localize_script( 'add-task', 'addTaskData', $client_data );
// Enqueue the script in WordPress
wp_enqueue_script( 'add-task' );
// Hook your task registration script to the relevant extended tasks filter
add_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 );
}
```
### Unregistering the task upon deactivation
It is also helpful to define a function that will unregister your task when your extension is deactivated.
```php
// Unregister task.
function my_extension_deactivate_task() {
remove_filter( 'woocommerce_get_registered_extended_tasks', 'my_extension_register_the_task', 10, 1 );
}
register_deactivation_hook( __FILE__, 'my_extension_deactivate_task' );
```
### Adding the task using JavaScript
Once the task has been registered in WooCommerce, you need to build the task component, set its configuration, and add it to the task list. For example, the JavaScript file for a simple task might look something like this:
```js
// External dependencies.
import { addFilter } from '@wordpress/hooks';
import apiFetch from '@wordpress/api-fetch';
import { Card, CardBody } from '@wordpress/components';
// WooCommerce dependencies.
import { getHistory, getNewPath } from '@woocommerce/navigation';
// Event handler for handling mouse clicks that mark a task complete.
const markTaskComplete = () => {
// Here we're using apiFetch to set option values in WooCommerce.
apiFetch( {
path: '/wc-admin/options',
method: 'POST',
data: { woocommerce_admin_add_task_example_complete: true },
} )
.then( () => {
// Set the local `isComplete` to `true` so that task appears complete on the list.
addTaskData.isComplete = true;
// Redirect back to the root WooCommerce Admin page.
getHistory().push( getNewPath( {}, '/', {} ) );
} )
.catch( ( error ) => {
// Something went wrong with our update.
console.log( error );
} );
};
// Event handler for handling mouse clicks that mark a task incomplete.
const markTaskIncomplete = () => {
apiFetch( {
path: '/wc-admin/options',
method: 'POST',
data: { woocommerce_admin_add_task_example_complete: false },
} )
.then( () => {
addTaskData.isComplete = false;
getHistory().push( getNewPath( {}, '/', {} ) );
} )
.catch( ( error ) => {
console.log( error );
} );
};
// Build the Task component.
const Task = () => {
return (
<Card className="woocommerce-task-card">
<CardBody>
Example task card content.
<br />
<br />
<div>
{ addTaskData.isComplete ? (
<button onClick={ markTaskIncomplete }>
Mark task incomplete
</button>
) : (
<button onClick={ markTaskComplete }>
Mark task complete
</button>
) }
</div>
</CardBody>
</Card>
);
};
// Use the 'woocommerce_admin_onboarding_task_list' filter to add a task.
addFilter(
'woocommerce_admin_onboarding_task_list',
'plugin-domain',
( tasks ) => {
return [
...tasks,
{
key: 'example',
title: 'Example',
content: 'This is an example task.',
container: <Task />,
completed: addTaskData.isComplete,
visible: true,
additionalInfo: 'Additional info here',
time: '2 minutes',
isDismissable: true,
onDismiss: () => console.log( 'The task was dismissed' ),
},
];
}
);
```
In the example above, the extension does a few different things. Lets break it down:
#### Handle imports
First, import any functions, components, or other utilities from external dependencies. Weve kept WooCommerce-related dependencies separate from others for the sake of keeping things tidy. In a real-world extension, you may be importing other local modules. In those cases, we recommend creating a visually separate section for those imports as well.
```js
// External dependencies
import { addFilter } from '@wordpress/hooks'``;
import apiFetch from '@wordpress/api-fetch'``;
import { Card, CardBody } from '@wordpress/components'``;
// WooCommerce dependencies
import { getHistory, getNewPath } from '@woocommerce/navigation'``;
```
The `addFilter` function allows us to hook in to JavaScript filters the same way that the traditional PHP call to `add_filter()` does. The `apiFetch` utility allows our extension to query the WordPress REST API without needing to deal with keys or authentication. Finally, the `Card` and `CardBody` are predefined React components that well use as building blocks for our extensions Task component.
#### Create Event Handlers
Next we define the logic for the functions that will handle events for our task. In the example above, we created two functions to handle mouse clicks that toggle the completion status of our task.
```js
const markTaskComplete = () => {
apiFetch( {
path: '/wc-admin/options',
method: 'POST',
data: { woocommerce_admin_add_task_example_complete: true },
} )
.then( () => {
addTaskData.isComplete = true;
getHistory().push( getNewPath( {}, '/', {} ) );
} )
.catch( ( error ) => {
console.log( error );
} );
};
```
In the example above, the event handler uses `apiFetch` to set the `woocommerce_admin_add_task_example_complete` options value to `true` and then updates the components state data and redirects the browser to the Admin root. In the case of an error, were simply logging it to the console, but you may want to implement your own solution here.
The `markTaskIncomplete` function is more or less an inverse of `markTaskComplete` that toggles the tasks completion status in the opposite direction.
#### Construct the component
Next, we create a [functional component](https://reactjs.org/docs/components-and-props.html) that returns our task card. The intermixed JavaScript/HTML syntax were using here is called JSX. If youre unfamiliar with it, you can [read more about it in the React docs](https://reactjs.org/docs/introducing-jsx.html).
```js
const Task = () => {
return (
<Card className="woocommerce-task-card">
<CardBody>
Example task card content.
<br />
<br />
<div>
{ addTaskData.isComplete ? (
<button onClick={ markTaskIncomplete }>
Mark task incomplete
</button>
) : (
<button onClick={ markTaskComplete }>
Mark task complete
</button>
) }
</div>
</CardBody>
</Card>
);
};
```
In the example above, were using the `Card` and `CardBody` components to construct our tasks component. The `div` inside the `CardBody` uses a [JavaScript expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#Expressions) (`{}`) to embed a ternary operator that uses the components state to determine whether to display the task as complete or incomplete.
#### Configure task and add it to the WooCommerce task list
Finally, well set some configuration values for our task and then use the `addFilter` function to append our task to the WooCommerce Admin Onboarding Task List.
```js
addFilter(
'woocommerce_admin_onboarding_task_list',
'plugin-domain',
( tasks ) => {
return [
...tasks,
{
key: 'example',
title: 'Example',
content: 'This is an example task.',
container: <Task />,
completed: addTaskData.isComplete,
visible: true,
additionalInfo: 'Additional info here',
time: '2 minutes',
isDismissable: true,
onDismiss: () => console.log( 'The task was dismissed' ),
},
];
}
);
```
In the example above, were setting our tasks configuration as we pass it into the filter for simplicity, but in a real-world extension, you might encapsulate this somewhere else for better separation of concerns. Below is a list of properties that the task-list component supports for tasks.
| Name | Type | Required | Description |
|----------------|------------|----------|-------------|
| key | String | Yes | Identifier |
| title | String | Yes | Task title |
| content | String | No | The content that will be visible in the Extensions setup list |
| container | Component | Yes | The task component that will be visible after selecting the item |
| completed | Boolean | Yes | Whether the task is completed or not |
| visible | Boolean | Yes | Whether the task is visible or not |
| additionalInfo | String | No | Additional information |
| time | String | Yes | Time it takes to finish up the task |
| isDismissable | Boolean | No | Whether the task is dismissable or not. If false the Dismiss button wont be visible |
| onDismiss | Function | No | Callback method that its triggered on dismission |
| type | String | Yes | Type of task list item, setup items will be in the store setup and extension in the extensions setup |
---
## Using Store Management Links
When a merchant completes all of the items on the onboarding task list, WooCommerce replaces it with a section containing a list of handy store management links. Discoverability can be a challenge for extensions, so this section is a great way to bring more attention to key features of your extension and help merchants navigate to them.
The store management section has a relatively narrow purpose, so this section does not currently support external links. Instead, it is meant for navigating quickly within WooCommerce.
Adding your own store management links is a simple process that involves:
- Installing dependencies for icon support
- Enqueuing an admin script in your PHP
- Hooking in via a JavaScript filter to provide your link object
### Installing the Icons package
Store management links use the `@wordpress/icons` package. If your extension isnt already using it, youll need to add it to your extensions list of dependencies.
`npm` `install` ` @wordpress``/icons ` `--save`
### Enqueuing the JavaScript
The logic that adds your custom link to the store management section will live in a JavaScript file. Well register and enqueue that file with WordPress in our PHP file:
```js
function custom_store_management_link() {
wp_enqueue_script(
'add-my-custom-link',
plugins_url( '/dist/add-my-custom-link.js', __FILE__ ),
array( 'wp-hooks' ),
10
);
}
add_action( 'admin_enqueue_scripts', 'custom_store_management_link' );
```
The first argument of this call is a handle, the name by which WordPress will refer to the script were enqueuing. The second argument is the URL where the script is located.
The third argument is an array of script dependencies. By supplying the `wp-hooks` handle in that array, were ensuring that our script will have access to the `addFilter` function well be using to add our link to WooCommerces list.
The fourth argument is a priority, which determines the order in which JavaScripts are loaded in WordPress. Were setting a priority of 10 in our example. Its important that your script runs before the store management section is rendered. With that in mind, make sure your priority value is lower than 15 to ensure your link is rendered properly.
### Supply your link via JavaScript
Finally, in the JavaScript file you enqueued above, hook in to the `woocommerce_admin_homescreen_quicklinks` filter and supply your task as a simple JavaScript object.
```js
import { megaphone } from '@wordpress/icons';
import { addFilter } from '@wordpress/hooks';
addFilter(
'woocommerce_admin_homescreen_quicklinks',
'my-extension',
( quickLinks ) => {
return [
...quickLinks,
{
title: 'My link',
href: 'link/to/something',
icon: megaphone,
},
];
}
);
```
---
## Using Admin Notes
Admin Notes are meant for displaying insightful information about your WooCommerce store, extensions, activity, and achievements. Theyre also useful for displaying information that can help with the day-to-day tasks of managing and optimizing a store. A good general rule is to use Admin Notes for information that is:
1. Timely
2. Relevant
3. Useful
With that in mind, you might consider using Admin Notes to celebrate a particular milestone that a merchant has passed, or to provide additional guidance about using a specific feature or flow. Conversely, you shouldnt use Admin Notes to send repeated messages about the same topic or target all users with a note that is only relevant to a subset of merchants. Its okay to use Admin Notes for specific promotions, but you shouldnt abuse the system. Use your best judgement and remember the home screen is meant to highlight a stores most important actionable tasks.
Despite being a part of the new React-powered admin experience in WooCommerce, Admin Notes are available to developers via a standard PHP interface.
The recommended approach for using Admin Notes is to encapsulate your note within its own class that uses the [NoteTraits](https://github.com/woocommerce/woocommerce-admin/blob/831c9ff13a862f22cf53d3ae676daeabbefe90ad/src/Notes/NoteTraits.php) trait included with WooCommerce Admin. Below is a simple example of what this might look like:
```php
<?php
/**
* Simple note provider
*
* Adds a note with a timestamp showing when the note was added.
*/
namespace My\Wonderfully\Namespaced\Extension\Area;
// Exit if this code is accessed outside of WordPress.
defined ( 'ABSPATH' ) || exit;
// Check for Admin Note support
if ( ! class_exists( 'Automattic\WooCommerce\Admin\Notes\Notes' ) ||
! class_exists( 'Automattic\WooCommerce\Admin\Notes\NoteTraits' )) {
return;
}
// Make sure the WooCommerce Data Store is available
if ( ! class_exists( 'WC_Data_Store' ) ) {
return;
}
/**
* Example note class.
*/
class ExampleNote {
// Use the Note class to create Admin Note objects
use Automatic\WooCommerce\Admin\Notes\Note;
// Use the NoteTraits trait, which handles common note operations.
use Automatic\WooCommerce\Admin\Notes\NoteTraits;
// Provide a note name.
const NOTE_NAME = 'my-prefix-example-note';
public static function get_note() {
// Our welcome note will include information about when the extension
// was activated. This is just for demonstration. You might include
// other logic here depending on what data your note should contain.
$activated_time = current_time( 'timestamp', 0 );
$activated_time_formatted = date( 'F jS', $activated_time );
// Instantiate a new Note object
$note = new Automattic\WooCommerce\Admin\Notes\Note();
// Set our note's title.
$note->set_title( 'Getting Started' );
// Set our note's content.
$note->set_content(
sprintf(
'Extension activated on %s.', $activated_time_formatted
)
);
// In addition to content, notes also support structured content.
// You can use this property to re-localize notes on the fly, but
// that is just one use. You can store other data here too. This
// is backed by a longtext column in the database.
$note->set_content_data( (object) array(
'getting_started' => true,
'activated' => $activated_time,
'activated_formatted' => $activated_time_formatted
) );
// Set the type of the note. Note types are defined as enum-style
// constants in the Note class. Available note types are:
// error, warning, update, info, marketing.
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
// Set the type of layout the note uses. Supported layout types are:
// 'banner', 'plain', 'thumbnail'
$note->set_layout( 'plain' );
// Set the image for the note. This property renders as the src
// attribute for an img tag, so use a string here.
$note->set_image( '' );
// Set the note name and source. You should store your extension's
// name (slug) in the source property of the note. You can use
// the name property of the note to support multiple sub-types of
// notes. This also gives you a handy way of namespacing your notes.
$note->set_source( 'inbox-note-example');
$note->set_name( self::NOTE_NAME );
// Add action buttons to the note. A note can support 0, 1, or 2 actions.
// The first parameter is the action name, which can be used for event handling.
// The second parameter renders as the label for the button.
// The third parameter is an optional URL for actions that require navigation.
$note->add_action(
'settings', 'Open Settings', '?page=wc-settings&tab=general'
);
$note->add_action(
'learn_more', 'Learn More', 'https://example.com'
);
return $note;
}
}
function my_great_extension_activate() {
// This uses the functionality from the NoteTraits trait to conditionally add your note if it passes all of the appropriate checks.
ExampleNote::possibly_add_note();
}
register_activation_hook( __FILE__, 'my_great_extension_activate' );
function my_great_extension_deactivate() {
// This uses the functionality from the NoteTraits trait to conditionally remove your note if it passes all of the appropriate checks.
ExampleNote::possibly_delete_note();
}
register_deactivation_hook( __FILE__, 'my_great_extension_deactivate' );
```
### Breaking it down
Lets break down the example above to examine what each section does.
#### Namespacing and feature availability checks
First, were doing some basic namespacing and feature availability checks, along with a safeguard to make sure this file only executes within the WordPress application space.
```php
namespace My\Wonderfully\Namespaced\Extension\Area;
defined ( 'ABSPATH' ) || exit;
if ( ! class_exists( 'Automattic\WooCommerce\Admin\Notes\Notes') ||
! class_exists( 'Automattic\WooCommerce\Admin\Notes\NoteTraits') ) {
return;
}
if ( ! class_exists( 'WC_Data_Store' ) ) {
return;
}
```
#### Using Note and NoteTraits objects
Next, we define a simple class that will serve as a note provider for our note. To create and manage note objects, well import the `Note` and `NotesTraits` classes from WooCommerce Admin.
```php
class ExampleNote {
use Automatic\WooCommerce\Admin\Notes\Note;
use Automatic\WooCommerce\Admin\Notes\NoteTraits;
}
```
#### Provide a unique note name
Before proceeding, create a constant called `NOTE_NAME` and assign a unique note name to it. The `NoteTraits` class uses this constant for queries and note operations.
`const NOTE_NAME = 'my-prefix-example-note';`
#### Configure the notes details
Once youve set your notes name, you can define and configure your note. The `NoteTraits` class will call `self::get_note()` when performing operations, so you should encapsulate your notes instantiation and configuration in a static function called `get_note()` that returns a `Note` object.
```php
public static function get_note() {
// We'll fill this in with logic that instantiates a Note object
// and sets its properties.
}
```
Inside our `get_note()` function, well handle any logic for collecting data our Note may need to display. Our example note will include information about when the extension was activated, so this bit of code is just for demonstration. You might include other logic here depending on what data your note should contain.
```php
$activated_time = current_time( 'timestamp', 0);
$activated_time_formatted = date( 'F jS', $activated_time );
```
Next, well instantiate a new `Note` object.
`$note = new Note();`
Once we have an instance of the Note class, we can work with its API to set its properties, starting with its title.
`$note->set_title( 'Getting Started' );`
Then well use some of the timestamp data we collected above to set the notes content.
```php
$note->set_content(
sprintf(
'Extension activated on %s.', $activated_time_formatted
)
);
```
In addition to regular content, notes also support structured content using the `content_data` property. You can use this property to re-localize notes on the fly, but that is just one use case. You can store other data here too. This is backed by a `longtext` column in the database.
```php
$note->set_content_data( (object) array(
'getting_started' => true,
'activated' => $activated_time,
'activated_formatted' => $activated_time_formatted
) );
```
Next, well set the notes `type` property. Note types are defined as enum-style class constants in the `Note` class. Available note types are _error_, _warning_, _update_, _info_, and _marketing_. When selecting a note type, be aware that the _error_ and _update_ result in the note being shown as a Store Alert, not in the Inbox. Its best to avoid using these types of notes unless you absolutely need to.
`$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );`
Admin Notes also support a few different layouts. You can specify `banner`, `plain`, or `thumbnail` as the layout. If youre interested in seeing the different layouts in action, take a look at [this simple plugin](https://gist.github.com/octaedro/864315edaf9c6a2a6de71d297be1ed88) that you can install to experiment with them.
Well choose `plain` as our layout, but its also the default, so we could leave this property alone and the effect would be the same.
`$note->set_layout( 'plain' );`
If you have an image that you want to add to your Admin Note, you can specify it using the `set_image` function. This property ultimately renders as the `src` attribute on an `img` tag, so use a string here.
`$note->set_image( '' );`
Next, well set the values for our Admin Notes `name` and `source` properties. As a best practice, you should store your extensions name (i.e. its slug) in the `source` property of the note. You can use the `name` property to support multiple sub-types of notes. This gives you a handy way of namespacing your notes and managing them at both a high and low level.
```php
$note->set_source( 'inbox-note-example');
$note->set_name( self::NOTE_NAME );
```
Admin Notes can support 0, 1, or 2 actions (buttons). You can use these actions to capture events that trigger asynchronous processes or help the merchant navigate to a particular view to complete a step, or even simply to provide an external link for further information. The `add_action()` function takes up to three arguments. The first is the action name, which can be used for event handling, the second renders as a label for the actions button, and the third is an optional URL for actions that require navigation.
```php
$note->add_action(
'settings', 'Open Settings', '?page=wc-settings&tab=general'
);
$note->add_action(
'learn_more', 'Learn More', 'https://example.com'
);
```
Finally, remember to have the `get_note()` function return the configured Note object.
`return $note;`
#### Adding and deleting notes
To add and delete notes, you can use the helper functions that are part of the `NoteTraits` class: `possibly_add_note()` and its counterpart `possibly_delete_note()`. These functions will handle some of the repetitive logic related to note management and will also run checks to help you avoid creating duplicate notes.
Our example extension ties these calls to activation and deactivation hooks for the sake of simplicity. While there are many events for which you may want to add Notes to a merchants inbox, deleting notes upon deactivation and uninstallation is an important part of managing your extensions lifecycle.
```php
function my_great_extension_activate() {
ExampleNote::possibly_add_note();
}
register_activation_hook( __FILE__, 'my_great_extension_activate' );
function my_great_extension_deactivate() {
ExampleNote::possibly_delete_note();
}
register_deactivation_hook( __FILE__, 'my_great_extension_deactivate' );
```

View File

@ -0,0 +1,73 @@
# Adding store management links
## Introduction
In the new and improved WooCommerce home screen, there are two points of extensibility for plugin developers that have recently had some attention. The first is the setup task list, allowing you to remind the user of tasks they need to complete and keeping track of their progress for them.
The second is the store management links section. Once the user has completed the setup tasks this will display for them. This section consolidates a list of handy navigation links that merchants can use to quickly find features in WooCommerce.
Discoverability can be hard for users so this can be a great place to bring attention to the features of your plugin and allow users to easily find their way to the key functionality your plugin provides.
Adding your own store management links is a simple process.
## Add your own store management link
Before we start, let's outline a couple of restrictions on this feature.
Right now these links are designed to keep the user within WooCommerce, so it does not support external links.
All the links you add will fall under a special category in the list called "Extensions". There is not currently any support for custom categories.
With those things in mind, let's start.
## Step 1 - Enqueue JavaScript
Adding a store management link will all be done in JavaScript, so the first step is enqueuing your script that will add the store management link. The most important thing here is ensuring that your script runs before the store management link section is rendered.
To ensure that your script runs before ours you'll need to enqueue it with a priority higher than 15. You'll also need to depend on `wp-hooks` to get access to `addFilter`.
Example:
```php
function enqueue_management_link_script() {
wp_enqueue_script( $script_name, $script_url, array( 'wp-hooks' ), 10 );
}
add_action( 'admin_enqueue_scripts', 'enqueue_management_link_script' );
```
## Step 2 - Install @wordpress/icons
To provide an icon of your choice for your store management link, you'll need to install `@wordpress/icons` in your JavaScript project:
```sh
npm install @wordpress/icons --save
```
## Step 3 - Add your filter
Your script will need to use `addFilter` to provide your custom link to the store management link section. And you'll need to import your icon of choice from `@wordpress/icons`. Here's an example:
```js
import { megaphone } from "@wordpress/icons";
import { addFilter } from "@wordpress/hooks";
addFilter(
"woocommerce_admin_homescreen_quicklinks",
"my-extension",
(quickLinks) => {
return [
...quickLinks,
{
title: "My link",
href: "link/to/something",
icon: megaphone,
},
];
}
);
```
Here's a screen shot using our new custom store management link:
![screen shot of custom store management link in wp-admin](https://i.imgur.com/yvXeSya.png)

View File

@ -0,0 +1,394 @@
# How to design a simple extension
## Introduction
Building a WooCommerce extension that provides a first-class experience for merchants and shoppers requires a hybrid development approach combining PHP and modern JavaScript. The PHP handles the lifecycle and server-side operations of your extension, while the modern JavaScript lets you shape the appearance and behavior of its user interface.
## The main plugin file
Your extensions main PHP file is a bootstrapping file. It contains important metadata about your extension that WordPress and WooCommerce use for a number of ecosystem integration processes, and it serves as the primary entry point for your extensions functionality. While there is not a particular rule enforced around naming this file, using a hyphenated version of the plugin name is a common best practice. (i.e. my-extension.php)
## Declaring extension metadata
Your extensions main plugin file should have a header comment that includes a number of important pieces of metadata about your extension. WordPress has a list of header requirements to which all plugins must adhere, but there are additional considerations for WooCommerce extensions:
- The `Author` and `Developer` fields are required and should be set to
either your name or your company name.
- The `Developer URI` field should be your official webpage URL.
- The `Plugin URI` field should contain the URL of the extensions product page in the WooCommerce Marketplace or the extensions official landing page on your website.
- For extensions listed in the WooCommerce Marketplace, to help facilitate the update process, add a `Woo` field and an appropriate value. WooCommerce Marketplace vendors can find this snippet by logging in to the Vendors Dashboard and navigating to `Extensions > All Extensions`. Then, select the product and click Edit product page. This snippet will be in the upper-right-hand corner of the screen.
Below is an example of what the header content might look like for an extension listed in the WooCommerce Marketplace.
```php
/**
* Plugin Name: My Great WooCommerce Extension
* Plugin URI: http://woocommerce.com/products/woocommerce-extension/
* Description: Your extension's description text.
* Version: 1.0.0
* Author: Your Name
* Author URI: http://yourdomain.com/
* Developer: Your Name
* Developer URI: http://yourdomain.com/
* Text Domain: my-extension
* Domain Path: /languages
*
* Woo: 12345:342928dfsfhsf8429842374wdf4234sfd
*
* License: GNU General Public License v3.0
* License URI: http://www.gnu.org/licenses/gpl-3.0.html
*/
```
## Preventing data leaks
As a best practice, your extensions PHP files should contain a conditional statement at the top that checks for WordPress ABSPATH constant. If this constant is not defined, the script should exit.
`defined( 'ABSPATH' ) || exit;`
This check prevents your PHP files from being executed via direct browser access and instead only allows them to be executed from within the WordPress application environment.
## Managing extension lifecycle
Because your main PHP file is the primary point of coupling between your extension and WordPress, you should use it as a hub for managing your extensions lifecycle. At a very basic level, this means handling:
- Activation
- Execution
- Deactivation
Starting with these three broad lifecycle areas, you can begin to break your extensions functionality down further to help maintain a good separation of concerns.
## Handling activation and deactivation
A common pattern in WooCommerce extensions is to create dedicated functions in your main PHP file to serve as activation and deactivation hooks. You then register these hooks with WordPress using the applicable registration function. This tells WordPess to call the function when the plugin is activated or deactivated. Consider the following examples:
```php
function my_extension_activate() {
// Your activation logic goes here.
}
register_activation_hook( __FILE__, 'my_extension_activate' );
```
```php
function my_extension_deactivate() {
// Your deactivation logic goes here.
}
register_deactivation_hook( __FILE__, 'my_extension_deactivate' );
```
## Maintaining a separation of concerns
There are numerous ways to organize the code in your extension. You can find a good overview of best practices in the WordPress Plugin Developer Handbook. Regardless of the approach you use for organizing your code, the nature of WordPress shared application space makes it imperative that you build with an eye toward interoperability. There are a few common principles that will help you optimize your extension and ensure it is a good neighbor to others:
- Use namespacing and prefixing to avoid conflicts with other extensions.
- Use classes to encapsulate your extensions functionality.
- Check for existing declarations, assignments, and implementations.
## The core extension class
As mentioned above, encapsulating different parts of your extensions functionality using classes is an important measure that not only helps with interoperability, but which also makes your code easier to maintain and debug. Your extension may have many different classes, each shouldering some piece of functionality. At a minimum, your extension should define a central class which can handle the setup, initialization and management of a single instance of itself.
## Implementing a singleton pattern
Unless you have a specific reason to create multiple instances of your main class when your extension runs, you should ensure that only one instance exists in the global scope at any time. A common way of doing this is to use a Singleton pattern. There are several ways to go about setting up a singleton in a PHP class. Below is a basic example of a singleton that also implements some of the best practices mentioned above about namespacing and pre-declaration checks:
```php
if ( ! class_exists( 'My_Extension' ) ) :
/**
* My Extension core class
*/
class My_Extension {
/**
* The single instance of the class.
*/
protected static $_instance = null;
/**
* Constructor.
*/
protected function __construct() {
// Instantiation logic will go here.
}
/**
* Main Extension Instance.
* Ensures only one instance of the extension is loaded or can be loaded.
*/
public static function instance() {
if ( is_null( self::$_instance ) ) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Cloning is forbidden.
*/
public function __clone() {
// Override this PHP function to prevent unwanted copies of your instance.
// Implement your own error or use `wc_doing_it_wrong()`
}
/**
* Unserializing instances of this class is forbidden.
*/
public function __wakeup() {
// Override this PHP function to prevent unwanted copies of your instance.
// Implement your own error or use `wc_doing_it_wrong()`
}
}
endif;
```
Notice that the example class above is designed to be instantiated by calling the static class method `instance()`, which will either return an existing instance of the class or create one and return it. In order to fully protect against unwanted instantiation, its also necessary to override the built-in magic methods `__clone()` and `__wakeup()`. You can implement your own error logging here or use something like `_doing_it_wrong()` which handles error logging for you. You can also use WooCommerces wrapper function `wc_doing_it_wrong()` here. Just be sure your code checks that the function exists first.
## Constructor
The example above includes an empty constructor for demonstration. In a real-world WooCommerce extension, however, this constructor should handle a few important tasks:
- Check for an active installation of WooCommerce & other sibling dependencies.
- Call a setup method that loads other files that your class depends on.
- Call an initialization method that gets your class and its dependencies ready to go.
If we build upon our example above, it might look something like this:
```php
protected function __construct() {
$this->includes();
$this->init();
// You might also include post-setup steps such as showing activation notices here.
}
```
## Loading dependencies
The includes() function above is where youll load other class dependencies, typically via an include or require constructs. A common way of managing and loading external dependencies is to use Composers autoload feature, but you can also load specific files individually. You can read more about how to autoload external dependencies in the Composer documentation. A basic example of a setup method that uses both Composer and internal inclusion is below.
```php
public function includes() {
$loader = include_once dirname( __FILE__ ) . '/' . 'vendor/autoload.php';
if ( ! $loader ) {
throw new Exception( 'vendor/autoload.php missing please run `composer install`' );
}
require_once dirname( __FILE__ ) . '/' . 'includes/my-extension-functions.php';
}
```
## Initialization
The `init()` function above is where you should handle any setup for the classes you loaded in the includes() method. This step is where youll often perform any initial registration with relevant actions or filters. Its also where you can register and enqueue your extensions JavaScripts and stylesheets.
Heres an example of what your initialization method might look like:
```php
private function init() {
// Set up cache management.
new My_Extension_Cache();
// Initialize REST API.
new My_Extension_REST_API();
// Set up email management.
new My_Extension_Email_Manager();
// Register with some-action hook
add_action( 'some-action', 'my-extension-function' );
}
```
There are many different ways that your core class initialization method might look, depending on the way that you choose to architect your extension. The important concept here is that this function serves as a central point for handling any initial registration and setup that your extension requires in order to respond to web requests going forward.
## Delaying initialization
The WordPress activation hook we set up above with register_activation_hook() may seem like a great place to instantiate our extensions main class, and in some cases it will work. By virtue of being a plugin for a plugin, however, WooCommerce extensions typically require WooCommerce to be loaded in order to function properly, so its often best to delay instantiation and initialization until after WordPress has loaded other plugins.
To do that, instead of hooking your instantiation to your extensions activation hook, use the plugins_loaded action in WordPress to instantiate your extensions core class and add its singleton to the $GLOBALS array.
```php
function my_extension_initialize() {
// This is also a great place to check for the existence of the WooCommerce class
if ( ! class_exists( 'WooCommerce' ) ) {
// You can handle this situation in a variety of ways,
// but adding a WordPress admin notice is often a good tactic.
return;
}
$GLOBALS['my_extension'] = My_Extension::instance();
}
add_action( 'plugins_loaded', 'my_extension_initialize', 10 );
```
In the example above, WordPress will wait until after all plugins have been loaded before trying to instantiate your core class. The third argument in add_action() represents the priority of the function, which ultimately determines the order of execution for functions that hook into the plugins_loaded action. Using a value of 10 here ensures that other WooCommerce-related functionality will run before our extension is instantiated.
## Handling execution
Once your extension is active and initialized, the possibilities are wide open. This is where the proverbial magic happens in an extension, and its largely up to you to define. While implementing specific functionality is outside the scope of this guide, there are some best practices to keep in mind as you think about how to build out your extensions functionality.
- Keep an event-driven mindset. Merchants and shoppers who use your extension will be interacting with WooCommerce using web requests, so it can be helpful to anchor your extension to some of the critical flows that users follow in WooCommerce.
- Keep business logic and presentation logic separate. This could be as simple as maintaining separate classes for handling back-end processing and front-end rendering.
- Where possible, break functionality into smaller parts and delegate responsibility to dedicated classes instead of building bloated classes and lengthy functions.
You can find detailed documentation of classes and hooks in the WooCommerce Core Code Reference and additional documentation of the REST API endpoints in the WooCommerce REST API Documentation.
## Handling deactivation
The WordPress deactivation hook we set up earlier in our main PHP file with register_deactivation_hook() is a great place to aggregate functionality for any cleanup that you need to handle when a merchant deactivates your extension. In addition to any WordPress-related deactivation tasks your extension needs to do, you should also account for WooCommerce-related cleanup, including:
- Removing Scheduled Actions
- Removing Notes in the Admin Inbox
- Removing Admin Tasks
## Uninstallation
While its certainly possible to completely reverse everything your extension has created when a merchant deactivates it, its not advisable nor practical in most cases. Instead, its best to reserve that behavior for uninstallation.
For handling uninstallation, its best to follow the guidelines in the WordPress Plugin Handbook.
## Putting it all together
Below is an example of what a main plugin file might look like for a very simple extension:
```php
/**
* Plugin Name: My Great WooCommerce Extension
* Plugin URI: http://woocommerce.com/products/woocommerce-extension/
* Description: Your extension's description text.
* Version: 1.0.0
* Author: Your Name
* Author URI: http://yourdomain.com/
* Developer: Your Name
* Developer URI: http://yourdomain.com/
* Text Domain: my-extension
* Domain Path: /languages
*
* Woo: 12345:342928dfsfhsf8429842374wdf4234sfd
*
* License: GNU General Public License v3.0
* License URI: http://www.gnu.org/licenses/gpl-3.0.html
*/
defined( 'ABSPATH' ) || exit;
/**
* Activation and deactivation hooks for WordPress
*/
function myPrefix_extension_activate() {
// Your activation logic goes here.
}
register_activation_hook( __FILE__, 'myPrefix_extension_activate' );
function myPrefix_extension_deactivate() {
// Your deactivation logic goes here.
// Don't forget to:
// Remove Scheduled Actions
// Remove Notes in the Admin Inbox
// Remove Admin Tasks
}
register_deactivation_hook( __FILE__, 'myPrefix_extension_deactivate' );
if ( ! class_exists( 'My_Extension' ) ) :
/**
* My Extension core class
*/
class My_Extension {
/**
* The single instance of the class.
*/
protected static $_instance = null;
/**
* Constructor.
*/
protected function __construct() {
$this->includes();
$this->init();
}
/**
* Main Extension Instance.
*/
public static function instance() {
if ( is_null( self::$_instance ) ) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Cloning is forbidden.
*/
public function __clone() {
// Override this PHP function to prevent unwanted copies of your instance.
// Implement your own error or use `wc_doing_it_wrong()`
}
/**
* Unserializing instances of this class is forbidden.
*/
public function __wakeup() {
// Override this PHP function to prevent unwanted copies of your instance.
// Implement your own error or use `wc_doing_it_wrong()`
}
/**
* Function for loading dependencies.
*/
private function includes() {
$loader = include_once dirname( __FILE__ ) . '/' . 'vendor/autoload.php';
if ( ! $loader ) {
throw new Exception( 'vendor/autoload.php missing please run `composer install`' );
}
require_once dirname( __FILE__ ) . '/' . 'includes/my-extension-functions.php';
}
/**
* Function for getting everything set up and ready to run.
*/
private function init() {
// Examples include:
// Set up cache management.
// new My_Extension_Cache();
// Initialize REST API.
// new My_Extension_REST_API();
// Set up email management.
// new My_Extension_Email_Manager();
// Register with some-action hook
// add_action('some-action', 'my-extension-function');
}
}
endif;
/**
* Function for delaying initialization of the extension until after WooComerce is loaded.
*/
function my_extension_initialize() {
// This is also a great place to check for the existence of the WooCommerce class
if ( ! class_exists( 'WooCommerce' ) ) {
// You can handle this situation in a variety of ways,
// but adding a WordPress admin notice is often a good tactic.
return;
}
$GLOBALS['my_extension'] = My_Extension::instance();
}
```

View File

@ -0,0 +1,5 @@
# Extension Development
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
This section will provide comprehensive guidance on developing extensions for WooCommerce. You will be able to dive into methodologies, best practices, and discover tutorials to create robust, user-friendly extensions that enhance the capabilities of the WooCommerce platform.

View File

@ -0,0 +1,54 @@
# Building blocks for low code builders
## Introduction
This guide provides an introduction to low-code solutions, such as Gutenberg and WooCommerce Store Editing, as well as other page builders and pre-built components for creating WooCommerce stores without custom coding. By understanding and leveraging these tools, low code builders can assemble stores with minimal coding and focus on the design and user experience aspects of their e-commerce websites.
## Audience
This guide is intended for low code builders or anyone with a basic understanding of WordPress and WooCommerce who wants to create online stores without the need for extensive custom coding.
## Prerequisites
To follow this guide, you should have:
1. A basic understanding of WordPress and WooCommerce.
2. A WordPress website with WooCommerce installed and activated.
## Step 1 — Using Gutenberg and WooCommerce Store Editing
Gutenberg is the default block editor in WordPress that allows you to create and edit pages by adding and customizing blocks. WooCommerce has extended Gutenberg's functionality, enabling you to create and customize WooCommerce-specific elements, such as product pages and shop pages.
To use Gutenberg and WooCommerce Store Editing:
1. Ensure your WordPress installation is up-to-date to have the latest version of Gutenberg.
2. Install and activate a Block Theme to enable Store Editing features for WooCommerce.
With Gutenberg and WooCommerce Store Editing, you can create and customize your store's pages using a wide variety of blocks, such as text, images, buttons, and WooCommerce-specific blocks like product grids and shopping carts.
## Step 2 — Exploring alternative page builders
While Gutenberg and WooCommerce Store Editing are powerful options for building low-code WooCommerce stores, you may also consider using other page builders for more advanced features or specific use cases. Some popular page builders compatible with WooCommerce include:
1. Elementor
2. Beaver Builder
3. Divi Builder
4. WPBakery Page Builder
Choose a page builder that fits your needs and budget, then install and activate it on your WordPress website. These page builders typically offer a library of pre-built components that you can use to create a fully functional WooCommerce store without writing custom code.
## Step 3 — Utilizing pre-built components and templates
Many page builders, including Gutenberg, offer pre-built components or blocks that can be easily added to your pages. These components can include design elements like buttons, forms, and image galleries, as well as WooCommerce-specific components like product grids and shopping carts.
Additionally, some page builders and WooCommerce extensions offer pre-built store templates that you can import and customize to create a fully functional online store quickly. These templates can save you time and effort by providing a professionally designed starting point for your store.
To use pre-built components and templates:
1. Open your preferred page builder's editor (Gutenberg or another page builder).
2. Browse through the available components/blocks or templates and find the ones that suit your needs.
3. Add the components to your pages and customize them using the provided settings and options.
## Conclusion
By leveraging low-code solutions like Gutenberg, WooCommerce Store Editing, and other page builders, you can create a fully functional WooCommerce store without the need for custom coding. This guide has introduced you to the basics of using these tools, helping you understand the available options and assemble your store with minimal coding. With the right combination of tools and templates, you can create a professional, user-friendly e-commerce website that meets your business needs.

View File

@ -0,0 +1,102 @@
# WooCommerce Developer Resources
This guide is a great starting point for WooCommerce development. From setting up your first online store to diving deep into advanced features, you'll find what you need here. New to WooCommerce? Start with the basics. Experienced and looking for specific documentation or community discussions? We've got that covered too. Navigate through the sections below to find the resources tailored for you.
## Getting Started
There are a few different ways you might want to get started utilizing WooCommerce. Choose a path below to start developing based on your code comfort level!
### [Installing and setting up WooCommerce](https://woocommerce.com/document/build-online-store/)
If youre brand new to Woo, this guide will show you How to build an online store on WooCommerce. This is where you can learn the ins and outs of how WooCommerce works before you start developing.
### [Extension Development Quick Start](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/create-woo-extension)
This no-configuration quick-start package will scaffold a local copy of an extension template for you. Just open up your terminal and follow the steps in GitHub.
### [Building your first extension](/extension-development/building-your-first-extension.md)
This guide will have you building your first extension with best practices and helpful tips.
### [Marketplace Contribution Guidelines](https://woocommerce.com/document/marketplace-overview/)
Are you hoping to sell your extension in the [Woo Marketplace](https://woocommerce.com/marketplace/)? Read our guidelines to make sure your extension is marketplace-ready.
### [Contributor Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md)
If you've ever wanted to contribute to the WooCommerce platform as a developer please read our guidelines for contribution first.
### [Contribution Environment Set-Up](https://github.com/woocommerce/woocommerce/tree/trunk)
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)
Check out our guide to learn more about developer tools, libraries, and utilities.
---
## API & Reference Docs
The resources below contain low-level documentation about features, libraries, extensions, and other pieces of WooCommerce architecture. Use them as a reference when building extensions or integrating with WooCommerce.
## [REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/)
The WooCommerce REST API lets you create, read, update, and delete WooCommerce data using HTTP requests, so you can integrate external applications with WooCommerce and build extensions that make use of asynchronous UI frameworks such as React.
### [Core API](https://docs.woocommerce.com/wc-apidocs/index.html)
The WooCommerce Core API code reference contains information about packages and classes that make up WooCommerce's core functionality.
### [Store API](https://github.com/woocommerce/woocommerce-blocks/tree/trunk/src/StoreApi)
The Store API provides public Rest API endpoints for the development of customer-facing cart, checkout, and product functionality. It follows many of the patterns used in the [WordPress REST API](https://developer.wordpress.org/rest-api/key-concepts/).
### [WooCommerce Blocks](https://github.com/woocommerce/woocommerce-gutenberg-products-block/#documentation)
WooCommerce Blocks give you the ability to integrate WooCommerce with Gutenberg. Use the documentation and resources here as a starting point for developing new block types for WooCommerce.
### [Core Action and Filter Hooks](https://docs.woocommerce.com/wc-apidocs/hooks/hooks.html)
This contains an index of hooks found across all template files, functions, shortcodes, widgets, data stores, and core classes. You can use these hooks to extend the core WooCommerce platform by introducing custom behavior or modifying data that WooCommerce passes around.
### [Shortcodes Included with WooCommerce](https://docs.woocommerce.com/document/woocommerce-shortcodes/)
While WooCommerce Blocks are now the easiest and most flexible way to display your products on posts and pages, WooCommerce still comes with several shortcodes to insert content.
---
## GitHub Repositories
### [WooCommerce on GitHub](https://github.com/woocommerce)
This is the official WooCommerce organization on GitHub. Here youll find the majority of development work that happens on open source projects that the WooCommerce team maintains.
### [Automattic on GitHub](https://github.com/automattic)
This is the official Automattic organization on GitHub. It is where you'll find the majority of development work that happens on open source projects that the Automattic team maintains.
### [WordPress on GitHub](https://github.com/wordpress)
This is the official WordPress organization on GitHub a go-to source for the development work that happens on open source projects that the WordPress community maintains.
---
## Ecosystem Resources
### [WordPress Developer Resources](https://developer.wordpress.org/)
All the resources you need for developing with WordPress. If youre not familiar with the WordPress development ecosystem, this is a great place to start.
### [WooCommerce Community Slack](https://woocommerce.com/community-slack)
Join our community on Slack. We hold regular sessions where we share information and field questions, but you can also connect with other developers to share challenges and ask questions.
### [WooCommerce Community Forum](https://wordpress.org/support/plugin/woocommerce/)
Use this forum to ask questions about WooCommerce. Our WooCommerce Happiness Engineers frequent this forum to answer questions, but there is also a wealth of knowledge that has been captured in these threads over the years.
### [WooCommerce on Reddit](https://www.reddit.com/r/woocommerce/)
Visit the WooCommerce subreddit to ask questions and share tips with other developers.

View File

@ -0,0 +1,89 @@
# WooCommerce Developer Tools
This guide provides an overview of essential tools and libraries for WooCommerce development. It's intended for developers looking to enhance their WooCommerce projects efficiently.
## Table of Contents
- [Productivity Tools](#productivity-tools)
- [Libraries](#libraries)
- [Utilities](#utilities)
### Productivity Tools
Use these resources to get a WooCommerce development environment up and running.
#### [wp-cli](https://wp-cli.org/)
This is the command-line interface for [WordPress](https://wordpress.org/). You can update plugins, configure multisite installations and much more, without using a web browser.
#### [wp-env](https://www.npmjs.com/package/@wordpress/env)
This command-line tool lets you easily set up a local WordPress environment for building and testing plugins and themes. Its simple to install and requires no configuration.
#### [eslint-plugin](https://www.npmjs.com/package/@woocommerce/eslint-plugin)
This is an [ESLint](https://eslint.org/) plugin including configurations and custom rules for WooCommerce development.
#### [e2e-environment](https://www.npmjs.com/package/@woocommerce/e2e-environment)
This is a reusable and extensible end-to-end testing environment for WooCommerce extensions. Additionally, it contains several files to serve as the base for a Docker container and Travis CI setup.
#### [WordPress Scripts](https://www.npmjs.com/package/@wordpress/scripts)
This is a collection of reusable scripts tailored for WordPress development.
---
### Libraries
Use these resources to help take some of the heavy lifting off of fetching and transforming data as well as creating UI elements.
#### API Clients
#### [WooCommerce REST API — JavaScript](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api)
The official JavaScript library for working with the WooCommerce REST API.
#### [api-fetch](https://www.npmjs.com/package/@wordpress/api-fetch)
This is a utility to make WordPress REST API requests. It's a wrapper around `window.fetch` that includes support for nonces, middleware, and custom fetch handlers.
#### Components
#### [WooCommerce Components](https://www.npmjs.com/package/@woocommerce/components)
This package includes a library of React components that can be used to create pages in the WooCommerce admin area.
#### [WordPress Components](https://www.npmjs.com/package/@wordpress/components)
This packages includes a library of generic WordPress components that can be used for creating common UI elements shared between screens and features of the WordPress dashboard.
---
### Utilities
#### [CSV Export](https://www.npmjs.com/package/@woocommerce/csv-export)
A set of functions to convert data into CSV values, and enable a browser download of the CSV data.
#### [Currency](https://www.npmjs.com/package/@woocommerce/currency)
A collection of utilities to display and work with currency values.
#### [Data](https://www.npmjs.com/package/@woocommerce/data)
Utilities for managing the WooCommerce Admin data store.
#### [Date](https://www.npmjs.com/package/@woocommerce/date)
A collection of utilities to display and work with date values.
#### [Navigation](https://www.npmjs.com/package/@woocommerce/navigation)
A collection of navigation-related functions for handling query parameter objects, serializing query parameters, updating query parameters, and triggering path changes.
#### [Number](https://www.npmjs.com/package/@woocommerce/number)
A collection of utilities to properly localize numerical values in WooCommerce.

View File

@ -0,0 +1,5 @@
# Getting-started
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Jumpstart your journey with WooCommerce. This category will cover the basics and essentials, from installation to initial setup, ensuring you have a solid foundation to build upon.

View File

@ -1,4 +1,4 @@
# HPOS # High Performance Order Storage (HPOS)
WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues, and that's why HPOS (High-Performance Order Storage) was developed. HPOS is the WooCommerce engine that stores orders in dedicated tables. WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues, and that's why HPOS (High-Performance Order Storage) was developed. HPOS is the WooCommerce engine that stores orders in dedicated tables.

View File

@ -0,0 +1,5 @@
# High Performance Order Storage
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
This section is where you can learn about High-Performance Order Storage (HPOS): a new database storage for orders to allow effortless scaling for large and high growth stores.

View File

@ -0,0 +1,67 @@
# Coding standards for the code snippets within the WooCommerce documentation
## Position of hooks
Position hooks below the function call, as this follows the common pattern in the WordPress and WooCommerce ecosystem.
### Example
```php
/**
* Add custom message.
*/
function YOUR_PREFIX_custom_message() {
echo 'This is a custom message';
}
add_action( 'wp_footer', 'YOUR_PREFIX_custom_message' );
```
## Prefixing function calls
Use a consistent prefix for all function calls. For the code snippets in this repo, use the prefix `YOUR_PREFIX`.
### Example
```php
/**
* Add custom discount.
*/
function YOUR_PREFIX_custom_discount( $price, $product ) {
return $price * 0.9; // 10% discount
}
add_filter( 'woocommerce_product_get_price', 'YOUR_PREFIX_custom_discount', 10, 2 );
```
## Translatable texts and text domains
Make all plain texts translatable, and use a consistent text domain. This aligns with the best practices for internationalisation. For the code snippets in this repo, use the textdomain `YOUR-TEXTDOMAIN`.
### Example
```php
/**
* Add custom message.
*/
function YOUR_PREFIX_welcome_message() {
echo __( 'Welcome to our website', 'YOUR-TEXTDOMAIN' );
}
add_action( 'wp_footer', 'YOUR_PREFIX_welcome_message' );
```
## Use of function_exists()
Wrap all function calls in a `function_exists()` call to prevent errors due to potential function redeclaration.
### Example
```php
/**
* Add thumbnail support.
*/
if ( ! function_exists( 'YOUR_PREFIX_theme_setup' ) ) {
function YOUR_PREFIX_theme_setup() {
add_theme_support( 'post-thumbnails' );
}
}
add_action( 'after_setup_theme', 'YOUR_PREFIX_theme_setup' );
```

View File

@ -0,0 +1,321 @@
# WooCommerce grammar, punctuation and capitalization guide
Following grammar, punctuation and style guidelines helps keep our presentation consistent. Users have a better experience if they know what to expect and where to find the information they need.
## Basics
**Be democratic**. Some people read every word. Some scan and search or prefer video. Help everyone.
**Be focused**. Lead with the most important information in sentences, paragraphs, and sections.
**Be concise**. Use plain language and brief sentences.
**Be consistent**. Follow our guidelines and style tips.
**Be specific**. Communicate crystal clear. Trim the fat.
## Guidelines
### Abbreviations and acronyms
Spell out the full version on first mention with abbreviation or acronym in parentheses. Use the short version on second and consecutive mentions.
- First use: Payment Card Industry Data Security Standard (PCI-DSS)
- Second use: PCI-DSS
If the abbreviation or acronym is widely known, use it as is. For example: API, FAQ, HTML, PHP, SQL, SSL.
### Active voice
With active voice, the subject in the sentence performs the action. With passive voice, the subject in the sentence has the action done unto it.
- Active: Jon downloaded his extension files.
- Passive: The extension files were downloaded by Jon.
### Capitalization
Cases when we capitalize:
- Blog post and documentation article titles: First word.
- Documentation headings (h2): Use sentence case (not title case) for docs titles and subheadings.
- Product names: Every word except prepositions and conjunctions.
- Sentences: First word.
- Unordered/Bulleted lists First word of each entry.
Cases when we use lower case:
- “ecommerce” (not “eCommerce”)
- email address — info@woocommerce.com
- website URL — developer.woocommerce.com
### Contractions
Use with discretion. Contractions, such as Im and theres, give writing an informal and conversational feel, but may be inappropriate if content is being translated. For example, sometimes the not in dont is ignored by online translators.
### Emoji
Emoji can add subtle emotion and humor or bring visual attention to your content. Use rarely and intentionally.
### Numbers
Spell out a number at the start of a sentence, and spell out numbers one through nine in all cases. Use numerals in all other cases.
- Ten products will launch in June. Not: 10 products will launch in June.
- Lance ran a marathon and won third place in his age group.
- I bought five hammers and 21 types of nails for the building project.
- There were 18 kinds of beer on tap at the pub.
Use a comma for numbers with more than three digits: 41,500, 170,000, 1,000,000 or 1 million.
#### Currency
Use currency codes and not only the symbol/sign when specifying dollars. Whole amounts need not have a decimal and two places.
- USD $20
- CAD $19.99
- AUD $39.50
When writing about other currencies, use the symbol/sign.
- €995
- ¥5,000
- £18.99
#### Dates
Spell out the day of the week and month, using the format:
- Monday, December 12, 2016
#### Decimals
Use decimal points when a number is difficult to convert to a fraction, such as 3.141 or 98.5 or 0.29.
#### Fractions
Spell out fractions: one-fourth
#### Percent
Spell out the word percent. Dont use % symbol unless space is limited, e.g., for use on social media.
#### Phone numbers
Use hyphens without spaces between numbers, not parentheses or periods. Use a [country code](https://countrycode.org/) for all countries.
- +1-555-867-5309
- +34-902-1899-00
#### Range and span
Use a hyphen to indicate a range or span of numbers: 20-30 days.
#### Temperature
Use the degree symbol and the capital C abbreviation for Celsius and capital F abbreviation for Fahrenheit.
- 27°C
- 98°F
#### Times
Use numbers and am or pm with a space and without periods.
- 7:00 am
- 7:30 pm
Use a hyphen between times to indicate a time period in am or pm. Use to if the time period spans am and pm.
- 7:00-9:00 am and 7:00 am to 10:30 pm
Specify a time zone when writing about an event with potential attendees worldwide. Automattic uses Coordinated Universal Time (UTC).
Abbreviate U.S. time zones:
- Eastern time: EDT or EST
- Central time: CDT or CST
- Mountain time: MDT or MST
- Pacific time: PDT or PST
#### Years
Abbreviate decades
- 80s and 90s
- 1900s and 1890s
### Punctuation
#### Ampersands
Ampersands need only be used when part of an official company/brand name. Should not be substituted for and.
- Ben & Jerrys
- Andre, Timo, and Donny went to a football game at Camp Nou.
#### Apostrophes
An apostrophe makes a word possessive. If a word already ends in s and is singular, add an s. If a word ends in s and is plural, add an apostrophe.
- A teammate borrowed Sams bike.
- A teammate borrowed Chriss bike.
- Employees hid the office managers pens.
These are possessives: FAQs questions, HEs weekly rotation. These are plural: FAQs and HEs.
#### Colons
Use a colon to create a list.
- Aaron ordered three kinds of donuts: glazed, chocolate, and pumpkin.
#### Commas
Use a serial comma, also known as an Oxford comma, when compiling a list.
- Jinny likes sunflowers, daisies, and peonies.
Use common sense for other cases. Read the sentence out loud, and use a comma where clarity or pause may be needed.
#### Dashes and hyphens
Use a hyphen without spaces on either side to link words, or indicate a span or range.
- first-time user
- Monday-Friday
Use an em dash — without spaces on either side to indicate an aside.
Use a true em dash, not hyphens or .
- Multivariate testing—just one of our new Pro features—can help you grow your business.
- Austin thought Brad was the donut thief, but he was wrong—it was Lain.
#### Ellipses
Ellipses … can be used to indicate an indefinite ending to a sentence or to show words are omitted when used in brackets […] Use rarely.
#### Exclamation points
Use an exclamation point rarely and use only one.
Exclamation points follow the same placement convention explained in Periods.
#### Periods
Periods should be:
- Inside quotation marks
- Outside parentheses when the portion in parentheses is part of a larger sentence
- Inside parentheses when the part in parentheses can stand on its own
Examples
- Jake said, “I had the best day ever.”
- She went to the supermarket (and to the nail salon).
- My mom loves pizza and beer. (Beer needs to be cold and dark.)
#### Question marks
Question marks follow the same placement convention explained in Periods.
#### Quotation marks
Periods and commas go within quotation marks. Question marks within quotes follow logic—if the question mark is part of the quotation, it goes within. If youre asking a question that ends with a quote, it goes outside the quote.
Use single quotation marks for quotes within quotes.
- Who sings, “All These Things That Ive Done”?
- Brandon Flowers of The Killers said, “I was inspired and on a roll when I wrote, I got soul, but Im not a soldier.’”
#### Semicolons
Semicolons can be used to join two related phrases.
- Their debut solo album hit the Top 10 in 20 countries; it was #1 in the UK.
### People, places, and things
#### Company names and products
Use brand identity names and products as written on official websites.
- Nestlé
- Pull&Bear
- UE Boom
Refer to a company or product as it (not they).
- WooCommerce is, and not WooCommerce are.
#### File extensions
A file extension type should be all uppercase without periods. Add a lowercase s to make plural.
- HTML
- JPEG
- PDF
A specific file should have a lowercase extension type:
- dancingcat.gif
- SalesReport2016.pdf
- firethatcannon.mp3
#### Names and titles
First mention of a person should include their first and last name. Second and consecutive mentions can use first name only.
Capitalize job titles, the names of teams, and departments.
- Happiness Engineers or HEs
- Team Apollo
- Legal
#### Pronouns
Use he/him/his and she/her/her as appropriate. Dont use “one” as a pronoun. Use they/them/their if gender is unknown or when referring to a group.
#### Quotations
Use present tense when quoting someone.
- “I love that WooCommerce is free and flexible,” says Brent Jamison.
#### Schools
The first time you mention a school, college, or university in a piece of writing, refer to it by its full official name. On all other mentions, use its more common abbreviation.
- Georgia Institute of Technology, Georgia Tech
- Georgia State University, GSU
#### States, cities, and countries
Spell out all city and state names. Dont abbreviate city names.
On first mention, write out United States. For further mentions, use U.S. The same applies to other countries or federations with a common abbreviation, such as European Union (EU) and United Kingdom (UK).
#### URLs and websites
Capitalize the names of websites and web publications. Dont italicize.
Avoid writing out URLs; omit `http://www` when its necessary.
### Slang and jargon
Write in plain English. Text should be universally understood, with potential for translation. Briefly define technical terms when needed.
### Text formatting
Use italics to indicate the title of a book, movie, or album.
- The Oren Klaff book Pitch Anything is on sale for USD $5.99.
Avoid:
- Underline formatting
- A mix of italic, bold, caps, and underline
Left-align text, never center or right-aligned.
Leave one space between sentences, never two.

View File

@ -0,0 +1,89 @@
# Performance optimization for WooCommerce stores
## Introduction
This guide covers best practices and techniques for optimizing the performance of WooCommerce stores, including caching, image optimization, database maintenance, code minification, and the use of Content Delivery Networks (CDNs). By following these recommendations, developers can build high-performing WooCommerce stores that provide a better user experience and contribute to higher conversion rates.
## Audience
This guide is intended for developers who are familiar with WordPress and WooCommerce and want to improve the performance of their online stores.
## Prerequisites
To follow this guide, you should have:
1. A basic understanding of WordPress and WooCommerce.
2. Access to a WordPress website with WooCommerce installed and activated.
## Step 1 — Implement caching
Caching plays a crucial role in speeding up your WooCommerce store by serving static versions of your pages to visitors, reducing the load on your server. There are several ways to implement caching for your WooCommerce store:
### Server-Side caching
Enable server-side caching through your hosting provider or by using server-level caching solutions like Varnish, NGINX FastCGI Cache, or Redis.
### WordPress caching plugins
Install and configure a WordPress caching plugin, such as WP Rocket, W3 Total Cache, or WP Super Cache. These plugins can help you set up page caching, browser caching, and object caching for your WooCommerce store.
### WooCommerce-Specific caching
Ensure that your caching solution is configured correctly for WooCommerce, allowing dynamic content such as cart and checkout pages to remain uncached. Some caching plugins, like WP Rocket, include built-in support for WooCommerce caching.
## Step 2 — Optimize images
Optimizing images can significantly improve your store's performance by reducing the size of image files without compromising quality. To optimize images for your WooCommerce store:
1. Use the right image format: Choose an appropriate format for your images, such as JPEG for photographs and PNG for graphics with transparency.
2. Compress images: Use an image compression tool like TinyPNG or ShortPixel to reduce file sizes before uploading them to your store.
3. Enable lazy loading: Lazy loading delays the loading of images until they're needed, improving initial page load times. Many caching plugins and performance optimization plugins offer built-in lazy loading options.
4. Use responsive images: Ensure that your theme and plugins serve appropriately sized images for different devices and screen resolutions.
## Step 3 — Minify and optimize code
Minifying and optimizing your store's HTML, CSS, and JavaScript files can help reduce file sizes and improve page load times. To minify and optimize code for your WooCommerce store:
1. Use a plugin: Install a performance optimization plugin like Autoptimize, WP Rocket, or W3 Total Cache to minify and optimize your store's HTML, CSS, and JavaScript files.
2. Combine and inline critical CSS: Where possible, combine and inline critical CSS to reduce the number of requests and improve page load times.
3. Defer non-critical JavaScript: Defer loading of non-critical JavaScript files to improve perceived page load times.
## Step 4 — Use a content delivery network (CDN)
A Content Delivery Network (CDN) can help speed up your WooCommerce store by serving static assets like images, CSS, and JavaScript files from a network of servers distributed across the globe. To use a CDN for your WooCommerce store:
1. Choose a CDN provider: Select a CDN provider like Cloudflare, Fastly, or Amazon CloudFront that fits your needs and budget.
2. Set up your CDN: Follow your chosen CDN provider's instructions to set up and configure the CDN for your WooCommerce store.
## Step 5 — Optimize database
Regularly optimizing your WordPress database can help improve your WooCommerce store's performance by removing unnecessary data and optimizing database tables. To optimize your database:
1. Use a plugin: Install a database optimization plugin like WP-Optimize, WP-Sweep, or Advanced Database Cleaner to clean up and optimize your WordPress database.
2. Remove unnecessary data: Regularly delete spam comments, post revisions, and expired transients to reduce database clutter.
3. Optimize database tables: Use the database optimization plugin to optimize your database tables, improving their efficiency and reducing query times.
## Step 6 — Choose a high-performance theme and plugins
The theme and plugins you choose for your WooCommerce store can have a significant impact on performance. To ensure your store runs efficiently:
1. Select a lightweight, performance-optimized theme: Choose a theme specifically designed for WooCommerce that prioritizes performance and follows best coding practices.
2. Evaluate plugin performance: Use tools like Query Monitor or WP Hive to analyze the performance impact of the plugins you install, and remove or replace those that negatively affect your store's performance.
## Step 7 — Enable GZIP compression
GZIP compression can help reduce the size of your store's HTML, CSS, and JavaScript files, leading to faster page load times. To enable GZIP compression:
1. Use a plugin: Install a performance optimization plugin like WP Rocket, W3 Total Cache, or WP Super Cache that includes GZIP compression options.
2. Configure your server: Alternatively, enable GZIP compression directly on your server by modifying your .htaccess file (for Apache servers) or nginx.conf file (for NGINX servers).
## Step 8 — Monitor and analyze performance
Continuously monitor and analyze your WooCommerce store's performance to identify potential bottlenecks and areas for improvement. To monitor and analyze performance:
1. Use performance testing tools: Regularly test your store's performance using tools like Google PageSpeed Insights, GTmetrix, or WebPageTest.
2. Implement performance monitoring: Install a performance monitoring plugin like New Relic or use a monitoring service like Uptime Robot to keep track of your store's performance over time.
## Conclusion
By following these best practices and techniques for performance optimization, you can build a high-performing WooCommerce store that offers a better user experience and contributes to higher conversion rates. Continuously monitor and analyze your store's performance to ensure it remains optimized as your store grows and evolves.

View File

@ -0,0 +1,5 @@
# Quality and Best Practices
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Ensuring the quality of your WooCommerce projects is essential. This section will delve into quality exoectations, best practices, coding standards, and other methodologies to ensure your projects stand out in terms of reliability, efficiency, user experience, and more.

View File

@ -0,0 +1,5 @@
# Reference Code
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Dive deep into code snippets, examples, and templates tailored for WooCommerce. This section will serve as a valuable resource for developers, providing reusable pieces of code that can be integrated into various WooCommerce projects.

View File

@ -0,0 +1,86 @@
# Adding columns to analytics reports and CSV downloads
Adding columns to analytics reports are a really interesting way to add functionality to WooCommerce. New data can be consumed in the table view of the user interface and in your user's favourite spreadsheet or third party application by generating a CSV.
These instructions assume that you have a test plugin for WooCommerce installed and activated. You can follow the ["Getting started" instructions](extending-woocommerce-admin-reports.md) to get a test plugin set up. That post also includes instructions to further modify the query that is executed to get the data in an advanced fashion - it isn't required to just add a simple column.
In WooCommerce, analytics CSVs are generated in two different ways: in the web browser using data already downloaded, or on the server using a new query. It uses the size of the data set to determine the method - if there is more than one page worth of results it generates the data on the server and emails a link to the user, but if the results fit on one page the data is generated and downloaded straight away in the browser.
We'll look at the on-server method for adding a column first, because this is also where the data sent to the browser is generated.
This example extends the Downloads analytics report. To get some data in your system for this report, create a downloadable product with a download expiry value, create an order purchasing the product, then download the product several times. In testing I created 26 downloads, which is enough that the report is spread over two pages when showing 25 items per page, and on a single page when showing 50 items per page. This let me test CSVs generated both on the server and in browser.
In the PHP for your plugin, add three filter handlers:
```php
// This adds the SELECT fragment to the report SQL
function add_access_expires_select( $report_columns, $context, $table_name ) {
if ( $context !== 'downloads' ) {
return $report_columns;
}
$report_columns['access_expires'] =
'product_permissions.access_expires AS access_expires';
return $report_columns;
}
add_filter( 'woocommerce_admin_report_columns', 'add_access_expires_select', 10, 3 );
// This adds the column header to the CSV
function add_column_header( $export_columns ) {
$export_columns['access_expires'] = 'Access expires';
return $export_columns;
}
add_filter( 'woocommerce_filter_downloads_export_columns', 'add_column_header' );
// This maps the queried item to the export item
function map_access_expires( $export_item, $item ) {
$export_item['access_expires'] = $item['access_expires'];
return $export_item;
}
add_filter( 'woocommerce_report_downloads_prepare_export_item', 'map_access_expires', 10, 2 );
```
This adds the access expiry timestamp to the Downloads table/CSV (when the CSV is generated on the server).
These three filters together add the new column to the database query, adds the new header to the CSV, and maps the data returned from the database to the CSV. The first filter `woocommerce_admin_report_columns` adds a SQL fragment to the `SELECT` statement generated for the data query. The second filter `woocommerce_filter_downloads_export_columns` adds the column header to the CSV generated on the server. The third filter `woocommerce_report_downloads_prepare_export_item` maps the value in the data returned from the database query `$item` to the export item for the CSV.
To finish this off by adding support for columns generated in browser, another filter needs to be added to your plugin's JavaScript:
```js
import { addFilter } from "@wordpress/hooks";
function addAccessExpiresToDownloadsReport(reportTableData) {
const { endpoint, items } = reportTableData;
if ("downloads" !== endpoint) {
return reportTableData;
}
reportTableData.headers = [
...reportTableData.headers,
{
label: "Access expires",
key: "access_expires",
},
];
reportTableData.rows = reportTableData.rows.map((row, index) => {
const item = items.data[index];
const newRow = [
...row,
{
display: item.access_expires,
value: item.access_expires,
},
];
return newRow;
});
return reportTableData;
}
addFilter(
"woocommerce_admin_report_table",
"dev-blog-example",
addAccessExpiresToDownloadsReport
);
```
This filter first adds the header to the CSV, then maps the data.
With the plugin you've created, you should now be able to add data to the analytics table, the CSV generated on the server, and the CSV generated on the browser.

View File

@ -0,0 +1,302 @@
# Extending WC-Admin reports
## Introduction
This document serves as a guide to extending WC-Admin Reports with a basic UI dropdown, added query parameters, and modification of SQL queries and resulting report data. This example will create a currency selector for viewing the Orders Report based on a specific currency.
Code from this guide can be viewed in the [wc-admin code repository](https://github.com/woocommerce/woocommerce-admin/tree/main/docs/examples/extensions/sql-modification).
## Getting started
We'll be using a local installation of WordPress with WooCommerce and the development version of WC-Admin to take advantage of `create-wc-extension` as a way to easily scaffold a modern WordPress JavaScript environment for plugins.
In your local install, clone and start WC-Admin if you haven't already.
```sh
cd wp-content/plugins
git clone git@github.com:woocommerce/woocommerce-admin.git
cd woocommerce-admin
npm run build
```
Once thats working, we can setup the extension folder ready for JavaScript development.
```sh
npm run create-wc-extension
```
After choosing a name, move into that folder and start webpack to watch and build files.
```sh
cd ../<my-plugin-name>
npm install
npm start
```
Don't forget to head over to `/wp-admin/plugins.php` and activate your plugin.
## Populating test data
Next, set up some orders to have sample data. Using WooCommerce > Settings > Currency, I added three test orders in Mexican Peso, US Dollar, and New Zealand Dollar.
After doing so, check out WC-Admin to make sure the orders are showing up by going to `/wp-admin/admin.php?page=wc-admin&period=today&path=%2Fanalytics%2Forders&compare=previous_year`. Note that without any modification currency figures show according to what I have currently in WooCommerce settings, which is New Zealand Dollar in this case.
![screenshot of wp-admin showing processing orders](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-12.11.34-pm.png?w=851)
We can confirm each order's currency by running the following query on the `wp_posts` table and joining `wp_postmeta` to gather currency meta values. Results show an order in NZD, USD, and MXN. This query is similar to the one we'll implement later in the guide to gather and display currency values.
```sql
SELECT
ID,
post_name,
post_type,
currency_postmeta.meta_value AS currency
FROM `wp_posts`
JOIN wp_postmeta currency_postmeta ON wp_posts.ID = currency_postmeta.post_id
WHERE currency_postmeta.meta_key = '_order_currency'
ORDER BY wp_posts.post_date DESC
LIMIT 3
```
![screenshot of resulting query](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-12.33.45-pm.png?w=756)
## Add a UI dropdown
In order to view reports in differing currencies, a filter or dropdown will be needed. We can add a basic filter to reports by adding a configuration object similar to [this one from the Orders Report](https://github.com/woocommerce/woocommerce-admin/blob/main/client/analytics/report/orders/config.js#L50-L62).
First, we need to populate the client with data to render the dropdown. The best way to do this is to add data to the `wcSettings` global. This global can be useful for transferring static configuration data from PHP to the client. In the main PHP file, add currency settings to the Data Registry to populate `window.wcSettings.multiCurrency`.
```php
function add_currency_settings() {
$currencies = array(
array(
'label' => __( 'United States Dollar', 'dev-blog-example' ),
'value' => 'USD',
),
array(
'label' => __( 'New Zealand Dollar', 'dev-blog-example' ),
'value' => 'NZD',
),
array(
'label' => __( 'Mexican Peso', 'dev-blog-example' ),
'value' => 'MXN',
),
);
$data_registry = Automattic\WooCommerce\Blocks\Package::container()->get(
Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class
);
$data_registry->add( 'multiCurrency', $currencies );
}
add_action( 'init', 'add_currency_settings' );
```
In the console, you can confirm the data has safely made its way to the client.
![screnshot of console](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-1.11.50-pm.png?w=476)
In `index.js` create the custom currency filter and add it the Orders Report.
```js
import { addFilter } from "@wordpress/hooks";
import { __ } from "@wordpress/i18n";
const addCurrencyFilters = (filters) => {
return [
{
label: __("Currency", "dev-blog-example"),
staticParams: [],
param: "currency",
showFilters: () => true,
defaultValue: "USD",
filters: [...(wcSettings.multiCurrency || [])],
},
...filters,
];
};
addFilter(
"woocommerce_admin_orders_report_filters",
"dev-blog-example",
addCurrencyFilters
);
```
If we check out the Orders Report, we can see our new dropdown. Play around with it and you'll notice the currency query parameter gets added to the url. If you check out the Network tab, you'll also see this value included in requests for data used to populate the report. For example, see the requests to orders stats endpoint, `/wp-json/wc-analytics/reports/orders/stats`. Next we'll use that query parameter to adjust report results.
![screenshot showing UI dropdown in wp-admin](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-1.16.44-pm.png?w=512)
## Handle currency parameters on the server
Now that our dropdown adds a `currency` query parameter to requests for data, the first thing we'll need to do is add the parameter as a query argument to the Orders Data Store and Orders Stats Data Store. Those data stores use query arguments for caching purposes, so by adding our parameter we can be sure a new database query will be performed when the parameter changes. Add the query argument in your main PHP file.
```php
function apply_currency_arg( $args ) {
$currency = 'USD';
if ( isset( $_GET['currency'] ) ) {
$currency = sanitize_text_field( wp_unslash( $_GET['currency'] ) );
}
$args['currency'] = $currency;
return $args;
}
add_filter( 'woocommerce_analytics_orders_query_args', 'apply_currency_arg' );
add_filter( 'woocommerce_analytics_orders_stats_query_args', 'apply_currency_arg' );
```
Now that we're sure a new database query is performed on mutations of the `currency` query parameter, we can start adding SQL statements to the queries that gather data.
Lets start by adding a JOIN for the orders table, orders stats, and orders chart.
```php
function add_join_subquery( $clauses ) {
global $wpdb;
$clauses[] = "JOIN {$wpdb->postmeta} currency_postmeta ON {$wpdb->prefix}wc_order_stats.order_id = currency_postmeta.post_id";
return $clauses;
}
add_filter( 'woocommerce_analytics_clauses_join_orders_subquery', 'add_join_subquery' );
add_filter( 'woocommerce_analytics_clauses_join_orders_stats_total', 'add_join_subquery' );
add_filter( 'woocommerce_analytics_clauses_join_orders_stats_interval', 'add_join_subquery' );
```
Next, add a WHERE clause
```php
function add_where_subquery( $clauses ) {
$currency = 'USD';
if ( isset( $_GET['currency'] ) ) {
$currency = sanitize_text_field( wp_unslash( $_GET['currency'] ) );
}
$clauses[] = "AND currency_postmeta.meta_key = '_order_currency' AND currency_postmeta.meta_value = '{$currency}'";
return $clauses;
}
add_filter( 'woocommerce_analytics_clauses_where_orders_subquery', 'add_where_subquery' );
add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', 'add_where_subquery' );
add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', 'add_where_subquery' );
```
And finally, a SELECT clause.
```php
function add_select_subquery( $clauses ) {
$clauses[] = ', currency_postmeta.meta_value AS currency';
return $clauses;
}
add_filter( 'woocommerce_analytics_clauses_select_orders_subquery', 'add_select_subquery' );
add_filter( 'woocommerce_analytics_clauses_select_orders_stats_total', 'add_select_subquery' );
add_filter( 'woocommerce_analytics_clauses_select_orders_stats_interval', 'add_select_subquery' );
```
Lets head back to the Orders Report and see if it works. You can manipulate the dropdown and see the relevant order reflected in the table.
![screenshot of WooCommerce Orders tab in wp-admin showing the relevant order reflected in the table.](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-1.38.54-pm.png?w=585)
## Finishing touches
The orders table could use some customisation to reflect the selected currency. We can add a column to display the currency in `index.js`. The `reportTableData` argument is an object of headers, rows, and items, which are arrays of data. We'll need to add a new header and append the currency to each row's data array.
```js
const addTableColumn = (reportTableData) => {
if ("orders" !== reportTableData.endpoint) {
return reportTableData;
}
const newHeaders = [
{
label: "Currency",
key: "currency",
},
...reportTableData.headers,
];
const newRows = reportTableData.rows.map((row, index) => {
const item = reportTableData.items.data[index];
const newRow = [
{
display: item.currency,
value: item.currency,
},
...row,
];
return newRow;
});
reportTableData.headers = newHeaders;
reportTableData.rows = newRows;
return reportTableData;
};
addFilter("woocommerce_admin_report_table", "dev-blog-example", addTableColumn);
```
![screenshot of customized table](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-4.02.15-pm.png?w=861)
While adding a column is certainly helpful, currency figures in the table and chart only reflect the store currency.
![screenshot of report](https://woocommerce.files.wordpress.com/2020/02/screen-shot-2020-02-19-at-4.03.42-pm.png?w=865)
In order to change a Report's currency and number formatting, we can make use of the `woocommerce_admin_report_currency` JS hook. You can see the store's default sent to the client in `wcSettings.currency`, but we'll need to change these depending on the currency being viewed and designated by the query parameter `?currency=NZD`.
![screenshot of currency settings](https://woocommerce.files.wordpress.com/2020/04/screen-shot-2020-04-03-at-11.18.42-am.png?w=238)
First, lets create some configs in index.js.
```js
const currencies = {
MXN: {
code: "MXN",
symbol: "$MXN", // For the sake of the example.
symbolPosition: "left",
thousandSeparator: ",",
decimalSeparator: ".",
precision: 2,
},
NZD: {
code: "NZD",
symbol: "$NZ",
symbolPosition: "left",
thousandSeparator: ",",
decimalSeparator: ".",
precision: 2,
},
};
```
Finally, add our function to the hook which applies a config based on the currency query parameter.
```js
const updateReportCurrencies = (config, { currency }) => {
if (currency && currencies[currency]) {
return currencies[currency];
}
return config;
};
addFilter(
"woocommerce_admin_report_currency",
"dev-blog-example",
updateReportCurrencies
);
```
🎉 We can now view our Orders Report and see the currency reflected in monetary values throughout the report.
![Screenshot of customized order report](https://woocommerce.files.wordpress.com/2020/04/screen-shot-2020-04-03-at-11.29.05-am.png?w=912)
## Conclusion
In this guide, we added a UI element to manipulate query parameters sent to the server and used those values to modify SQL statements which gather report data. In doing so, we established a way to highly customise WC-Admin reports. Hopefully this example illustrates how the platform can be tailored by extensions to bring a powerful experience to users.

5
docs/reporting/readme.md Normal file
View File

@ -0,0 +1,5 @@
# Reporting
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Understanding your WooCommerce store's performance is crucial. This section will provide insights into generating, understanding, and optimizing reports to make informed decisions about WooCommerce projects.

5
docs/rest-api/readme.md Normal file
View File

@ -0,0 +1,5 @@
# REST API
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Harness the power of WooCommerce's REST API. This section will help you discover comprehensive documentation on endpoints, authentication, and best practices, aiding developers in integrating and manipulating WooCommerce functionalities programmatically.

5
docs/security/readme.md Normal file
View File

@ -0,0 +1,5 @@
# Security
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
Security is paramount. This section will dive into best practices, guidelines, and insights to ensure your WooCommerce projects remain secure from threats.

View File

@ -0,0 +1,10 @@
# Reporting security issues
WooCommerce cares deeply about security and works hard to keep our merchants and their customers safe.
You can find our security policy [over here](https://github.com/woocommerce/woocommerce/security/policy) and, if you believe you have discovered a vulnerability, we encourage you to follow it and submit your findings via [HackerOne](https://hackerone.com/automattic?type=team)—a trusted third party service that facilitates reporting of security issues. Please refer to the policy for more details, however some key points are as follows:
- We operate a [bug bounty program](https://hackerone.com/automattic?type=team), so you can be rewarded for valid reports, but not everything is in scope. Please check the guidance before posting.
- We strongly encourage [responsible disclosure](https://www.hackerone.com/disclosure-guidelines). To better protect everyone, please use HackerOne and **do not** post your findings in a public forum.
Thank you for being a responsible reporter!

View File

@ -0,0 +1,85 @@
# WooCommerce security best practices
## Introduction
This guide covers the best practices for securing WooCommerce stores, including hardening WordPress, keeping plugins and themes up to date, implementing secure coding practices, and protecting user data. By following these recommendations, developers can build secure and resilient WooCommerce stores that protect both their business and their customers.
## Audience
This guide is intended for developers who are familiar with WordPress and WooCommerce and want to improve the security of their online stores.
## Prerequisites
To follow this guide, you should have:
1. A basic understanding of WordPress and WooCommerce.
2. Access to a WordPress website with WooCommerce installed and activated.
## Step 1 — Keep WordPress, WooCommerce, and plugins up to date
Regularly updating WordPress, WooCommerce, and all installed plugins is crucial to maintaining a secure online store. Updates often include security patches that address vulnerabilities and help protect your store from attacks. To keep your WordPress and WooCommerce installations up to date:
1. Enable automatic updates for WordPress core.
2. Regularly check for and install updates for WooCommerce and all plugins.
## Step 2 — Choose secure plugins and themes
The plugins and themes you use can have a significant impact on the security of your WooCommerce store. To ensure your store is secure:
1. Install plugins and themes from reputable sources, such as the WordPress Plugin Directory and Theme Directory.
2. Regularly review and update the plugins and themes you use, removing any that are no longer maintained or have known security vulnerabilities.
3. Avoid using nulled or pirated plugins and themes, which may contain malicious code.
## Step 3 — Implement secure coding practices
Secure coding practices are essential for building a secure WooCommerce store. To implement secure coding practices:
1. Follow the WordPress Coding Standards when developing custom themes or plugins.
2. Use prepared statements and parameterized queries to protect against SQL injection attacks.
3. Validate and sanitize user input to prevent cross-site scripting (XSS) attacks and other vulnerabilities.
4. Regularly review and update your custom code to address potential security vulnerabilities.
## Step 4 — Harden WordPress security
Hardening your WordPress installation can help protect your WooCommerce store from attacks. To harden your WordPress security:
1. Use strong, unique passwords for all user accounts.
2. Limit login attempts and enable two-factor authentication (2FA) to protect against brute-force attacks.
3. Change the default "wp\_" table prefix in your WordPress database.
4. Disable XML-RPC and REST API access when not needed.
5. Keep file permissions secure and restrict access to sensitive files and directories.
## Step 5 — Secure user data
Protecting your customers' data is a critical aspect of securing your WooCommerce store. To secure user data:
1. Use SSL certificates to encrypt data transmitted between your store and your customers.
2. Store customer data securely and limit access to sensitive information.
3. Comply with data protection regulations, such as the GDPR, to ensure you handle customer data responsibly.
## Step 6 — Implement a security plugin
Using a security plugin can help you monitor and protect your WooCommerce store from potential threats. To implement a security plugin:
1. Choose a reputable security plugin, such as Wordfence, Sucuri, or iThemes Security.
2. Configure the plugin's settings to enable features like malware scanning, firewall protection, and login security.
## Step 7 — Regularly monitor and audit your store's security
Continuously monitor and audit your WooCommerce store's security to identify potential vulnerabilities and address them before they can be exploited. To monitor and audit your store's security:
1. Use a security plugin to perform regular scans for malware and other security threats.
2. Monitor your site's activity logs to identify suspicious activity and potential security issues.
3. Perform regular security audits to evaluate your store's overall security and identify areas for improvement.
## Step 8 — Create regular backups
Backing up your WooCommerce store is essential for quickly recovering from security incidents, such as data loss or site compromise. To create regular backups:
1. Choose a reliable backup plugin, such as UpdraftPlus, BackupBuddy, or Duplicator.
2. Configure the plugin to automatically create regular backups of your entire site, including the database, files, and media.
3. Store your backups securely off-site to ensure they are accessible in case of an emergency.
## Conclusion
By following these security best practices, you can build a secure and resilient WooCommerce store that protects both your business and your customers. Regularly monitoring, auditing, and updating your store's security measures will help ensure it remains protected as new threats and vulnerabilities emerge.

View File

@ -2,7 +2,9 @@
Various code snippets you can add to your site to enable custom functionality: Various code snippets you can add to your site to enable custom functionality:
- [Add a currency and symbol](./add-a-currency-symbol.md)
- [Add a message above the login / register form](./before-login--register-form.md) - [Add a message above the login / register form](./before-login--register-form.md)
- [Add or modify states](./add-or-modify-states.md) - [Add or modify states](./add-or-modify-states.md)
- [Change a currency symbol](./change-a-currency-symbol.md)
- [Change number of related products output](./number-of-products-per-row.md) - [Change number of related products output](./number-of-products-per-row.md)
- [Unhook and remove WooCommerce emails](./unhook--remove-woocommerce-emails.md) - [Unhook and remove WooCommerce emails](./unhook--remove-woocommerce-emails.md)

View File

@ -0,0 +1,38 @@
# Add a currency and symbol
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. Avoid adding custom code directly to your parent themes functions.php file, as this will be wiped entirely when you update the theme.
```php
if ( ! function_exists( 'YOUR_PREFIX_add_currency_name' ) ) {
/**
* Add custom currency
*
* @param array $currencies Existing currencies.
* @return array $currencies Updated currencies.
*/
function YOUR_PREFIX_add_currency_name( $currencies ) {
$currencies['ABC'] = __( 'Currency name', 'YOUR-TEXTDOMAIN' );
return $currencies;
}
add_filter( 'woocommerce_currencies', 'YOUR_PREFIX_add_currency_name' );
}
if ( ! function_exists( 'YOUR_PREFIX_add_currency_symbol' ) ) {
/**
* Add custom currency symbol
*
* @param string $currency_symbol Existing currency symbols.
* @param string $currency Currency code.
* @return string $currency_symbol Updated currency symbol(s).
*/
function YOUR_PREFIX_add_currency_symbol( $currency_symbol, $currency ) {
switch( $currency ) {
case 'ABC': $currency_symbol = '$'; break;
}
return $currency_symbol;
}
add_filter('woocommerce_currency_symbol', 'YOUR_PREFIX_add_currency_symbol', 10, 2);
}
```

View File

@ -0,0 +1,25 @@
# Rename a country
See the [currency list](https://woocommerce.github.io/code-reference/files/woocommerce-includes-wc-core-functions.html#source-view.475) for reference on currency codes.
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. Avoid adding custom code directly to your parent themes functions.php file, as this will be wiped entirely when you update the theme.
```php
if ( ! function_exists( 'YOUR_PREFIX_change_currency_symbol' ) ) {
/**
* Change a currency symbol
*
* @param string $currency_symbol Existing currency symbols.
* @param string $currency Currency code.
* @return string $currency_symbol Updated currency symbol(s).
*/
function YOUR_PREFIX_change_currency_symbol( $currency_symbol, $currency ) {
switch ( $currency ) {
case 'AUD': $currency_symbol = 'AUD$'; break;
}
return $currency_symbol;
}
add_filter( 'woocommerce_currency_symbol', 'YOUR_PREFIX_change_currency_symbol', 10, 2 );
}
```

118
docs/style-guide.md Normal file
View File

@ -0,0 +1,118 @@
# Technical documentation style guide
This style guide is intended to provide guidelines for creating effective and user-friendly tutorials and how-to guides for WooCommerce technical documentation that will live in repo and be editable and iterative by open source contributors and WooCommerce teams.
## Writing style
### Language style
- Its important to use clear and concise language that is easy to understand. Use active voice and avoid using jargon or technical terms that may be unfamiliar to the user. The tone should be friendly and approachable, and should encourage the user to take action.
- Articles are written in the 3rd-person voice.
Example: “Add an embed block to your page.”
- Use American English for spelling and punctuation styles, or consider using a different word that doesnt have a different spelling in other English variants.
- Use sentence case (not title case) for docs titles and subheadings.
Example: “Introduction to the launch experience” rather than “Introduction to the Launch Experience.”
- When referring to files or directories, the text formatting eliminates the need to include articles such as “the” and clarifying nouns such as “file” or “directory”.
Example: “files stored in ~~the~~ `/wp-content/uploads/` ~~directory~~” or “edit ~~the~~ `/config/config.yml` ~~file~~ with”
### Writing tips
- Our target audience has a range of roles and abilities. When creating a tutorial or how-to guide, its important to consider the intended audience. Are they beginners or advanced users? What is their technical background? Understanding the audience can help guide the level of detail and the choice of language used in the guide.
- Use language understable even by readers with little technical knowledge and readers whose first language might not be English.
- Consider that this might be the first WooCommerce documentation page the reader has seen. They may have arrived here via a Google search or another website. Give the reader enough context about the topic and link words and phrases to other relevant Docs articles as often as possible.
- Consider notes and sections that provide insights, tips, or cautionary information to expand on topics with context that would be relevant to the reader.
- When providing specific direction, best practices, or requirements, we recommend including a description of the potential consequences or impacts of not following the provided guidance. This can help seed additional search keywords into the document and provide better context when support links to the documentation.
- Always write a conceptual, high-level introduction to the topic first, above any H2 subheading.
### Tutorials
Tutorials are comprehensive and designed to teach a new skill or concept.
> You are the teacher, and you are responsible for what the student will do. Under your instruction, the student will execute a series of actions to achieve some end.
>
> [Divio Framework on Tutorial Writing](https://documentation.divio.com/tutorials/)
### How-to guides
How-to guides are focused and specific, providing instructions on how to accomplish a particular task or solve a particular problem.
> How-to guides are wholly distinct from tutorials and must not be confused with them:
>
> - A tutorial is what you decide a beginner needs to know.
> - A how-to guide is an answer to a question that only a user with some experience could even formulate.
>
> [Divio Framework on How-to-Guide Writing](https://documentation.divio.com/how-to-guides/)
## Formatting
### Visual style
- Use the H2 style for main headings to be programmatically listed in the articles table of contents.
- File names and directory paths should be stylized as code per the [HTML spec](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-code-element).
Example: `/wp-content/uploads/`
- References to a single directory should have a trailing slash (eg. “/” appended) to the name.
Example: “uploads/“
- References to repositories should appear without forward slashes and not be formatted in any way. The first appearance of a repository in article content should link to the URL of the repository source whenever possible.
Example: “[woocommerce-blocks](https://github.com/woocommerce/woocommerce-blocks)” followed by “woocommerce-blocks”
- Inline references to functions and command line operations should be formatted as inline code.
Example: “Use `dig` to retrieve DNS information.”
- Functions should be styled with “Inline code” formatting and retain upper and lower case formatting as established from their source.
Example: `WP_Query` (not WP_query)
### Visual aids
Visual aids such as screenshots, diagrams, code snippets and videos can be very helpful in a how-to guide. They provide a visual reference that can help the user understand the instructions more easily. When including visual aids, be sure to label them clearly and provide a caption or description that explains what is being shown.
### Acronyms
Phrases that are more familiarly known in their acronym form can be used. The first time an acronym appears on any page, the full phrase must be included, followed by its acronym in parentheticals.
Example: Weve enhanced the querying functionality in WooCommerce with the introduction of High Performance Order Storage (HPOS).
After that, the acronym can be used for the remainder of the page.
When deciding if a term is common, consider the impact on translation and future internationalization (i18n) efforts.
## Patterning
### Article content
When creating a how-to guide, its important to use a consistent and easy-to-follow format. Here is a suggested template for a software how-to guide:
**Introduction**: Provide an overview of the task or feature that the guide covers.
**Prerequisites**: List any prerequisites that are required to complete the task or use the feature.
**Step-by-step instructions**: Provide detailed, step-by-step instructions for completing the task or using the feature. Use numbered steps and include screenshots or other visual aids where appropriate.
**Troubleshooting**: Include a troubleshooting section that addresses common issues or errors that users may encounter.
**Conclusion**: Summarize the key points covered in the guide and provide any additional resources or references that may be helpful.
## Terminology
### Reference to components and features
- “**WordPress Admin dashboard**” should be presented in its complete form the first time it appears in an article, followed by its abbreviated form in parentheses (“WP Admin”). Thereafter the abbreviated form can be used for any reference to the WordPress Admin dashboard within the same article.
- When referring to the URL of the WordPress Admin dashboard, the shortened form `wp-admin` can be used.
## Testing
Before publishing a tutorial or guide, its important to test it thoroughly to ensure that the instructions are accurate and easy to follow.
## Structure
### Atomizing the docs
Articles that cover too many topics in one place can make it difficult for users to find the information they are looking for. “Atomizing” the Docs refers to breaking down extensive articles into a group of smaller related articles. This group of articles often has a main “landing page” with a high-level overview of the group of articles, and the descriptive text provides links to the related articles that a user will find relevant. These groups of articles can be considered an information “molecule” formed by the smaller, atomized articles.
Breaking out smaller chunks of content into their own articles makes it easier to link to specific topics rather than relying on links to more extensive articles with anchor tags. This more specific linking approach is helpful to our Support team but is also useful for cross-linking articles throughout the Docs site.

View File

@ -0,0 +1,85 @@
# Theme Design and User Experience Guidelines
This guide covers general guidelines and best practices to follow in order to ensure your theme experience aligns with ecommerce industry standards and WooCommerce for providing a great online shopping experience, maximizing sales, ensuring ease of use, seamless integration, and strong UX adoption.
We recommend you review the [UI best practices for WordPress](https://developer.wordpress.org/themes/advanced-topics/ui-best-practices/) to ensure your theme is aligned with the WordPress theme requirements.
Make sure your theme fits one or more industries currently available in the [WooCommerce themes store](https://woocommerce.com/product-category/themes). Its important that the theme offers enough originality and distinctiveness in its design, while keeping it familiar, in order to be distinguished from other themes on the WooCommerce theme store. Your theme should avoid copying existing themes on the WooCommerce theme store or other WordPress theme marketplaces.
## Design
High-quality design is an important aspect of an online store, and that is driven by the theme design and content. The design of the theme should be simple, consistent, uncluttered, memorable, intuitive, efficient, and functional. When designing a new theme for WooCommerce special attention should be given to:
### Layout
The theme should be up to industry standards in terms of hierarchy, flow, content balance, and white space.
Theme authors must ensure that store pages (shop, product page, categories, cart, checkout, profile page, etc) fit seamlessly with the theme since they are the central point of a WooCommerce theme.
The Theme is expected to be fully functional and optimized to be accessed on common device types such as laptops, tablets, and smartphones.
### Typography
The theme should provide elegant and legible font pairings that promote a comfortable reading experience.
Consistent and harmonious font sizes, line widths and spacing must be employed across all pages and device types.
The theme typography must consist of a small number of typefaces that complement each other, generally no more than two.
Proper capitalization is used, avoiding all caps (with the exception of some UI elements such as buttons, tabs, etc).
### Iconography
Icons used in the theme portray a direct meaning of the actions/situations they are representing and are used consistently regarding sizing positioning and color.
### Color
The theme must follow a harmonious and consistent color scheme across UI elements and all pages. The color scheme should consist of small number of colors that contain:
- A primary/accent dominant color
- One or two secondary colors that complement the primary
- Neutral colors (white, black, gray)
The color palette used in text and graphical UI components must be compliant with the [WCAG AA conformance level](https://www.w3.org/TR/WCAG20/#conformance) or above.
### Patterns
The theme must employ a consistent set of patterns that are used across pages, such as:
- Navigation, sidebars, footer
- Content blocks (titles, paragraphs, lists, product details, reviews, image showcases, etc)
- Forms structure and elements (fields, drop-downs, buttons, etc)
- Tables
- Lists
- Notices
## Accessibility
The theme must meet the [Web Content Accessibility Guidelines](https://www.w3.org/TR/WCAG20/) (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://make.wordpress.org/accessibility/handbook/best-practices/quick-start-guide/).
## Customization
Themes have to rely on the customizer for any type of initial set up. Specific onboarding flows are not permitted.
Any customization supported by the theme, such as layout options, additional features, block options, etc, should be delivered in the customizer or on block settings for blocks that are included in the theme.
Themes should not bundle or require the installation of additional plugins/extensions (or frameworks) that provide additional options or functionality. For more information on customisation, check out the [WordPress theme customization API](https://codex.wordpress.org/Theme_Customization_API)**.**
On activation, themes shouldnt override the WordPress theme activation flow by taking the user into other pages.
## Branding
The theme must not contain any branding or references to theme authors in locations that interfere with the normal operation of an online store. Theme authors can include links to their websites on the theme footer. Affiliate linking is not permitted.
The interface should solely focus on the experience, the usage of notices, banners, large logos, or any promotional materials is not allowed in the admin interface.
## Demos and sample content
Upon submission theme authors must provide a way for the theme to be showcased and tested. The sample content/demo should refrain from using custom graphics/assets that will not be present in the deliverables to avoid merchant confusion and broken expectations (examples: using logos, illustrations). When creating a theme for a specific vertical theme authors should consider using sample content that aligns with the vertical.
All imagery and text should be appropriate for all ages/family-friendly. The theme author should consider using imagery that is inclusive of ages, nationalities, etc. The theme should refrain from using imagery that looks like stock photography.
The theme must be distributed and cleared of all the necessary licenses for assets such as images, fonts, icons, etc.

View File

@ -0,0 +1,253 @@
# Adding a custom field to simple and variable products
In this tutorial you will learn how to create a custom field for a product and show it in your store. Together we will set up the skeleton plugin, and learn about WP naming conventions and WooCommerce hooks. In the end, you will have a functioning plugin for adding a custom field.
The [full plugin code](https://github.com/EdithAllison/woo-product-custom-fields) was written based on WordPress 6.2 and WooCommerce 7.6.0
## Prerequisites
To do this tutorial you will need to have a WordPress install with the WooCommerce plugin activated, and you will need at least one [simple product set up](https://woocommerce.com/document/managing-products/), or you can [import the WooCommerce sample product range](https://woocommerce.com/document/importing-woocommerce-sample-data/).
## Setting up the plugin
To get started, lets do the steps to [create a skeleton plugin](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/create-woo-extension).
First, navigate to your wp-content/plugins folder, then run:
```sh
npx @wordpress/create-block -t @woocommerce/create-woo-extension woo-product-fields
```
Then we navigate to our new folder and run the install and build:
```sh
cd woo-product-fields
npm install # Install dependencies
npm run build # Build the javascript
```
WordPress has its own class file naming convention which doesnt work with PSR-4 out of the box. To learn more about Naming Conventions see the [WP Handbook](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions). We will use the standard format of “class-my-classname.php” format, so lets go to the composer.json file and change the autoload to:
```json
"autoload": {
"classmap": ["includes/", "includes/admin/"]
},
```
After saving run dump-autoload to generate the class map by running in the Terminal:
```sh
composer dump-autoload -o
```
This generates a new vendor/composer/autoload_classmap.php file containing a list of all our classes in the /includes/ and /includes/admin/ folder. We will need to repeat this command when we add, delete or move class files.
## WooCommerce Hooks
Our aim is to create a new custom text field for WooCommerce products to save new stock information for display in the store. To do this, we need to modify the section of the Woo data in the admin area which holds the stock info.
WooCommerce allows us to add our code to these sections through [hooks](https://developer.wordpress.org/plugins/hooks/), which are a standard WordPress method to extend code. In the “Inventory” section we have the following action hooks available to us:
For our Woo extension, well be appending our field right at the end with `woocommerce_product_options_inventory_product_data`.
## Creating our class
Lets get started with creating a new class which will hold the code for the field. Add a new file with the name `class-product-fields.php` to the `/includes/admin/` folder. Within the class, we add our namespace, an abort if anyone tries to call the file directly and a \_\_construct method which calls the `hooks()` method:
```php
<?php
namespace WooProductField\Admin;
defined( 'ABSPATH' ) || exit;
class ProductFields {
public function __construct() {
$this->hooks();
}
private function hooks() {}
}
```
Then in Terminal we run `composer dump-autoload -o` to regenerate the class map. Once thats done, we add the class to our `setup.php` \_\_construct() function like so:
```php
class Setup {
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
new ProductFields();
}
```
## Adding the custom field
With the class set up and being called, we can create a function to add the custom field. WooCommerce has its own `woocommerce_wp_text_input( $args )` function which we can use here. `$args` is an array which allows us to set the text input data, and we will be using the global $product_object to access stored metadata.
```php
public function add_field() {
global $product_object;
?>
<div class="inventory_new_stock_information options_group show_if_simple show_if_variable">
<?php woocommerce_wp_text_input(
array(
'id' => '_new_stock_information',
'label' => __( 'New Stock', 'woo_product_field' ),
'description' => __( 'Information shown in store', 'woo_product_field' ),
'desc_tip' => true,
'value' => $product_object->get_meta( '_new_stock_information' )
)
); ?>
</div>
<?php
}
```
Lets take a look at the arguments in the array. The ID will be used as meta_key in the database. The Label and Description are shown in the data section, and by setting desc_tip to true, it will be shown as a hover over the info icon. The last argument value ensures that if a value is already stored, then it will be shown.
For the div class, the class names `show_if_simple` and `show_if_variable` will control when our section is shown. This is linked to JS code which dynamically hides/reveals sections. If for example, we wanted to hide the section from variable products, then we can simply delete `show_if_variable`.
Now that we have our field, we need to save it. For this, we can hook into woocommerce_process_product_meta which takes two arguments, `$post_id` and `$post`:
```php
public function save_field( $post_id, $post ) {
if ( isset( $_POST['_new_stock_information'] ) ) {
$product = wc_get_product( intval( $post_id ) );
$product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'] ) );
$product->save_meta_data();
}
}
```
This function checks if our new field is in the POST array. If yes, we create the product object, update our metadata and save the metadata. The `update_meta_data` function will either update an existing meta field or add a new one. And as were inserting into the database, we must [sanitize our field value](https://developer.wordpress.org/apis/security/sanitizing/).
And to make it all work, we add the hooks:
```php
private function hooks() {
add_action( 'woocommerce_product_options_inventory_product_data', array( $this, 'add_field' ) );
add_action( 'woocommerce_process_product_meta', array( $this, 'save_field' ), 10, 2 );
}
```
Now if we refresh our product screen, we can see our new field.
If we add data and save the product, then the new meta data is inserted into the database.
At this point you have a working extension that saves a custom field for a product as product meta.
Showing the field in the store
If we want to display the new field in our store, then we can do this with the `get_meta()` method of the Woo product class: `$product->get_meta( '\_new_stock_information' )`
Lets get started by creating a new file /includes/class-product.php. You may have noticed that this is outside the `/admin/` folder as this code will run in the front. So when we set up the class, we also adjust the namespace accordingly:
```php
<?php
namespace WooProductField;
defined( 'ABSPATH' ) || exit;
class Product {
public function __construct() {
$this->hooks();
}
private function hooks() { }
}
```
Again we run `composer dump-autoload -o` to update our class map.
If you took a look at the extension setup you may have noticed that `/admin/setup.php` is only called if were within WP Admin. So to call our new class well add it directly in `/woo-product-field.php`:
```php
public function __construct() {
if ( is_admin() ) {
new Setup();
}
new WooProductField\Product();
}
```
For adding the field to the front we have several options. We could create a theme template, but if we are working with a WooCommerce-compatible theme and dont need to make any other changes then a quick way is to use hooks. If we look into `/woocommerce/includes/wc-template-hooks.php` we can see all the existing actions for `woocommerce_single_product_summary` which controls the section at the top of the product page:
For our extension, let's add the new stock information after the excerpt by using 21 as the priority:
```php
private function hooks() {
add_action( 'woocommerce_single_product_summary', array( $this, 'add_stock_info' ), 21 );
}
```
In our function we output the stock information with the [appropriate escape function](https://developer.wordpress.org/apis/security/escaping/), in this case, Im suggesting to use `esc_html()` to force plain text.
```php
public function add_stock_info() {
global $product;
?>
<p><?php echo esc_html( $product->get_meta( '_new_stock_information' ) ); ?> </p>
<?php
}
```
Now if we refresh the product page our stock information will be shown just below the excerpt:
Fantastic! You have completed this tutorial and have a working WooCommerce extension that adds a new custom field and shows it in the store! 🎉I hope its shown you how easily you can extend WooCommerce through hooks and tailor it to your or your clients shop requirements!
Below is a bonus task if you are interested in variable products. Feel free to come back to this later.
## How to handle variable products?
The above example was done with a simple product. But what if we have variations, for example, a T-Shirt in multiple sizes and we wanted to store different stock information for each variant? WooCommerce lets us do that with the [variable product type](https://woocommerce.com/document/variable-product/).
A variable product type has variations as its children. To add a custom field to a variation, we can use the `woocommerce_variation_options_inventory` hook, and to save `woocommerce_save_product_variation` so lets update our `hooks()` method with the new action hooks like so:
```php
private function hooks() {
add_action( 'woocommerce_product_options_inventory_product_data', array( $this, 'add_field' ) );
add_action( 'woocommerce_process_product_meta', array( $this, 'save_field' ), 10, 2 );
add_action( 'woocommerce_variation_options_inventory', array( $this, 'add_variation_field' ), 10, 3 );
add_action( 'woocommerce_save_product_variation', array( $this, 'save_variation_field' ), 10, 2 );
}
```
The setup is very similar to simple products, the main difference is that we need to use the $loop id which distinguishes between the variations, and we will be using the `wrapper_class` to show it as a full width text input:
```php
public function add_variation_field( $loop, $variation_data, $variation ) {
$variation_product = wc_get_product( $variation->ID );
woocommerce_wp_text_input(
array(
'id' => '\_new_stock_information' . '[' . $loop . ']',
'label' => \_\_( 'New Stock Information', 'woo_product_field' ),
'wrapper_class' => 'form-row form-row-full',
'value' => $variation_product->get_meta( '\_new_stock_information' )
)
);
}
```
For saving we use:
```php
public function save_variation_field( $variation_id, $i ) {
if ( isset( $_POST['_new_stock_information'][$i] ) ) {
$variation_product = wc_get_product( $variation_id );
$variation_product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'][$i] ) );
$variation_product->save_meta_data();
}
}
```
And we now have a new variation field that stores our new stock information. If you cannot see the new field, please make sure to enable “Manage Stock” for the variation by ticking the checkbox in the variation details.
Displaying the variation in the front store works a bit differently for variable products as only some content on the page is updated when the customer makes a selection. This exceeds the scope of this tutorial, but if you are interested have a look at `/woocommerce/assets/js/frontend/add-to-cart-variation.js` to see how WooCommerce does it.
## How to find hooks?
Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.

5
docs/tutorials/readme.md Normal file
View File

@ -0,0 +1,5 @@
# Tutorials
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
This section will contain step-by-step guides and walkthroughs tailored for both novice and seasoned WooCommerce enthusiasts. Whether it's setting up a new feature or diving into complex customizations, our tutorials will cover a wide range of topics to help you achieve your goals.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add quick actions dropdown menu to variation items

View File

@ -49,6 +49,12 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
&--delete {
&.components-button.components-menu-item__button.is-link {
text-decoration: none;
}
}
.components-button { .components-button {
position: relative; position: relative;
color: var(--wp-admin-theme-color); color: var(--wp-admin-theme-color);
@ -63,9 +69,15 @@
} }
} }
.components-button svg { .components-button {
&.components-dropdown-menu__toggle.has-icon svg {
fill: inherit;
}
svg {
fill: none; fill: none;
} }
}
.components-button--visible { .components-button--visible {
color: $gray-700; color: $gray-700;

View File

@ -1,22 +1,29 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { Button, Spinner, Tooltip } from '@wordpress/components'; import {
Button,
DropdownMenu,
MenuGroup,
MenuItem,
Spinner,
Tooltip,
} from '@wordpress/components';
import { import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
ProductVariation, ProductVariation,
} from '@woocommerce/data'; } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { ListItem, Pagination, Sortable, Tag } from '@woocommerce/components';
import { import {
Link, useContext,
ListItem, useState,
Pagination, createElement,
Sortable, Fragment,
Tag, } from '@wordpress/element';
} from '@woocommerce/components';
import { getNewPath } from '@woocommerce/navigation';
import { useContext, useState, createElement } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
import { moreVertical } from '@wordpress/icons';
import classnames from 'classnames'; import classnames from 'classnames';
import truncate from 'lodash/truncate'; import truncate from 'lodash/truncate';
import { CurrencyContext } from '@woocommerce/currency'; import { CurrencyContext } from '@woocommerce/currency';
@ -34,6 +41,7 @@ import { getProductStockStatus, getProductStockStatusClass } from '../../utils';
import { import {
DEFAULT_PER_PAGE_OPTION, DEFAULT_PER_PAGE_OPTION,
PRODUCT_VARIATION_TITLE_LIMIT, PRODUCT_VARIATION_TITLE_LIMIT,
TRACKS_SOURCE,
} from '../../constants'; } from '../../constants';
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' ); const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
@ -87,7 +95,7 @@ export function VariationsTable() {
[ currentPage, perPage, productId ] [ currentPage, perPage, productId ]
); );
const { updateProductVariation } = useDispatch( const { updateProductVariation, deleteProductVariation } = useDispatch(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
); );
@ -124,6 +132,29 @@ export function VariationsTable() {
); );
} }
function handleDeleteVariationClick( variationId: number ) {
if ( isUpdating[ variationId ] ) return;
setIsUpdating( ( prevState ) => ( {
...prevState,
[ variationId ]: true,
} ) );
deleteProductVariation< Promise< ProductVariation > >( {
product_id: productId,
id: variationId,
} )
.then( () => {
recordEvent( 'product_variations_delete', {
source: TRACKS_SOURCE,
} );
} )
.finally( () =>
setIsUpdating( ( prevState ) => ( {
...prevState,
[ variationId ]: false,
} ) )
);
}
return ( return (
<div className="woocommerce-product-variations"> <div className="woocommerce-product-variations">
{ isLoading || { isLoading ||
@ -266,17 +297,70 @@ export function VariationsTable() {
</Tooltip> </Tooltip>
) } ) }
<Link <DropdownMenu
href={ getNewPath( icon={ moreVertical }
{}, label={ __( 'Actions', 'woocommerce' ) }
`/product/${ productId }/variation/${ variation.id }`, toggleProps={ {
{} onClick() {
) } recordEvent(
type="wc-admin" 'product_variations_menu_view',
className="components-button" {
source: TRACKS_SOURCE,
}
);
},
} }
> >
{ __( 'Edit', 'woocommerce' ) } { ( { onClose } ) => (
</Link> <>
<MenuGroup
label={ sprintf(
/** Translators: Variation ID */
__(
'Variation Id: %s',
'woocommerce'
),
variation.id
) }
>
<MenuItem
href={ variation.permalink }
onClick={ () => {
recordEvent(
'product_variations_preview',
{
source: TRACKS_SOURCE,
}
);
} }
>
{ __(
'Preview',
'woocommerce'
) }
</MenuItem>
</MenuGroup>
<MenuGroup>
<MenuItem
isDestructive
variant="link"
onClick={ () => {
handleDeleteVariationClick(
variation.id
);
onClose();
} }
className="woocommerce-product-variations__actions--delete"
>
{ __(
'Delete',
'woocommerce'
) }
</MenuItem>
</MenuGroup>
</>
) }
</DropdownMenu>
</div> </div>
</ListItem> </ListItem>
) ) } ) ) }

View File

@ -30,7 +30,7 @@ export const getTourConfig = ( {
spotlight: { spotlight: {
interactivity: { interactivity: {
enabled: true, enabled: true,
rootElementSelector: '.woocommerce.wc-addons-wrap', rootElementSelector: '.woocommerce-marketplace',
}, },
}, },
autoScroll: { autoScroll: {
@ -39,21 +39,6 @@ export const getTourConfig = ( {
}, },
}, },
popperModifiers: [ popperModifiers: [
{
name: 'arrow',
options: {
padding: ( {
popper,
}: {
popper: { width: number };
} ) => {
return {
// Align the arrow to the left of the popper.
right: popper.width - 34,
};
},
},
},
{ {
name: 'offset', name: 'offset',
options: { options: {

View File

@ -10,13 +10,15 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
return [ return [
{ {
referenceElements: { referenceElements: {
desktop: '#adminmenu a[href="admin.php?page=wc-addons"]', desktop:
'#adminmenu a[href="admin.php?page=wc-admin&path=%2Fextensions"]',
}, },
focusElement: { focusElement: {
desktop: '#adminmenu a[href="admin.php?page=wc-addons"]', desktop:
'#adminmenu a[href="admin.php?page=wc-admin&path=%2Fextensions"]',
}, },
meta: { meta: {
name: 'wc-addons-menu-item', name: 'wc-extensions-menu-item',
heading: __( heading: __(
'Welcome to the WooCommerce Marketplace', 'Welcome to the WooCommerce Marketplace',
'woocommerce' 'woocommerce'
@ -24,7 +26,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
descriptions: { descriptions: {
desktop: createInterpolateElement( desktop: createInterpolateElement(
__( __(
'Power up your store by adding extra functionality using extensions, find a fresh new look with themes, or integrate your store with other software and services.<br/><br/>The WooCommerce Marketplace is your go-to for all of the above, and the only place youll find products that have been reviewed and approved by the WooCommerce team.<br/><br/>Whether youre looking to improve your store or grow your business, you can find a solution here. There are hundreds of options available, and new products are added regularly.<br/><br/>The WooCommerce Marketplace is also available at WooCommerce.com.', "Power up your store by adding extra functionality with extensions or integrate your store with other software and services.<br/><br/>Here you'll find hundreds of trusted solutions for your store — all reviewed and approved by the Woo team.<br/><br/>You can also browse the Woo Marketplace at WooCommerce.com.",
'woocommerce' 'woocommerce'
), ),
{ {
@ -36,17 +38,17 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
}, },
{ {
referenceElements: { referenceElements: {
desktop: '.marketplace-header__search-form', desktop: '.woocommerce-marketplace__search',
}, },
focusElement: { focusElement: {
desktop: '.marketplace-header__search-form', desktop: '.woocommerce-marketplace__search',
}, },
meta: { meta: {
name: 'wc-addons-search', name: 'wc-extensions-search',
heading: __( 'Find exactly what you need', 'woocommerce' ), heading: __( 'Find exactly what you need', 'woocommerce' ),
descriptions: { descriptions: {
desktop: __( desktop: __(
'Use the search box to find specific products or solutions.', 'Use the search box to find specific extensions or solutions.',
'woocommerce' 'woocommerce'
), ),
}, },
@ -54,10 +56,10 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
}, },
{ {
referenceElements: { referenceElements: {
desktop: '#marketplace-current-section-dropdown', desktop: '.woocommerce-marketplace__tab-browse',
}, },
focusElement: { focusElement: {
desktop: '#marketplace-current-section-dropdown', desktop: '.woocommerce-marketplace__tab-browse',
}, },
meta: { meta: {
name: 'wc-addons-categories', name: 'wc-addons-categories',
@ -65,7 +67,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
descriptions: { descriptions: {
desktop: createInterpolateElement( desktop: createInterpolateElement(
__( __(
'Or browse all available products by category.', "Or if you're not sure exactly what you need, you can browse all available extensions by category.",
'woocommerce' 'woocommerce'
), ),
{ {
@ -77,18 +79,18 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
}, },
{ {
referenceElements: { referenceElements: {
desktop: '.addon-product-group:first-child', desktop: '.woocommerce-marketplace__discover:first-child',
}, },
focusElement: { focusElement: {
desktop: '.addon-product-group:first-child', desktop: '.woocommerce-marketplace__discover:first-child',
}, },
meta: { meta: {
name: 'wc-addons-featured', name: 'wc-addons-featured',
heading: __( 'Learn more about products', 'woocommerce' ), heading: __( 'Learn more about each product', 'woocommerce' ),
descriptions: { descriptions: {
desktop: createInterpolateElement( desktop: createInterpolateElement(
__( __(
'Scroll down to see all available products for a search or selected category.<br/><br/>Click on any product to see more information about it, including features, requirements, and available documentation.', 'Scroll down to see all of the relevant extensions and solutions.<br/><br/>Click on any solution to learn more about its features, any installation requirements, and available documentation.',
'woocommerce' 'woocommerce'
), ),
{ {
@ -100,10 +102,10 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
}, },
{ {
referenceElements: { referenceElements: {
desktop: '.marketplace-header__tab-link_helper', desktop: '.woocommerce-marketplace__header-meta',
}, },
focusElement: { focusElement: {
desktop: '.marketplace-header__tab-link_helper', desktop: '.woocommerce-marketplace__header-meta',
}, },
meta: { meta: {
name: 'wc-addons-my-subscriptions', name: 'wc-addons-my-subscriptions',
@ -111,7 +113,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => {
descriptions: { descriptions: {
desktop: createInterpolateElement( desktop: createInterpolateElement(
__( __(
"Products purchased from the WooCommerce Marketplace can be managed in My Subscriptions, either here or on WooCommerce.com.<br/><br/>Every purchase is backed by our <a1>30-day money-back guarantee</a1>, and includes <a2>email and live chat support</a2>.<br/><br/>That's it! We hope the WooCommerce Marketplace helps you build the business of your dreams.", "All of your Woo Marketplace purchases can be found here, or on WooCommerce.com.<br/><br/>Every purchase is backed by our <a1>30-day money-back guarantee</a1>, and includes <a2>email and live chat support</a2>.<br/><br/>That's it! You're now ready to power up your store.",
'woocommerce' 'woocommerce'
), ),
{ {

View File

@ -16,7 +16,7 @@ import { scrollPopperToVisibleAreaIfNeeded } from './utils';
import { getSteps } from './get-steps'; import { getSteps } from './get-steps';
const WCAddonsTour = () => { const WCAddonsTour = () => {
const [ showTour, setShowTour ] = useState( false ); const [ showTour, setShowTour ] = useState( true );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
@ -62,7 +62,7 @@ const WCAddonsTour = () => {
const timeoutId = setTimeout( showPopper, 500 ); const timeoutId = setTimeout( showPopper, 500 );
const intervalId = observePositionChange( const intervalId = observePositionChange(
'.wc-addons-wrap', '.woocommerce-marketplace',
showPopper, showPopper,
150 150
); );

View File

@ -57,6 +57,9 @@ const MarketingOverviewMultichannel = lazy( () =>
/* webpackChunkName: "multichannel-marketing" */ '../marketing/overview-multichannel' /* webpackChunkName: "multichannel-marketing" */ '../marketing/overview-multichannel'
) )
); );
const Marketplace = lazy( () =>
import( /* webpackChunkName: "marketplace" */ '../marketplace' )
);
const ProfileWizard = lazy( () => const ProfileWizard = lazy( () =>
import( /* webpackChunkName: "profile-wizard" */ '../profile-wizard' ) import( /* webpackChunkName: "profile-wizard" */ '../profile-wizard' )
); );
@ -177,6 +180,25 @@ export const getPages = () => {
} ); } );
} }
if ( isFeatureEnabled( 'marketplace' ) ) {
pages.push( {
container: Marketplace,
layout: {
header: false,
},
path: '/extensions',
breadcrumbs: [
[ '/extensions', __( 'Extensions', 'woocommerce' ) ],
__( 'Extensions', 'woocommerce' ),
],
wpOpenMenu: 'toplevel_page_woocommerce',
capability: 'manage_woocommerce',
navArgs: {
id: 'woocommerce-marketplace',
},
} );
}
if ( isFeatureEnabled( 'product_block_editor' ) ) { if ( isFeatureEnabled( 'product_block_editor' ) ) {
const productPage = { const productPage = {
container: ProductPage, container: ProductPage,

View File

@ -0,0 +1,12 @@
<svg width="74" height="100" viewBox="0 0 74 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M67.7896 0L62.6619 4.11855L57.5289 0L52.4011 4.11855L47.2682 0L42.1353 4.11855L37.0075 0L31.8746 4.11855L26.7468 0L21.6138 4.11855L16.4809 0L11.348 4.11855L6.21501 0L0.916992 4.25258V95.7474L6.20986 100L11.3428 95.8763L16.4706 100L21.6035 95.8763L26.7313 100L31.8642 95.8763L36.9972 100L42.125 95.8763L47.2579 100L52.3857 95.8763L57.5186 100L62.6515 95.8763L67.7896 100L73.0825 95.7474V4.25258L67.7896 0Z" fill="#E0E0E0"/>
<path d="M49.4449 18.7216H7.10547V22.5103H49.4449V18.7216Z" fill="#757575"/>
<path d="M49.4449 30.2784H7.10547V34.067H49.4449V30.2784Z" fill="#757575"/>
<path d="M49.4449 41.8351H7.10547V45.6237H49.4449V41.8351Z" fill="#757575"/>
<path d="M66.8991 18.7216H56.9102V22.5103H66.8991V18.7216Z" fill="white"/>
<path d="M66.8991 30.2783H56.9102V34.067H66.8991V30.2783Z" fill="white"/>
<path d="M66.8991 41.835H56.9102V45.6237H66.8991V41.835Z" fill="white"/>
<path d="M66.8993 63.9176H50.4043V71.1341H66.8993V63.9176Z" fill="#757575"/>
<path d="M7.13379 55.5258H66.8714" stroke="#271B3D" stroke-width="0.510311" stroke-miterlimit="10"/>
<path d="M51.2154 89.8917C50.6639 89.8917 50.1845 89.7731 49.8082 89.4999C48.0556 88.2473 47.8597 85.768 47.6845 83.5772C47.4886 81.0618 47.2876 79.5205 45.736 79.5205C44.437 79.5205 43.5659 81.7731 42.7205 83.9484C41.705 86.5669 40.6535 89.2731 38.6122 89.2731C36.4473 89.2731 36.1895 87.1752 35.937 85.1494C35.6741 83.0205 35.4266 81.0051 33.2256 81.0051V80.4948C35.8802 80.4948 36.1792 82.9329 36.4473 85.0875C36.7308 87.3762 37.1586 88.4071 38.6071 88.4535C39.8648 88.4896 41.2875 86.2267 42.2411 83.768C43.1896 81.3247 44.2978 78.2886 45.9473 78.2886C48.0504 78.2886 48.2721 80.3762 48.4061 82.8195C48.5401 85.2628 48.5401 87.9741 50.102 89.0927C52.8546 91.0618 62.4011 83.2473 66.2259 79.4174L67.0558 80.3711C66.5609 80.804 56.0093 89.8968 51.2154 89.8968V89.8917Z" fill="#CCCCCC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="W" clip-path="url(#clip0_1256_184463)">
<rect width="16" height="16" fill="#646970"/>
<rect id="Rectangle 1" width="16" height="16" rx="2" fill="#646970"/>
<g id="Speech bubble">
<path id="Vector" d="M3.04855 3.86047C3.2036 3.67312 3.42971 3.55683 3.6752 3.55037C4.17911 3.51807 4.46983 3.75711 4.54735 4.26747C4.85745 6.34771 5.19339 8.11786 5.54871 9.57144L7.73878 5.41096C7.93905 5.03626 8.18454 4.83599 8.48818 4.81661C8.92749 4.7843 9.19882 5.0621 9.30865 5.65645C9.51538 6.81932 9.83194 7.96281 10.2519 9.06753C10.5167 6.53506 10.956 4.70032 11.5698 3.56975C11.6925 3.31134 11.9445 3.14337 12.2287 3.13045C12.4549 3.11107 12.681 3.18213 12.8554 3.33072C13.0363 3.46639 13.1461 3.67958 13.159 3.90569C13.1719 4.07366 13.1396 4.24163 13.0621 4.38376C12.6745 5.10732 12.3515 6.30895 12.0995 7.98865C11.854 9.6102 11.7571 10.8829 11.8217 11.7938C11.8476 12.0199 11.8024 12.246 11.7054 12.4463C11.6085 12.653 11.4018 12.7952 11.1757 12.8081C10.9108 12.8275 10.6524 12.7047 10.3875 12.4398C9.45724 11.4902 8.72076 10.0753 8.17808 8.19538C7.53851 9.47453 7.05398 10.4371 6.73742 11.0702C6.14953 12.2008 5.64562 12.7758 5.23215 12.8081C4.96082 12.8275 4.72825 12.6014 4.5409 12.1233C4.03053 10.8183 3.4814 8.29229 2.8935 4.54527C2.84182 4.29978 2.89996 4.05428 3.04855 3.86047Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1256_184463">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,108 @@
/**
* External dependencies
*/
import { Dropdown } from '@wordpress/components';
import { chevronDown, chevronUp, Icon } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { Category } from './types';
function DropdownContent( props: {
readonly categories: Category[];
readonly selected?: Category;
readonly onClick: () => void;
} ): JSX.Element {
function updateCategorySelection(
event: React.MouseEvent< HTMLButtonElement >
) {
const slug = event.currentTarget.value;
if ( ! slug ) {
return;
}
/**
* Trigger the onClick event on the parent component to close the dropdown.
* This closes the dropdown automatically when a user clicks on an item.
*/
props.onClick();
navigateTo( {
url: getNewPath( { category: slug } ),
} );
}
return (
<ul className="woocommerce-marketplace__category-dropdown-list">
{ props.categories.map( ( category ) => (
<li
className="woocommerce-marketplace__category-dropdown-item"
key={ category.slug }
>
<button
className={ classNames(
'woocommerce-marketplace__category-dropdown-item-button',
{
'woocommerce-marketplace__category-dropdown-item-button--selected':
category.slug === props.selected?.slug,
}
) }
value={ category.slug }
onClick={ updateCategorySelection }
>
{ category.label }
</button>
</li>
) ) }
</ul>
);
}
type CategoryDropdownProps = {
label: string;
categories: Category[];
className?: string;
buttonClassName?: string;
contentClassName?: string;
arrowIconSize?: number;
selected?: Category;
};
export default function CategoryDropdown(
props: CategoryDropdownProps
): JSX.Element {
return (
<Dropdown
renderToggle={ ( { isOpen, onToggle } ) => (
<button
onClick={ onToggle }
className={ props.buttonClassName }
aria-label={ __(
'Toggle category dropdown',
'woocommerce'
) }
>
{ props.label }
<Icon
icon={ isOpen ? chevronUp : chevronDown }
size={ props.arrowIconSize }
/>
</button>
) }
className={ props.className }
renderContent={ ( { onToggle } ) => (
<DropdownContent
categories={ props.categories }
selected={ props.selected }
onClick={ onToggle }
/>
) }
contentClassName={ props.contentClassName }
/>
);
}

View File

@ -0,0 +1,44 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { Category } from './types';
export default function CategoryLink( props: Category ): JSX.Element {
function updateCategorySelection(
event: React.MouseEvent< HTMLButtonElement >
) {
const slug = event.currentTarget.value;
if ( ! slug ) {
return;
}
navigateTo( {
url: getNewPath( { category: slug } ),
} );
}
const classes = classNames(
'woocommerce-marketplace__category-item-button',
{
'woocommerce-marketplace__category-item-button--selected':
props.selected,
}
);
return (
<button
className={ classes }
onClick={ updateCategorySelection }
value={ props.slug }
>
{ props.label }
</button>
);
}

View File

@ -0,0 +1,126 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace__category-selector {
display: flex;
align-items: stretch;
margin: $grid-unit-20 0 0 0;
}
.woocommerce-marketplace__category-item {
cursor: pointer;
.components-dropdown {
height: 100%;
}
}
.woocommerce-marketplace__category-item-button {
display: flex;
align-items: center;
cursor: pointer;
border: none;
border-radius: 2px;
color: $wp-gray-60;
background-color: $wp-gray-0;
padding: 6px $grid-unit-10;
margin-right: $grid-unit-10;
line-height: 20px;
height: 100%;
&--selected {
color: $white;
background-color: $gray-900;
fill: $white;
}
}
.woocommerce-marketplace__category-item-content {
.components-popover__content {
min-width: 200px;
}
}
.woocommerce-marketplace__category-selector--full-width {
display: none;
margin-top: $grid-unit-15;
}
@media screen and (max-width: $break-medium) {
.woocommerce-marketplace__category-selector--full-width {
display: flex;
}
.woocommerce-marketplace__category-selector {
display: none;
}
}
.woocommerce-marketplace__category-dropdown {
width: 100%;
}
.woocommerce-marketplace__category-dropdown-button {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
border: 1px solid $gray-600;
border-radius: 2px;
background-color: $white;
width: 100%;
font-size: 13px;
line-height: 20px;
padding: $grid-unit-15 $grid-unit-10;
text-align: left;
}
.woocommerce-marketplace__category-dropdown-content {
background-color: $white;
color: $gray-900;
font-size: 13px;
min-width: 280px;
width: calc(100% - 32px);
.components-popover__content {
width: 100%;
}
}
.woocommerce-marketplace__category-dropdown-list {
margin: 0;
line-height: 20px;
}
.woocommerce-marketplace__category-dropdown-item {
border-radius: 2px;
&:hover {
background-color: $gutenberg-gray-100;
}
}
.woocommerce-marketplace__category-dropdown-item-button {
border: none;
cursor: pointer;
background-color: inherit;
color: $gray-900;
text-align: left;
padding: 6px $grid-unit-10;
line-height: 20px;
width: 100%;
&--selected {
color: $white;
background-color: $gray-900;
}
}
.woocommerce-marketplace__category-selector-loading {
display: flex;
margin-top: $grid-unit-20;
p {
margin: 0;
line-height: $grid-unit-30;
}
}

View File

@ -0,0 +1,160 @@
/**
* External dependencies
*/
import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Spinner } from '@wordpress/components';
import { useQuery } from '@woocommerce/navigation';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import CategoryLink from './category-link';
import CategoryDropdown from './category-dropdown';
import { Category, CategoryAPIItem } from './types';
import { fetchCategories } from '../../utils/functions';
import './category-selector.scss';
const ALL_CATEGORIES_SLUG = '_all';
export default function CategorySelector(): JSX.Element {
const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] );
const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] );
const [ selected, setSelected ] = useState< Category >();
const [ isLoading, setIsLoading ] = useState( false );
const query = useQuery();
useEffect( () => {
// If no category is selected, show All as selected
let categoryToSearch = ALL_CATEGORIES_SLUG;
if ( query.category ) {
categoryToSearch = query.category;
}
const allCategories = visibleItems.concat( dropdownItems );
const selectedCategory = allCategories.find(
( category ) => category.slug === categoryToSearch
);
if ( selectedCategory ) {
setSelected( selectedCategory );
}
}, [ query, visibleItems, dropdownItems ] );
useEffect( () => {
setIsLoading( true );
fetchCategories()
.then( ( categoriesFromAPI: CategoryAPIItem[] ) => {
const categories: Category[] = categoriesFromAPI
.map( ( categoryAPIItem: CategoryAPIItem ): Category => {
return {
...categoryAPIItem,
selected: false,
};
} )
.filter( ( category: Category ): boolean => {
// The "featured" category is returned from the API for legacy reasons, but we don't need it:
return category.slug !== '_featured';
} );
// Split array into two from 7th item
const visibleCategoryItems = categories.slice( 0, 7 );
const dropdownCategoryItems = categories.slice( 7 );
setVisibleItems( visibleCategoryItems );
setDropdownItems( dropdownCategoryItems );
} )
.catch( () => {
setVisibleItems( [] );
setDropdownItems( [] );
} )
.finally( () => {
setIsLoading( false );
} );
}, [] );
function mobileCategoryDropdownLabel() {
const allCategoriesText = __( 'All Categories', 'woocommerce' );
if ( ! selected ) {
return allCategoriesText;
}
if ( selected.label === 'All' ) {
return allCategoriesText;
}
return selected.label;
}
function isSelectedInDropdown() {
if ( ! selected ) {
return false;
}
return dropdownItems.find(
( category ) => category.slug === selected.slug
);
}
if ( isLoading ) {
return (
<div className="woocommerce-marketplace__category-selector-loading">
<p>{ __( 'Loading categories…', 'woocommerce' ) }</p>
<Spinner />
</div>
);
}
return (
<>
<ul className="woocommerce-marketplace__category-selector">
{ visibleItems.map( ( category ) => (
<li
className="woocommerce-marketplace__category-item"
key={ category.slug }
>
<CategoryLink
{ ...category }
selected={ category.slug === selected?.slug }
/>
</li>
) ) }
<li className="woocommerce-marketplace__category-item">
{ dropdownItems.length > 0 && (
<CategoryDropdown
label={ __( 'More', 'woocommerce' ) }
categories={ dropdownItems }
buttonClassName={ classNames(
'woocommerce-marketplace__category-item-button',
{
'woocommerce-marketplace__category-item-button--selected':
isSelectedInDropdown(),
}
) }
contentClassName="woocommerce-marketplace__category-item-content"
arrowIconSize={ 20 }
selected={ selected }
/>
) }
</li>
</ul>
<div className="woocommerce-marketplace__category-selector--full-width">
<CategoryDropdown
label={ mobileCategoryDropdownLabel() }
categories={ visibleItems.concat( dropdownItems ) }
buttonClassName="woocommerce-marketplace__category-dropdown-button"
className="woocommerce-marketplace__category-dropdown"
contentClassName="woocommerce-marketplace__category-dropdown-content"
selected={ selected }
/>
</div>
</>
);
}

View File

@ -0,0 +1,10 @@
export type Category = {
readonly slug: string;
readonly label: string;
selected: boolean;
};
export type CategoryAPIItem = {
readonly slug: string;
readonly label: string;
};

View File

@ -0,0 +1,3 @@
export const DEFAULT_TAB_KEY = 'discover';
export const MARKETPLACE_PATH = '/extensions';
export const MARKETPLACE_URL = 'https://woocommerce.com';

View File

@ -0,0 +1,14 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace__content {
box-sizing: content-box;
margin: auto;
max-width: $content-max-width;
padding: $header-height-mobile $content-spacing-small $content-spacing-small;
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace__content {
padding: $header-height-desktop $content-spacing-large $content-spacing-large;
}
}

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import './content.scss';
import Discover from '../discover/discover';
import Extensions from '../extensions/extensions';
import Footer from '../footer/footer';
import FeedbackModal from '../feedback-modal/feedback-modal';
export interface ContentProps {
selectedTab?: string | undefined;
}
const renderContent = ( selectedTab?: string ): JSX.Element => {
switch ( selectedTab ) {
case 'extensions':
return <Extensions />;
default:
return <Discover />;
}
};
export default function Content( props: ContentProps ): JSX.Element {
const { selectedTab } = props;
return (
<>
<div className="woocommerce-marketplace__content">
{ renderContent( selectedTab ) }
</div>
<Footer />
<FeedbackModal />
</>
);
}

View File

@ -0,0 +1,10 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__discover {
display: flex;
flex-direction: column;
align-items: center;
gap: 40px;
}
}

View File

@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { useEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import ProductList from '../product-list/product-list';
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
import ProductLoader from '../product-loader/product-loader';
import './discover.scss';
export default function Discover(): JSX.Element | null {
const [ productGroups, setProductGroups ] = useState<
Array< ProductGroup >
>( [] );
const [ isLoading, setIsLoading ] = useState( false );
useEffect( () => {
setIsLoading( true );
fetchDiscoverPageData()
.then( ( products: Array< ProductGroup > ) => {
setProductGroups( products );
} )
.finally( () => {
setIsLoading( false );
} );
}, [] );
if ( isLoading ) {
return <ProductLoader />;
}
const groupsList = productGroups.flatMap( ( group ) => group );
return (
<div className="woocommerce-marketplace__discover">
{ groupsList.map( ( groups ) => (
<ProductList
key={ groups.id }
title={ groups.title }
products={ groups.items }
groupURL={ groups.url }
/>
) ) }
</div>
);
}

View File

@ -0,0 +1,6 @@
.woocommerce-marketplace {
&__extensions {
display: flex;
flex-direction: column;
}
}

View File

@ -0,0 +1,59 @@
/**
* External dependencies
*/
import { useContext } from '@wordpress/element';
import { __, _n, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './extensions.scss';
import CategorySelector from '../category-selector/category-selector';
import { ProductListContext } from '../../contexts/product-list-context';
import ProductListContent from '../product-list-content/product-list-content';
import ProductLoader from '../product-loader/product-loader';
import NoResults from '../product-list-content/no-results';
export default function Extensions(): JSX.Element {
const productListContextValue = useContext( ProductListContext );
const { productList, isLoading } = productListContextValue;
const products = productList.slice( 0, 60 );
let title = __( '0 extensions found', 'woocommerce' );
if ( products.length > 0 ) {
title = sprintf(
// translators: %s: number of extensions
_n(
'%s extension',
'%s extensions',
products.length,
'woocommerce'
),
products.length
);
}
function content() {
if ( isLoading ) {
return <ProductLoader />;
}
if ( products.length === 0 ) {
return <NoResults />;
}
return <ProductListContent products={ products } />;
}
return (
<div className="woocommerce-marketplace__extensions">
<h2 className="woocommerce-marketplace__product-list-title woocommerce-marketplace__product-list-title--extensions">
{ title }
</h2>
<CategorySelector />
{ content() }
</div>
);
}

View File

@ -0,0 +1,9 @@
.woocommerce-marketplace__feedback-modal {
max-width: 666px;
}
.woocommerce-marketplace__feedback-modal-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
}

View File

@ -0,0 +1,243 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Modal, Button, TextareaControl } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './feedback-modal.scss';
import LikertScale from '../likert-scale/likert-scale';
export default function FeedbackModal(): JSX.Element {
const CUSTOMER_EFFORT_SCORE_ACTION = 'marketplace_redesign_2023';
const LOCALSTORAGE_KEY_DISMISSAL_COUNT =
'marketplace_redesign_2023_dismissals'; // ensure we don't ask for feedback if the user's already given feedback or declined to multiple times
const LOCALSTORAGE_KEY_LAST_REQUESTED_DATE =
'marketplace_redesign_2023_last_shown_date'; // ensure we don't ask for feedback more than once per day
const SUPPRESS_IF_DISMISSED_X_TIMES = 1; // if the user dismisses the snackbar this many times, stop asking for feedback
const SUPPRESS_IF_AFTER_DATE = '2024-01-01'; // if this date is reached, stop asking for feedback
// Save that we dismissed the dialog or snackbar TODAY so we don't show it again until tomorrow (if ever)
const dismissToday = () =>
localStorage.setItem(
LOCALSTORAGE_KEY_LAST_REQUESTED_DATE,
new Date().toDateString()
);
// Returns the number of times that the request for feedback has been dismissed
const dismissedTimes = () =>
parseInt(
localStorage.getItem( LOCALSTORAGE_KEY_DISMISSAL_COUNT ) || '0',
10
);
// Increment the number of times that the request for feedback has been dismissed
const incrementDismissedTimes = () => {
dismissToday();
localStorage.setItem(
LOCALSTORAGE_KEY_DISMISSAL_COUNT,
`${ dismissedTimes() + 1 }`
);
};
// Dismiss forever (by incrementing the number of dismissals to a high number), e.g. when feedback is provided
const dismissForever = () => {
dismissToday();
localStorage.setItem(
LOCALSTORAGE_KEY_DISMISSAL_COUNT,
`${ SUPPRESS_IF_DISMISSED_X_TIMES }`
);
};
// Returns true if dismissed forever (either by dismissing at least SUPPRESS_IF_DISMISSED_X_TIMES times, or by submitting feedback)
const isDismissedForever = () =>
dismissedTimes() >= SUPPRESS_IF_DISMISSED_X_TIMES;
const [ isOpen, setOpen ] = useState( false );
const [ thoughts, setThoughts ] = useState( '' );
const [ easyToFind, setEasyToFind ] = useState( 0 );
const [ easyToFindValidiationFailed, setEasyToFindValidiationFailed ] =
useState( false );
const [ meetsMyNeeds, setMeetsMyNeeds ] = useState( 0 );
const [ meetsMyNeedsValidiationFailed, setMeetsMyNeedsValidiationFailed ] =
useState( false );
const openModal = () => setOpen( true );
const closeModal = () => {
incrementDismissedTimes();
setOpen( false );
};
const { createNotice } = useDispatch( 'core/notices' );
function maybeShowSnackbar() {
// don't show if the user has already given feedback or otherwise suppressed:
if ( isDismissedForever() ) {
return;
}
// don't show if the user has already declined to provide feedback today:
const today = new Date().toDateString();
if (
today ===
localStorage.getItem( LOCALSTORAGE_KEY_LAST_REQUESTED_DATE )
) {
return;
}
createNotice(
'success',
__( 'How easy is it to find an extension?', 'woocommerce' ),
{
type: 'snackbar',
icon: (
<>
<svg
color="#fff"
strokeWidth="1.5"
viewBox="0 0 28.873 8.9823"
style={ { height: '8px', marginLeft: '-7px' } }
>
<path
className="l"
d="m4.1223 1.1216 19.12-0.014142 4.3982 3.38-4.3982 3.38-19.12-0.014142a3.34 3.34 0 0 1-2.39-0.97581 3.37 3.37 0 0 1 0.00707-4.773 3.34 3.34 0 0 1 2.383-0.98288z"
stroke="#fff"
/>
<line
className="l"
x1="6.7669"
x2="6.7669"
y1="7.8533"
y2="1.1216"
stroke="#fff"
/>
<path
className="l"
d="m23.235 1.1146 4.4053 3.3729-4.3982 3.38a6.59 6.59 0 0 1-0.89096-3.3517 6.59 6.59 0 0 1 0.88388-3.4012z"
stroke="#fff"
/>
<line
className="l"
x1="6.7669"
x2="22.323"
y1="4.4875"
y2="4.4875"
stroke="#fff"
/>
</svg>
</>
),
explicitDismiss: true,
onDismiss: incrementDismissedTimes,
actions: [
{
onClick: openModal,
label: 'Give feedback',
},
],
}
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- [] => we only want this effect to run once, on first render
useEffect( maybeShowSnackbar, [] );
// We don't want the "How easy was it to find an extension?" dialog to appear forever:
const FEEDBACK_DIALOG_CAN_APPEAR =
new Date( SUPPRESS_IF_AFTER_DATE ) > new Date();
if ( ! FEEDBACK_DIALOG_CAN_APPEAR ) {
return <></>;
}
function easyToFindChanged( value: number ) {
setEasyToFindValidiationFailed( false );
setEasyToFind( value );
}
function meetsMyNeedsChanged( value: number ) {
setMeetsMyNeedsValidiationFailed( false );
setMeetsMyNeeds( value );
}
function submit() {
// Validate:
if ( easyToFind === 0 || meetsMyNeeds === 0 ) {
if ( easyToFind === 0 ) setEasyToFindValidiationFailed( true );
if ( meetsMyNeeds === 0 ) setMeetsMyNeedsValidiationFailed( true );
return;
}
// Send event to CES:
recordEvent( 'ces_feedback', {
action: CUSTOMER_EFFORT_SCORE_ACTION,
score: easyToFind,
score_second_question: meetsMyNeeds,
score_combined: easyToFind + meetsMyNeeds,
thoughts,
} );
// Close the modal:
setOpen( false );
// Ensure we don't ask for feedback again:
dismissForever();
}
return (
<>
{ isOpen && (
<Modal
title={ __(
'How easy was it to find an extension?',
'woocommerce'
) }
onRequestClose={ closeModal }
className="woocommerce-marketplace__feedback-modal"
>
<p>
{ __(
'Your feedback will help us create a better experience for people like you! Please tell us to what extent you agree or disagree with the statements below.',
'woocommerce'
) }
</p>
<LikertScale
fieldName="extension_screen_easy_to_find"
title={ __(
'It was easy to find an extension',
'woocommerce'
) }
onValueChange={ easyToFindChanged }
validationFailed={ easyToFindValidiationFailed }
/>
<LikertScale
fieldName="extension_screen_meets_my_needs"
title={ __(
'The Extensions screens functionality meets my needs',
'woocommerce'
) }
onValueChange={ meetsMyNeedsChanged }
validationFailed={ meetsMyNeedsValidiationFailed }
/>
<TextareaControl
label={ __( 'Additional thoughts', 'woocommerce' ) }
value={ thoughts }
onChange={ ( value: string ) => setThoughts( value ) }
/>
<p className="woocommerce-marketplace__feedback-modal-buttons">
<Button
variant="tertiary"
onClick={ closeModal }
text={ __( 'Cancel', 'woocommerce' ) }
/>
<Button
variant="primary"
onClick={ submit }
text={ __( 'Send', 'woocommerce' ) }
/>
</p>
</Modal>
) }
</>
);
}

View File

@ -0,0 +1,55 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-admin-page__extensions .woocommerce-layout__footer {
background: #f6f7f7;
// Undo default fixed footer style used in WC Admin
position: relative;
width: 100%;
}
.woocommerce-marketplace__footer {
box-sizing: content-box;
max-width: $content-max-width;
margin: auto;
padding: $content-spacing-xlarge $content-spacing-small;
&-title {
color: $gutenberg-gray-900;
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 28px;
max-width: 389px;
margin: 0 0 $content-spacing-large;
}
a {
text-decoration: none;
}
&-columns {
display: flex;
flex-direction: column;
gap: $large-gap;
}
&-logo {
color: $wp-gray-50;
display: flex;
font-size: 14px;
font-weight: 600;
line-height: 20px;
gap: $small-gap;
margin: 48px 0 0;
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace__footer {
padding: $content-spacing-xlarge $content-spacing-large;
}
.woocommerce-marketplace__footer-columns {
flex-direction: row;
}
}

View File

@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { WooFooterItem } from '@woocommerce/admin-layout';
import { __ } from '@wordpress/i18n';
import { check, commentContent, shield } from '@wordpress/icons';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import './footer.scss';
import IconWithText from '../icon-with-text/icon-with-text';
import WooIcon from '../../assets/images/woo-icon.svg';
import { MARKETPLACE_URL } from '../constants';
const refundPolicyTitle = createInterpolateElement(
__( '30 day <a>money back guarantee</a>', 'woocommerce' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ MARKETPLACE_URL + '/refund-policy/' } />,
}
);
const supportTitle = createInterpolateElement(
__( '<a>Get help</a> when you need it', 'woocommerce' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ MARKETPLACE_URL + '/docs/' } />,
}
);
const paymentTitle = createInterpolateElement(
__( '<a>Products</a> you can trust', 'woocommerce' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ MARKETPLACE_URL + '/products/' } />,
}
);
function FooterContent(): JSX.Element {
return (
<div className="woocommerce-marketplace__footer">
<h2 className="woocommerce-marketplace__footer-title">
{ __(
'Hundreds of vetted products and services. Unlimited potential.',
'woocommerce'
) }
</h2>
<div className="woocommerce-marketplace__footer-columns">
<IconWithText
icon={ check }
title={ refundPolicyTitle }
description={ __(
"If you change your mind within 30 days of your purchase, we'll give you a full refund — hassle-free.",
'woocommerce'
) }
/>
<IconWithText
icon={ commentContent }
title={ supportTitle }
description={ __(
'With detailed documentation and a global support team, help is always available if you need it.',
'woocommerce'
) }
/>
<IconWithText
icon={ shield }
title={ paymentTitle }
description={ __(
'Everything in the Marketplace has been built by our own team or by our trusted partners, so you can be sure of its quality.',
'woocommerce'
) }
/>
</div>
<div className="woocommerce-marketplace__footer-logo">
<img src={ WooIcon } alt="Woo Logo" aria-hidden="true" />
<span>{ __( 'Woo Marketplace', 'woocommerce' ) }</span>
</div>
</div>
);
}
export default function Footer(): JSX.Element {
return (
<WooFooterItem>
<FooterContent />
</WooFooterItem>
);
}

View File

@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { Button, ButtonGroup, Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export interface HeaderAccountModalProps {
setIsModalOpen: ( value: boolean ) => void;
disconnectURL: string;
}
export default function HeaderAccountModal(
props: HeaderAccountModalProps
): JSX.Element {
const { setIsModalOpen, disconnectURL } = props;
const [ isBusy, setIsBusy ] = useState( false );
const toggleIsBusy = () => setIsBusy( ! isBusy );
const closeModal = () => setIsModalOpen( false );
return (
<Modal
title={ __( 'Are you sure?', 'woocommerce' ) }
onRequestClose={ closeModal }
focusOnMount={ true }
className="woocommerce-marketplace__header-account-modal"
style={ { borderRadius: 4 } }
overlayClassName="woocommerce-marketplace__header-account-modal-overlay"
>
<p className="woocommerce-marketplace__header-account-modal-text">
{ __(
'Keep your your account connected to manage your subscriptions, get updates and support for your extensions and themes.',
'woocommerce'
) }
</p>
<ButtonGroup className="woocommerce-marketplace__header-account-modal-button-group">
<Button
variant="tertiary"
href={ disconnectURL }
onClick={ toggleIsBusy }
isBusy={ isBusy }
isDestructive={ true }
className="woocommerce-marketplace__header-account-modal-button"
>
{ __( 'Disconnect account', 'woocommerce' ) }
</Button>
<Button
variant="primary"
onClick={ closeModal }
className="woocommerce-marketplace__header-account-modal-button"
>
{ __( 'Keep connected', 'woocommerce' ) }
</Button>
</ButtonGroup>
</Modal>
);
}

View File

@ -0,0 +1,62 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__menu-item span {
white-space: normal;
}
&__menu-avatar-image {
border-radius: 50%;
height: $grid-unit-30;
width: $grid-unit-30;
}
&__menu-icon {
flex-shrink: 0;
margin-right: $grid-unit-10;
}
&__menu-text {
display: flex;
flex-direction: column;
}
&__sub-text {
color: $gray-700;
font-size: 12px;
}
&__header-account-modal {
&__header-account-modal-text {
margin-bottom: $grid-unit-10;
}
}
&__header-account-modal-overlay {
// This is to ensure the modal is above the user menu popover.
z-index: 1000000;
}
&__header-account-modal-button-group {
display: inline-flex;
gap: $grid-unit-10;
justify-content: flex-end;
margin-top: $grid-unit-30;
width: 100%;
.woocommerce-marketplace__header-account-modal-button,
.woocommerce-marketplace__header-account-modal-button.is-primary {
border-radius: 2px;
box-shadow: none;
}
}
}
@media screen and (min-width: $break-small) {
.woocommerce-marketplace {
&__header-account-modal {
max-width: 350px;
}
}
}

View File

@ -0,0 +1,144 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
import { Icon, commentAuthorAvatar, external, linkOff } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './header-account.scss';
import { getAdminSetting } from '../../../utils/admin-settings';
import HeaderAccountModal from './header-account-modal';
import { MARKETPLACE_URL } from '../constants';
export default function HeaderAccount(): JSX.Element {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const openModal = () => setIsModalOpen( true );
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
const isConnected = wccomSettings?.isConnected ?? false;
const connectionURL = wccomSettings?.connectURL ?? '';
const userEmail = wccomSettings?.userEmail;
const avatarURL = wccomSettings?.userAvatar ?? commentAuthorAvatar;
// This is a hack to prevent TypeScript errors. The MenuItem component passes these as an href prop to the underlying button
// component. That component is either an anchor with href if provided or a button that won't accept an href if no href is provided.
// Due to early erroring of TypeScript, it only takes the button version into account which doesn't accept href.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountURL: any = MARKETPLACE_URL + '/my-dashboard/';
const accountOrConnect = isConnected ? accountURL : connectionURL;
const avatar = () => {
if ( ! isConnected ) {
return commentAuthorAvatar;
}
return (
<img
src={ avatarURL }
alt=""
className="woocommerce-marketplace__menu-avatar-image"
/>
);
};
const connectionStatusText = isConnected
? __( 'Connected', 'woocommerce' )
: __( 'Not Connected', 'woocommerce' );
const connectionDetails = () => {
if ( isConnected ) {
return (
<>
<Icon
icon={ commentAuthorAvatar }
size={ 24 }
className="woocommerce-marketplace__menu-icon"
/>
<span className="woocommerce-marketplace__main-text">
{ userEmail }
</span>
</>
);
}
return (
<>
<Icon
icon={ commentAuthorAvatar }
size={ 24 }
className="woocommerce-marketplace__menu-icon"
/>
<div className="woocommerce-marketplace__menu-text">
{ __( 'Connect account', 'woocommerce' ) }
<span className="woocommerce-marketplace__sub-text">
{ __(
'Manage your subscriptions, get updates and support for your extensions and themes.',
'woocommerce'
) }
</span>
</div>
</>
);
};
return (
<>
<DropdownMenu
className="woocommerce-marketplace__user-menu"
icon={ avatar() }
label={ __( 'User options', 'woocommerce' ) }
>
{ () => (
<>
<MenuGroup
className="woocommerce-layout__homescreen-display-options"
label={ connectionStatusText }
>
<MenuItem
className="woocommerce-marketplace__menu-item"
href={ accountOrConnect }
>
{ connectionDetails() }
</MenuItem>
<MenuItem href={ accountURL }>
<Icon
icon={ external }
size={ 24 }
className="woocommerce-marketplace__menu-icon"
/>
{ __(
'WooCommerce.com account',
'woocommerce'
) }
</MenuItem>
</MenuGroup>
{ isConnected && (
<MenuGroup className="woocommerce-layout__homescreen-display-options">
<MenuItem onClick={ openModal }>
<Icon
icon={ linkOff }
size={ 24 }
className="woocommerce-marketplace__menu-icon"
/>
{ __(
'Disconnect account',
'woocommerce'
) }
</MenuItem>
</MenuGroup>
) }
</>
) }
</DropdownMenu>
{ isModalOpen && (
<HeaderAccountModal
setIsModalOpen={ setIsModalOpen }
disconnectURL={ connectionURL }
/>
) }
</>
);
}

View File

@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export default function HeaderSearchButton() {
return (
<button className="woocommerce-marketplace__header-search-button">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="Union"
fillRule="evenodd"
clipRule="evenodd"
d="M19.0001 11C19.0001 14.3137 16.3138 17 13.0001 17C11.6135 17 10.3369 16.5297 9.32086 15.7399L5.53039 19.5304L4.46973 18.4697L8.26019 14.6793C7.47038 13.6632 7.00006 12.3865 7.00006 11C7.00006 7.68629 9.68635 5 13.0001 5C16.3138 5 19.0001 7.68629 19.0001 11ZM17.5001 11C17.5001 13.4853 15.4853 15.5 13.0001 15.5C10.5148 15.5 8.50006 13.4853 8.50006 11C8.50006 8.51472 10.5148 6.5 13.0001 6.5C15.4853 6.5 17.5001 8.51472 17.5001 11Z"
fill="#1E1E1E"
/>
</svg>
<span className="screen-reader-text">
{ __( 'Search', 'woocommerce' ) }
</span>
</button>
);
}

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
export default function HeaderSearch() {
return (
<form className="woocommerce-marketplace__header-search">
<input
type="search"
className="woocommerce-marketplace__header-search-field"
placeholder={ __(
'Search extensions and themes',
'woocommerce'
) }
/>
<button className="woocommerce-marketplace__header-search-button">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="Union"
fillRule="evenodd"
clipRule="evenodd"
d="M19.0001 11C19.0001 14.3137 16.3138 17 13.0001 17C11.6135 17 10.3369 16.5297 9.32086 15.7399L5.53039 19.5304L4.46973 18.4697L8.26019 14.6793C7.47038 13.6632 7.00006 12.3865 7.00006 11C7.00006 7.68629 9.68635 5 13.0001 5C16.3138 5 19.0001 7.68629 19.0001 11ZM17.5001 11C17.5001 13.4853 15.4853 15.5 13.0001 15.5C10.5148 15.5 8.50006 13.4853 8.50006 11C8.50006 8.51472 10.5148 6.5 13.0001 6.5C15.4853 6.5 17.5001 8.51472 17.5001 11Z"
fill="#1E1E1E"
/>
</svg>
<span className="screen-reader-text">
{ __( 'Search', 'woocommerce' ) }
</span>
</button>
</form>
);
}

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export default function HeaderTitle() {
return (
<h1 className="woocommerce-marketplace__header-title">
{ __( 'Extensions', 'woocommerce' ) }
</h1>
);
}

View File

@ -0,0 +1,81 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace__header {
align-items: center;
background: #fff;
border-bottom: 1px solid $gutenberg-gray-300;
display: grid;
grid-template: 'mktpl-title mktpl-search mktpl-meta' 60px
'mktpl-tabs mktpl-tabs mktpl-tabs' auto / 1fr 320px 36px;
left: 0;
padding: 0 $content-spacing-large;
position: absolute;
top: -60px;
width: 100%;
/* On narrow screens, "stack" header items and hide the bottom border */
@media (width <= $breakpoint-medium) {
border-bottom: 0;
grid-template: 'mktpl-title mktpl-meta' 60px
'mktpl-tabs mktpl-tabs' 48px
'mktpl-search mktpl-search' auto / auto 48px;
padding: 0;
}
.woocommerce-marketplace__header-title {
font-size: 14px;
font-weight: 600;
margin: 0;
padding: 10px 0 0;
@media (width <= $breakpoint-medium) {
padding-left: var(--large-gap);
}
}
}
.woocommerce-marketplace__header-title {
align-items: center;
align-self: stretch;
display: flex;
grid-area: mktpl-title;
line-height: 18px;
@media (width <= $breakpoint-medium) {
padding: 0 $content-spacing-small;
}
}
.woocommerce-marketplace__header-meta {
grid-area: mktpl-meta;
justify-self: end;
padding-top: 10px;
@media (width <= $breakpoint-medium) {
margin-right: $content-spacing-small;
padding: 0;
}
}
.woocommerce-marketplace__header-tabs {
align-self: end;
grid-area: mktpl-tabs;
@media (width <= $breakpoint-medium) {
padding: 0 $content-spacing-small;
}
}
.woocommerce-marketplace__search {
margin-right: $medium-gap;
margin-top: 10px;
input[type='search'] {
all: unset;
flex-grow: 1;
}
@media (width <= $breakpoint-medium) {
margin: $content-spacing-small;
}
}

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import './header.scss';
import HeaderTitle from '../header-title/header-title';
import HeaderAccount from '../header-account/header-account';
import Tabs from '../tabs/tabs';
import Search from '../search/search';
export interface HeaderProps {
selectedTab?: string | undefined;
setSelectedTab: ( value: string ) => void;
}
export default function Header( props: HeaderProps ) {
const { selectedTab, setSelectedTab } = props;
return (
<header className="woocommerce-marketplace__header">
<HeaderTitle />
<Tabs
additionalClassNames={ [
'woocommerce-marketplace__header-tabs',
] }
selectedTab={ selectedTab }
setSelectedTab={ setSelectedTab }
/>
<Search />
<div className="woocommerce-marketplace__header-meta">
<HeaderAccount />
</div>
</header>
);
}

View File

@ -0,0 +1,29 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__icon-group {
flex: 1;
max-width: 382px;
&-headline {
display: flex;
gap: $small-gap;
}
&-title {
color: #101517;
font-size: 14px;
font-weight: 600;
line-height: 20px;
margin: 0 0 8px;
}
&-description {
color: $wp-gray-50;
font-size: 13px;
font-weight: 400;
line-height: 20px;
margin: 0;
}
}
}

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { Icon } from '@wordpress/icons';
import { ReactElement } from 'react';
/**
* Internal dependencies
*/
import './icon-with-text.scss';
export interface IconWithTextProps {
icon: JSX.Element;
title: ReactElement;
description: string;
}
export default function IconWithText( props: IconWithTextProps ): JSX.Element {
const { icon, title, description } = props;
return (
<div className="woocommerce-marketplace__icon-group">
<div className="woocommerce-marketplace__icon-group-headline">
<Icon
icon={ icon }
size={ 20 }
className="woocommerce-marketplace__icon-group-icon"
/>
<h3 className="woocommerce-marketplace__icon-group-title">
{ title }
</h3>
</div>
<p className="woocommerce-marketplace__icon-group-description">
{ description }
</p>
</div>
);
}

View File

@ -0,0 +1,52 @@
.woocommerce-marketplace__likert-scale {
display: flex;
list-style: none;
margin: 0;
border: 2px solid transparent;
&.validation-failed {
border-color: $alert-red;
}
}
.woocommerce-marketplace__likert-scale-item {
margin: 0;
width: 100%;
font-size: 11px;
label {
align-items: center;
aspect-ratio: 91.6 / 48;
border: 1.5px solid transparent;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 8px;
justify-content: center;
padding: 16px 8px;
text-align: center;
}
input:checked + label {
border-color: var(--wp-admin-theme-color-darker-10);
background: #e6f1f5;
}
// Improve a11y (especially keyboard navigation) by transferring the outline from the (now hidden) checkbox to its label
input:focus {
margin-top: -99px;
& + label {
outline: 2px solid var(--wp-admin-theme-color);
outline-offset: 2px;
}
}
}
.woocommerce-marketplace__likert-scale-icon {
font-size: 24px;
}
.woocommerce-marketplace__likert-scale-text {
font-size: 11px;
}

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import './likert-scale.scss';
export interface LikertScaleProps {
title: string;
fieldName: string;
onValueChange: ( value: number ) => void;
validationFailed?: boolean;
}
export interface LikertChangeEvent {
target: {
value: number;
};
}
export default function LikertScale( props: LikertScaleProps ): JSX.Element {
const { title, fieldName, onValueChange, validationFailed } = props;
const scaleOptions = [
{
value: 1,
emoji: '😔',
label: __( 'Strongly disagree', 'woocommerce' ),
},
{
value: 2,
emoji: '🙁',
label: __( 'Disagree', 'woocommerce' ),
},
{
value: 3,
emoji: '😐',
label: __( 'Neutral', 'woocommerce' ),
},
{
value: 4,
emoji: '🙂',
label: __( 'Agree', 'woocommerce' ),
},
{
value: 5,
emoji: '😍',
label: __( 'Strongly agree', 'woocommerce' ),
},
];
const classes = classnames( 'woocommerce-marketplace__likert-scale', {
'validation-failed': validationFailed,
} );
function valueChanged( e: React.ChangeEvent< HTMLInputElement > ) {
onValueChange( parseInt( e.target.value, 10 ) );
}
return (
<>
<h2>{ title }</h2>
<ol className={ classes }>
{ scaleOptions.map( ( option ) => {
const key = `${ fieldName }_${ option.value }`;
return (
<li
key={ key }
className="woocommerce-marketplace__likert-scale-item"
>
<input
type="radio"
name={ fieldName }
value={ option.value }
id={ key }
onChange={ valueChanged }
className="screen-reader-text"
/>
<label htmlFor={ key }>
<div className="woocommerce-marketplace__likert-scale-icon">
{ option.emoji }
</div>
<div className="woocommerce-marketplace__likert-scale-text">
{ option.label }
</div>
</label>
</li>
);
} ) }
</ol>
</>
);
}

View File

@ -0,0 +1,133 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__product-card {
padding: $large-gap;
border-radius: $grid-unit-05 !important;
&:hover {
outline: 1.5px solid var(--wp-admin-theme-color);
}
&__content {
display: grid;
align-items: flex-start;
gap: $medium-gap;
justify-content: space-between;
height: 100%;
grid-template-rows: auto 1fr 20px;
}
&__header {
align-self: stretch;
}
&__icon {
width: $grid-unit-60;
height: $grid-unit-60;
flex-shrink: 0;
border-radius: $grid-unit;
}
&__details {
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: $medium-gap;
background: $white;
}
&__meta {
display: flex;
flex-direction: column;
gap: 2px;
color: $gray-900;
}
&__title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
color: $gray-900;
font-size: $editor-font-size;
font-style: normal;
font-weight: 600;
line-height: $large-gap;
margin: -4px 0 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
}
&__link {
&,
&:hover,
&:active {
color: $gray-900;
text-decoration: none;
}
/* Use the ::after trick to make the whole card clickable: */
&::after {
bottom: 0;
content: '';
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
&__vendor {
display: flex;
gap: $grid-unit-05;
margin: 0;
padding: 0;
/* Allow vendor link to "punch through" the "whole card clickable" trick: */
position: relative;
}
&__vendor a {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-decoration: none;
}
&__description {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
}
&__price {
align-items: flex-end;
gap: $grid-unit-05;
align-self: stretch;
text-decoration: none !important;
color: $gray-900 !important;
font-style: normal;
font-weight: 500;
line-height: $medium-gap;
}
&__price-billing {
color: $gray-600;
font-size: $default-font-size;
font-style: normal;
font-weight: 400;
line-height: $medium-gap;
}
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace {
&__product-card {
margin-top: 0;
}
}
}

View File

@ -0,0 +1,99 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Card } from '@wordpress/components';
/**
* Internal dependencies
*/
import './product-card.scss';
import { Product } from '../product-list/types';
import { appendUTMParams } from '../../utils/functions';
export interface ProductCardProps {
type?: string;
product: Product;
}
function ProductCard( props: ProductCardProps ): JSX.Element {
const { product } = props;
// We hardcode this for now while we only display prices in USD.
const currencySymbol = '$';
// Append UTM parameters to the vendor URL
let vendorUrl = '';
if ( product.vendorUrl ) {
vendorUrl = appendUTMParams( product.vendorUrl, [
[ 'utm_source', 'extensionsscreen' ],
[ 'utm_medium', 'product' ],
[ 'utm_campaign', 'wcaddons' ],
[ 'utm_content', 'devpartner' ],
] );
}
let productVendor: string | JSX.Element | null = product?.vendorName;
if ( product?.vendorName && product?.vendorUrl ) {
productVendor = (
<a href={ vendorUrl } target="_blank" rel="noopener noreferrer">
{ product.vendorName }
</a>
);
}
return (
<Card className="woocommerce-marketplace__product-card">
<div className="woocommerce-marketplace__product-card__content">
<div className="woocommerce-marketplace__product-card__header">
<div className="woocommerce-marketplace__product-card__details">
{ product.icon && (
<img
className="woocommerce-marketplace__product-card__icon"
src={ product.icon }
alt={ product.title }
/>
) }
<div className="woocommerce-marketplace__product-card__meta">
<h2 className="woocommerce-marketplace__product-card__title">
<a
className="woocommerce-marketplace__product-card__link"
href={ product.url }
target="_blank"
rel="noopener noreferrer"
>
{ product.title }
</a>
</h2>
{ productVendor && (
<p className="woocommerce-marketplace__product-card__vendor">
<span>{ __( 'By ', 'woocommerce' ) }</span>
{ productVendor }
</p>
) }
</div>
</div>
</div>
<p className="woocommerce-marketplace__product-card__description">
{ product.description }
</p>
<div className="woocommerce-marketplace__product-card__price">
<span>
{
// '0' is a free product
product.price === 0
? __( 'Free download', 'woocommerce' )
: currencySymbol + product.price
}
</span>
<span className="woocommerce-marketplace__product-card__price-billing">
{ product.price === 0
? ''
: __( ' annually', 'woocommerce' ) }
</span>
</div>
</div>
</Card>
);
}
export default ProductCard;

View File

@ -0,0 +1,31 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace__no-results__content {
border: 1px solid $gutenberg-gray-100;
padding: $grid-unit-80 $grid-unit-40;
display: flex;
flex-direction: column;
margin-top: $grid-unit-30;
}
.woocommerce-marketplace__no-results__product-group {
margin-top: $grid-unit-60;
}
.woocommerce-marketplace__no-results__icon {
height: 100px;
}
.woocommerce-marketplace__no-results__description {
text-align: center;
font-size: 13px;
p {
color: $gutenberg-gray-700;
}
}
.woocommerce-marketplace__no-results__description--bold {
font-weight: 600;
font-size: 16px;
}

View File

@ -0,0 +1,118 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import NoResultsIcon from '../../assets/images/no-results.svg';
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
import ProductLoader from '../product-loader/product-loader';
import ProductList from '../product-list/product-list';
import './no-results.scss';
export default function NoResults(): JSX.Element {
const [ productGroup, setProductGroup ] = useState< ProductGroup >();
const [ isLoadingProductGroup, setisLoadingProductGroup ] =
useState( false );
const [ noResultsTerm, setNoResultsTerm ] = useState< string >( '' );
const query = useQuery();
useEffect( () => {
if ( query.term ) {
setNoResultsTerm( query.term );
return;
}
if ( query.category ) {
/**
* Trim understore from start and end of a category. Some categories have underscores at the start and end
* and we don't want to show them for the no results term
*/
const categoryTerm = query.category.replace( /^_+|_+$/g, '' );
setNoResultsTerm( categoryTerm );
}
}, [ query ] );
useEffect( () => {
setisLoadingProductGroup( true );
fetchDiscoverPageData()
.then( ( products: ProductGroup[] ) => {
const mostPopularGroup = products.find(
( group ) => group.id === 'most-popular'
);
if ( ! mostPopularGroup ) {
return;
}
mostPopularGroup.items = mostPopularGroup.items.slice( 0, 9 );
setProductGroup( mostPopularGroup );
} )
.catch( () => {
setProductGroup( undefined );
} )
.finally( () => {
setisLoadingProductGroup( false );
} );
}, [] );
function renderProductGroup() {
if ( isLoadingProductGroup ) {
return <ProductLoader />;
}
if ( ! productGroup ) {
return <></>;
}
return (
<ProductList
title={ productGroup.title }
products={ productGroup.items }
groupURL={ productGroup.url }
/>
);
}
return (
<div className="woocommerce-marketplace__no-results">
<div className="woocommerce-marketplace__no-results__content">
<img
className="woocommerce-marketplace__no-results__icon"
src={ NoResultsIcon }
alt={ __( 'No results.', 'woocommerce' ) }
/>
<div className="woocommerce-marketplace__no-results__description">
<h3 className="woocommerce-marketplace__no-results__description--bold">
{ sprintf(
// translators: %s: search term
__(
'We didn\'t find any results for "%s"',
'woocommerce'
),
noResultsTerm
) }
</h3>
<p>
{ __(
'Try searching again using a different term, or take a look at some of our most popular extensions below.',
'woocommerce'
) }
</p>
</div>
</div>
<div className="woocommerce-marketplace__no-results__product-group">
{ renderProductGroup() }
</div>
</div>
);
}

View File

@ -0,0 +1,41 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__product-list-content {
display: grid;
gap: $medium-gap;
margin-top: $grid-unit-20;
}
&__extension-card {
background-color: #3c3c3c;
color: $white;
height: 270px;
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace {
&__product-list-content {
gap: $large-gap;
grid-template-columns: repeat(2, 1fr);
}
}
}
@media screen and (min-width: $breakpoint-large) {
.woocommerce-marketplace {
&__product-list-content {
gap: $large-gap;
grid-template-columns: repeat(3, 1fr);
}
}
}
@media screen and (min-width: $breakpoint-huge) {
.woocommerce-marketplace {
&__product-list-content {
grid-template-columns: repeat(4, 1fr);
}
}
}

View File

@ -0,0 +1,31 @@
/**
* Internal dependencies
*/
import './product-list-content.scss';
import ProductCard from '../product-card/product-card';
import { Product } from '../product-list/types';
export default function ProductListContent( props: {
products: Product[];
} ): JSX.Element {
const { products } = props;
return (
<div className="woocommerce-marketplace__product-list-content">
{ products.map( ( product ) => (
<ProductCard
key={ product.id }
type="classic"
product={ {
title: product.title,
icon: product.icon,
vendorName: product.vendorName,
vendorUrl: product.vendorUrl,
price: product.price,
url: product.url,
description: product.description,
} }
/>
) ) }
</div>
);
}

View File

@ -0,0 +1,38 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__product-list-header {
display: flex;
justify-content: center;
gap: $medium-gap;
align-self: stretch;
}
&__product-list-title {
flex: 1 0 0;
font-size: 20px;
font-style: normal;
font-weight: 500;
margin-bottom: $medium-gap;
margin-top: $small-gap;
}
&__product-list-title--extensions {
margin-bottom: 0;
}
&__product-list-link {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
}
&__product-list-link a {
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
}
}

View File

@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { Link } from '@woocommerce/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import './product-list-header.scss';
interface ProductListHeaderProps {
title: string;
groupURL: string;
}
export default function ProductListHeader(
props: ProductListHeaderProps
): JSX.Element {
const { title, groupURL } = props;
return (
<div className="woocommerce-marketplace__product-list-header">
<h2 className="woocommerce-marketplace__product-list-title">
{ title }
</h2>
{ groupURL !== null && (
<span className="woocommerce-marketplace__product-list-link">
<Link href={ groupURL } target="_blank">
{ __( 'See more', 'woocommerce' ) }
</Link>
</span>
) }
</div>
);
}

View File

@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import ProductListContent from '../product-list-content/product-list-content';
import ProductListHeader from '../product-list-header/product-list-header';
import { Product } from './types';
interface ProductListProps {
title: string;
products: Product[];
groupURL: string;
}
export default function ProductList( props: ProductListProps ): JSX.Element {
const { title, products, groupURL } = props;
return (
<div className="woocommerce-marketplace__product-list">
<ProductListHeader title={ title } groupURL={ groupURL } />
<ProductListContent products={ products } />
</div>
);
}

View File

@ -0,0 +1,31 @@
export type SearchAPIProductType = {
title: string;
image: string;
excerpt: string;
link: string;
demo_url: string;
price: string;
raw_price: number;
hash: string;
slug: string;
id: number;
rating: number | null;
reviews_count: number | null;
vendor_name: string;
vendor_url: string;
icon: string;
};
export interface Product {
id?: number;
title: string;
description: string;
vendorName: string;
vendorUrl: string;
icon: string;
url: string;
price: number;
productType?: string;
averageRating?: number | null;
reviewsCount?: number | null;
}

View File

@ -0,0 +1,72 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__product-loader {
margin-top: $grid-unit-20;
}
&__product-loader-cards {
display: grid;
background: linear-gradient(to right, $gray-0 40%, $gray-5 60%, $gray-0 80%);
background-color: $gray-0;
background-size: 500% 200%;
animation: GradientSlide 4s linear infinite;
height: 270px;
}
&__product-loader-divider {
background: #fff;
width: 24px;
display: none;
}
.divider-1 {
grid-column-start: 2;
}
}
@media screen and (min-width: $breakpoint-medium) {
.woocommerce-marketplace {
&__product-loader-cards {
grid-template-columns: repeat(2, 1fr);
}
.divider-1 {
display: block;
}
}
}
@media screen and (min-width: $breakpoint-large) {
.woocommerce-marketplace {
&__product-loader-cards {
grid-template-columns: repeat(3, 1fr);
}
.divider-2 {
display: block;
}
}
}
@media screen and (min-width: $breakpoint-xlarge) {
.woocommerce-marketplace {
&__product-loader-cards {
grid-template-columns: repeat(4, 1fr);
}
.divider-3 {
display: block;
}
}
}
@keyframes GradientSlide {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import './product-loader.scss';
export default function ProductLoader(): JSX.Element {
return (
<div className="woocommerce-marketplace__product-loader">
<div className="woocommerce-marketplace__product-loader-cards">
<div className="woocommerce-marketplace__product-loader-divider divider-1"></div>
<div className="woocommerce-marketplace__product-loader-divider divider-2"></div>
<div className="woocommerce-marketplace__product-loader-divider divider-3"></div>
</div>
</div>
);
}

View File

@ -0,0 +1,29 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace__search {
grid-area: mktpl-search;
background: $gutenberg-gray-100;
border-radius: 2px;
display: flex;
height: 40px;
padding: 4px 8px 4px 12px;
input[type='search'] {
all: unset;
flex-grow: 1;
}
&:focus-within {
background: #fff;
border: 1.5px solid var(--wp-admin-theme-color, #3858e9);
}
@media (width <= $breakpoint-medium) {
margin: $content-spacing-small;
}
}
.woocommerce-marketplace__search-button {
all: unset;
cursor: pointer;
}

View File

@ -0,0 +1,89 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Icon, search } from '@wordpress/icons';
import { useEffect, useState } from '@wordpress/element';
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import './search.scss';
const searchPlaceholder = __( 'Search for extensions', 'woocommerce' );
/**
* Search component.
*
* @return {JSX.Element} Search component.
*/
function Search(): JSX.Element {
const [ searchTerm, setSearchTerm ] = useState( '' );
const query = useQuery();
useEffect( () => {
if ( query.term ) {
setSearchTerm( query.term );
}
}, [ query.term ] );
const runSearch = () => {
const term = searchTerm.trim();
// When the search term changes, we reset the category on purpose.
navigateTo( {
url: getNewPath( { term, category: null, tab: 'extensions' } ),
} );
return [];
};
const handleInputChange = (
event: React.ChangeEvent< HTMLInputElement >
) => {
setSearchTerm( event.target.value );
};
const handleKeyUp = ( event: { key: string } ) => {
if ( event.key === 'Enter' ) {
runSearch();
}
if ( event.key === 'Escape' ) {
setSearchTerm( '' );
}
};
return (
<div className="woocommerce-marketplace__search">
<label
className="screen-reader-text"
htmlFor="woocommerce-marketplace-search-query"
>
{ searchPlaceholder }
</label>
<input
id="woocommerce-marketplace-search-query"
value={ searchTerm }
className="woocommerce-marketplace__search-input"
type="search"
name="woocommerce-marketplace-search-query"
placeholder={ searchPlaceholder }
onChange={ handleInputChange }
onKeyUp={ handleKeyUp }
/>
<button
id="woocommerce-marketplace-search-button"
className="woocommerce-marketplace__search-button"
aria-label={ __( 'Search', 'woocommerce' ) }
onClick={ runSearch }
>
<Icon icon={ search } size={ 32 } />
</button>
</div>
);
}
export default Search;

View File

@ -0,0 +1,35 @@
@import '../../stylesheets/_variables.scss';
.woocommerce-marketplace {
&__tabs {
box-sizing: content-box;
display: flex;
gap: 24px;
}
&__tab-button {
border-bottom: 1.5px solid transparent;
border-radius: 0;
color: $mauve-light-12;
font-size: 13px;
font-style: normal;
font-weight: 600;
height: 48px;
line-height: 16px;
padding: 0;
&:focus:not(:disabled) {
box-shadow: none;
}
&.is-active {
border-color: var(--wp-admin-theme-color);
}
}
}
@media (width <= $breakpoint-medium) {
.woocommerce-marketplace__tabs {
border-bottom: 1px solid $gutenberg-gray-300;
}
}

View File

@ -0,0 +1,136 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element';
import { Button } from '@wordpress/components';
import classNames from 'classnames';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import './tabs.scss';
import { DEFAULT_TAB_KEY, MARKETPLACE_PATH } from '../constants';
export interface TabsProps {
selectedTab?: string | undefined;
setSelectedTab: ( value: string ) => void;
additionalClassNames?: Array< string > | undefined;
}
interface Tab {
name: string;
title: string;
href?: string;
}
interface Tabs {
[ key: string ]: Tab;
}
const tabs: Tabs = {
discover: {
name: 'discover',
title: __( 'Discover', 'woocommerce' ),
},
extensions: {
name: 'extensions',
title: __( 'Browse', 'woocommerce' ),
},
'my-subscriptions': {
name: 'my-subscriptions',
title: __( 'My Subscriptions', 'woocommerce' ),
href: getNewPath(
{
page: 'wc-addons',
section: 'helper',
},
''
),
},
};
const setUrlTabParam = ( tabKey: string ) => {
if ( tabKey === DEFAULT_TAB_KEY ) {
navigateTo( {
url: getNewPath( {}, MARKETPLACE_PATH, {} ),
} );
return;
}
navigateTo( {
url: getNewPath( { tab: tabKey } ),
} );
};
const renderTabs = ( props: TabsProps ) => {
const { selectedTab, setSelectedTab } = props;
const tabContent = [];
for ( const tabKey in tabs ) {
tabContent.push(
tabs[ tabKey ]?.href ? (
<a
className={ classNames(
'woocommerce-marketplace__tab-button',
'components-button',
`woocommerce-marketplace__tab-${ tabKey }`
) }
href={ tabs[ tabKey ]?.href }
key={ tabKey }
>
{ tabs[ tabKey ]?.title }
</a>
) : (
<Button
className={ classNames(
'woocommerce-marketplace__tab-button',
`woocommerce-marketplace__tab-${ tabKey }`,
{
'is-active': tabKey === selectedTab,
}
) }
onClick={ () => {
setSelectedTab( tabKey );
setUrlTabParam( tabKey );
} }
key={ tabKey }
>
{ tabs[ tabKey ]?.title }
</Button>
)
);
}
return tabContent;
};
const Tabs = ( props: TabsProps ): JSX.Element => {
const { setSelectedTab, additionalClassNames } = props;
interface Query {
path?: string;
tab?: string;
}
const query: Query = useQuery();
useEffect( () => {
if ( query?.tab && tabs[ query.tab ] ) {
setSelectedTab( query.tab );
} else {
setSelectedTab( DEFAULT_TAB_KEY );
}
}, [ query, setSelectedTab ] );
return (
<nav
className={ classNames(
'woocommerce-marketplace__tabs',
additionalClassNames || []
) }
>
{ renderTabs( props ) }
</nav>
);
};
export default Tabs;

View File

@ -0,0 +1,98 @@
/**
* External dependencies
*/
import { useQuery } from '@woocommerce/navigation';
import { useState, useEffect, createContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import {
Product,
SearchAPIProductType,
} from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants';
type ProductListContextType = {
productList: Product[];
isLoading: boolean;
};
export const ProductListContext = createContext< ProductListContextType >( {
productList: [],
isLoading: false,
} );
export function ProductListContextProvider( props: {
children: JSX.Element;
} ): JSX.Element {
const [ isLoading, setIsLoading ] = useState( false );
const [ productList, setProductList ] = useState< Product[] >( [] );
const contextValue = {
productList,
isLoading,
};
const query = useQuery();
useEffect( () => {
setIsLoading( true );
const params = new URLSearchParams();
if ( query.term ) {
params.append( 'term', query.term );
}
if ( query.category ) {
params.append( 'category', query.category );
}
const wccomSearchEndpoint =
MARKETPLACE_URL +
'/wp-json/wccom-extensions/1.0/search?' +
params.toString();
// Fetch data from WCCOM API
fetch( wccomSearchEndpoint )
.then( ( response ) => response.json() )
.then( ( response ) => {
/**
* Product card component expects a Product type.
* So we build that object from the API response.
*/
const products = response.products.map(
( product: SearchAPIProductType ): Product => {
return {
id: product.id,
title: product.title,
description: product.excerpt,
vendorName: product.vendor_name,
vendorUrl: product.vendor_url,
icon: product.icon,
url: product.link,
// Due to backwards compatibility, raw_price is from search API, price is from featured API
price: product.raw_price ?? product.price,
averageRating: product.rating ?? 0,
reviewsCount: product.reviews_count ?? 0,
};
}
);
setProductList( products );
} )
.catch( () => {
setProductList( [] );
} )
.finally( () => {
setIsLoading( false );
} );
}, [ query ] );
return (
<ProductListContext.Provider value={ contextValue }>
{ props.children }
</ProductListContext.Provider>
);
}

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import './marketplace.scss';
import { DEFAULT_TAB_KEY } from './components/constants';
import Header from './components/header/header';
import Content from './components/content/content';
import { ProductListContextProvider } from './contexts/product-list-context';
export default function Marketplace() {
const [ selectedTab, setSelectedTab ] = useState( DEFAULT_TAB_KEY );
return (
<ProductListContextProvider>
<div className="woocommerce-marketplace">
<Header
selectedTab={ selectedTab }
setSelectedTab={ setSelectedTab }
/>
<Content selectedTab={ selectedTab } />
</div>
</ProductListContextProvider>
);
}

View File

@ -0,0 +1,13 @@
@import './stylesheets/_variables.scss';
.woocommerce-admin-page__extensions {
background: #fff;
.woocommerce-layout__primary {
margin: 0;
}
.woocommerce-layout__main {
padding: 0;
}
}

View File

@ -0,0 +1,38 @@
@import '@wordpress/base-styles/_colors.native.scss';
// Spacings
// Taken from base style system
// @wordpress/base-styles/_variables.scss
$small-gap: $grid-unit-10; // 8px
$medium-gap: $grid-unit-20; // 16px
$large-gap: $grid-unit-30; // 24px
$xlarge-gap: $grid-unit-40; // 32px
// Layout
$content-spacing-small: $grid-unit-20;
$content-spacing-large: $grid-unit-40;
$content-spacing-small: $medium-gap;
$content-spacing-large: $xlarge-gap;
$content-spacing-xlarge: $grid-unit-60;
$content-max-width: 1600px;
// Breakpoints
$breakpoint-medium: 769px;
$breakpoint-large: 1024px;
$breakpoint-xlarge: 1500px;
$breakpoint-huge: 1920px;
// Header
$header-height-desktop: 96px;
$header-height-mobile: 129px;
// Colours
$gutenberg-gray-100: $gray-0; // replaced with closest colour from _colors.native.scss
$gutenberg-gray-300: $gray-300; // anything above gray-100 is from the default _colors.scss
$gutenberg-gray-700: $gray-700;
$gutenberg-gray-900: $gray-900;
$mauve-light-12: $gray-900;
$woo-purple-50: #7f54b3;
$wp-gray-0: $gray-0;
$wp-gray-50: $gray-50;
$wp-gray-60: $gray-60;

View File

@ -0,0 +1,79 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { Product } from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants';
import { CategoryAPIItem } from '../components/category-selector/types';
import { LOCALE } from '../../utils/admin-settings';
interface ProductGroup {
id: string;
title: string;
items: Product[];
url: string;
}
// Fetch data for the discover page from the WooCommerce.com API
async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
let url = '/wc/v3/marketplace/featured';
if ( LOCALE.userLocale ) {
url = `${ url }?locale=${ LOCALE.userLocale }`;
}
try {
return await apiFetch( { path: url.toString() } );
} catch ( error ) {
return [];
}
}
function fetchCategories(): Promise< CategoryAPIItem[] > {
let url = MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories';
if ( LOCALE.userLocale ) {
url = `${ url }?locale=${ LOCALE.userLocale }`;
}
return fetch( url.toString() )
.then( ( response ) => {
if ( ! response.ok ) {
throw new Error( response.statusText );
}
return response.json();
} )
.then( ( json ) => {
return json;
} )
.catch( () => {
return [];
} );
}
// Append UTM parameters to a URL, being aware of existing query parameters
const appendUTMParams = (
url: string,
utmParams: Array< [ string, string ] >
): string => {
const urlObject = new URL( url );
if ( ! urlObject ) {
return url;
}
utmParams.forEach( ( [ key, value ] ) => {
urlObject.searchParams.set( key, value );
} );
return urlObject.toString();
};
export {
fetchDiscoverPageData,
fetchCategories,
ProductGroup,
appendUTMParams,
};

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