Merge branch 'trunk' into fix/readme-php-version

This commit is contained in:
Ron Rennick 2023-08-22 16:12:36 -03:00
commit 10094d682d
253 changed files with 11289 additions and 1061 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

@ -17,7 +17,7 @@ permissions: {}
jobs:
test:
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }}
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:

View File

@ -15,7 +15,7 @@ permissions: {}
jobs:
test:
if: ${{ github.event.pull_request.user.login != 'github-actions[bot]' }}
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }}
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }} ${{ matrix.hpos && 'HPOS' || '' }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:

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.**
Available READMe files for the WooCommerce plugin:
* [`Root README`](../plugins/woocommerce/README.md)
* [`i18n/languages`](../plugins/woocommerce/i18n/languages/README.md)
* [`includes`](../plugins/woocommerce/includes/README.md)
* [`lib`](../plugins/woocommerce/lib/README.md)
* [`packages`](../plugins/woocommerce/packages/README.md)
* [`src`](../plugins/woocommerce/src/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/Blocks`](../plugins/woocommerce/src/Blocks/README.md)
* [`src/Internal`](../plugins/woocommerce/src/Internal/README.md)
* [`src/Internal/Admin/ProductForm`](../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md)
* [`tests`](../plugins/woocommerce/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-pw`](../plugins/woocommerce/tests/e2e-pw/README.md)
* [`tests/performance`](../plugins/woocommerce/tests/performance/README.md)
* [`tests/Tools/CodeHacking`](../plugins/woocommerce/tests/Tools/CodeHacking/README.md)
- [`Root README`](../plugins/woocommerce/README.md)
- [`i18n/languages`](../plugins/woocommerce/i18n/languages/README.md)
- [`includes`](../plugins/woocommerce/includes/README.md)
- [`lib`](../plugins/woocommerce/lib/README.md)
- [`packages`](../plugins/woocommerce/packages/README.md)
- [`src`](../plugins/woocommerce/src/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/Blocks`](../plugins/woocommerce/src/Blocks/README.md)
- [`src/Internal`](../plugins/woocommerce/src/Internal/README.md)
- [`src/Internal/Admin/ProductForm`](../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md)
- [`tests`](../plugins/woocommerce/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-pw`](../plugins/woocommerce/tests/e2e-pw/README.md)
- [`tests/performance`](../plugins/woocommerce/tests/performance/README.md)
- [`tests/Tools/CodeHacking`](../plugins/woocommerce/tests/Tools/CodeHacking/README.md)
Available READMe files for the WooCommerce Admin plugin:
* [`Root README`](../plugins/woocommerce-admin/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-header`](../plugins/woocommerce-admin/client/activity-panel/activity-header/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/dashboard`](../plugins/woocommerce-admin/client/dashboard/README.md)
* [`client/header`](../plugins/woocommerce-admin/client/header/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/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/utils`](../plugins/woocommerce-admin/client/utils/README.md)
* [`client/wp-admin-scripts`](../plugins/woocommerce-admin/client/wp-admin-scripts/README.md)
* [`docs`](../plugins/woocommerce-admin/docs/README.md)
* [`docs/examples`](../plugins/woocommerce-admin/docs/examples/README.md)
* [`docs/examples/extensions`](../plugins/woocommerce-admin/docs/examples/extensions/README.md)
* [`docs/features`](../plugins/woocommerce-admin/docs/features/README.md)
* [`docs/woocommerce.com`](../plugins/woocommerce-admin/docs/woocommerce.com/README.md)
- [`Root README`](../plugins/woocommerce-admin/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-header`](../plugins/woocommerce-admin/client/activity-panel/activity-header/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/dashboard`](../plugins/woocommerce-admin/client/dashboard/README.md)
- [`client/header`](../plugins/woocommerce-admin/client/header/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/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/utils`](../plugins/woocommerce-admin/client/utils/README.md)
- [`client/wp-admin-scripts`](../plugins/woocommerce-admin/client/wp-admin-scripts/README.md)
- [`docs`](../plugins/woocommerce-admin/docs/README.md)
- [`docs/examples`](../plugins/woocommerce-admin/docs/examples/README.md)
- [`docs/examples/extensions`](../plugins/woocommerce-admin/docs/examples/extensions/README.md)
- [`docs/features`](../plugins/woocommerce-admin/docs/features/README.md)
- [`docs/woocommerce.com`](../plugins/woocommerce-admin/docs/woocommerce.com/README.md)
Available READMe files for the WooCommerce Beta Tested plugin:
* [`Root README`](../plugins/woocommerce-beta-tester/README.md)
* [`src/tools`](../plugins/woocommerce-beta-tester/src/tools/README.md)
* [`userscripts`](../plugins/woocommerce-beta-tester/userscripts/README.md)
- [`Root README`](../plugins/woocommerce-beta-tester/README.md)
- [`src/tools`](../plugins/woocommerce-beta-tester/src/tools/README.md)
- [`userscripts`](../plugins/woocommerce-beta-tester/userscripts/README.md)

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.

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.

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`.

View File

@ -0,0 +1,191 @@
Like many WordPress plugins, WooCommerce provides a range of actions and filters through which developers can extend and modify the platform.
Often, when writing new code or revising existing code, there is a desire to add new hooks—but this should always be done with thoughtfulness and care. This document aims to provide high-level guidance on the matter.
Practices we generally allow, support and encourage include:
* [Using existing hooks (or other alternatives) in preference to adding new hooks](#prefer-existing-hooks-or-other-alternatives)
* [Adding lifecycle hooks](#adding-lifecycle-hooks)
* [Optional escape hooks](#escape-hooks)
* [Modifying the inputs and outputs of global rendering functions](#modifying-function-input-and-output-global-rendering-functions)
* [Preferring the passing of objects over IDs](#prefer-passing-objects-over-ids)
On the flip side, there are several practices we discourage:
* [Tying lifecycle hooks to methods of execution](#tying-lifecycle-hooks-to-methods-of-execution)
* [Using filters as feature flags](#using-filters-as-feature-flags)
* [Placing filter hooks inside templates and data stores](#placement-of-filter-hooks)
* [Enumeration values within hook names](#enumeration-values-inside-hook-names)
Beyond those items, we generally otherwise adhere to WordPress coding standards. In regards to hooks, that specifically means following the:
* [Documentation standards for hooks](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/php/#4-hooks-actions-and-filters)
* [Guidance on Dynamic hook names](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#interpolation-for-naming-dynamic-hooks)
Please note that we provide example code throughout this guide to help illustrate some of the principles. However, to keep things concise, we usually omit unnecessary detail, including doc blocks (in practice, though, hooks should always be accompanied by doc blocks!).
### Prefer existing hooks (or other alternatives)
Hooks come with a long-term obligation: the last thing we want is to add a new hook that developers come to depend on, only to strip it away again. However, this can lead to difficulties when the time comes to refactor a piece of code that contains hooks, sometimes delaying meaningful change or limiting how easily we can implement a change without compromising on backward compatibility commitments.
For those reasons, we always prefer that—wherever reasonable—an existing hook or alternative approach in preference to adding a new hook.
### Adding lifecycle hooks
Lifecycle hooks can be used to communicate that a lifecycle event is about to start, or that it has concluded. Examples of such events include:
* Main product loop
* Dispatching emails
* Rendering a template
* Product or order status changes
In general, lifecycle hooks:
* Come in pairs (before and after)
* Are always actions, never filters
* The before hook will generally always provide callbacks with the arguments array, if there is one
* The after hook will generally also provide callbacks with the functions return value, if there is one
Note that lifecycle hooks primarily exist to let other systems observe, rather than to modify the result. Of course, this does not stop the function author from additionally providing a filter hook that serves this function.
For example, noting that it is the process of fetching the promotions which we view as the “lifecycle event”, and not the function itself:
```php
function woocommerce_get_current_promotions( ...$args ) {
/* Any initial prep, then first lifecycle hook... */
do_action( 'woocommerce_before_get_current_promotions', $args );
/* ...Do actual work, then final lifecycle hook... */
do_action( 'woocommerce_after_get_current_promotions', $result, $args );
/* ...Return the result, optionally via a filter... */
return apply_filters( 'woocommerce_get_current_promotions', $result, $args );
}
```
### Escape hooks
In some cases, it may be appropriate to support short-circuiting of functions or methods. This is what we call an escape hook, and can be useful as a means of overriding code when a better way of doing so is not available.
* Escape hooks are always filters
* They should always supply null as the initial filterable value
* If the value is changed to a non-null value, then the function should exit early by returning that new value
For type safety, care should be taken to ensure that, if a function is short-circuited, the return type matches the function signature and/or return type stated in the function doc block.
Example:
```php
function get_product_metrics( $args ): array {
$pre = apply_filters( 'pre_woocommerce_get_product_metrics', null, $args );
if ( $pre !== null ) {
return (array) $pre;
}
/* ...Default logic... */
return $metrics;
}
```
### Modifying function input and output (global rendering functions)
In the case of global rendering or formatting functions (so-called “template tags”), where it is not readily possible to implement better alternatives, it is permissible to add filters for both the function arguments and the functions return value.
This should be done sparingly, and only where necessary. Remember that while providing opportunities for other components to perform extensive customization, it can potentially derail other components which expect unmodified output.
Example:
```php
function woocommerce_format_sale_price( ...$args ): string {
/* Prep to fill in any missing $args values... */
$args = (array) apply_filters( 'woocommerce_format_sale_price_args', $args );
/* ...Actual work to determine the $price string... */
return (string) apply_filters( 'woocommerce_format_sale_price', $price, $args );
}
```
### Prefer passing objects over IDs
Some actions or filters provide an object ID (such as a product ID) as their primary value, while others will provide the actual object itself (such as a product object). For consistency, it is preferred that objects be passed.
Example:
```php
function get_featured_product_for_current_customer( ) {
/* ...Logic to find the featured product for this customer… */
return apply_filters(
'woocommerce_featured_product_for_current_customer',
$product, /* WC_Product */
$customer
);
}
```
### Tying lifecycle hooks to methods of execution
There can sometimes be multiple paths leading to the same action. For instance, an order can be updated via the REST API, through the admin environment, or on the front end. It may additionally happen via ajax, or via a regular request.
It is important however not to tie hooks for high-level processes to specific execution paths. For example, an action that fires when an order is created must not only be fired when this happens in the admin environment via an ajax request.
Instead, prefer a more generic hook that passes context about the method of execution to the callback.
Example of what we wish to avoid:
```php
/**
* Pretend this function is only called following an ajax request
* (perhaps it is itself hooked in using a `wp_ajax_*` action).
*/
function on_ajax_order_creation() {
/* Avoid this! */
do_action( 'woocommerce_on_order_creation' );
}
```
### Using filters as feature flags
It is sometimes tempting to use a filter as a sort of feature flag, that enables or disables a piece of functionality. This should be avoided! Prefer using an option:
* Options persist in the database.
* Options are already filterable (ideal for a temporary override).
Example of what we wish to avoid:
```php
/* Avoid this */
$super_products_enabled = (bool) apply_filters( 'woocommerce_super_products_are_enabled', true );
/* Prefer this */
$super_products_enabled = get_option( 'woocommerce_super_products_are_enabled', 'no' ) === 'yes';
```
### Placement of filter hooks
Filters should not be placed inside templates—only actions. If it is important that a value used within a template be filterable, then the relevant logic should be moved to whichever function or method decides to load the template—the result being passed in as a template variable.
It is also preferred that filter hooks not be placed inside data-store classes, as this can reduce the integrity of those components: since, by design, they are replaceable by custom implementations—the risk of accidentally breaking those custom stores is higher.
### Enumeration values inside hook names
Though there is a case for dynamic hook names (where part of the hook name is created using a variable), a good rule of thumb is to avoid this if the variable contains what might be considered an enumeration value.
This might for instance include a case where an error code forms part of the hook name.
Example (of what we wish to avoid):
```php
if ( is_wp_error( $result ) ) {
/* Avoid this */
$error_code = $result->get_error_code();
do_action( "woocommerce_foo_bar_{$error_code}_problem", $intermediate_result );
/* Prefer this */
do_action( 'woocommerce_foo_bar_problem', $result );
}
```
The primary reason for avoiding this is that the more values there are in the enumeration set, the more filters developers have to include in their code.
### Summary
This document is a high-level guide to the inclusion and placement of hooks, not an exhaustive list. There will occasionally be exceptions, and there may be good rules and methodologies we are missing: if you have suggestions or ideas for improvement, please reach out!

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 Tooltip to each list item when need it

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Allow users to select multiple items from the media library while adding images #39741

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update ImageGallery block toolbar, moving some options to an ellipsis dropdown menu.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Categories dropdown display error #39810

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { Tooltip } from '@wordpress/components';
import { createElement, CSSProperties, ReactElement } from 'react';
/**
@ -15,6 +16,7 @@ export type MenuItemProps< ItemType > = {
children: ReactElement | string;
getItemProps: getItemPropsType< ItemType >;
activeStyle?: CSSProperties;
tooltipText?: string;
};
export const MenuItem = < ItemType, >( {
@ -24,7 +26,9 @@ export const MenuItem = < ItemType, >( {
isActive,
activeStyle = { backgroundColor: '#bde4ff' },
item,
tooltipText,
}: MenuItemProps< ItemType > ) => {
function renderListItem() {
return (
<li
style={ isActive ? activeStyle : {} }
@ -34,4 +38,15 @@ export const MenuItem = < ItemType, >( {
{ children }
</li>
);
}
if ( tooltipText ) {
return (
<Tooltip text={ tooltipText } position="top center">
{ renderListItem() }
</Tooltip>
);
}
return renderListItem();
};

View File

@ -104,6 +104,7 @@ export const SelectTreeMenu = ( {
}
) }
position={ position }
flip={ false }
animate={ false }
onFocusOutside={ ( event ) => {
if ( isEventOutside( event ) ) {

View File

@ -9,7 +9,7 @@
}
}
&:not(.is-toolbar-visible){
&:not(.is-toolbar-visible) {
img:hover {
border: 1.5px solid #007cba;
}
@ -37,12 +37,10 @@
.components-toolbar-group {
flex-wrap: inherit;
border-right: 1px solid #ccc;
svg {
width: 24px;
margin-top: $gap-smallest;
}
}
}

View File

@ -0,0 +1,6 @@
// Hack to hide when the media modal is open
// Otherwise in Firefox the popover remains visible on top of the modal
// Changing the z-index of Popovers have wider implications.
.modal-open .woocommerce-image-gallery__toolbar-dropdown-popover {
display: none;
}

View File

@ -0,0 +1,101 @@
/**
* External dependencies
*/
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
import { moreVertical } from '@wordpress/icons';
import {
Children,
cloneElement,
createElement,
Fragment,
isValidElement,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { MediaItem, MediaUpload } from '@wordpress/media-utils';
/**
* Internal dependencies
*/
import { MediaUploadComponentType } from './types';
const POPOVER_PROPS = {
className: 'woocommerce-image-gallery__toolbar-dropdown-popover',
placement: 'bottom-start',
};
type ImageGalleryToolbarDropdownProps = {
onReplace: ( media: { id: number } & MediaItem ) => void;
onRemove: () => void;
canRemove?: boolean;
removeBlockLabel?: string;
MediaUploadComponent: MediaUploadComponentType;
};
export function ImageGalleryToolbarDropdown( {
children,
onReplace,
onRemove,
canRemove,
removeBlockLabel,
MediaUploadComponent = MediaUpload,
...props
}: React.PropsWithChildren< ImageGalleryToolbarDropdownProps > ) {
return (
<DropdownMenu
icon={ moreVertical }
label={ __( 'Options', 'woocommerce' ) }
className="woocommerce-image-gallery__toolbar-dropdown"
popoverProps={ POPOVER_PROPS }
{ ...props }
>
{ ( { onClose } ) => (
<>
<MenuGroup>
<MediaUploadComponent
onSelect={ ( media ) => {
onReplace( media as MediaItem );
onClose();
} }
allowedTypes={ [ 'image' ] }
render={ ( { open } ) => (
<MenuItem
onClick={ () => {
open();
} }
>
{ __( 'Replace', 'woocommerce' ) }
</MenuItem>
) }
/>
</MenuGroup>
{ typeof children === 'function'
? children( { onClose } )
: Children.map(
children,
( child ) =>
isValidElement< { onClose: () => void } >(
child
) &&
cloneElement< { onClose: () => void } >(
child,
{ onClose }
)
) }
{ canRemove && (
<MenuGroup>
<MenuItem
onClick={ () => {
onClose();
onRemove();
} }
>
{ removeBlockLabel ||
__( 'Remove', 'woocommerce' ) }
</MenuItem>
</MenuGroup>
) }
</>
) }
</DropdownMenu>
);
}

View File

@ -1,4 +1,5 @@
.woocommerce-image-gallery__toolbar {
width: max-content;
position: absolute;
top: -58px;
left: 50%;

View File

@ -2,17 +2,25 @@
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { Toolbar, ToolbarButton, ToolbarGroup } from '@wordpress/components';
import { chevronRight, chevronLeft, trash } from '@wordpress/icons';
import { MediaItem, MediaUpload } from '@wordpress/media-utils';
import { __ } from '@wordpress/i18n';
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
ToolbarItem,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { CoverImageIcon } from './icons';
import { SortableHandle } from '../sortable';
import { MediaUploadComponentType } from './types';
import { ImageGalleryToolbarDropdown } from './image-gallery-toolbar-dropdown';
export type ImageGalleryToolbarProps = {
childIndex: number;
@ -88,11 +96,13 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
<ToolbarGroup>
<ToolbarButton
onClick={ () => setAsCoverImage( childIndex ) }
icon={ CoverImageIcon }
label={ __( 'Set as cover', 'woocommerce' ) }
/>
>
{ __( 'Set as cover', 'woocommerce' ) }
</ToolbarButton>
</ToolbarGroup>
) }
{ isCoverItem && (
<ToolbarGroup className="woocommerce-image-gallery__toolbar-media">
<MediaUploadComponent
onSelect={ ( media ) =>
@ -106,6 +116,8 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
) }
/>
</ToolbarGroup>
) }
{ isCoverItem && (
<ToolbarGroup>
<ToolbarButton
onClick={ () => removeItem( childIndex ) }
@ -113,6 +125,31 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
label={ __( 'Remove', 'woocommerce' ) }
/>
</ToolbarGroup>
) }
{ ! isCoverItem && (
<ToolbarGroup>
<ToolbarItem>
{ ( toggleProps: {
'data-toolbar-item': boolean;
ref: React.ForwardedRef<
typeof ImageGalleryToolbarDropdown
>;
} ) => (
<ImageGalleryToolbarDropdown
canRemove={ true }
onRemove={ () => removeItem( childIndex ) }
onReplace={ ( media ) =>
replaceItem( childIndex, media )
}
MediaUploadComponent={
MediaUploadComponent
}
{ ...toggleProps }
/>
) }
</ToolbarItem>
</ToolbarGroup>
) }
</Toolbar>
</div>
);

View File

@ -129,6 +129,20 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
event.relatedTarget as Element
).closest(
'.media-modal, .components-modal__frame'
) ) ||
( event.relatedTarget &&
// Check if not a button within the toolbar is clicked, to prevent hiding the toolbar.
(
event.relatedTarget as Element
).closest(
'.woocommerce-image-gallery__toolbar'
) ) ||
( event.relatedTarget &&
// Prevent toolbar from hiding if the dropdown is clicked within the toolbar.
(
event.relatedTarget as Element
).closest(
'.woocommerce-image-gallery__toolbar-dropdown-popover'
) )
) {
return;

View File

@ -1,3 +1,4 @@
@import 'image-gallery.scss';
@import 'image-gallery-item.scss';
@import 'image-gallery-toolbar.scss';
@import "image-gallery.scss";
@import "image-gallery-item.scss";
@import "image-gallery-toolbar.scss";
@import "image-gallery-toolbar-dropdown.scss";

View File

@ -24,7 +24,7 @@ type MediaUploaderProps = {
MediaUploadComponent?: < T extends boolean = false >(
props: MediaUpload.Props< T >
) => JSX.Element;
multipleSelect?: boolean;
multipleSelect?: boolean | string;
onSelect?: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: ( { id: number } & { [ k: string ]: any } ) | MediaItem[]
@ -96,6 +96,7 @@ export const MediaUploader = ( {
<MediaUploadComponent
onSelect={ onSelect }
allowedTypes={ allowedMediaTypes }
// @ts-expect-error - TODO multiple also accepts string.
multiple={ multipleSelect }
render={ ( { open } ) => (
<Button

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Bump required PHP version to 7.4

View File

@ -10,7 +10,7 @@
* Version: {{version}}
* Requires at least: 6.2
* WC requires at least: 7.8
* Requires PHP: 7.3
* Requires PHP: 7.4
{{#author}}
* Author: {{author}}
{{/author}}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Refactored core profiler loader to be more generalizable and moved to @woocommerce/onboarding

View File

@ -36,13 +36,17 @@
"@wordpress/components": "wp-6.0",
"@wordpress/element": "wp-6.0",
"@wordpress/i18n": "wp-6.0",
"classnames": "^2.3.1",
"gridicons": "^3.4.0",
"react": "^17.0.2",
"string-similarity": "4.0.4"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@storybook/addon-knobs": "^7.0.2",
"@testing-library/react": "^12.1.3",
"@types/jest": "^27.4.1",
"@types/react": "^17.0.2",
"@types/string-similarity": "4.0.0",
"@types/wordpress__components": "^19.10.3",
"@types/wordpress__data": "^6.0.0",

View File

@ -0,0 +1,62 @@
.woocommerce-onboarding-progress-bar__container {
height: 8px;
width: 100%;
}
// Min width equal to height. This means small values look like each other, but all bars have a consistent radius.
.woocommerce-onboarding-progress-bar__filler {
height: 100%;
min-width: 8px;
}
// Loader page
.woocommerce-onboarding-loader {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: white;
@include breakpoint( '<782px' ) {
padding: 0 20px;
}
.woocommerce-onboarding-loader__title {
padding: 0;
margin: 58px 0 0 0;
font-size: 28px;
font-weight: 500;
@include breakpoint( '<782px' ) {
font-size: 20px;
}
}
.woocommerce-onboarding-loader-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 520px;
}
.woocommerce-onboarding-progress-bar {
width: 520px;
margin: 16px 0 16px 0;
@include breakpoint( '<782px' ) {
width: 100%;
}
}
.woocommerce-onboarding-progress-bar__container {
height: 4px;
}
.woocommerce-onboarding-loader__paragraph {
font-size: 16px;
line-height: 24px;
color: #2f2f2f;
opacity: 0.8;
text-align: center;
@include breakpoint( '<782px' ) {
font-size: 14px;
line-height: 20px;
}
}
}

View File

@ -0,0 +1,132 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import {
useState,
useEffect,
Children,
createElement,
Fragment,
} from '@wordpress/element';
import type { ReactNode } from 'react';
/**
* Internal dependencies
*/
import ProgressBar from './ProgressBar';
export const Loader = ( {
children,
className,
}: {
children: ReactNode;
className?: string;
} ) => {
return (
<div
className={ classNames(
'woocommerce-onboarding-loader',
className
) }
>
{ children }
</div>
);
};
type withClassName = {
className?: string;
};
type withReactChildren = {
children: ReactNode;
};
Loader.Layout = ( {
children,
className,
}: withClassName & withReactChildren ) => {
return (
<div
className={ classNames(
'woocommerce-onboarding-loader-wrapper',
className
) }
>
{ children }
</div>
);
};
Loader.Illustration = ( { children }: withReactChildren ) => {
return <>{ children }</>;
};
Loader.Title = ( {
children,
className,
}: withClassName & withReactChildren ) => {
return (
<h1
className={ classNames(
'woocommerce-onboarding-loader__title',
className
) }
>
{ children }
</h1>
);
};
Loader.ProgressBar = ( {
progress,
className,
}: { progress: number } & withClassName ) => {
return (
<ProgressBar
className={ classNames( 'progress-bar', className ) }
percent={ progress ?? 0 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ '#E0E0E0' }
/>
);
};
Loader.Subtext = ( {
children,
className,
}: withReactChildren & withClassName ) => {
return (
<p
className={ classNames(
'woocommerce-onboarding-loader__paragraph',
className
) }
>
{ children }
</p>
);
};
const LoaderSequence = ( {
interval,
children,
}: { interval: number } & withReactChildren ) => {
const [ index, setIndex ] = useState( 0 );
useEffect( () => {
const rotateInterval = setInterval( () => {
setIndex(
( prevIndex ) => ( prevIndex + 1 ) % Children.count( children )
);
}, interval );
return () => clearInterval( rotateInterval );
}, [ interval, children ] );
const childToDisplay = Children.toArray( children )[ index ];
return <>{ childToDisplay }</>;
};
Loader.Sequence = LoaderSequence; // eslint rule-of-hooks can't handle the compound component definition directly

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { createElement, HTMLAttributes } from 'react';
type ProgressBarProps = {
/** Component classname */
className?: string;
/** Progress percentage (0 to 100) */
percent?: number;
/** Color of the progress bar */
color?: string;
/** Background color of the progress container */
bgcolor?: string;
};
const ProgressBar = ( {
className = '',
percent = 0,
color = '#674399',
bgcolor = 'var(--wp-admin-theme-color)',
}: ProgressBarProps ) => {
const containerStyles = {
backgroundColor: bgcolor,
};
const fillerStyles: HTMLAttributes< HTMLDivElement >[ 'style' ] = {
backgroundColor: color,
width: `${ percent }%`,
display: percent === 0 ? 'none' : 'inherit',
};
return (
<div className={ `woocommerce-onboarding-progress-bar ${ className }` }>
<div
className="woocommerce-onboarding-progress-bar__container"
style={ containerStyles }
>
<div
className="woocommerce-onboarding-progress-bar__filler"
style={ fillerStyles }
/>
</div>
</div>
);
};
export default ProgressBar;

View File

@ -0,0 +1 @@
export { Loader } from './Loader';

View File

@ -0,0 +1,78 @@
/**
* External dependencies
*/
import React, { createElement } from 'react';
/**
* Internal dependencies
*/
import { Loader } from '../';
/** Simple straightforward example of how to use the <Loader> compound component */
export const ExampleSimpleLoader = () => (
<Loader>
<Loader.Layout>
<Loader.Illustration>
<img
src="https://placekitten.com/200/200"
alt="a cute kitteh"
/>
</Loader.Illustration>
<Loader.Title>Very Impressive Title</Loader.Title>
<Loader.ProgressBar progress={ 30 } />
<Loader.Sequence interval={ 1000 }>
<Loader.Subtext>Message 1</Loader.Subtext>
<Loader.Subtext>Message 2</Loader.Subtext>
<Loader.Subtext>Message 3</Loader.Subtext>
</Loader.Sequence>
</Loader.Layout>
</Loader>
);
/** <Loader> component story with controls */
const Template = ( { progress, title, messages } ) => (
<Loader>
<Loader.Layout>
<Loader.Illustration>
<img
src="https://placekitten.com/200/200"
alt="a cute kitteh"
/>
</Loader.Illustration>
<Loader.Title>{ title }</Loader.Title>
<Loader.ProgressBar progress={ progress } />
<Loader.Sequence interval={ 1000 }>
{ messages.map( ( message, index ) => (
<Loader.Subtext key={ index }>{ message }</Loader.Subtext>
) ) }
</Loader.Sequence>
</Loader.Layout>
</Loader>
);
export const ExampleLoaderWithControls = Template.bind( {} );
ExampleLoaderWithControls.args = {
title: 'Very Impressive Title',
progress: 30,
messages: [ 'Message 1', 'Message 2', 'Message 3' ],
};
export default {
title: 'WooCommerce Admin/Onboarding/Loader',
component: ExampleLoaderWithControls,
argTypes: {
title: {
control: 'text',
},
progress: {
control: {
type: 'range',
min: 0,
max: 100,
},
},
messages: {
control: 'object',
},
},
};

View File

@ -16,3 +16,4 @@ export { WooOnboardingTaskListItem } from './components/WooOnboardingTaskListIte
export { WooOnboardingTaskListHeader } from './components/WooOnboardingTaskListHeader';
export { WooOnboardingTask } from './components/WooOnboardingTask';
export * from './utils/countries';
export { Loader } from './components/Loader';

View File

@ -2,3 +2,4 @@
@import 'components/WCPayBanner/WCPayBanner.scss';
@import 'components/WCPayBenefits/WCPayBenefits.scss';
@import 'components/RecommendedRibbon/RecommendedRibbon.scss';
@import 'components/Loader/Loader.scss';

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Disable attributes used in different sections

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Allow users to select multiple items from the media library while adding images #39741

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
[Product Block Editor] Disable tabs in parent product page with variations #39459

View File

@ -84,7 +84,7 @@ export function Edit() {
</div>
) : (
<MediaUploader
multipleSelect={ true }
multipleSelect={ 'add' }
onError={ () => null }
onFileUploadChange={ onFileUpload }
onMediaGalleryOpen={ () => {

View File

@ -25,3 +25,4 @@ export { init as initVariations } from './variations';
export { init as initRequirePassword } from './password';
export { init as initVariationItems } from './variation-items';
export { init as initVariationOptions } from './variation-options';
export { init as initNotice } from './notice';

View File

@ -0,0 +1,38 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-has-variations-notice",
"title": "Notice",
"category": "woocommerce",
"description": "Notice description",
"keywords": [ "products", "notice" ],
"textdomain": "default",
"attributes": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"content": {
"type": "string"
},
"buttonText": {
"type": "string"
},
"type": {
"type": "string"
}
},
"supports": {
"align": false,
"html": false,
"multiple": true,
"reusable": false,
"inserter": false,
"lock": false,
"__experimentalToolbar": false
},
"editorStyle": "file:./editor.css",
"templateLock": "contentOnly"
}

View File

@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { createElement } from '@wordpress/element';
import type { BlockAttributes, BlockEditProps } from '@wordpress/blocks';
import { Button } from '@wordpress/components';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
import { Product } from '@woocommerce/data';
import { useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { Notice } from '../../components/notice';
import { hasAttributesUsedForVariations } from '../../utils';
export interface NoticeBlockAttributes extends BlockAttributes {
buttonText: string;
content: string;
title: string;
type: 'error-type' | 'success' | 'warning' | 'info';
}
export function Edit( {
attributes,
}: BlockEditProps< NoticeBlockAttributes > ) {
const blockProps = useBlockProps();
const { buttonText, content, title, type = 'info' } = attributes;
const [ productAttributes ] = useEntityProp< Product[ 'attributes' ] >(
'postType',
'product',
'attributes'
);
const isOptionsNoticeVisible =
hasAttributesUsedForVariations( productAttributes );
return (
<div { ...blockProps }>
{ isOptionsNoticeVisible && (
<Notice content={ content } title={ title } type={ type }>
<Button
isSecondary={ true }
onClick={ () =>
navigateTo( {
url: getNewPath( { tab: 'variations' } ),
} )
}
>
{ buttonText }
</Button>
</Notice>
) }
</div>
);
}

View File

@ -0,0 +1,7 @@
.woocommerce-product-notice {
margin-top: $gap + $gap-large * 2;
button {
pointer-events: all;
cursor: pointer;
}
}

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json';
import { Edit, NoticeBlockAttributes } from './edit';
const { name, ...metadata } =
blockConfiguration as BlockConfiguration< NoticeBlockAttributes >;
export { metadata, name };
export const settings: Partial< BlockConfiguration< NoticeBlockAttributes > > =
{
example: {},
edit: Edit,
};
export function init() {
initBlock( { name, metadata, settings } );
}

View File

@ -5,6 +5,7 @@
@import 'inventory-email/editor.scss';
@import 'inventory-sku/editor.scss';
@import 'name/editor.scss';
@import 'notice/editor.scss';
@import 'pricing/editor.scss';
@import 'regular-price/editor.scss';
@import 'sale-price/editor.scss';

View File

@ -139,6 +139,9 @@ export function Edit() {
product_block_variable_options_notice_dismissed: 'yes',
} )
}
disabledAttributeIds={ entityAttributes
.filter( ( attr ) => ! attr.variation )
.map( ( attr ) => attr.id ) }
uiStrings={ {
notice,
globalAttributeHelperMessage: '',

View File

@ -35,12 +35,7 @@ import {
} from '../../hooks/use-product-attributes';
import { getAttributeId } from '../../components/attribute-control/utils';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
function hasAttributesUsedForVariations(
productAttributes: Product[ 'attributes' ]
) {
return productAttributes.some( ( { variation } ) => variation );
}
import { hasAttributesUsedForVariations } from '../../utils';
function getFirstOptionFromEachAttribute(
attributes: Product[ 'attributes' ]
@ -163,6 +158,9 @@ export function Edit( {
selectedAttributeIds={ variationOptions.map(
( attr ) => attr.id
) }
disabledAttributeIds={ productAttributes
.filter( ( attr ) => ! attr.variation )
.map( ( attr ) => attr.id ) }
/>
) }
</div>

View File

@ -48,6 +48,7 @@ type AttributeControlProps = {
onNoticeDismiss?: () => void;
createNewAttributesAsGlobal?: boolean;
useRemoveConfirmationModal?: boolean;
disabledAttributeIds?: number[];
uiStrings?: {
notice?: string | React.ReactElement;
emptyStateSubtitle?: string;
@ -59,7 +60,8 @@ type AttributeControlProps = {
attributeRemoveLabel?: string;
attributeRemoveConfirmationMessage?: string;
attributeRemoveConfirmationModalMessage?: string;
globalAttributeHelperMessage: string;
globalAttributeHelperMessage?: string;
disabledAttributeMessage?: string;
};
};
@ -80,6 +82,7 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
uiStrings,
createNewAttributesAsGlobal = false,
useRemoveConfirmationModal = false,
disabledAttributeIds = [],
} ) => {
uiStrings = {
newAttributeListItemLabel: __( 'Add new', 'woocommerce' ),
@ -279,6 +282,10 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
onAdd={ handleAdd }
selectedAttributeIds={ value.map( ( attr ) => attr.id ) }
createNewAttributesAsGlobal={ createNewAttributesAsGlobal }
disabledAttributeIds={ disabledAttributeIds }
disabledAttributeMessage={
uiStrings.disabledAttributeMessage
}
/>
) }
<SelectControlMenuSlot />
@ -292,7 +299,9 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
customAttributeHelperMessage={
uiStrings.customAttributeHelperMessage
}
globalAttributeHelperMessage={ createInterpolateElement(
globalAttributeHelperMessage={
uiStrings.globalAttributeHelperMessage
? createInterpolateElement(
uiStrings.globalAttributeHelperMessage,
{
link: (
@ -307,7 +316,9 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
</Link>
),
}
) }
)
: undefined
}
onCancel={ () => {
closeEditModal( currentAttribute );
onEditModalCancel( currentAttribute );

View File

@ -55,6 +55,8 @@ type NewAttributeModalProps = {
onAdd: ( newCategories: EnhancedProductAttribute[] ) => void;
selectedAttributeIds?: number[];
createNewAttributesAsGlobal?: boolean;
disabledAttributeIds?: number[];
disabledAttributeMessage?: string;
};
type AttributeForm = {
@ -88,6 +90,11 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
onAdd,
selectedAttributeIds = [],
createNewAttributesAsGlobal = false,
disabledAttributeIds = [],
disabledAttributeMessage = __(
'Already used in Attributes',
'woocommerce'
),
} ) => {
const scrollAttributeIntoView = ( index: number ) => {
setTimeout( () => {
@ -317,6 +324,12 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
createNewAttributesAsGlobal={
createNewAttributesAsGlobal
}
disabledAttributeIds={
disabledAttributeIds
}
disabledAttributeMessage={
disabledAttributeMessage
}
/>
</td>
<td className="woocommerce-new-attribute-modal__table-attribute-value-column">

View File

@ -8,3 +8,13 @@
margin-right: $gap-small;
}
}
.woocommerce-experimental-select-control__popover-menu-container {
.woocommerce-experimental-select-control__menu-item[disabled] {
pointer-events: none;
color: $gray-600;
}
.disabled-element-wrapper {
cursor: not-allowed;
}
}

View File

@ -5,7 +5,7 @@ import { sprintf, __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { Spinner, Icon } from '@wordpress/components';
import { plus } from '@wordpress/icons';
import { createElement } from '@wordpress/element';
import { createElement, useMemo } from '@wordpress/element';
import {
EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME,
QueryProductAttribute,
@ -27,7 +27,9 @@ import {
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
import { TRACKS_SOURCE } from '../../constants';
type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >;
type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' > & {
isDisabled?: boolean;
};
type AttributeInputFieldProps = {
value?: EnhancedProductAttribute | null;
@ -39,6 +41,8 @@ type AttributeInputFieldProps = {
label?: string;
placeholder?: string;
disabled?: boolean;
disabledAttributeIds?: number[];
disabledAttributeMessage?: string;
ignoredAttributeIds?: number[];
createNewAttributesAsGlobal?: boolean;
};
@ -53,6 +57,8 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
placeholder,
label,
disabled,
disabledAttributeIds = [],
disabledAttributeMessage,
ignoredAttributeIds = [],
createNewAttributesAsGlobal = false,
} ) => {
@ -72,6 +78,18 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
};
} );
const markedAttributes = useMemo(
function setDisabledAttribute() {
return (
attributes?.map( ( attribute ) => ( {
...attribute,
isDisabled: disabledAttributeIds.includes( attribute.id ),
} ) ) ?? []
);
},
[ attributes, disabledAttributeIds ]
);
const getFilteredItems = (
allItems: NarrowedQueryAttribute[],
inputValue: string
@ -139,7 +157,7 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
return (
<SelectControl< NarrowedQueryAttribute >
className="woocommerce-attribute-input-field"
items={ attributes || [] }
items={ markedAttributes || [] }
label={ label || '' }
disabled={ disabled }
getFilteredItems={ getFilteredItems }
@ -179,7 +197,15 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
index={ index }
isActive={ highlightedIndex === index }
item={ item }
getItemProps={ getItemProps }
getItemProps={ ( options ) => ( {
...getItemProps( options ),
disabled: item.isDisabled || undefined,
} ) }
tooltipText={
item.isDisabled
? disabledAttributeMessage
: undefined
}
>
{ isNewAttributeListItem( item ) ? (
<div className="woocommerce-attribute-input-field__add-new">

View File

@ -3,6 +3,7 @@
*/
import { createElement } from '@wordpress/element';
import { ProductAttribute } from '@woocommerce/data';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
/**
@ -31,6 +32,15 @@ export const Attributes: React.FC< AttributesProps > = ( {
return (
<AttributeControl
value={ attributes }
disabledAttributeIds={ value
.filter( ( attr ) => !! attr.variation )
.map( ( attr ) => attr.id ) }
uiStrings={ {
disabledAttributeMessage: __(
'Already used in Variations',
'woocommerce'
),
} }
onAdd={ () => {
recordEvent( 'product_add_attributes_modal_add_button_click' );
} }

View File

@ -0,0 +1 @@
export * from './notice';

View File

@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { ReactNode } from 'react';
import { createElement } from '@wordpress/element';
import classNames from 'classnames';
export type NoticeProps = {
title?: string;
content?: string;
className?: string;
type?: 'error-type' | 'success' | 'warning' | 'info';
children?: ReactNode;
};
export function Notice( {
title = '',
content = '',
className,
type = 'info',
children,
}: NoticeProps ) {
return (
<div
className={ classNames(
className,
type,
'woocommerce-product-notice'
) }
>
{ title && (
<h3 className="woocommerce-product-notice__title">{ title }</h3>
) }
{ content && (
<p className="woocommerce-product-notice__content">
{ content }
</p>
) }
<div className="woocommerce-product-notice__content">
{ children }
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
.woocommerce-product-notice {
padding: $gap-small $gap;
p {
color: $gray-900;
letter-spacing: 0.24px;
}
button {
letter-spacing: 0.24px;
}
&.info {
background-color: #f0f6fc;
}
}

View File

@ -49,6 +49,12 @@
align-items: center;
justify-content: flex-end;
&--delete {
&.components-button.components-menu-item__button.is-link {
text-decoration: none;
}
}
.components-button {
position: relative;
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;
}
}
.components-button--visible {
color: $gray-700;

View File

@ -1,22 +1,29 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Spinner, Tooltip } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import {
Button,
DropdownMenu,
MenuGroup,
MenuItem,
Spinner,
Tooltip,
} from '@wordpress/components';
import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
ProductVariation,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { ListItem, Pagination, Sortable, Tag } from '@woocommerce/components';
import {
Link,
ListItem,
Pagination,
Sortable,
Tag,
} from '@woocommerce/components';
import { getNewPath } from '@woocommerce/navigation';
import { useContext, useState, createElement } from '@wordpress/element';
useContext,
useState,
createElement,
Fragment,
} from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { moreVertical } from '@wordpress/icons';
import classnames from 'classnames';
import truncate from 'lodash/truncate';
import { CurrencyContext } from '@woocommerce/currency';
@ -34,6 +41,7 @@ import { getProductStockStatus, getProductStockStatusClass } from '../../utils';
import {
DEFAULT_PER_PAGE_OPTION,
PRODUCT_VARIATION_TITLE_LIMIT,
TRACKS_SOURCE,
} from '../../constants';
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
@ -87,7 +95,7 @@ export function VariationsTable() {
[ currentPage, perPage, productId ]
);
const { updateProductVariation } = useDispatch(
const { updateProductVariation, deleteProductVariation } = useDispatch(
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 (
<div className="woocommerce-product-variations">
{ isLoading ||
@ -266,17 +297,70 @@ export function VariationsTable() {
</Tooltip>
) }
<Link
href={ getNewPath(
{},
`/product/${ productId }/variation/${ variation.id }`,
{}
) }
type="wc-admin"
className="components-button"
<DropdownMenu
icon={ moreVertical }
label={ __( 'Actions', 'woocommerce' ) }
toggleProps={ {
onClick() {
recordEvent(
'product_variations_menu_view',
{
source: TRACKS_SOURCE,
}
);
},
} }
>
{ __( 'Edit', 'woocommerce' ) }
</Link>
{ ( { onClose } ) => (
<>
<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>
</ListItem>
) ) }

View File

@ -15,6 +15,7 @@
@import "components/content-preview/style.scss";
@import "components/radio-field/style.scss";
@import "components/notice/style.scss";
@import "components/iframe-editor/style.scss";
@import "components/details-categories-field/style.scss";
@import "components/details-categories-field/create-category-modal.scss";

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { Product } from '@woocommerce/data';
/**
* Determine if any attribute in the list is used for variations.
*
* @param {Array} attributeList - List of product attributes.
* @return {boolean} True if any attribute is used for variations.
*/
export const hasAttributesUsedForVariations = (
attributeList: Product[ 'attributes' ]
) => {
if ( ! Array.isArray( attributeList ) || ! attributeList.length ) {
return false;
}
return attributeList.some( ( { variation } ) => variation );
};

View File

@ -18,6 +18,7 @@ import {
getTruncatedProductVariationTitle,
} from './get-product-variation-title';
import { preventLeavingProductForm } from './prevent-leaving-product-form';
import { hasAttributesUsedForVariations } from './has-attributes-used-for-variations';
import { isValidEmail } from './validate-email';
export * from './create-ordered-children';
@ -39,6 +40,7 @@ export {
getProductTitle,
getProductVariationTitle,
getTruncatedProductVariationTitle,
hasAttributesUsedForVariations,
isValidEmail,
preventLeavingProductForm,
PRODUCT_STATUS_LABELS,

View File

@ -27,7 +27,7 @@
<!-- Configs -->
<config name="minimum_supported_wp_version" value="5.2" />
<config name="testVersion" value="7.3-" />
<config name="testVersion" value="7.4-" />
<!-- Rules -->
<rule ref="WooCommerce-Core" />

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Woo AI - Fix store branding settings retrieval for use with description generation.

View File

@ -29,6 +29,13 @@ class Woo_AI_Settings {
*/
protected $id = 'woo-ai-settings-tab';
/**
* Tone of voice select options.
*
* @var array
*/
private $tone_of_voice_select_options;
/**
* Constants used for naming of saved options in the database.
*/
@ -52,6 +59,17 @@ class Woo_AI_Settings {
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'add_woo_ai_settings_script' ) );
add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_woo_ai_settings' ), 10, 2 );
add_filter( 'woocommerce_settings_groups', array( $this, 'add_woo_ai_settings_group' ) );
add_filter( 'woocommerce_settings-woo-ai', array( $this, 'add_woo_ai_settings_group_settings' ) );
$this->tone_of_voice_select_options = array(
'informal' => __( 'Relaxed and friendly.', 'woocommerce' ),
'humorous' => __( 'Light-hearted and fun.', 'woocommerce' ),
'neutral' => __( 'A balanced tone that uses casual expressions.', 'woocommerce' ),
'youthful' => __( 'Friendly and cheeky tone.', 'woocommerce' ),
'formal' => __( 'Direct yet respectful formal tone.', 'woocommerce' ),
'motivational' => __( 'Passionate and inspiring.', 'woocommerce' ),
);
$this->add_sanitization_hooks();
}
@ -78,6 +96,49 @@ class Woo_AI_Settings {
return wp_strip_all_tags( $raw_value ?? '' );
}
/**
* Adds settings which can be retrieved via the WooCommerce Settings API.
*
* @see https://github.com/woocommerce/woocommerce/wiki/Settings-API
*
* @param array $settings The original settings array.
* @return array The modified settings array.
*/
public function add_woo_ai_settings_group_settings( $settings ) {
$settings[] = array(
'id' => 'tone-of-voice',
'option_key' => self::TONE_OF_VOICE_OPTION_KEY,
'label' => __( 'Storewide Tone of Voice', 'woocommerce' ),
'description' => __( 'This controls the conversational tone that will be used when generating content.', 'woocommerce' ),
'default' => 'neutral',
'type' => 'select',
'options' => $this->tone_of_voice_select_options,
);
$settings[] = array(
'id' => 'store-description',
'option_key' => self::STORE_DESCRIPTION_OPTION_KEY,
'label' => __( 'Store Description', 'woocommerce' ),
'description' => __( 'This is a short description of your store which could be used to help generate content.', 'woocommerce' ),
'type' => 'textarea',
);
return $settings;
}
/**
* Register our Woo AI plugin group to the WooCommerce Settings API.
*
* @param array $locations The original settings array.
* @return array The modified settings array.
*/
public function add_woo_ai_settings_group( $locations ) {
$locations[] = array(
'id' => 'woo-ai',
'label' => __( 'Woo AI', 'woocommerce' ),
'description' => __( 'Settings for the Woo AI plugin.', 'woocommerce' ),
);
return $locations;
}
/**
* Add settings to the AI section.
*
@ -121,14 +182,7 @@ class Woo_AI_Settings {
'name' => __( 'Tone of voice', 'woocommerce' ),
'desc' => __( 'Select the tone of voice for the AI', 'woocommerce' ),
'type' => 'select',
'options' => array(
'informal' => __( 'Relaxed and friendly.', 'woocommerce' ),
'humorous' => __( 'Light-hearted and fun.', 'woocommerce' ),
'neutral' => __( 'A balanced tone that uses casual expressions.', 'woocommerce' ),
'youthful' => __( 'Friendly and cheeky tone.', 'woocommerce' ),
'formal' => __( 'Direct yet respectful formal tone.', 'woocommerce' ),
'motivational' => __( 'Passionate and inspiring.', 'woocommerce' ),
),
'options' => $this->tone_of_voice_select_options,
'css' => 'min-width:300px;',
);

View File

@ -14,6 +14,7 @@ import {
PluginsLearnMoreLinkClicked,
PluginsInstallationCompletedWithErrorsEvent,
PluginsInstallationCompletedEvent,
PluginsInstallationRequestedEvent,
} from '..';
import { POSSIBLY_DEFAULT_STORE_NAMES } from '../pages/BusinessInfo';
import {
@ -97,6 +98,20 @@ const recordTracksBusinessInfoCompleted = (
} );
};
const recordTracksPluginsInstallationRequest = (
_context: CoreProfilerStateMachineContext,
event: Extract<
PluginsInstallationRequestedEvent,
{ type: 'PLUGINS_INSTALLATION_REQUESTED' }
>
) => {
recordEvent( 'coreprofiler_store_extensions_continue', {
shown: event.payload.pluginsShown || [],
selected: event.payload.pluginsSelected || [],
unselected: event.payload.pluginsUnselected || [],
} );
};
const recordTracksPluginsLearnMoreLinkClicked = (
_context: unknown,
_event: PluginsLearnMoreLinkClicked,
@ -164,4 +179,5 @@ export default {
recordTracksPluginsLearnMoreLinkClicked,
recordFailedPluginInstallations,
recordSuccessfulPluginInstallation,
recordTracksPluginsInstallationRequest,
};

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import React from 'react';
import { Loader } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '../..';
import { getLoaderStageMeta } from '../../utils/get-loader-stage-meta';
import './loader.scss';
export type Stage = {
title: string;
image?: string | JSX.Element;
paragraphs: Array< {
label: string;
text: string;
duration?: number;
element?: JSX.Element;
} >;
};
export type Stages = Array< Stage >;
export type LoaderContextProps = Pick<
CoreProfilerStateMachineContext,
'loader'
>;
export const CoreProfilerLoader = ( {
context,
}: {
context: LoaderContextProps;
} ) => {
const stages = getLoaderStageMeta( context.loader.useStages ?? 'default' );
const currentStage = stages[ context.loader.stageIndex ?? 0 ];
return (
<Loader className={ context.loader.className }>
<Loader.Layout>
<Loader.Illustration>
{ currentStage.image }
</Loader.Illustration>
<Loader.Title>{ currentStage.title }</Loader.Title>
<Loader.ProgressBar
progress={ context.loader?.progress ?? 0 }
/>
<Loader.Sequence interval={ 3000 }>
{ currentStage.paragraphs.map( ( paragraph, index ) => (
<Loader.Subtext key={ index }>
<b>{ paragraph?.label }</b>
{ paragraph?.text }
{ paragraph?.element }
</Loader.Subtext>
) ) }
</Loader.Sequence>
</Loader.Layout>
</Loader>
);
};

View File

@ -0,0 +1,8 @@
// Loader page
.woocommerce-onboarding-loader {
.loader-hearticon {
position: relative;
top: 2px;
left: 2px;
}
}

View File

@ -53,7 +53,7 @@ import {
} from './pages/BusinessInfo';
import { BusinessLocation } from './pages/BusinessLocation';
import { getCountryStateOptions } from './services/country';
import { Loader } from './pages/Loader';
import { CoreProfilerLoader } from './components/loader/Loader';
import { Plugins } from './pages/Plugins';
import { getPluginSlug, useFullScreen } from '~/utils';
import './style.scss';
@ -108,7 +108,9 @@ export type BusinessLocationEvent = {
export type PluginsInstallationRequestedEvent = {
type: 'PLUGINS_INSTALLATION_REQUESTED';
payload: {
plugins: CoreProfilerStateMachineContext[ 'pluginsSelected' ];
pluginsShown: string[];
pluginsSelected: string[];
pluginsUnselected: string[];
};
};
@ -342,7 +344,19 @@ const redirectToJetpackAuthPage = (
_context: CoreProfilerStateMachineContext,
event: { data: { url: string } }
) => {
window.location.href = event.data.url + '&installed_ext_success=1';
const url = new URL( event.data.url );
url.searchParams.set( 'installed_ext_success', '1' );
const selectedPlugin = _context.pluginsSelected.find(
( plugin ) => plugin === 'jetpack' || plugin === 'jetpack-boost'
);
if ( selectedPlugin ) {
const pluginName =
selectedPlugin === 'jetpack' ? 'jetpack-ai' : 'jetpack-boost';
url.searchParams.set( 'plugin_name', pluginName );
}
window.location.href = url.toString();
};
const updateTrackingOption = async (
@ -551,7 +565,7 @@ const updateQueryStep: CoreProfilerMachineAssign = (
const assignPluginsSelected = assign( {
pluginsSelected: ( _context, event: PluginsInstallationRequestedEvent ) => {
return event.payload.plugins.map( getPluginSlug );
return event.payload.pluginsSelected.map( getPluginSlug );
},
} );
@ -1109,7 +1123,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
meta: {
component: Loader,
component: CoreProfilerLoader,
},
},
},
@ -1174,7 +1188,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
},
meta: {
component: Loader,
component: CoreProfilerLoader,
},
},
plugins: {
@ -1202,7 +1216,10 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
PLUGINS_INSTALLATION_REQUESTED: {
target: 'installPlugins',
actions: [ 'assignPluginsSelected' ],
actions: [
'assignPluginsSelected',
'recordTracksPluginsInstallationRequest',
],
},
},
meta: {
@ -1233,7 +1250,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
},
meta: {
component: Loader,
component: CoreProfilerLoader,
progress: 100,
},
},
@ -1255,7 +1272,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
},
meta: {
component: Loader,
component: CoreProfilerLoader,
progress: 100,
},
},
@ -1286,7 +1303,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
meta: {
component: Loader,
component: CoreProfilerLoader,
progress: 100,
},
},
@ -1381,7 +1398,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
meta: {
component: Loader,
component: CoreProfilerLoader,
},
},
},

View File

@ -1,76 +0,0 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import { useState, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '..';
import ProgressBar from '../components/progress-bar/progress-bar';
import { getLoaderStageMeta } from '../utils/get-loader-stage-meta';
export type Stage = {
title: string;
image?: string | JSX.Element;
paragraphs: Array< {
label: string;
text: string;
duration?: number;
element?: JSX.Element;
} >;
};
export type Stages = Array< Stage >;
export type LoaderContextProps = Pick<
CoreProfilerStateMachineContext,
'loader'
>;
export const Loader = ( { context }: { context: LoaderContextProps } ) => {
const stages = getLoaderStageMeta( context.loader.useStages ?? 'default' );
const currentStage = stages[ context.loader.stageIndex ?? 0 ];
const [ currentParagraph, setCurrentParagraph ] = useState( 0 );
useEffect( () => {
const interval = setInterval( () => {
setCurrentParagraph( ( _currentParagraph ) =>
currentStage.paragraphs[ _currentParagraph + 1 ]
? _currentParagraph + 1
: 0
);
}, currentStage.paragraphs[ currentParagraph ]?.duration ?? 3000 );
return () => clearInterval( interval );
}, [ currentParagraph, currentStage.paragraphs ] );
return (
<div
className={ classNames(
'woocommerce-profiler-loader',
context.loader.className
) }
>
<div className="woocommerce-profiler-loader-wrapper">
{ currentStage.image && currentStage.image }
<h1 className="woocommerce-profiler-loader__title">
{ currentStage.title }
</h1>
<ProgressBar
className={ 'progress-bar' }
percent={ context.loader.progress ?? 0 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ '#E0E0E0' }
/>
<p className="woocommerce-profiler-loader__paragraph">
<b>
{ currentStage.paragraphs[ currentParagraph ]?.label }{ ' ' }
</b>
{ currentStage.paragraphs[ currentParagraph ]?.text }
{ currentStage.paragraphs[ currentParagraph ]?.element }
</p>
</div>
</div>
);
};

View File

@ -72,13 +72,33 @@ export const Plugins = ( {
type: 'PLUGINS_PAGE_SKIPPED',
} );
};
const submitInstallationRequest = () => {
const selectedPluginSlugs = selectedPlugins.map( ( plugin ) =>
plugin.key.replace( ':alt', '' )
);
const pluginsShown: string[] = [];
const pluginsUnselected: string[] = [];
context.pluginsAvailable.forEach( ( plugin ) => {
const pluginSlug = plugin.key.replace( ':alt', '' );
pluginsShown.push( pluginSlug );
if (
! plugin.is_activated &&
! selectedPluginSlugs.includes( pluginSlug )
) {
pluginsUnselected.push( pluginSlug );
}
} );
return sendEvent( {
type: 'PLUGINS_INSTALLATION_REQUESTED',
payload: {
plugins: selectedPlugins.map( ( plugin ) =>
plugin.key.replace( ':alt', '' )
),
pluginsShown,
pluginsSelected: selectedPluginSlugs,
pluginsUnselected,
},
} );
};

View File

@ -1,23 +1,25 @@
/**
* Internal dependencies
*/
import { Loader } from '../pages/Loader';
import { CoreProfilerLoader } from '../components/loader/Loader';
import { WithSetupWizardLayout } from './WithSetupWizardLayout';
import '../style.scss';
export const Short = () => (
<Loader
<CoreProfilerLoader
context={ { loader: { progress: 10, useStages: 'skipGuidedSetup' } } }
/>
);
export const Plugins = () => (
<Loader context={ { loader: { progress: 10, useStages: 'plugins' } } } />
<CoreProfilerLoader
context={ { loader: { progress: 10, useStages: 'plugins' } } }
/>
);
export default {
title: 'WooCommerce Admin/Application/Core Profiler/Loader',
component: Loader,
component: CoreProfilerLoader,
decorators: [ WithSetupWizardLayout ],
};

View File

@ -273,63 +273,6 @@
}
}
// Loader page
.woocommerce-profiler-loader {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
@include breakpoint( '<782px' ) {
padding: 0 20px;
}
.loader-hearticon {
position: relative;
top: 2px;
left: 2px;
}
h1 {
padding: 0;
margin: 58px 0 0 0;
font-size: 28px;
font-weight: 500;
@include breakpoint( '<782px' ) {
font-size: 20px;
}
}
.woocommerce-profiler-loader-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 520px;
}
.woocommerce-profiler-progress-bar {
width: 520px;
margin: 16px 0 16px 0;
@include breakpoint( '<782px' ) {
width: 100%;
}
}
.woocommerce-profiler-progress-bar__container {
height: 4px;
}
.woocommerce-profiler-loader__paragraph {
font-size: 16px;
line-height: 24px;
color: #2f2f2f;
opacity: 0.8;
text-align: center;
@include breakpoint( '<782px' ) {
font-size: 14px;
line-height: 20px;
}
}
}
// User profile page
.woocommerce-profiler-user-profile {
.woocommerce-profiler-user-profile__content {

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