# WooCommerce Documentation - Complete This file contains the complete content of all documentation files combined. ## Getting Started with WooCommerce APIs *Source: apis/README.md* # Getting Started with WooCommerce APIs WooCommerce provides a number of programmatic APIs to interact with WooCommerce store data. ## WC REST API The WC REST API is a powerful part of WooCommerce which lets you read and write various parts of WooCommerce data such as orders, products, coupons, customers, and shipping zones. It based on the [WordPress REST API](https://developer.wordpress.org/rest-api/). Explore the [WC REST API](./rest-api/README.md) documentation. ## Store API The Store API provides public REST API endpoints for the development of customer-facing cart, checkout, and product functionality. In contrast to the WooCommerce REST API, the Store API is unauthenticated and does not provide access to sensitive store data or other customer information. Explore the [Store API](./store-api/README.md) documentation. ## Other Resources Beyond the powerful REST APIs, WooCommerce offers a suite of PHP-based APIs designed for developers to deeply integrate and extend the core functionality of their store. These APIs allow for direct interaction with WooCommerce classes, enabling custom behaviors for settings, payment gateways, shipping methods, and more. ### Settings API The Settings API is used by extensions to display, save, and load settings. Explore the [Settings API](/docs/extensions/settings-and-config/settings-api) documentation. ### Payment Gateway API The Payment Gateway API is used by extensions to interact with the payment gateway. Explore the [Payment Gateway API](/docs/features/payments/payment-gateway-api/) documentation. ### Shipping Method API The Shipping Method API is used by extensions to extend shipping methods and add their own rates. Explore the [Shipping Method API](/docs/features/shipping/shipping-method-api/) documentation. ### Payment Token API The Payment Token API is used for storing and managing payment tokens for gateways. Explore the [Payment Token API](/docs/features/payments/payment-token-api/) documentation. ### WooCommerce Code Reference The WooCommerce Code Reference is a comprehensive documentation of the WooCommerce API. It is a great resource for developers to learn about the WooCommerce API and how to use it. The WooCommerce Code Reference is a comprehensive documentation of the internal WooCommerce Classes, API, and functions. It is a great resource for developers to learn about the WooCommerce functionality and how to extend it. Explore the [WooCommerce Code Reference](https://developer.wordpress.org/reference/classes/woocommerce/) documentation. --- ## WooCommerce REST API *Source: apis/rest-api/README.md* # WooCommerce REST API The [REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction) is a powerful part of WooCommerce which lets you read and write various parts of WooCommerce data such as orders, products, coupons, customers, and shipping zones. ## Requirements In order to access the REST API using the standard endpoint URI structure (e.g. `wc/v3/products`), you must have your WordPress permalinks configured to something other than "Plain". Go to **Settings > Permalinks** and choose an option. ![Permalinks options](https://developer.woocommerce.com/wp-content/uploads/2023/12/permalinks.webp) ## API reference [WooCommerce REST API Docs](https://woocommerce.github.io/woocommerce-rest-api-docs/) provides technical details and code samples for each API endpoint. ## Authentication Authentication is usually the part most developers get stuck on, so this guide will cover a quick way to test that your API is working on your server and you can authenticate. We'll use both [Postman](https://www.getpostman.com/) and [Insomnia](https://insomnia.rest/) clients in these examples. Both are free and will help you visualise what the API offers. Before proceeding, please read the [REST API docs on authentication which covers the important parts concerning API Keys and Auth](https://woocommerce.github.io/woocommerce-rest-api-docs/#authentication). We're only covering connecting over HTTPS here since it's the simplest and most secure method. You should avoid HTTP if possible. ## Generate keys To start using REST API, you first need to generate API keys. 1. Go to *WooCommerce > Settings > Advanced* 2. Go to the *REST API* tab and click *Add key*. 3. Give the key a description for your own reference, choose a user with access to orders etc, and give the key *read/write* permissions. 4. Click *Generate api key*. 5. Your keys will be shown - do not close this tab yet, the secret will be hidden if you try to view the key again. ![Generated API Keys](https://developer.woocommerce.com/wp-content/uploads/2023/12/keys.png) ## Make a basic request The request URL we'll test is `wp-json/wc/v3/orders`. On localhost the full URL may look something like this: `https://localhost:8888/wp-json/wc/v3/orders`. Modify this to use your own site URL. In Postman, you need to set the fields for request type, request URL, and the settings on the authorization tab. For Authorization, choose *basic auth* and enter your *consumer key* and *consumer secret* keys from WooCommerce into the username and password fields Once done, hit send and you'll see the JSON response from the API if all worked well. You should see something like this: ![Generated API Keys](https://developer.woocommerce.com/wp-content/uploads/2023/12/postman.png) Insomnia is almost identical to Postman; fill in the same fields and again use basic auth. ![Insomnia](https://developer.woocommerce.com/wp-content/uploads/2023/12/insomnia.png) That's it! The API is working. If you have problems connecting, you may need to disable SSL verification - see the connection issues section below. ## Common connection issues ### Connection issues with localhost and self-signed SSL certificates If you're having problems connecting to the REST API on your localhost and seeing errors like this: ![SSL Error](https://developer.woocommerce.com/wp-content/uploads/2023/12/sslerror.png) You need to disable SSL verification. In Postman you can find this in the settings: ![Postman settings](https://developer.woocommerce.com/wp-content/uploads/2023/12/postman-ssl.png) Insomnia also has this setting the preferences area: ![Insomnia settings](https://developer.woocommerce.com/wp-content/uploads/2023/12/insomnia-ssl.png) ### 401 Unauthorized Your API keys or signature is wrong. Ensure that: - The user you generated API keys for actually has access to those resources. - The username when authenticating is your consumer key. - The password when authenticating is your consumer secret. - Make a new set of keys to be sure. If your server utilizes FastCGI, check that your [authorization headers are properly read](https://web.archive.org/web/20230330133128/https://support.metalocator.com/en/articles/1654091-wp-json-basic-auth-with-fastcgi). ### Consumer key is missing Occasionally servers may not parse the Authorization header correctly (if you see a "Consumer key is missing" error when authenticating over SSL, you have a server issue). In this case, you may provide the consumer key/secret as query string parameters instead. Example: ```text https://local.wordpress.dev/wp-json/wc/v2/orders?consumer_key=XXXX&consumer_secret=XXXX ``` ### Server does not support POST/DELETE/PUT Ideally, your server should be configured to accept these types of API request, but if not you can use the [`_method` property](https://developer.wordpress.org/rest-api/using-the-rest-api/global-parameters/#_method-or-x-http-method-override-header). --- ## WooCommerce Store API *Source: apis/store-api/README.md* # WooCommerce Store API **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/).** In contrast to the WooCommerce REST API, the Store API is unauthenticated and does not provide access to sensitive store data or other customer information. Example of a valid API request using cURL: ```sh curl "https://example-store.com/wp-json/wc/store/v1/products" ``` Possible uses of the Store API include: 1. Obtaining a list of products to display that can be searched or filtered 2. Adding products to the cart and returning an updated cart object for display 3. Obtaining shipping rates for a cart 4. Converting a customer’s cart to an Order, collecting addresses, and then facilitating payment ## Requirements and limitations * This is an unauthenticated API. It does not require API keys or authentication tokens for access. * All API responses return JSON-formatted data. * Data returned from the API is reflective of the current user (customer). Customer sessions in WooCommerce are cookie-based. * Store API cannot be used to look up other customers and orders by ID; only data belonging to the current user. * Likewise, Store API cannot be used to write store data e.g. settings. For more extensive access, use the authenticated [WC REST API.](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction) * Endpoints that do allow writes, for example, updating the current customer address, require a [nonce-token](https://developer.wordpress.org/plugins/security/nonces/). * Store API is render-target agnostic and should not make assumptions about where content will be displayed. For example, returning HTML would be discouraged unless the data type itself is HTML. ## Store API Namespace Resources in the Store API are all found within the `wc/store/v1` namespace, and since this API extends the WordPress API, accessing it requires the `/wp-json/` base. Currently, the only version is `v1`. If the version is omitted, `v1` will be served. Examples: ```http GET /wp-json/wc/store/v1/products GET /wp-json/wc/store/v1/cart ``` The API uses JSON to serialize data. You don’t need to specify `.json` at the end of an API URL. ## Resources and endpoints Available resources in the Store API are listed below, with links to more detailed documentation. | Resource | Methods | Endpoints | | :----------------------------------------------------------- | :----------------------------- | --------------------------------------------------------------------------------------------- | | [`Cart`](/docs/apis/store-api/resources-endpoints/cart) | `GET` | [`/wc/store/v1/cart`](/docs/apis/store-api/resources-endpoints/cart#get-cart) | | | `POST` | [`/wc/store/v1/cart/add-item`](/docs/apis/store-api/resources-endpoints/cart#add-item) | | | `POST` | [`/wc/store/v1/cart/remove-item`](/docs/apis/store-api/resources-endpoints/cart#remove-item) | | | `POST` | [`/wc/store/v1/cart/update-item`](/docs/apis/store-api/resources-endpoints/cart#update-item) | | | `POST` | [`/wc/store/v1/cart/apply-coupon`](/docs/apis/store-api/resources-endpoints/cart#apply-coupon) | | | `POST` | [`/wc/store/v1/cart/remove-coupon`](/docs/apis/store-api/resources-endpoints/cart#remove-coupon) | | | `POST` | [`/wc/store/v1/cart/update-customer`](/docs/apis/store-api/resources-endpoints/cart#update-customer) | | | `POST` | [`/wc/store/v1/cart/select-shipping-rate`](/docs/apis/store-api/resources-endpoints/cart#select-shipping-rate) | | [`Cart Items`](/docs/apis/store-api/resources-endpoints/cart-items) | `GET`, `POST`, `DELETE` | [`/wc/store/v1/cart/items`](/docs/apis/store-api/resources-endpoints/cart-items#list-cart-items) | | | `GET`, `POST`, `PUT`, `DELETE` | [`/wc/store/v1/cart/items/:key`](/docs/apis/store-api/resources-endpoints/cart-items#single-cart-item) | | [`Cart Coupons`](/docs/apis/store-api/resources-endpoints/cart-coupons) | `GET`, `POST`, `DELETE` | [`/wc/store/v1/cart/coupons`](/docs/apis/store-api/resources-endpoints/cart-coupons#list-cart-coupons) | | | `GET`, `DELETE` | [`/wc/store/v1/cart/coupon/:code`](/docs/apis/store-api/resources-endpoints/cart-coupons#single-cart-coupon) | | [`Checkout`](/docs/apis/store-api/resources-endpoints/checkout) | `GET`, `POST`, `PUT` | [`/wc/store/v1/checkout`](/docs/apis/store-api/resources-endpoints/checkout) | | [`Checkout order`](/docs/apis/store-api/resources-endpoints/checkout-order) | `POST` | [`/wc/store/v1/checkout/:id`](/docs/apis/store-api/resources-endpoints/checkout-order) | | [`Order`](/docs/apis/store-api/resources-endpoints/order) | `GET` | [`/wc/store/v1/order/:id`](/docs/apis/store-api/resources-endpoints/order) | | [`Products`](/docs/apis/store-api/resources-endpoints/products) | `GET` | [`/wc/store/v1/products`](/docs/apis/store-api/resources-endpoints/products#list-products) | | | `GET` | [`/wc/store/v1/products/:id`](/docs/apis/store-api/resources-endpoints/products#single-product-by-id) | | [`Product Collection Data`](/docs/apis/store-api/resources-endpoints/product-collection-data) | `GET` | [`/wc/store/v1/products/collection-data`](/docs/apis/store-api/resources-endpoints/product-collection-data) | | [`Product Attributes`](/docs/apis/store-api/resources-endpoints/product-attributes) | `GET` | [`/wc/store/v1/products/attributes`](/docs/apis/store-api/resources-endpoints/product-attributes#list-product-attributes) | | | `GET` | [`/wc/store/v1/products/attributes/:id`](/docs/apis/store-api/resources-endpoints/product-attributes#single-product-attribute) | | [`Product Attribute Terms`](/docs/apis/store-api/resources-endpoints/product-attribute-terms) | `GET` | [`/wc/store/v1/products/attributes/:id/terms`](/docs/apis/store-api/resources-endpoints/product-attribute-terms) | | [`Product Categories`](/docs/apis/store-api/resources-endpoints/product-categories) | `GET` | [`/wc/store/v1/products/categories`](/docs/apis/store-api/resources-endpoints/product-categories) | | [`Product Brands`](/docs/apis/store-api/resources-endpoints/product-brands) | `GET` | [`/wc/store/v1/products/brands`](/docs/apis/store-api/resources-endpoints/product-brands) | | [`Product Reviews`](/docs/apis/store-api/resources-endpoints/product-reviews) | `GET` | [`/wc/store/v1/products/reviews`](/docs/apis/store-api/resources-endpoints/product-reviews) | | [`Product Tags`](/docs/apis/store-api/resources-endpoints/product-tags) | `GET` | [`/wc/store/v1/products/tags`](/docs/apis/store-api/resources-endpoints/product-tags) | ## Pagination If collections contain many results, they may be paginated. When listing resources you can pass the following parameters: | Parameter | Description | | :--------- | :------------------------------------------------------------------------------------- | | `page` | Current page of the collection. Defaults to `1`. | | `per_page` | Maximum number of items to be returned in result set. Defaults to `10`. Maximum `100`. | In the example below, we list 20 products per page and return page 2. ```sh curl "https://example-store.com/wp-json/wc/store/v1/products?page=2&per_page=20" ``` Additional pagination headers are also sent back with extra information. | Header | Description | | :---------------- | :------------------------------------------------------------------------ | | `X-WP-Total` | The total number of items in the collection. | | `X-WP-TotalPages` | The total number of pages in the collection. | | `Link` | Contains links to other pages; `next`, `prev`, and `up` where applicable. | ## Status codes The following table gives an overview of how the API functions generally behave. | Request type | Description | | :----------- | :---------------------------------------------------------------------------------------------------------- | | `GET` | Access one or more resources and return `200 OK` and the result as JSON. | | `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | | `PUT` | Return `200 OK` if the resource is modified successfully. The modified result is returned as JSON. | | `DELETE` | Returns `204 No Content` if the resource was deleted successfully. | The following table shows the possible return codes for API requests. | Response code | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | | `200 OK` | The request was successful, the resource(s) itself is returned as JSON. | | `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. | | `201 Created` | The POST request was successful and the resource is returned as JSON. | | `400 Bad Request` | A required attribute of the API request is missing. | | `403 Forbidden` | The request is not allowed. | | `404 Not Found` | A resource could not be accessed, for example it doesn't exist. | | `405 Method Not Allowed` | The request is not supported. | | `409 Conflict` | The request could not be completed due to a conflict with the current state of the target resource. The current state may also be returned. | | `500 Server Error` | While handling the request something went wrong server-side. | ## Contributing There are 3 main parts to each route in the Store API: 1. Route - Responsible for mapping requests to endpoints. Routes in the Store API extend the `AbstractRoute` class; this class contains shared functionality for handling requests and returning JSON responses. Routes ensure a valid response is returned and handle collections, errors, and pagination. 2. Schema - Routes do not format resources. Instead we use _Schema_ classes that represent each type of resource, for example, a Product, a Cart, or a Cart Item. Schema classes in the Store API should extend the `AbstractSchema` class. 3. Utility - In more advanced cases where the Store API needs to access complex data from WooCommerce core, or where multiple routes need access to the same data, routes should use a Controller or Utility class. For example, the Store API has an Order Controller and a Cart Controller for looking up order and cart data respectfully. Typically, routes handle the following types of requests: * `GET` requests to read product, cart, or checkout data. * `POST` and `PUT` requests to update cart and checkout data. * `DELETE` requests to remove cart data. * `OPTIONS` requests to retrieve the JSON schema for the current route. Please review the [Store API Guiding principles](/docs/apis/store-api/guiding-principles). This covers our approach to development, and topics such as versioning, what data is safe to include, and how to build new routes. ## Extensibility The approach to extensibility within the Store API is to expose certain routes and schema to the ExtendSchema class. [Documentation for contributors on this can be found here](/docs/apis/store-api/extending-store-api/). If a route includes the extensibility interface, 3rd party developers can use the shared `ExtendSchema::class` instance to register additional endpoint data and additional schema. This differs from the traditional filter hook approach in that it is more limiting, but it reduces the likelihood of a 3rd party extension breaking routes and endpoints or overwriting returned data which other apps may rely upon. If new schema is required, and any of the following statements are true, choose to _extend_ the Store API rather than introducing new schema to existing Store API schemas: * The data is part of an extension, not core * The data is related to a resource, but not technically part of it * The data is difficult to query (performance wise) or has a very narrow or niche use-case If the data is sensitive (for example, a core setting that should be private), or not related to the current user (for example, looking up an order by order ID), [choose to use the authenticated WC REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction). If you're looking to add _new routes and endpoints_, rather than extending the Store API _schema_, extending the Store API is not necessary. You can instead utilize core WordPress functionality to create new routes, choosing to use the same pattern of Store API if you wish. See: * [`register_rest_route()`](https://developer.wordpress.org/reference/functions/register_rest_route/) * [Rest API Permissions Callbacks](https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/#permissions-callback) --- ## Cart Tokens *Source: apis/store-api/cart-tokens.md* # Cart Tokens Cart tokens can be used instead of cookies based sessions for headless interaction with carts. When using a `Cart-Token` a [Nonce Token](/docs/apis/store-api/nonce-tokens) is not required. ## Obtaining a Cart Token Requests to `/cart` endpoints return a `Cart-Token` header alongside the response. This contains a token which can later be sent as a request header to the Store API Cart and Checkout endpoints to identify the cart. The quickest method of obtaining a Cart Token is to make a GET request `/wp-json/wc/store/v1/cart` and observe the response headers. You should see a `Cart-Token` header there. ## How to use a Cart-Token To use a `Cart-Token`, include it as a header with your request. The response will contain the current cart state from the session associated with the `Cart-Token`. **Example:** ```sh curl --header "Cart-Token: 12345" --request GET https://example-store.com/wp-json/wc/store/v1/cart ``` The same method will allow you to checkout using a `Cart-Token` on the `/checkout` route. --- ## Extending the Store API *Source: apis/store-api/extending-store-api/README.md* # Extending the Store API Your application can change the way the Store API works by extending certain endpoints. It can add data to certain endpoints to make your server-side data available to the client-side. You can also use the Store API trigger a server-side cart update from the client which will then update the client-side cart with the data returned by the API. The documents listed below contain further details on how to achieve the above. | Document | Description | |----------|-------------| | [Exposing your data](./extend-store-api-add-data/) | Explains how you can add additional data to Store API endpoints. | | [Available extensible endpoints](./available-endpoints-to-extend/) | A list of all available endpoints to extend. | | [Available Formatters](./extend-store-api-formatters/) | Available `Formatters` to format data for use in the Store API. | | [Updating the cart on-demand](./extend-store-api-update-cart/) | Update the server-side cart following an action from the front-end. | | [Adding fields and passing values](./extend-store-api-add-custom-fields/) | How to add custom fields to Store API endpoints. | --- ## Available extensible endpoints *Source: apis/store-api/extending-store-api/available-endpoints-to-extend.md* # Available extensible endpoints Some endpoints of the Store API are extensible via a class called `ExtendSchema`. This allows you to customise the data (including the schema) that is returned by the Store API so that it can be consumed by your application or plugin. For more information about extending the Store API, you may also be interested in: - [How to add your data to Store API using `ExtendSchema`](./extend-store-api-add-data.md) - [How to add a new endpoint](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/internal-developers/rest-api/extend-rest-api-new-endpoint.md) Below is a list of available endpoints that you can extend using `ExtendSchema`, as well as some example use-cases. ## Products The main `wc/store/products` endpoint is extensible via ExtendSchema. The data is available via the `extensions` key for each `product` in the response array. This endpoint can be extended using the `ProductSchema::IDENTIFIER` key. For this endpoint, your `data_callback` callback function is passed `$product` as a parameter. Your `schema_callback` function is passed no additional parameters; all products should share the same schema. ### Use Cases This endpoint is useful for adding additional data about individual products. This could be some meta data, additional pricing, or anything else to support custom blocks or components on the products page. ### Example ```php woocommerce_store_api_register_endpoint_data( array( 'endpoint' => ProductSchema::IDENTIFIER, 'namespace' => 'my_plugin_namespace', 'data_callback' => function( $product ) { return array( 'my_meta_data' => get_post_meta( $product->get_id(), 'my_meta_data', true ), ); }, 'schema_callback' => function() { return array( 'properties' => array( 'my_meta_data' => array( 'type' => 'string', ), ), ); }, 'schema_type' => ARRAY_A, ) ); ``` ## Cart The main `wc/store/cart` endpoint is extensible via ExtendSchema. The data is available via the `extensions` key in the response. This endpoint can be extended using the `CartSchema::IDENTIFIER` key. For this endpoint, your `data_callback` and `schema_callback` functions are passed no additional parameters. ### Use Cases This endpoint is useful for adding additional data to the cart page, for example, extra data about the cart items, or anything else needed to support custom blocks displayed on the cart page. ### Example ```php woocommerce_store_api_register_endpoint_data( array( 'endpoint' => CartSchema::IDENTIFIER, 'namespace' => 'my_plugin_namespace', 'data_callback' => function() { return array( 'foo' => 'bar', ); }, 'schema_callback' => function() { return array( 'properties' => array( 'foo' => array( 'type' => 'string', ), ), ); }, 'schema_type' => ARRAY_A, ) ); ``` ## Cart Items The `wc/store/cart/items` endpoint, which is also available on `wc/store/cart` inside the `items` key. The data would be available inside each item of the `items` array. This endpoint can be extended using the `CartItemSchema::IDENTIFIER` key. For this endpoint, your `data_callback` callback function is passed `$cart_item` as a parameter. Your `schema_callback` function is passed no additional parameters; all cart items should share the same schema. ### Use Cases This endpoint is useful for adding additional data about individual cart items. This could be some meta data, additional pricing, or anything else to support custom blocks or components on the cart page. ### Example ```php woocommerce_store_api_register_endpoint_data( array( 'endpoint' => CartItemSchema::IDENTIFIER, 'namespace' => 'my_plugin_namespace', 'data_callback' => function( $cart_item ) { $product = $cart_item['data']; return array( 'my_meta_data' => get_post_meta( $product->get_id(), 'my_meta_data', true ), ); }, 'schema_callback' => function() { return array( 'properties' => array( 'my_meta_data' => array( 'type' => 'string', ), ), ); }, 'schema_type' => ARRAY_A, ) ); ``` ## Checkout The `wc/store/checkout` endpoint is extensible via ExtendSchema. Additional data is available via the `extensions` key in the response. This endpoint can be extended using the `CheckoutSchema::IDENTIFIER` key. For this endpoint, your `data_callback` and `schema_callback` functions are passed no additional parameters. ### Use Cases This endpoint is useful for adding additional data to the checkout page, such as a custom payment method which requires additional data to be collected from the user or server. ⚠ **Important: Do **not** reveal any sensitive data in this endpoint, as it is publicly accessible. This includes private keys for payment services.** ### Example ```php woocommerce_store_api_register_endpoint_data( array( 'endpoint' => CheckoutSchema::IDENTIFIER, 'namespace' => 'my_plugin_namespace', 'data_callback' => function() { return array( 'foo' => 'bar', ); }, 'schema_callback' => function() { return array( 'properties' => array( 'foo' => array( 'type' => 'string', ), ), ); }, 'schema_type' => ARRAY_A, ) ); ``` --- ## Adding fields and passing values *Source: apis/store-api/extending-store-api/extend-store-api-add-custom-fields.md* # Adding fields and passing values This document describes how a developer can insert an input field into the Checkout block and have its value passed to the Store API so it's available when processing the checkout. ## Overview Developers can extend the Checkout block to add new inner blocks and process additional data through the checkout POST request. This involves leveraging the extensibility interfaces provided by Gutenberg and WooCommerce Blocks. This is demonstrated in more detail in our tutorial: [Tutorial: Extending the WooCommerce Checkout Block ](https://developer.woocommerce.com/2023/08/07/extending-the-woocommerce-checkout-block-to-add-custom-shipping-options/). ## Prerequisites - Basic understanding of React and the Gutenberg block editor. - Familiarity with WooCommerce Blocks' extensibility interfaces and the Store API. ## Step-by-Step Guide ### 1. Set Up Your Development Environment Ensure you have the following files in your project: - `index.js`: Entry point for Webpack, imports, and registers the block type. - `edit.js`: Handles the rendering of the block in the editor interface. - `block.json`: Provides metadata and configurations for the block. - `block.js`: Manages the block's state and user interactions. - `frontend.js`: Registers the checkout block component for the frontend. Refer to [this tutorial](https://developer.woocommerce.com/2023/08/07/extending-the-woocommerce-checkout-block-to-add-custom-shipping-options/) for an example of adding a custom shipping option to the checkout block. ### 2. Add a new field block to the Checkout Block To add a field block to the Checkout Block you will need to add the following entries to the `block.json` file of your block: ```json "parent": [ "woocommerce/checkout-shipping-methods-block" ], "attributes": { "lock": { "type": "object", "default": { "remove": true, "move": true } } } ``` - The [lock attribute](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-templates/#individual-block-locking) is an object that controls whether the block can be removed or moved. By default, the lock attribute is set to allow the block to be removed and moved. However, by modifying the lock attribute, you can “force” the block to be non-removable. For example, you can set both remove and move properties to false in order to prevent the block from being removed or moved. - The [parent attribute](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#parent) specifies the parent block that this block should be nested within it. It determines where the block will render. In our example, the block is a child of the `woocommerce/checkout-shipping-methods-block`. This means that your block will be rendered within the `woocommerce/checkout-shipping-methods-block`. If the shipping methods block is not required, your block will not be rendered. ### 3. Setting custom checkout data We can set the added field data to send it to the `wc/store/checkout` endpoint when processing orders using the function `setExtensionData`: ```JavaScript setExtensionData( 'namespace-of-your-block', 'key-of-your-data', value ); ``` #### Parameters - namespace `string` - The namespace of your block. - key `string` - The key of your data. - value `any` - The value of your data. #### How it works 1. `setExtensionData` is passed to inner blocks via props. 2. It updates the `extensionData` key of the `wc/store/checkout` data store. 3. This key is passed as part of the request body when POSTing to the checkout endpoint. #### Code Example ```JavaScript // block.js export const Block = ( { checkoutExtensionData, extensions } ) => { /** * setExtensionData will update the wc/store/checkout data store with the values supplied. It * can be used to pass data from the client to the server when submitting the checkout form. */ const { setExtensionData } = checkoutExtensionData; } // ... Some code here useEffect( () => { /** * This code should use `setExtensionData` to update the `key-of-your-data` key * in the `namespace-of-your-block` namespace of the checkout data store. */ setExtensionData( 'namespace-of-your-block', 'key-of-your-data', value ); }, [ setExtensionData, value ] ); ``` #### Screenshots Screenshots of Redux Dev tool showing the data store before and after the setExtensionData call: | Before | After | | ------ | ----- | | ![Redux Dev tool before setExtensionData call](https://github.com/woocommerce/woocommerce-blocks/assets/14235870/948581f5-fdc2-4df1-963f-9aeb4b18b042) | ![Redux Dev tool after setExtensionData call](https://github.com/woocommerce/woocommerce-blocks/assets/14235870/ddc7dbe7-3fad-44cd-bd19-ce78bc49b951) | ### 4. Processing the Checkout POST Request To process the added field data, we'll need extend the Store API to tell it to expect additional data. See more details in the [Exposing your data in the Store API](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/rest-api/extend-store-api-add-data.md) #### Code Example We will use the following PHP files in our example: - The `custom-inner-block-blocks-integration.php` file: Enqueue scripts, styles, and data on the frontend when the Checkout blocks is being used. See more details in the [IntegrationInterface](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/checkout-block/integration-interface.md) documentation. ```php use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface; /** * Class for integrating with WooCommerce Blocks */ class Custom_Inner_Block_Blocks_Integration implements IntegrationInterface { /** * The name of the integration. * * @return string */ public function get_name() { return 'new-field-block'; } /** * When called invokes any initialization/setup for the integration. */ public function initialize() { // ... Some code here: (e.g. init functions that registers scripts and styles, and other instructions) } // ... Other functions here } ``` - The `custom-inner-block-extend-store-endpoint.php` file: extends the [Store API](https://github.com/woocommerce/woocommerce-blocks/tree/trunk/src/StoreApi) and adds hooks to save and display your new field block instructions. This doesn't save the data from the custom block anywhere by default, but you can add your own logic to save the data to the database. ```php use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CartSchema; use Automattic\WooCommerce\Blocks\StoreApi\Schemas\CheckoutSchema; /** * Your New Field Block Extend Store API. */ class Custom_Inner_Block_Extend_Store_Endpoint { /** * Stores Rest Extending instance. * * @var ExtendRestApi */ private static $extend; /** * Plugin Identifier, unique to each plugin. * * @var string */ const IDENTIFIER = 'new-field-block'; /** * Bootstraps the class and hooks required data. * */ public static function init() { self::$extend = Automattic\WooCommerce\StoreApi\StoreApi::container()->get( Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::class ); self::extend_store(); } /** * Registers the actual data into each endpoint. */ public static function extend_store() { if ( is_callable( [ self::$extend, 'register_endpoint_data' ] ) ) { self::$extend->register_endpoint_data( [ 'endpoint' => CheckoutSchema::IDENTIFIER, 'namespace' => self::IDENTIFIER, 'schema_callback' => [ 'Custom_Inner_Block_Extend_Store_Endpoint', 'extend_checkout_schema' ], 'schema_type' => ARRAY_A, ] ); } } /** * Register the new field block schema into the Checkout endpoint. * * @return array Registered schema. * */ public static function extend_checkout_schema() { return [ 'Value_1' => [ 'description' => 'A description of the field', 'type' => 'string', // ... type of the field, this should be a string 'context' => [ 'view', 'edit' ], // ... context of the field, this should be an array containing 'view' and 'edit' 'readonly' => true, // ... whether the field is readonly or not, this should be a boolean 'optional' => true, // ... whether the field is optional or not, this should be a boolean ], // ... other values ]; } } ``` - The `new-field-block.php` file: the main plugin file that loads the `custom-inner-block-blocks-integration.php` and `custom-inner-block-extend-store-endpoint.php` files. ```php register( new Custom_Inner_Block_Blocks_Integration() ); } ); } ); // ... Some code here ``` Here is an example from our [tutorial](https://developer.woocommerce.com/2023/08/07/extending-the-woocommerce-checkout-block-to-add-custom-shipping-options/) of how to get this custom field's data while processing the checkout. This example is from the `shipping-workshop-blocks-integration.php` file. The complete code can be found in this [GitHub repository](https://github.com/woocommerce/wceu23-shipping-workshop-final/blob/main/shipping-workshop-blocks-integration.php#L42-L83). ```php private function save_shipping_instructions() { /** * We write a hook, using the `woocommerce_store_api_checkout_update_order_from_request` action * that will update the order metadata with the shipping-workshop alternate shipping instruction. * * The documentation for this hook is at: https://github.com/woocommerce/woocommerce-blocks/blob/b73fbcacb68cabfafd7c3e7557cf962483451dc1/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_store_api_checkout_update_order_from_request */ add_action( 'woocommerce_store_api_checkout_update_order_from_request', function( \WC_Order $order, \WP_REST_Request $request ) { $shipping_workshop_request_data = $request['extensions'][$this->get_name()]; $alternate_shipping_instruction = $shipping_workshop_request_data['alternateShippingInstruction']; $other_shipping_value = $shipping_workshop_request_data['otherShippingValue']; $order->update_meta_data( 'shipping_workshop_alternate_shipping_instruction', $alternate_shipping_instruction ); $order->save(); }, 10, 2 ); } ``` ## Conclusion By following the steps above, you can add and process new field blocks in the WooCommerce checkout block. For complete implementation and additional examples, refer to the provided [tutorial](https://developer.woocommerce.com/2023/08/07/extending-the-woocommerce-checkout-block-to-add-custom-shipping-options/) and the corresponding [GitHub repository](https://github.com/woocommerce/wceu23-shipping-workshop-final/). --- ## Exposing your data *Source: apis/store-api/extending-store-api/extend-store-api-add-data.md* # Exposing your data ## The problem You want to extend the Mini-Cart, Cart and Checkout blocks, but you want to use some custom data not available on Store API or the context. You don't want to create your own endpoints or Ajax actions. You want to piggyback on the existing StoreAPI calls. ## Solution ExtendSchema offers the possibility to add contextual custom data to Store API endpoints, like `wc/store/cart` and `wc/store/cart/items` endpoints. That data is namespaced to your plugin and protected from other plugins causing it to malfunction. The data is available on all frontend filters and slotFills for you to consume. ## Basic usage You can use ExtendSchema by registering a couple of functions, `schema_callback` and `data_callback` on a specific endpoint namespace. ExtendSchema will call them at execution time and will pass them relevant data as well. This example below uses the Cart endpoint, [see passed parameters.](./available-endpoints-to-extend.md) **Note: Make sure to read the "Things to consider" section below.** ```php use Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema; add_action('woocommerce_blocks_loaded', function() { woocommerce_store_api_register_endpoint_data( array( 'endpoint' => CartSchema::IDENTIFIER, 'namespace' => 'plugin_namespace', 'data_callback' => 'my_data_callback', 'schema_callback' => 'my_schema_callback', 'schema_type' => ARRAY_A, ) ); }); function my_data_callback() { return [ 'custom-key' => 'custom-value', ]; } function my_schema_callback() { return [ 'custom-key' => [ 'description' => __( 'My custom data', 'plugin-namespace' ), 'type' => 'string', 'readonly' => true, ] ]; } ``` Data callback and Schema callback can also receive parameters: ```php function my_cart_item_callback( $cart_item ) { $product = $cart_item['data']; if ( is_my_custom_product_type( $product ) ) { $custom_value = get_custom_value( $product ); return [ 'custom-key' => $custom_value, ]; } } ``` ## Things To Consider ### ExtendSchema is a shared instance The ExtendSchema is stored as a shared instance between the API and consumers (third-party developers). So you shouldn't initiate the class yourself with `new ExtendSchema` because it would not work. Instead, you should always use the shared instance from the StoreApi dependency injection container like this. ```php $extend = StoreApi::container()->get( ExtendSchema::class ); ``` Also note that the dependency injection container is not available until after the `woocommerce_blocks_loaded` action has been fired, so you should hook your file that action: ```php use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; add_action( 'woocommerce_blocks_loaded', function() { $extend = StoreApi::container()->get( ExtendSchema::class ); // my logic. }); ``` Or use the global helper functions: - `woocommerce_store_api_register_endpoint_data( $args )` - `woocommerce_store_api_register_update_callback( $args )` - `woocommerce_store_api_register_payment_requirements( $args )` - `woocommerce_store_api_get_formatter( $name )` ### Errors and fatals are silence for non-admins If your callback functions `data_callback` and `schema_callback` throw an exception or an error, or you passed the incorrect type of parameter to `register_endpoint_data`; that error would be caught and logged into WooCommerce error logs. If the current user is a shop manager or an admin, and has WP_DEBUG enabled, the error would be surfaced to the frontend. ### Callbacks should always return an array To reduce the chances of breaking your client code or passing the wrong type, and also to keep a consistent REST API response, callbacks like `data_callback` and `schema_callback` should always return an array, even if it was empty. ## API Definition - `ExtendSchema::register_endpoint_data`: Used to register data to a custom endpoint. It takes an array of arguments: | Attribute | Type | Required | Description | | :---------------- | :------- | :----------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | `endpoint` | string | Yes | The endpoint you're trying to extend. It is suggested that you use the `::IDENTIFIER` available on the route Schema class to avoid typos. | | `namespace` | string | Yes | Your plugin namespace, the data will be available under this namespace in the StoreAPI response. | | `data_callback` | callback | Yes | A callback that returns an array with your data. | | `schema_callback` | callback | Yes | A callback that returns the shape of your data. | | `schema_type` | string | No (default: `ARRAY_A` ) | The type of your data. If you're adding an object (key => values), it should be `ARRAY_A`. If you're adding a list of items, it should be `ARRAY_N`. | ## Putting it all together This is a complete example that shows how you can register contextual WooCommerce Subscriptions data in each cart item (simplified). This example uses [Formatters](./extend-store-api-formatters.md), utility classes that allow you to format values so that they are compatible with the StoreAPI. ```php get( ExtendSchema::class ); WC_Subscriptions_Extend_Store_Endpoint::init( $extend ); }); class WC_Subscriptions_Extend_Store_Endpoint { /** * Stores Rest Extending instance. * * @var ExtendSchema */ private static $extend; /** * Plugin Identifier, unique to each plugin. * * @var string */ const IDENTIFIER = 'subscriptions'; /** * Bootstraps the class and hooks required data. * * @param ExtendSchema $extend_rest_api An instance of the ExtendSchema class. * * @since 3.1.0 */ public static function init( ExtendSchema $extend_rest_api ) { self::$extend = $extend_rest_api; self::extend_store(); } /** * Registers the actual data into each endpoint. */ public static function extend_store() { // Register into `cart/items` self::$extend->register_endpoint_data( array( 'endpoint' => CartItemSchema::IDENTIFIER, 'namespace' => self::IDENTIFIER, 'data_callback' => array( 'WC_Subscriptions_Extend_Store_Endpoint', 'extend_cart_item_data' ), 'schema_callback' => array( 'WC_Subscriptions_Extend_Store_Endpoint', 'extend_cart_item_schema' ), 'schema_type' => ARRAY_A, ) ); } /** * Register subscription product data into cart/items endpoint. * * @param array $cart_item Current cart item data. * * @return array $item_data Registered data or empty array if condition is not satisfied. */ public static function extend_cart_item_data( $cart_item ) { $product = $cart_item['data']; $item_data = array( 'billing_period' => null, 'billing_interval' => null, 'subscription_length' => null, 'trial_length' => null, 'trial_period' => null, 'sign_up_fees' => null, 'sign_up_fees_tax' => null, ); if ( in_array( $product->get_type(), array( 'subscription', 'subscription_variation' ), true ) ) { $item_data = array_merge( array( 'billing_period' => WC_Subscriptions_Product::get_period( $product ), 'billing_interval' => (int) WC_Subscriptions_Product::get_interval( $product ), 'subscription_length' => (int) WC_Subscriptions_Product::get_length( $product ), 'trial_length' => (int) WC_Subscriptions_Product::get_trial_length( $product ), 'trial_period' => WC_Subscriptions_Product::get_trial_period( $product ), ), self::format_sign_up_fees( $product ) ); } return $item_data; } /** * Register subscription product schema into cart/items endpoint. * * @return array Registered schema. */ public static function extend_cart_item_schema() { return array( 'billing_period' => array( 'description' => __( 'Billing period for the subscription.', 'woocommerce-subscriptions' ), 'type' => array( 'string', 'null' ), 'enum' => array_keys( wcs_get_subscription_period_strings() ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'billing_interval' => array( 'description' => __( 'The number of billing periods between subscription renewals.', 'woocommerce-subscriptions' ), 'type' => array( 'integer', 'null' ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'subscription_length' => array( 'description' => __( 'Subscription Product length.', 'woocommerce-subscriptions' ), 'type' => array( 'integer', 'null' ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'trial_period' => array( 'description' => __( 'Subscription Product trial period.', 'woocommerce-subscriptions' ), 'type' => array( 'string', 'null' ), 'enum' => array_keys( wcs_get_subscription_period_strings() ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'trial_length' => array( 'description' => __( 'Subscription Product trial interval.', 'woocommerce-subscriptions' ), 'type' => array( 'integer', 'null' ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sign_up_fees' => array( 'description' => __( 'Subscription Product signup fees.', 'woocommerce-subscriptions' ), 'type' => array( 'string', 'null' ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'sign_up_fees_tax' => array( 'description' => __( 'Subscription Product signup fees taxes.', 'woocommerce-subscriptions' ), 'type' => array( 'string', 'null' ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), ); } /** * Format sign-up fees. * * @param \WC_Product $product current product. * @return array */ private static function format_sign_up_fees( $product ) { $fees_excluding_tax = wcs_get_price_excluding_tax( $product, array( 'qty' => 1, 'price' => WC_Subscriptions_Product::get_sign_up_fee( $product ), ) ); $fees_including_tax = wcs_get_price_including_tax( $product, array( 'qty' => 1, 'price' => WC_Subscriptions_Product::get_sign_up_fee( $product ), ) ); $money_formatter = self::$extend->get_formatter( 'money' ); return array( 'sign_up_fees' => $money_formatter->format( $fees_excluding_tax ), 'sign_up_fees_tax' => $money_formatter->format( $fees_including_tax - $fees_excluding_tax ), ); } } ``` ## Formatting your data You may wish to use our pre-existing Formatters to ensure your data is passed through the Store API in the correct format. More information on the Formatters can be found in the [StoreApi Formatters documentation](./extend-store-api-formatters.md). --- ## Available Formatters *Source: apis/store-api/extending-store-api/extend-store-api-formatters.md* # Available Formatters `Formatters` are utility classes that allow you to format values to so that they are compatible with the StoreAPI. Default formatters handle values such as monetary amounts, currency information, or HTML. It is recommended that you use these formatters when you are extending the StoreAPI. ## Why are formatters useful? Using the formatter utilities when returning certain types of data will ensure that your custom data is consistent and compatible with other endpoints. They also take care of any store specific settings that may affect the formatting of the data, such as currency settings. Store API includes formatters for: - [Money](#moneyformatter) - [Currency](#currencyformatter) - [HTML](#htmlformatter) ## How to use formatters To get a formatter, you can use the `get_formatter` method of the `ExtendSchema` class. This method accepts a string, which is the name of the formatter you want to use. ```php get_formatter('money'); // For the MoneyFormatter get_formatter('html'); // For the HtmlFormatter get_formatter('currency'); // CurrencyFormatter ``` This returns a `FormatterInterface` which has the `format` method. The `format` method signature is: ```php format( $value, array $options = [] ); ``` Only `MoneyFormatter`'s behavior can be controlled by the `$options` parameter. This parameter is optional. ### Real world example Let's say we're going to be returning some extra price data via the API. We want to return the price in cents, and also the currency data for the store. We can use the `MoneyFormatter` and `CurrencyFormatter` to do this. First we need to ensure we can access the formatter classes. We can do this by using the `use` keyword: ```php use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\Utilities\ExtendSchema; $extend = StoreApi::container()->get( ExtendSchema::class ); $my_custom_price = $extend->get_formatter( 'money' )->format( '10.00', [ 'rounding_mode' => PHP_ROUND_HALF_DOWN, 'decimals' => 2 ] ); $price_response = $extend->get_formatter( 'currency' )->format( [ 'price' => $my_custom_price, ] ); ``` The above code would result in `$price_response` being set to: ```text [ 'price' => '1000' 'currency_code' => 'GBP' 'currency_symbol' => '£' 'currency_minor_unit' => 2 'currency_decimal_separator' => '.' 'currency_thousand_separator' => ',' 'currency_prefix' => '£' 'currency_suffix' => '' ] ``` ## MoneyFormatter The [`MoneyFormatter`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/src/StoreApi/Formatters/MoneyFormatter.php) class can be used to format a monetary value using the store settings. The store settings may be overridden by passing options to this formatter's `format` method. Values are returned in cents to avoid floating point rounding errors, so when using this formatter you'll most likely also be returning the currency data using the [`CurrencyFormatter`](#currencyformatter) alongside it. This will allow the consumer of the API to display the value in the intended format. ### Arguments | Argument | Type | Description | | --------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$value` | `number` | The number you want to format into a monetary value | | `$options` | `array` | Should contain two keys, `decimals` which should be an `integer`, | | `$options['decimals']` | `number` | Used to control how many decimal places should be displayed in the monetary value. Defaults to the store setting. | | `$options['rounding_mode']` | `number` | Used to determine how to round the monetary value. This should be one of the PHP rounding modes described in the [PHP round() documentation](https://www.php.net/manual/en/function.round.php). Defaults to `PHP_ROUND_HALF_UP`. | ### Example use and returned value ```php get_formatter( 'money' )->format( 10.443, [ 'rounding_mode' => PHP_ROUND_HALF_DOWN, 'decimals' => 2 ] ); ``` returns `1044` ## CurrencyFormatter This formatter takes an array of prices, and returns the same array but with currency data appended to it. The currency data added is: | Key | Type | Description | | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------- | | `currency_code` | `string` | The string representation of the currency, e.g. GBP or USD | | `currency_symbol` | `string` | The symbol of the currency, e.g. £ or \$ | | `currency_minor_unit` | `number` | How many decimal places will be shown in the currency | | `currency_decimal_separator` | `string` | The string used to separate the whole value and the decimal value in the currency. | | `currency_thousand_separator` | `string` | The string used to separate thousands in the currency, for example: £10,000 or €10.000 | | `currency_prefix` | `string` | A string that should appear before the currency value. | | `currency_suffix` | `string` | A string that should appear after the currency value. | This data can then be used by the client/consumer to format prices correctly according to store settings. Important: the array of prices passed to this formatted should already be in monetary format so you should use the [`MoneyFormatter`](#moneyformatter) first. ### Arguments | Argument | Type | Description | | -------- | ---------- | ---------------------------------------------------------------------------- | | `$value` | `number[]` | An array of prices that you want to merge with the store's currency settings | ### Example use and returned value ```php get_formatter( 'currency' )->format( [ 'price' => 1800, 'regular_price' => 1800, 'sale_price' => 1800, ] ); ``` returns ```text 'price' => '1800' 'regular_price' => '1800' 'sale_price' => '1800' 'price_range' => null 'currency_code' => 'GBP' 'currency_symbol' => '£' 'currency_minor_unit' => 2 'currency_decimal_separator' => '.' 'currency_thousand_separator' => ',' 'currency_prefix' => '£' 'currency_suffix' => '' ``` ## HtmlFormatter This formatter will take an HTML value, run it through: [`wptexturize`](https://developer.wordpress.org/reference/functions/wptexturize/), [`convert_chars`](https://developer.wordpress.org/reference/functions/convert_chars/), [`trim`](https://www.php.net/manual/en/function.trim.php), and [`wp_kses_post`](https://developer.wordpress.org/reference/functions/wp_kses_post/) before returning it. The purpose of this formatter is to make HTML "safe" (in terms of correctly formatted characters). `wp_kses_post` will ensure only HTML tags allowed in the context of a `post` are present in the string. ### Arguments | Argument | Type | Description | | -------- | -------- | ----------------------------------------------- | | `$value` | `string` | The string you want to format into "safe" HTML. | ### Example use and returned value ```php get_formatter( 'html' )->format( " This \"coffee\" is very strong." ); ``` returns: ```text alert('bad script!') This “coffee” is very strong. ``` This formatter should be used when returning HTML from the StoreAPI regardless of whether the HTML is user generated or not. This will ensure the consumer/client can display the HTML safely and without encoding issues. --- ## Updating the cart on-demand *Source: apis/store-api/extending-store-api/extend-store-api-update-cart.md* # Updating the cart on-demand ## The problem You're an extension developer, and your extension does some server-side processing as a result of some client-side input, i.e. a shopper filling in an input field in the Cart sidebar, and then pressing a button. This server-side processing causes the state of the cart to change, and you want to update the data displayed in the client-side Cart or Checkout block. You can't simply update the client-side cart state yourself. This is restricted to prevent malfunctioning extensions inadvertently updating it with malformed or invalid data which will cause the whole block to break. ## The solution `ExtendSchema` offers the ability for extensions to register callback functions to be executed when signalled to do so by the client-side Cart or Checkout. WooCommerce Blocks also provides a front-end function called `extensionCartUpdate` which can be called by client-side code, this will send data (specified by you when calling `extensionCartUpdate`) to the `cart/extensions` endpoint. When this endpoint gets hit, any relevant (based on the namespace provided to `extensionCartUpdate`) callbacks get executed, and the latest server-side cart data gets returned and the block is updated with this new data. ## Basic usage In your extension's server-side integration code: ```php add_action('woocommerce_blocks_loaded', function() { woocommerce_store_api_register_update_callback( [ 'namespace' => 'extension-unique-namespace', 'callback' => /* Add your callable here */ ] ); } ); ``` and on the client side: ```ts const { extensionCartUpdate } = wc.blocksCheckout; const { processErrorResponse } = wc.wcBlocksData; extensionCartUpdate( { namespace: 'extension-unique-namespace', data: { key: 'value', another_key: 100, third_key: { fourth_key: true, }, }, } ).then( () => { // Cart has been updated. } ).catch( ( error ) => { // Handle error. processErrorResponse(error); } ); ``` ## Things to consider ### Extensions cannot update the client-side cart state themselves You may be wondering why it's not possible to just make a custom AJAX endpoint for your extension that will update the cart. As mentioned, extensions are not permitted to update the client-side cart's state, because doing this incorrectly would cause the entire block to break, preventing the user from continuing their checkout. Instead you _must_ do this through the `extensionCartUpdate` function. ### Only one callback for a given namespace may be registered With this in mind, if your extension has several client-side interactions that result in different code paths being executed on the server-side, you may wish to pass additional data through in `extensionCartUpdate`. For example if you have two actions the user can take, one to _add_ a discount, and the other to _remove_ it, you may wish to pass a key called `action` along with the other data to `extensionCartUpdate`. Then in your callback, you can check this value to distinguish which code path you should execute. Example: ```php 'extension-unique-namespace', 'callback' => function( $data ) { if ( $data['action'] === 'add' ) { add_discount( ); } if ( $data['action'] === 'remove' ) { remove_discount(); } } ] ); } ); ``` If you try to register again, under the same namespace, the previously registered callback will be overwritten. ## API Definition ### PHP `ExtendSchema::register_update_callback`: Used to register a callback to be executed when the `cart/extensions` endpoint gets hit with a given namespace. It takes an array of arguments | Attribute | Type | Required | Description | | ----------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `namespace` | `string` | Yes | The namespace of your extension. This is used to determine which extension's callbacks should be executed. | | `callback` | `Callable` | Yes | The function/method (or Callable) that will be executed when the `cart/extensions` endpoint is hit with a `namespace` that matches the one supplied. The callable should take a single argument. The data passed into the callback via this argument will be an array containing whatever data you choose to pass to it. The callable does not need to return anything, if it does, then its return value will not be used. | ### JavaScript `extensionCartUpdate`: Used to signal that you want your registered callback to be executed, and to pass data to the callback. It takes an object as its only argument. | Attribute | Type | Required | Description | | ----------- |-----------|----------| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `namespace` | `string` | Yes | The namespace of your extension. This is used to determine which extension's callbacks should be executed. | | `data` | `Object` | No | The data you want to pass to your callback. Anything in the `data` key will be passed as the first (and only) argument to your callback as an associative array. | | `overwriteDirtyCustomerData` | `boolean` | No | Whether to overwrite the customer data in the client with the data returned from the server, even if it is dirty (i.e. it hasn't been pushed to the server yet). | ## Putting it all together You are the author of an extension that lets the shopper redeem points that they earn on your website for a discount on their order. There is a text field where the shopper can enter how many points they want to redeem, and a submit button that will apply the redemption. Your extension adds these UI elements to the sidebar in the Cart and Checkout blocks using the [`DiscountsMeta`](/docs/block-development/extensible-blocks/cart-and-checkout-blocks/available-slot-fills/) Slot. More information on how to use Slots is available in our [Slots and Fills documentation](/docs/block-development/reference/slot-fills/). Once implemented, the sidebar has a control added to it like this: ![image](https://user-images.githubusercontent.com/5656702/125109827-bf7c8300-e0db-11eb-9e51-59921b38a0c2.png) ### The "Redeem" button In your UI, you are tracking the value the shopper enters into the `Enter amount` box using a React `useState` variable. The variable in this example shall be called `pointsInputValue`. When the `Redeem` button gets clicked, you want to tell the server how many points to apply to the shopper's basket, based on what they entered into the box, apply the relevant discount, update the server-side cart, and then show the updated price in the client-side sidebar. To do this, you will need to use `extensionCartUpdate` to tell the server you want to execute your callback, and have the new cart state loaded into the UI. The `onClick` handler of the button may look like this: ```js const { extensionCartUpdate } = window.wc.blocksCheckout; const buttonClickHandler = () => { extensionCartUpdate( { namespace: 'super-coupons', data: { pointsInputValue, }, } ); }; ``` ### Registering a callback to run when the `cart/extensions` endpoint is hit So far, we haven't registered a callback with WooCommerce Blocks yet, so when `extensionCartUpdate` causes the `cart/extensions` endpoint to get hit, nothing will happen. Much like adding data to the Store API (described in more detail in [Exposing your data in the Store API](./extend-store-api-add-data.md).) we can add the callback by invoking the `register_update_callback` method on the `ExtendSchema` class from WooCommerce Blocks. We have written a function called `redeem_points` which applies a discount to the WooCommerce cart. This function does not return anything. Note, the actual implementation of this function is not the focus of this document, so has been omitted. All that is important to note is that it modifies the WooCommerce cart. ```php 'super-coupons', 'callback' => function( $data ) { redeem_points( $data['points'] ); }, ] ); } ); ``` Now that this is registered, when the button is pressed, the `cart/extensions` endpoint is hit, with a `namespace` of `super-coupons` our `redeem_points` function will be executed. After this has finished processing, the client-side cart will be updated by WooCommerce Blocks. --- ## Store API Guiding principles *Source: apis/store-api/guiding-principles.md* # Store API Guiding principles The following principles should be considered when extending, creating, or updating endpoints in the Store API. ## Routes must include a [well-defined JSON schema](https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/) Each route/endpoint requires a particular structure of input data and should return data using a defined and predictable structure. This is defined in the JSON schema, which contains a comprehensive list of all of the properties the API can return and which input parameters it can accept. Well-defined schema also provides a layer of security, as it enables us to validate and sanitize the requests being made to the API. When defining schema, take note of the [WordPress REST API handbook](https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/) which documents available properties and types, as well as the [JSON schema standard](http://json-schema.org/). In addition to this: * Properties should use snake_case 🐍 * Ambiguous terms should be avoided, and property names should try to use understandable language, rather than "WooCommerce" terminology or setting names * Properties should be defined using US English, but the descriptions of fields should be localized * Multiple types are permitted, for example, using a `null` type if a value is not applicable * `sanitize_callback` and `validate_callback` are encouraged where possible to ensure data is received in the correct format before processing requests If you’re struggling to define a consistent schema, your approach may be flawed. A common real-world example of this would be representing something like _Post Tags_. It may be tempting to use the Slug as the property field name in the response: ```php tags: [ "my-tag": { // ...tag data }, "my-other-tag": { // ...tag data } ] ``` However, this is difficult to represent in Schema and is not predictable for the client. A better approach would be to use an array of data, with one of the properties being the Slug: ```php tags: [ { "slug": "my-tag", // ...tag data }, { "slug": "my-other-tag", // ...tag data } ] ``` ## Routes should be designed around resources with a single type of schema Routes should be designed around resources (nouns) rather than operations (verbs). Routes should also return only one type of data defined by their Schema. For example: | Route | Resource type | Expected data | | ------------------------ | ------------- | --------------------------- | | `wc/store/v1/cart` | Cart | A cart object | | `wc/store/v1/cart/items` | Cart Item | A list of cart item objects | | `wc/store/v1/products` | Product | A list of product objects | | `wc/store/v1/products/1` | Product | A product object | There are 2 notable exceptions to this rule in the Store API; _Errors_ and _Cart Operations_. ### Error Handling Errors, including validation errors, should return an error response code (4xx or 5xx) and a [`WP_Error` object](https://developer.wordpress.org/reference/classes/wp_error/). The `AbstractRoute` class will handle the conversion of the `WP_Error` object into a valid JSON response. Error messages should be localized, but do not need to be written with language aimed at customers (clients should use the given error code to create customer-facing notices as needed). Error codes should have the prefix `woocommerce_rest_`. ### Cart Operations Some endpoints are designed around operations to avoid clients needing to make multiple round trips to the API. This is purely for convenience. An example would be the `wc/store/v1/cart/add-item` endpoint which accepts a quantity and product ID, but returns a full cart object, rather than just an updated list of items. ## Exposed data must belong to the current user or be non-sensitive Resources, including customer and order data, should reflect only the current session. Do not return data for other customers as this would be a breach of privacy and security issue. Store data such as settings (for example, store currency) is permitted in responses, but _private or sensitive data_ must be avoided. To allow more extensive access to data, you must use the authenticated [WC REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction). Data returned from the API should not be [escaped](https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/) (this is left to the client rendering it), but it should be sanitized. For example, HTML should be run through [`wp_kses_post`](https://developer.wordpress.org/reference/functions/wp_kses_post/). It is the client’s responsibility to properly escape data that comes from the API, but we should try to avoid returning data that is potentially unsafe. ## Collections of resources should be paginated Large volumes of data should be paginated to avoid overwhelming the server. For example, returning a collection of products. * Use the response Headers `X-WP-Total`, `X-WP-TotalPages`, and Link to indicate available resources. * Use parameters `page` and `per_page` to retrieve certain pages. * The maximum allowed value for `per_page` is 100. ## API Responses should use standard HTTP status codes When returning content, use a valid HTTP response code such as: * `200 OK` for successful responses (this is the default response code). * `201 Created` when creating a resource, for example, adding a new cart item or applying a new coupon. * `204 No Content` for successful deletes. * `400 Bad Request` when a required parameter is not set. * `403 Forbidden` when a request is not allowed, for example, if the provided security nonce is invalid. * `404 Not Found` if a resource does not exist. * `409 Conflict` if a resource cannot be updated, for example, if something in the cart is invalid and removed during the request. A note on `DELETE` requests, a common pattern in the WordPress REST API is to return the deleted object. In the case of the Store API, we opt to return an empty response with status code `204 No Content` instead. This is more efficient. [A full list of HTTP status codes can be found here.](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) ## Breaking changes should be avoided where possible The Store API establishes a contract between itself and API consumers via the use of Schema. This contract should not be broken unless absolutely necessary. If a breaking change were necessary, a new version of the Store API would need to be released. A breaking change is anything that changes the format of existing Schema, removes a Schema property, removes an existing route, or makes a backwards-incompatible change to anything public that may already be in use by consumers. Breaking changes can be avoided by [deprecating existing properties](http://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.3) rather than removing them, or [deprecating routes](https://datatracker.ietf.org/doc/html/rfc8594) and replacing them with a different route if major changes are needed. Non-breaking changes are always permitted without the need to increase the API version. Some examples of these include: * Adding new properties to schema * Adding new routes, endpoints, methods * Adding optional request parameters * Re-ordering response fields The version will not increase for bug fixes unless the scope of the bug causes a backwards-incompatible change. Fixes would not be rolled back to past API versions with the exception of security issues that require backporting. --- ## Nonce Tokens *Source: apis/store-api/nonce-tokens.md* # Nonce Tokens Nonces are generated numbers used to verify origin and intent of requests for security purposes. You can read more about [nonces in the WordPress codex](https://developer.wordpress.org/apis/security/nonces/). ## Store API Endpoints that Require Nonces POST requests to the `/cart` endpoints and all requests to the `/checkout` endpoints require a nonce to function. Failure to provide a valid nonce will return an error response, unless you're using [Cart Tokens](/docs/apis/store-api/cart-tokens) instead. ## Sending Nonce Tokens with requests Nonce tokens are included with the request headers. Create a request header named `Nonce`. This will be validated by the API. **Example:** ```sh curl --header "Nonce: 12345" --request GET https://example-store.com/wp-json/wc/store/v1/checkout ``` After making a successful request, an updated `Nonce` header will be sent back--this needs to be stored and updated by the client to make subsequent requests. ## Generating security nonces from WordPress Nonces must be created using the [`wp_create_nonce` function](https://developer.wordpress.org/reference/functions/wp_create_nonce/) with the key `wc_store_api`. ```php wp_create_nonce( 'wc_store_api' ) ``` There is no other mechanism in place for creating nonces. ## Disabling Nonces for Development If you want to test REST endpoints without providing a nonce, you can use the following filter: ```php add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); ``` Nonce checks will be bypassed if `woocommerce_store_api_disable_nonce_check` evaluates to `true`. NOTE: This should only be done on development sites where security is not important. Do not enable this in production. --- ## Rate Limiting for Store API endpoints *Source: apis/store-api/rate-limiting.md* # Rate Limiting for Store API endpoints [Rate Limiting](https://github.com/woocommerce/woocommerce-blocks/pull/5962) is available for Store API endpoints. This is optional and disabled by default. It can be enabled by following [these instructions](#rate-limiting-options-filter). The main purpose prevent abuse on endpoints from excessive calls and performance degradation on the machine running the store. Rate limit tracking is controlled by either `USER ID` (logged in), `IP ADDRESS` (unauthenticated requests) or filter defined logic to fingerprint and group requests. It also offers standard support for running behind a proxy, load balancer, etc. This also optional and disabled by default. ## UI Control Currently, this feature is only controlled via the `woocommerce_store_api_rate_limit_options` filter. To control it via a UI, you can use the following community plugin: [Rate Limiting UI for WooCommerce](https://wordpress.org/plugins/rate-limiting-ui-for-woocommerce/). ## Checkout rate limiting You can enable rate limiting for Checkout place order and `POST /checkout` endpoint only via the UI by going to WooCommerce -> Settings -> Advanced -> Features and enabling "Rate limiting Checkout block and Store API". When enabled via the UI, the rate limiting will only be applied to the `POST /checkout` and Place Order flow for Checkout block. The limit will be a maximum of 3 requests per 60 seconds. ## Limit information A default maximum of 25 requests can be made within a 10-second time frame. These can be changed through an [options filter](#rate-limiting-options-filter). ## Methods restricted by Rate Limiting Only `POST` requests are rate limited. Requests using the `X-HTTP-Method-Override` header (such as `PUT`, `PATCH` requests sent via `wp.apiFetch`) are excluded from rate limiting. ## Rate Limiting options filter A filter is available for setting options for rate limiting: ```php add_filter( 'woocommerce_store_api_rate_limit_options', function() { return [ 'enabled' => false, // enables/disables Rate Limiting. Default: false 'proxy_support' => false, // enables/disables Proxy support. Default: false 'limit' => 25, // limit of request per timeframe. Default: 25 'seconds' => 10, // timeframe in seconds. Default: 10 ]; } ); ``` ## Proxy standard support If the Store is running behind a proxy, load balancer, cache service, CDNs, etc. keying limits by IP is supported through standard IP forwarding headers, namely: * `X_REAL_IP`|`CLIENT_IP` _Custom popular implementations that simplify obtaining the origin IP for the request_ * `X_FORWARDED_FOR` _De-facto standard header for identifying the originating IP, [Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)_ * `X_FORWARDED` _[Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded), [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)_ This is disabled by default. ## Enable Rate Limit by request custom fingerprinting For more advanced use cases, you can enable rate limiting by custom fingerprinting. This allows for a custom implementation to group requests without relying on logged-in User ID or IP Address. ### Custom basic example for grouping requests by User-Agent and Accept-Language combination ```php add_filter( 'woocommerce_store_api_rate_limit_id', function() { $accept_language = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; return md5( wc_get_user_agent() . $accept_language ); } ); ``` ## Limit usage information observability Current limit information can be observed via custom response headers: * `RateLimit-Limit` _Maximum requests per time frame._ * `RateLimit-Remaining` _Requests available during current time frame._ * `RateLimit-Reset` _Unix timestamp of next time frame reset._ * `RateLimit-Retry-After` _Seconds until requests are unblocked again. Only shown when the limit is reached._ ### Response headers example ```http RateLimit-Limit: 5 RateLimit-Remaining: 0 RateLimit-Reset: 1654880642 RateLimit-Retry-After: 28 ``` ## Tracking limit abuses This uses a modified wc_rate_limit table with an additional remaining column for tracking the request count in any given request window. A custom action `woocommerce_store_api_rate_limit_exceeded` was implemented for extendability in tracking such abuses. ### Custom tracking usage example ```php add_action( 'woocommerce_store_api_rate_limit_exceeded', function ( $offending_ip, $action_id ) { /* Custom tracking implementation */ } ); ``` --- ## Cart Coupons API *Source: apis/store-api/resources-endpoints/cart-coupons.md* # Cart Coupons API ## List Cart Coupons ```http GET /cart/coupons ``` There are no parameters required for this endpoint. ```sh curl "https://example-store.com/wp-json/wc/store/v1/cart/coupons" ``` **Example response:** ```json [ { "code": "20off", "type": "fixed_cart", "totals": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "total_discount": "1667", "total_discount_tax": "333" }, "_links": { "self": [ { "href": "http://local.wordpress.test/wp-json/wc/store/v1/cart/coupons/20off" } ], "collection": [ { "href": "http://local.wordpress.test/wp-json/wc/store/v1/cart/coupons" } ] } } ] ``` ## Single Cart Coupon Get a single cart coupon. ```http GET /cart/coupons/:code ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :---------------------------------------------- | | `code` | string | Yes | The coupon code of the cart coupon to retrieve. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/cart/coupons/20off" ``` **Example response:** ```json { "code": "halfprice", "type": "percent", "totals": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "total_discount": "9950", "total_discount_tax": "0" } } ``` ## Add Cart Coupon Apply a coupon to the cart. Returns the new coupon object that was applied, or an error if it was not applied. ```http POST /cart/coupons/ ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :--------------------------------------------- | | `code` | string | Yes | The coupon code you wish to apply to the cart. | ```sh curl --request POST https://example-store.com/wp-json/wc/store/v1/cart/coupons?code=20off ``` **Example response:** ```json { "code": "20off", "type": "percent", "totals": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "total_discount": "1667", "total_discount_tax": "333" } } ``` ## Delete Single Cart Coupon Delete/remove a coupon from the cart. ```http DELETE /cart/coupons/:code ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :------------------------------------------------ | | `code` | string | Yes | The coupon code you wish to remove from the cart. | ```sh curl --request DELETE https://example-store.com/wp-json/wc/store/v1/cart/coupons/20off ``` ## Delete All Cart Coupons Delete/remove all coupons from the cart. ```http DELETE /cart/coupons/ ``` There are no parameters required for this endpoint. ```sh curl --request DELETE https://example-store.com/wp-json/wc/store/v1/cart/coupons ``` **Example response:** ```json [] ``` --- ## Cart Items API *Source: apis/store-api/resources-endpoints/cart-items.md* # Cart Items API ## List Cart Items ```http GET /cart/items ``` There are no extra parameters needed to use this endpoint. ```sh curl "https://example-store.com/wp-json/wc/store/v1/cart/items" ``` **Example response:** ```json [ { "key": "c74d97b01eae257e44aa9d5bade97baf", "id": 16, "quantity": 1, "type": "simple", "quantity_limits": { "minimum": 1, "maximum": 1, "multiple_of": 1, "editable": false }, "name": "Beanie", "short_description": "

This is a simple product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "sku": "woo-beanie", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": true, "permalink": "https://store.local/product/beanie/", "images": [ { "id": 45, "src": "https://store.local/wp-content/uploads/2023/01/beanie-2.jpg", "thumbnail": "https://store.local/wp-content/uploads/2023/01/beanie-2-450x450.jpg", "srcset": "https://store.local/wp-content/uploads/2023/01/beanie-2.jpg 801w, https://store.local/wp-content/uploads/2023/01/beanie-2-450x450.jpg 450w, https://store.local/wp-content/uploads/2023/01/beanie-2-100x100.jpg 100w, https://store.local/wp-content/uploads/2023/01/beanie-2-600x600.jpg 600w, https://store.local/wp-content/uploads/2023/01/beanie-2-300x300.jpg 300w, https://store.local/wp-content/uploads/2023/01/beanie-2-150x150.jpg 150w, https://store.local/wp-content/uploads/2023/01/beanie-2-768x768.jpg 768w", "sizes": "(max-width: 801px) 100vw, 801px", "name": "beanie-2.jpg", "alt": "" } ], "variation": [], "item_data": [], "prices": { "price": "1800", "regular_price": "2000", "sale_price": "1800", "price_range": null, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "", "raw_prices": { "precision": 6, "price": "18000000", "regular_price": "20000000", "sale_price": "18000000" } }, "totals": { "line_subtotal": "1800", "line_subtotal_tax": "360", "line_total": "1800", "line_total_tax": "360", "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, "catalog_visibility": "visible", "extensions": {}, "_links": { "self": [ { "href": "https://store.local/wp-json/wc/store/v1/cart/items/c74d97b01eae257e44aa9d5bade97baf" } ], "collection": [ { "href": "https://store.local/wp-json/wc/store/v1/cart/items" } ] } }, { "key": "e03e407f41901484125496b5ec69a76f", "id": 29, "quantity": 1, "type": "variation", "quantity_limits": { "minimum": 1, "maximum": 9999, "multiple_of": 1, "editable": true }, "name": "Hoodie", "short_description": "", "description": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.

", "sku": "woo-hoodie-red", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": false, "permalink": "https://store.local/product/hoodie/?attribute_pa_color=red&attribute_logo=No", "images": [ { "id": 40, "src": "https://store.local/wp-content/uploads/2023/01/hoodie-2.jpg", "thumbnail": "https://store.local/wp-content/uploads/2023/01/hoodie-2-450x450.jpg", "srcset": "https://store.local/wp-content/uploads/2023/01/hoodie-2.jpg 801w, https://store.local/wp-content/uploads/2023/01/hoodie-2-450x450.jpg 450w, https://store.local/wp-content/uploads/2023/01/hoodie-2-100x100.jpg 100w, https://store.local/wp-content/uploads/2023/01/hoodie-2-600x600.jpg 600w, https://store.local/wp-content/uploads/2023/01/hoodie-2-300x300.jpg 300w, https://store.local/wp-content/uploads/2023/01/hoodie-2-150x150.jpg 150w, https://store.local/wp-content/uploads/2023/01/hoodie-2-768x768.jpg 768w", "sizes": "(max-width: 801px) 100vw, 801px", "name": "hoodie-2.jpg", "alt": "" } ], "variation": [ { "raw_attribute": "attribute_pa_color", "attribute": "Color", "value": "Red" }, { "raw_attribute": "attribute_logo", "attribute": "Logo", "value": "No" } ], "item_data": [], "prices": { "price": "4200", "regular_price": "4500", "sale_price": "4200", "price_range": null, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "", "raw_prices": { "precision": 6, "price": "42000000", "regular_price": "45000000", "sale_price": "42000000" } }, "totals": { "line_subtotal": "4200", "line_subtotal_tax": "840", "line_total": "4200", "line_total_tax": "840", "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, "catalog_visibility": "visible", "extensions": {}, "_links": { "self": [ { "href": "https://store.local/wp-json/wc/store/v1/cart/items/e03e407f41901484125496b5ec69a76f" } ], "collection": [ { "href": "https://store.local/wp-json/wc/store/v1/cart/items" } ] } } ] ``` ## Single Cart Item Get a single cart item by its key. ```http GET /cart/items/:key ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :------------------------------------ | | `key` | string | Yes | The key of the cart item to retrieve. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/cart/items/c74d97b01eae257e44aa9d5bade97baf" ``` **Example response:** ```json { "key": "c74d97b01eae257e44aa9d5bade97baf", "id": 16, "quantity": 1, "quantity_limits": { "minimum": 1, "maximum": 1, "multiple_of": 1, "editable": false }, "name": "Beanie", "short_description": "

This is a simple product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "sku": "woo-beanie", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": true, "permalink": "https://store.local/product/beanie/", "images": [ { "id": 45, "src": "https://store.local/wp-content/uploads/2023/01/beanie-2.jpg", "thumbnail": "https://store.local/wp-content/uploads/2023/01/beanie-2-450x450.jpg", "srcset": "https://store.local/wp-content/uploads/2023/01/beanie-2.jpg 801w, https://store.local/wp-content/uploads/2023/01/beanie-2-450x450.jpg 450w, https://store.local/wp-content/uploads/2023/01/beanie-2-100x100.jpg 100w, https://store.local/wp-content/uploads/2023/01/beanie-2-600x600.jpg 600w, https://store.local/wp-content/uploads/2023/01/beanie-2-300x300.jpg 300w, https://store.local/wp-content/uploads/2023/01/beanie-2-150x150.jpg 150w, https://store.local/wp-content/uploads/2023/01/beanie-2-768x768.jpg 768w", "sizes": "(max-width: 801px) 100vw, 801px", "name": "beanie-2.jpg", "alt": "" } ], "variation": [], "item_data": [], "prices": { "price": "1800", "regular_price": "2000", "sale_price": "1800", "price_range": null, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "", "raw_prices": { "precision": 6, "price": "18000000", "regular_price": "20000000", "sale_price": "18000000" } }, "totals": { "line_subtotal": "1800", "line_subtotal_tax": "360", "line_total": "1800", "line_total_tax": "360", "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, "catalog_visibility": "visible", "extensions": {}, "_links": { "self": [ { "href": "https://store.local/wp-json/wc/store/v1/cart/items/(?P[\\w-]{32})/c74d97b01eae257e44aa9d5bade97baf" } ], "collection": [ { "href": "https://store.local/wp-json/wc/store/v1/cart/items/(?P[\\w-]{32})" } ] } } ``` ## Add Cart Item Add an item to the cart. Returns the new cart item that was added, or an error response. ```http POST /cart/items/ ``` | Attribute | Type | Required | Description | | :---------- | :------ | :------: | :--------------------------------------------------------------------------------------------------- | | `id` | integer | Yes | The cart item product or variation ID. | | `quantity` | integer | Yes | Quantity of this item in the cart. | | `variation` | array | Yes | Chosen attributes (for variations) containing an array of objects with keys `attribute` and `value`. | ```sh curl --request POST https://example-store.com/wp-json/wc/store/v1/cart/items?id=100&quantity=1 ``` For an example response, see [Single Cart Item](#single-cart-item). If you're looking to add multiple items to the cart at once, please take a look at [batching](/docs/apis/store-api/resources-endpoints/cart#add-item). ## Edit Single Cart Item Edit an item in the cart. ```http PUT /cart/items/:key ``` | Attribute | Type | Required | Description | | :--------- | :------ | :------: | :--------------------------------- | | `key` | string | Yes | The key of the cart item to edit. | | `quantity` | integer | Yes | Quantity of this item in the cart. | ```sh curl --request PUT https://example-store.com/wp-json/wc/store/v1/cart/items/e369853df766fa44e1ed0ff613f563bd?quantity=10 ``` For an example response, see [Single Cart Item](#single-cart-item). ## Delete Single Cart Item Removes an item from the cart by its key. ```http DELETE /cart/items/:key ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :-------------------------------- | | `key` | string | Yes | The key of the cart item to edit. | ```sh curl --request DELETE https://example-store.com/wp-json/wc/store/v1/cart/items/e369853df766fa44e1ed0ff613f563bd ``` ## Delete All Cart Items Removes all items from the cart at once. ```http DELETE /cart/items/ ``` There are no extra parameters needed to use this endpoint. ```sh curl --request DELETE https://example-store.com/wp-json/wc/store/v1/cart/items ``` **Example response:** ```json [] ``` --- ## Cart API *Source: apis/store-api/resources-endpoints/cart.md* # Cart API The cart API returns the current state of the cart for the current session or logged in user. All POST endpoints require a [Nonce Token](/docs/apis/store-api/nonce-tokens) or a [Cart Token](/docs/apis/store-api/cart-tokens) and return the updated state of the full cart once complete. ## Get Cart ```http GET /cart ``` There are no parameters required for this endpoint. ```sh curl "https://example-store.com/wp-json/wc/store/v1/cart" ``` Returns the full cart object response (see [Cart Response](#cart-response)). ## Responses All endpoints under `/cart` (listed in this doc) return responses in the same format; a cart object which includes cart items, applied coupons, shipping addresses and rates, and non-sensitive customer data. ### Cart Response ```json { "items": [ { "key": "a5771bce93e200c36f7cd9dfd0e5deaa", "id": 38, "quantity": 1, "quantity_limits": { "minimum": 1, "maximum": 9999, "multiple_of": 1, "editable": true }, "name": "Beanie with Logo", "short_description": "

This is a simple product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "sku": "Woo-beanie-logo", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": false, "permalink": "https://local.wordpress.test/product/beanie-with-logo/", "images": [ { "id": 61, "src": "https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-450x450.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-450x450.jpg 450w, https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-600x600.jpg 600w, https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2023/03/beanie-with-logo-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "beanie-with-logo-1.jpg", "alt": "" } ], "variation": [], "item_data": [], "prices": { "price": "1800", "regular_price": "2000", "sale_price": "1800", "price_range": null, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "", "raw_prices": { "precision": 6, "price": "18000000", "regular_price": "20000000", "sale_price": "18000000" } }, "totals": { "line_subtotal": "1800", "line_subtotal_tax": "180", "line_total": "1530", "line_total_tax": "153", "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, "catalog_visibility": "visible", "extensions": {} }, { "key": "b6d767d2f8ed5d21a44b0e5886680cb9", "id": 22, "quantity": 1, "quantity_limits": { "minimum": 1, "maximum": 9999, "multiple_of": 1, "editable": true }, "name": "Belt", "short_description": "

This is a simple product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "sku": "woo-belt", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": false, "permalink": "https://local.wordpress.test/product/belt/", "images": [ { "id": 51, "src": "https://local.wordpress.test/wp-content/uploads/2023/03/belt-2.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-450x450.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2023/03/belt-2.jpg 801w, https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-450x450.jpg 450w, https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-600x600.jpg 600w, https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2023/03/belt-2-768x768.jpg 768w", "sizes": "(max-width: 801px) 100vw, 801px", "name": "belt-2.jpg", "alt": "" } ], "variation": [], "item_data": [], "prices": { "price": "5500", "regular_price": "6500", "sale_price": "5500", "price_range": null, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "", "raw_prices": { "precision": 6, "price": "55000000", "regular_price": "65000000", "sale_price": "55000000" } }, "totals": { "line_subtotal": "5500", "line_subtotal_tax": "550", "line_total": "4675", "line_total_tax": "468", "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, "catalog_visibility": "visible", "extensions": {} } ], "coupons": [ { "code": "test", "discount_type": "percent", "totals": { "total_discount": "1095", "total_discount_tax": "109", "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" } } ], "fees": [], "totals": { "total_items": "7300", "total_items_tax": "730", "total_fees": "0", "total_fees_tax": "0", "total_discount": "1095", "total_discount_tax": "110", "total_shipping": "1300", "total_shipping_tax": "130", "total_price": "8256", "total_tax": "751", "tax_lines": [ { "name": "Tax", "price": "751", "rate": "10%" } ], "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, "shipping_address": { "first_name": "John", "last_name": "Doe", "company": "", "address_1": "Hello street", "address_2": "", "city": "beverly hills", "state": "CA", "postcode": "90211", "country": "US", "phone": "123456778" }, "billing_address": { "first_name": "John", "last_name": "Doe", "company": "", "address_1": "Hello street", "address_2": "", "city": "beverly hills", "state": "CA", "postcode": "90211", "country": "US", "email": "checkout@templates.com", "phone": "123456778" }, "needs_payment": true, "needs_shipping": true, "payment_requirements": [ "products" ], "has_calculated_shipping": true, "shipping_rates": [ { "package_id": 0, "name": "Shipment 1", "destination": { "address_1": "Hello street", "address_2": "", "city": "beverly hills", "state": "CA", "postcode": "90211", "country": "US" }, "items": [ { "key": "a5771bce93e200c36f7cd9dfd0e5deaa", "name": "Beanie with Logo", "quantity": 1 }, { "key": "b6d767d2f8ed5d21a44b0e5886680cb9", "name": "Belt", "quantity": 1 } ], "shipping_rates": [ { "rate_id": "flat_rate:10", "name": "Flat rate", "description": "", "delivery_time": "", "price": "1300", "taxes": "130", "instance_id": 10, "method_id": "flat_rate", "meta_data": [ { "key": "Items", "value": "Beanie with Logo × 1, Belt × 1" } ], "selected": true, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, { "rate_id": "free_shipping:12", "name": "Free shipping", "description": "", "delivery_time": "", "price": "0", "taxes": "0", "instance_id": 12, "method_id": "free_shipping", "meta_data": [ { "key": "Items", "value": "Beanie with Logo × 1, Belt × 1" } ], "selected": false, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" }, { "rate_id": "local_pickup:13", "name": "Local pickup", "description": "", "delivery_time": "", "price": "0", "taxes": "0", "instance_id": 13, "method_id": "local_pickup", "meta_data": [ { "key": "Items", "value": "Beanie with Logo × 1, Belt × 1" } ], "selected": false, "currency_code": "USD", "currency_symbol": "$", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "$", "currency_suffix": "" } ] } ], "items_count": 2, "items_weight": 0, "cross_sells": [], "errors": [], "payment_methods": [ "bacs", "cod" ], "extensions": {} } ``` ### Error Response If a cart action cannot be performed, an error response will be returned. This will include a reason code and an error message: ```json { "code": "woocommerce_rest_cart_invalid_product", "message": "This product cannot be added to the cart.", "data": { "status": 400 } } ``` Some error responses indicate conflicts (error 409), for example, when an item cannot be found or a coupon is no longer applied. When this type of response is returned, the current state of the cart from the server is also returned as part of the error data: ```json { "code": "woocommerce_rest_cart_invalid_key", "message": "Cart item no longer exists or is invalid.", "data": { "status": 409, "cart": { ... } } } ``` This allows the client to remain in sync with the cart data without additional requests, should the cart change or become outdated. ## Add Item Add an item to the cart and return the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/add-item ``` | Attribute | Type | Required | Description | | :---------- | :------ | :------: | :---------------------------------------------------------------------------------------------------------------------------------------- | | `id` | integer | Yes | The cart item product or variation ID. | | `quantity` | integer | Yes | Quantity of this item in the cart. | | `variation` | array | Yes | Chosen attributes (for variations) containing an array of objects with keys `attribute` and `value`. See notes on attribute naming below. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/cart/add-item?id=100&quantity=1 ``` Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. If you want to add supplemental cart item data before it is passed into `CartController::add_to_cart` use the [`woocommerce_store_api_add_to_cart_data`](https://github.com/woocommerce/woocommerce-blocks/blob/4d1c295a2bace9a4f6397cfd5469db31083d477a/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_store_api_add_to_cart_data) filter. For example: ```php add_filter( 'woocommerce_store_api_add_to_cart_data', function( $add_to_cart_data, \WP_REST_Request $request ) { if ( ! empty( $request['custom-request-param'] ) ) { $add_to_cart_data['cart_item_data']['custom-request-data'] = sanitize_text_field( $request['custom-request-param'] ); } return $add_to_cart_data; }, 10, 2 ); ``` **Variation attribute naming:** When adding variations to the cart, the naming of the attribute is important. For global attributes, the attribute posted to the API should be the slug of the attribute. This should have a `pa_` prefix. For example, if you have an attribute named `Color`, the slug will be `pa_color`. For product specific attributes, the attribute posted to the API can be one of the following: - the name of the attribute. For example, if you have an attribute named `Size`, the name will be `Size`. This is case-sensitive. - the slug of the attribute. For example, if you have an attribute named `Autograph ✏️`, the name will be `attribute_autograph-%e2%9c%8f%ef%b8%8f`. This is case-sensitive. You can get this slug from the related `select` on the product page. **Example POST body:** ```json { "id": 13, "quantity": 1, "variation": [ { "attribute": "pa_color", "value": "blue" }, { "attribute": "attribute_autograph-%e2%9c%8f%ef%b8%8f", "value": "Yes" }, { "attribute": "Logo", "value": "Yes" } ] } ``` The above example adds a product variation to the cart with attributes size and color. **Batching:** If you want to add multiple items at once, you need to use the batch endpoint: ```http POST /wc/store/v1/batch ``` The JSON payload for adding multiple items to the cart would look like this: ```json { "requests": [ { "path": "/wc/store/v1/cart/add-item", "method": "POST", "cache": "no-store", "body": { "id": 26, "quantity": 1 }, "headers": { "Nonce": "1db1d13784" } }, { "path": "/wc/store/v1/cart/add-item", "method": "POST", "cache": "no-store", "body": { "id": 27, "quantity": 1 }, "headers": { "Nonce": "1db1d13784" } } ] } ``` ## Remove Item Remove an item from the cart and return the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/remove-item ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :-------------------------------- | | `key` | string | Yes | The key of the cart item to edit. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/cart/remove-item?key=e369853df766fa44e1ed0ff613f563bd ``` Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. ## Update Item Update an item in the cart and return the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/update-item ``` | Attribute | Type | Required | Description | | :--------- | :------ | :------: | :--------------------------------- | | `key` | string | Yes | The key of the cart item to edit. | | `quantity` | integer | Yes | Quantity of this item in the cart. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/cart/update-item?key=e369853df766fa44e1ed0ff613f563bd&quantity=10 ``` Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. ## Apply Coupon Apply a coupon to the cart and return the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/apply-coupon/ ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :--------------------------------------------- | | `code` | string | Yes | The coupon code you wish to apply to the cart. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/cart/apply-coupon?code=20off ``` Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. ## Remove Coupon Remove a coupon from the cart and return the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/remove-coupon/ ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :------------------------------------------------ | | `code` | string | Yes | The coupon code you wish to remove from the cart. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/cart/remove-coupon?code=20off ``` Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. ## Update Customer Update customer data and return the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/update-customer ``` | Attribute | Type | Required | Description | | :---------------------------- | :----- | :------: | :--------------------------------------------------------------------------------------- | | `billing_address` | object | no | Customer billing address. | | `billing_address.first_name` | string | no | Customer first name. | | `billing_address.last_name` | string | no | Customer last name. | | `billing_address.address_1` | string | no | First line of the address being shipped to. | | `billing_address.address_2` | string | no | Second line of the address being shipped to. | | `billing_address.city` | string | no | City of the address being shipped to. | | `billing_address.state` | string | no | ISO code, or name, for the state, province, or district of the address being shipped to. | | `billing_address.postcode` | string | no | Zip or Postcode of the address being shipped to. | | `billing_address.country` | string | no | ISO code for the country of the address being shipped to. | | `billing_address.email` | string | no | Email for the customer. | | `billing_address.phone` | string | no | Phone number of the customer. | | `shipping_address` | object | no | Customer shipping address. | | `shipping_address.first_name` | string | no | Customer first name. | | `shipping_address.last_name` | string | no | Customer last name. | | `shipping_address.address_1` | string | no | First line of the address being shipped to. | | `shipping_address.address_2` | string | no | Second line of the address being shipped to. | | `shipping_address.city` | string | no | City of the address being shipped to. | | `shipping_address.state` | string | no | ISO code, or name, for the state, province, or district of the address being shipped to. | | `shipping_address.postcode` | string | no | Zip or Postcode of the address being shipped to. | | `shipping_address.country` | string | no | ISO code for the country of the address being shipped to. | Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. ## Select Shipping Rate Selects an available shipping rate for a package, then returns the full cart response, or an error. This endpoint will return an error unless a valid [Nonce Token](/docs/apis/store-api/nonce-tokens) or [Cart Token](/docs/apis/store-api/cart-tokens) is provided. ```http POST /cart/select-shipping-rate ``` | Attribute | Type | Required | Description | | :----------- | :------ | :------: | :---------------------------------------------- | | `package_id` | integer | yes | The ID of the shipping package within the cart. | | `rate_id` | string | yes | The chosen rate ID for the package. | ```sh curl --header "Nonce: 12345" --request POST /cart/select-shipping-rate?package_id=1&rate_id=flat_rate:1 ``` Returns the full [Cart Response](#cart-response) on success, or an [Error Response](#error-response) on failure. --- ## Checkout order API *Source: apis/store-api/resources-endpoints/checkout-order.md* # Checkout order API The checkout order API facilitates the processing of existing orders and handling payments. All checkout order endpoints require a [Nonce Token](/docs/apis/store-api/nonce-tokens) or a [Cart Token](/docs/apis/store-api/cart-tokens) otherwise these endpoints will return an error. ## Process Order and Payment Accepts the final chosen payment method, and any additional payment data, then attempts payment and returns the result. ```http POST /wc/store/v1/checkout/{ORDER_ID} ``` | Attribute | Type | Required | Description | | :----------------- | :----- | :------: | :------------------------------------------------------------------ | | `key` | string | Yes | The key for the order verification. | | `billing_email` | string | No | The email address used to verify guest orders. | | `billing_address` | object | Yes | Object of updated billing address data for the customer. | | `shipping_address` | object | Yes | Object of updated shipping address data for the customer. | | `payment_method` | string | Yes | The ID of the payment method being used to process the payment. | | `payment_data` | array | No | Data to pass through to the payment method when processing payment. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/checkout/{ORDER_ID} -d '{"key":"wc_order_oFmQYREzh9Tfv","billing_email":"admin@example.com","payment_method":"cheque","billing_address":{...},"shipping_address":{...}' ``` **Example request:** ```json { "key": "wc_order_oFmQYREzh9Tfv", "billing_email": "admin@example.com", "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "phone": "555-2368" }, "payment_method": "cheque", "payment_data": [] } ``` **Example response:** ```json { "order_id": 146, "status": "on-hold", "order_key": "wc_order_oFmQYREzh9Tfv", "customer_note": "", "customer_id": 1, "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "phone": "555-2368" }, "payment_method": "cheque", "payment_result": { "payment_status": "success", "payment_details": [], "redirect_url": "https://local.wordpress.test/block-checkout/order-received/146/?key=wc_order_VPffqyvgWVqWL" } } ``` ## Payment Data There are many payment gateways available for merchants to use, and each one will be expecting different `payment_data`. We cannot comprehensively list all expected requests for all payment gateways, and we would recommend reaching out to the authors of the payment gateway plugins you're working with for further information. An example of the payment data sent to the Checkout Order endpoint when using the [WooCommerce Stripe Payment Gateway](https://wordpress.org/plugins/woocommerce-gateway-stripe/) is shown below. For further information on generating a `stripe_source` please check [the Stripe documentation](https://stripe.com/docs). ```json { "payment_data": [ { "key": "stripe_source", "value": "src_xxxxxxxxxxxxx" }, { "key": "billing_email", "value": "myemail@email.com" }, { "key": "billing_first_name", "value": "Jane" }, { "key": "billing_last_name", "value": "Doe" }, { "key": "paymentMethod", "value": "stripe" }, { "key": "paymentRequestType", "value": "cc" }, { "key": "wc-stripe-new-payment-method", "value": true } ] } ``` --- ## Checkout API *Source: apis/store-api/resources-endpoints/checkout.md* # Checkout API The checkout API facilitates the creation of orders (from the current cart) and handling payments for payment methods. All checkout endpoints require either a [Nonce Token](/docs/apis/store-api/nonce-tokens) or a [Cart Token](/docs/apis/store-api/cart-tokens) otherwise these endpoints will return an error. ## Get Checkout Data Returns data required for the checkout. This includes a draft order (created from the current cart) and customer billing and shipping addresses. The payment information will be empty, as it's only persisted when the order gets updated via POST requests (right before payment processing). ```http GET /wc/store/v1/checkout ``` There are no parameters required for this endpoint. ```sh curl --header "Nonce: 12345" --request GET https://example-store.com/wp-json/wc/store/v1/checkout ``` ### Example Response ```json { "order_id": 146, "status": "checkout-draft", "order_key": "wc_order_VPffqyvgWVqWL", "customer_note": "", "customer_id": 1, "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US" }, "payment_method": "", "payment_result": { "payment_status": "", "payment_details": [], "redirect_url": "" } } ``` ## Update checkout data This endpoint allows you to update the checkout data for the current order. This can be called from the frontend to persist checkout fields, for example. ```http PUT /wc/store/v1/checkout?__experimental_calc_totals=true ``` Note the `__experimental_calc_totals` parameter. This is used to determine if the cart totals should be recalculated. This should be set to true if the cart totals are being updated in response to a PUT request, false otherwise. | Attribute | Type | Required | Description | | :------------------ | :----- | :------: | :-------------------------------------------------- | | `additional_fields` | object | No | Name => value pairs of additional fields to update. | | `payment_method` | string | No | The ID of the payment method selected. | | `order_notes` | string | No | Order notes. | ```sh curl --header "Nonce: 12345" --request PUT https://example-store.com/wp-json/wc/store/v1/checkout?additional_fields[plugin-namespace/leave-on-porch]=true&additional_fields[plugin-namespace/location-on-porch]=dsdd&payment_method=bacs&order_notes=Please%20leave%20package%20on%20back%20porch ``` ### Example Request ```json { "additional_fields": { "plugin-namespace/leave-on-porch": true, "plugin-namespace/location-on-porch": "dsdd" }, "payment_method": "bacs", "order_notes": "Please leave package on back porch" } ``` ### Example Response ```json { "order_id": 1486, "status": "checkout-draft", "order_key": "wc_order_KLpMaJ054PVlb", "order_number": "1486", "customer_note": "", "customer_id": 1, "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US" }, "payment_method": "bacs", "payment_result": null, "additional_fields": { "plugin-namespace/leave-on-porch": true, "plugin-namespace/location-on-porch": "dsdd" }, "__experimentalCart": { ... }, "extensions": {} } ``` Note the `__experimentalCart` field that is returned as part of the response. Totals will be updated on the front-end following a PUT request. This makes it possible to manipulate cart totals in response to fields persisted via the PUT request. ## Process Order and Payment Accepts the final customer addresses and chosen payment method, and any additional payment data, then attempts payment and returns the result. ```http POST /wc/store/v1/checkout ``` | Attribute | Type | Required | Description | | :------------------ | :----- | :------: | :------------------------------------------------------------------ | | `billing_address` | object | Yes | Object of updated billing address data for the customer. | | `shipping_address` | object | Yes | Object of updated shipping address data for the customer. | | `customer_note` | string | No | Note added to the order by the customer during checkout. | | `payment_method` | string | Yes | The ID of the payment method being used to process the payment. | | `payment_data` | array | No | Data to pass through to the payment method when processing payment. | | `customer_password` | string | No | Optionally define a password for new accounts. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/checkout?payment_method=paypal&payment_data[0][key]=test-key&payment_data[0][value]=test-value ``` ### Example Request ```json { "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US" }, "customer_note": "Test notes on order.", "create_account": false, "payment_method": "cheque", "payment_data": [], "extensions": { "some-extension-name": { "some-data-key": "some data value" } } } ``` ### Example Response ```json { "order_id": 146, "status": "on-hold", "order_key": "wc_order_VPffqyvgWVqWL", "customer_note": "", "customer_id": 1, "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US" }, "payment_method": "cheque", "payment_result": { "payment_status": "success", "payment_details": [], "redirect_url": "https://local.wordpress.test/block-checkout/order-received/146/?key=wc_order_VPffqyvgWVqWL" } } ``` ## Payment Data There are many payment gateways available for merchants to use, and each one will be expecting different `payment_data`. We cannot comprehensively list all expected requests for all payment gateways, and we would recommend reaching out to the authors of the payment gateway plugins you're working with for further information. An example of the payment data sent to the Checkout endpoint when using the [WooCommerce Stripe Payment Gateway](https://wordpress.org/plugins/woocommerce-gateway-stripe/) is shown below. For further information on generating a `stripe_source` please check [the Stripe documentation](https://stripe.com/docs). ```json { "payment_data": [ { "key": "stripe_source", "value": "src_xxxxxxxxxxxxx" }, { "key": "billing_email", "value": "myemail@email.com" }, { "key": "billing_first_name", "value": "Jane" }, { "key": "billing_last_name", "value": "Doe" }, { "key": "paymentMethod", "value": "stripe" }, { "key": "paymentRequestType", "value": "cc" }, { "key": "wc-stripe-new-payment-method", "value": true } ] } ``` --- ## Order API *Source: apis/store-api/resources-endpoints/order.md* # Order API The order API returns the pay-for-order order. ## Get Order ```http GET /order/{ORDER_ID}?key={KEY}&billing_email={BILLING_EMAIL} ``` There is one required parameter for this endpoint which is `key`. `billing_email` must be added for guest orders. ```sh curl "https://example-store.com/wp-json/wc/store/v1/order/{ORDER_ID}?key={KEY}&billing_email={BILLING_EMAIL}" ``` Returns the full order object response (see [Order Response](#order-response)). ## Responses Order endpoints return responses in the same format as `/cart`; an order object which includes order items, applied coupons, shipping addresses and rates, and non-sensitive customer data. ### Order Response ```json { "id": 147, "status": "pending", "coupons": [ { "code": "discount20", "totals": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "total_discount": "421", "total_discount_tax": "0" } } ], "shipping_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "phone": "555-2368" }, "billing_address": { "first_name": "Peter", "last_name": "Venkman", "company": "", "address_1": "550 Central Park West", "address_2": "Corner Penthouse Spook Central", "city": "New York", "state": "NY", "postcode": "10023", "country": "US", "email": "admin@example.com", "phone": "555-2368" }, "items": [ { "key": "9bf31c7ff062936a96d3c8bd1f8f2ff3", "id": 15, "quantity": 1, "quantity_limits": { "minimum": 1, "maximum": 99, "multiple_of": 1, "editable": true }, "name": "Beanie", "short_description": "

This is a simple product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "sku": "woo-beanie", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": false, "permalink": "https://local.wordpress.test/product/beanie/", "images": [ { "id": 44, "src": "https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-324x324.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2.jpg 801w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/beanie-2-768x768.jpg 768w", "sizes": "(max-width: 801px) 100vw, 801px", "name": "beanie-2.jpg", "alt": "" } ], "variation": [], "prices": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "price": "1000", "regular_price": "2000", "sale_price": "1000", "price_range": null, "raw_prices": { "precision": 6, "price": "10000000", "regular_price": "20000000", "sale_price": "10000000" } }, "item_data": [], "totals": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "line_subtotal": "1000", "line_subtotal_tax": "0", "line_total": "800", "line_total_tax": "0" }, "catalog_visibility": "view" }, { "key": "e369853df766fa44e1ed0ff613f563bd", "id": 34, "quantity": 1, "quantity_limits": { "minimum": 1, "maximum": 99, "multiple_of": 1, "editable": true }, "name": "WordPress Pennant", "short_description": "

This is an external product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "sku": "wp-pennant", "low_stock_remaining": null, "backorders_allowed": false, "show_backorder_badge": false, "sold_individually": false, "permalink": "https://local.wordpress.test/product/wordpress-pennant/", "images": [ { "id": 57, "src": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "pennant-1.jpg", "alt": "" } ], "variation": [], "prices": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "price": "1105", "regular_price": "1105", "sale_price": "1105", "price_range": null, "raw_prices": { "precision": 6, "price": "11050000", "regular_price": "11050000", "sale_price": "11050000" } }, "item_data": [], "totals": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "line_subtotal": "1105", "line_subtotal_tax": "0", "line_total": "884", "line_total_tax": "0" }, "catalog_visibility": "view" } ], "needs_payment": true, "needs_shipping": true, "totals": { "subtotal":"2105", "total_discount": "421", "total_shipping": "500", "total_fees": "0", "total_tax": "0", "total_refund": "0", "total_price": "2184", "total_items": "2105", "total_items_tax": "0", "total_fees_tax": "0", "total_discount_tax": "0", "total_shipping_tax": "0", "tax_lines": [] }, "errors": [], "payment_requirements": [ "products" ], } ``` ### Error Response If an order action cannot be performed, an error response will be returned. This will include a reason code and an error message: ```json { "code": "woocommerce_rest_invalid_order", "message": "Invalid order ID or key provided.", "data": { "status": 401 } } ``` --- ## Product Attribute Terms API *Source: apis/store-api/resources-endpoints/product-attribute-terms.md* # Product Attribute Terms API ```http GET /products/attributes/:id/terms GET /products/attributes/:id/terms?orderby=slug ``` | Attribute | Type | Required | Description | | :-------- | :------ | :------: |:--------------------------------------------------------------------------------------------------------------| | `id` | integer | Yes | The ID of the attribute to retrieve terms for. | | `order` | string | no | Order ascending or descending. Allowed values: `asc`, `desc` | | `orderby` | string | no | Sort collection by object attribute. Allowed values: `id`, `name`, `name_num`, `slug`, `count`, `menu_order`. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/attributes/1/terms" ``` **Example response:** ```json [ { "id": 22, "name": "Blue", "slug": "blue", "count": 5 }, { "id": 48, "name": "Burgundy", "slug": "burgundy", "count": 1 } ] ``` --- ## Product Attributes API *Source: apis/store-api/resources-endpoints/product-attributes.md* # Product Attributes API ## List Product Attributes ```http GET /products/attributes ``` There are no parameters required for this endpoint. ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/attributes" ``` Example response: ```json [ { "id": 1, "name": "Color", "taxonomy": "pa_color", "type": "select", "order": "menu_order", "has_archives": false }, { "id": 2, "name": "Size", "taxonomy": "pa_size", "type": "select", "order": "menu_order", "has_archives": false } ] ``` ## Single Product Attribute Get a single attribute taxonomy. ```http GET /products/attributes/:id ``` | Attribute | Type | Required | Description | | :-------- | :------ | :------: | :----------------------------------- | | `id` | integer | Yes | The ID of the attribute to retrieve. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/attributes/1" ``` **Example response:** ```json { "id": 1, "name": "Color", "taxonomy": "pa_color", "type": "select", "order": "menu_order", "has_archives": false } ``` --- ## Product Brands API *Source: apis/store-api/resources-endpoints/product-brands.md* # Product Brands API ## List Product Brands ```http GET /products/brands ``` | Attribute | Type | Required | Description | | :----------- | :------ | :------: | :-------------------------------------------------------------------------------------------------------------------- | | `context` | string | No | Scope under which the request is made; determines fields present in response. | | `page` | integer | No | Current page of the collection. Defaults to `1`. | | `per_page` | integer | No | Maximum number of items to be returned in result set. Defaults to no limit. Values between `0` and `100` are allowed. | | `search` | string | No | Limit results to those matching a string. | | `exclude` | array | No | Ensure result set excludes specific IDs. | | `include` | array | No | Limit result set to specific IDs. | | `order` | string | No | Sort ascending or descending. Allowed values: `asc`, `desc`. Defaults to `asc`. | | `orderby` | string | No | Sort by term property. Allowed values: `name`, `slug`, `count`. Defaults to `name`. | | `hide_empty` | boolean | No | If true, empty terms will not be returned. Defaults to `true`. | | `parent` | integer | No | Limit results to those with a specific parent ID. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/brands" ``` Example response: ```json [ { "id": 16, "name": "Nike", "slug": "nike", "description": "This is the Nike brand.", "parent": 0, "count": 11, "image": { "id": 55, "src": "https://store.local/wp-content/uploads/2021/11/nike-logo.jpg", "thumbnail": "https://store.local/wp-content/uploads/2021/11/nike-logo-324x324.jpg", "srcset": "https://store.local/wp-content/uploads/2021/11/nike-logo.jpg 800w, https://store.local/wp-content/uploads/2021/11/nike-logo-324x324.jpg 324w, https://store.local/wp-content/uploads/2021/11/nike-logo-100x100.jpg 100w, https://store.local/wp-content/uploads/2021/11/nike-logo-416x416.jpg 416w, https://store.local/wp-content/uploads/2021/11/nike-logo-300x300.jpg 300w, https://store.local/wp-content/uploads/2021/11/nike-logo-150x150.jpg 150w, https://store.local/wp-content/uploads/2021/11/nike-logo-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "nike-logo.jpg", "alt": "" }, "review_count": 2, "permalink": "https://store.local/product-brand/nike/" }, { "id": 21, "name": "Adidas", "slug": "adidas", "description": "", "parent": 0, "count": 1, "image": null, "review_count": 1, "permalink": "https://store.local/product-brand/adidas/" } ] ``` ## Single Product Brand Get a single brand. ```http GET /products/brands/:id ``` or ```http GET /products/brands/:slug ``` | Parameter | Type | Required | Description | | :-------- | :------ | :------: |:---------------------------------------------------------------------| | `identifier` | string | Yes | The identifier of the brand to retrieve. Can be a brand ID or slug. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/brands/1" ``` or ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/brands/adidas" ``` **Example response:** ```json { "id": 1, "name": "Adidas", "slug": "adidas", "description": "", "parent": 0, "count": 1, "image": null, "review_count": 1, "permalink": "https://store.local/product-brand/adidas/" } ``` --- ## Product Categories API *Source: apis/store-api/resources-endpoints/product-categories.md* # Product Categories API ## List Product Categories ```http GET /products/categories ``` | Attribute | Type | Required | Description | | :----------- | :------ | :------: | :-------------------------------------------------------------------------------------------------------------------- | | `context` | string | No | Scope under which the request is made; determines fields present in response. | | `page` | integer | No | Current page of the collection. Defaults to `1`. | | `per_page` | integer | No | Maximum number of items to be returned in result set. Defaults to no limit. Values between `0` and `100` are allowed. | | `search` | string | No | Limit results to those matching a string. | | `exclude` | array | No | Ensure result set excludes specific IDs. | | `include` | array | No | Limit result set to specific IDs. | | `order` | string | No | Sort ascending or descending. Allowed values: `asc`, `desc`. Defaults to `asc`. | | `orderby` | string | No | Sort by term property. Allowed values: `name`, `slug`, `count`. Defaults to `name`. | | `hide_empty` | boolean | No | If true, empty terms will not be returned. Defaults to `true`. | | `parent` | integer | No | Limit results to those with a specific parent ID. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/categories" ``` Example response: ```json [ { "id": 16, "name": "Clothing", "slug": "clothing", "description": "This is the clothing category.", "parent": 0, "count": 11, "image": { "id": 55, "src": "https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1.jpg", "thumbnail": "https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-324x324.jpg", "srcset": "https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1.jpg 800w, https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-324x324.jpg 324w, https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-100x100.jpg 100w, https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-416x416.jpg 416w, https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-300x300.jpg 300w, https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-150x150.jpg 150w, https://store.local/wp-content/uploads/2021/11/t-shirt-with-logo-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "t-shirt-with-logo-1.jpg", "alt": "" }, "review_count": 2, "permalink": "https://store.local/product-category/clothing/" }, { "id": 21, "name": "Decor", "slug": "decor", "description": "", "parent": 0, "count": 1, "image": null, "review_count": 1, "permalink": "https://store.local/product-category/decor/" } ] ``` ## Single Product Category Get a single category. ```http GET /products/categories/:id ``` | Category | Type | Required | Description | | :------- | :------ | :------: | :---------------------------------- | | `id` | integer | Yes | The ID of the category to retrieve. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/categories/1" ``` **Example response:** ```json { "id": 1, "name": "Decor", "slug": "decor", "description": "", "parent": 0, "count": 1, "image": null, "review_count": 1, "permalink": "https://store.local/product-category/decor/" } ``` --- ## Product Collection Data API *Source: apis/store-api/resources-endpoints/product-collection-data.md* # Product Collection Data API This endpoint allows you to get aggregate data from a collection of products, for example, the min and max price in a collection of products (ignoring pagination). This is used by blocks for product filtering widgets, since counts are based on the product catalog being viewed. ```http GET /products/collection-data GET /products/collection-data?calculate_price_range=true GET /products/collection-data?calculate_attribute_counts[0][query_type]=or&calculate_attribute_counts[0][taxonomy]=pa_color GET /products/collection-data?calculate_rating_counts=true GET /products/collection-data?calculate_taxonomy_counts=product_cat ``` | Attribute | Type | Required | Description | | :------------------------------ | :----- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `calculate_price_range` | bool | No | Returns the min and max price for the product collection. If false, only `null` will be returned. | | `calculate_attribute_counts` | object | No | Returns attribute counts for a list of attribute taxonomies you pass in via this parameter. Each should be provided as an object with keys "taxonomy" and "query_type". If empty, `null` will be returned. | | `calculate_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. | | `calculate_stock_status_counts` | bool | No | Returns counts of products with each stock status (in stock, out of stock, on backorder). If false, only `null` will be returned. | | `calculate_taxonomy_counts` | array | No | Returns taxonomy counts for a list of taxonomies you pass in via this parameter. Each should be provided as a taxonomy name string. If empty, `null` will be returned. | **In addition to the above attributes**, all product list attributes are supported. This allows you to get data for a certain subset of products. See [the products API list products section](/docs/apis/store-api/resources-endpoints/products#list-products) for the full list. ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/collection-data?calculate_price_range=true&calculate_attribute_counts=pa_size,pa_color&calculate_rating_counts=true&calculate_taxonomy_counts=product_cat,product_tag" ``` **Example response:** ```json { "price_range": [ "currency_minor_unit": 2, "min_price": "0", "max_price": "9000", "currency_code": "USD", "currency_decimal_separator": ".", "currency_minor_unit": 2, "currency_prefix": "$", "currency_suffix": "", "currency_symbol": "$", "currency_thousand_separator": ",", ], "attribute_counts": [ { "term": 22, "count": 4 }, { "term": 23, "count": 3 }, { "term": 24, "count": 4 } ], "rating_counts": [ { "rating": 3, "count": 1 }, { "rating": 4, "count": 1 } ], "taxonomy_counts": [ { "term": 25, "count": 8 }, { "term": 26, "count": 6 }, { "term": 27, "count": 2 } ] } ``` --- ## Product Reviews API *Source: apis/store-api/resources-endpoints/product-reviews.md* # Product Reviews API ## List Product Reviews This endpoint returns product reviews (comments) and can also show results from either specific products or specific categories. ```http GET /products/reviews GET /products/reviews?category_id=1,2,3 GET /products/reviews?product_id=1,2,3 GET /products/reviews?orderby=rating&order=desc ``` | Attribute | Type | Required | Description | | :------------ | :------ | :------: | :-------------------------------------------------------------------------------------------------- | | `page` | integer | no | Current page of the collection. | | `per_page` | integer | no | Maximum number of items to be returned in result set. | | `offset` | integer | no | Offset the result set by a specific number of items. | | `order` | string | no | Order sort attribute ascending or descending. Allowed values: `asc`, `desc` | | `orderby` | string | no | Sort collection by object attribute. Allowed values : `date`, `date_gmt`, `id`, `rating`, `product` | | `category_id` | string | no | Limit result set to reviews from specific category IDs. | | `product_id` | string | no | Limit result set to reviews from specific product IDs. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/collection-data?calculate_price_range=true&calculate_attribute_counts=pa_size,pa_color&calculate_rating_counts=true" ``` **Example response:** ```json [ { "id": 83, "date_created": "2022-01-12T15:42:14", "formatted_date_created": "January 12, 2022", "date_created_gmt": "2022-01-12T15:42:14", "product_id": 33, "product_name": "Beanie with Logo", "product_permalink": "https://store.local/product/beanie-with-logo/", "product_image": { "id": 56, "src": "https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1.jpg", "thumbnail": "https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-324x324.jpg", "srcset": "https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1.jpg 800w, https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-324x324.jpg 324w, https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-100x100.jpg 100w, https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-416x416.jpg 416w, https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-300x300.jpg 300w, https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-150x150.jpg 150w, https://store.local/wp-content/uploads/2021/11/beanie-with-logo-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "beanie-with-logo-1.jpg", "alt": "" }, "reviewer": "reviewer-name", "review": "

This is a fantastic product.

\n", "rating": 5, "verified": true, "reviewer_avatar_urls": { "24": "https://secure.gravatar.com/avatar/12345?s=24&d=mm&r=g", "48": "https://secure.gravatar.com/avatar/12345?s=48&d=mm&r=g", "96": "https://secure.gravatar.com/avatar/12345?s=96&d=mm&r=g" } } ] ``` --- ## Product Tags API *Source: apis/store-api/resources-endpoints/product-tags.md* # Product Tags API ## List Product Tags ```http GET /products/tags ``` | Attribute | Type | Required | Description | | :----------- | :------ | :------: | :-------------------------------------------------------------------------------------------------------------------- | | `context` | string | No | Scope under which the request is made; determines fields present in response. | | `page` | integer | No | Current page of the collection. Defaults to `1`. | | `per_page` | integer | No | Maximum number of items to be returned in result set. Defaults to no limit. Values between `0` and `100` are allowed. | | `search` | string | No | Limit results to those matching a string. | | `exclude` | array | No | Ensure result set excludes specific IDs. | | `include` | array | No | Limit result set to specific IDs. | | `order` | string | No | Sort ascending or descending. Allowed values: `asc`, `desc`. Defaults to `asc`. | | `orderby` | string | No | Sort by term property. Allowed values: `name`, `slug`, `count`. Defaults to `name`. | | `hide_empty` | boolean | No | If true, empty terms will not be returned. Defaults to `true`. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/tags" ``` Example response: ```json [ { "id": 1, "name": "Test Tag", "slug": "test-tag", "description": "", "parent": 0, "count": 1 }, { "id": 2, "name": "Another Tag", "slug": "another-tag", "description": "", "parent": 0, "count": 1 } ] ``` --- ## Products API *Source: apis/store-api/resources-endpoints/products.md* # Products API The store products API provides public product data so it can be rendered on the client side. ## Product Visibility ### Draft and non-published products Only published products are accessible via the Store API. Requesting a draft, pending, or other non-published product by ID or slug returns a `404` error. Non-published products are also excluded from the collection endpoint. ### Password-protected products Password-protected products are visible in the API, but their `description` and `short_description` fields are redacted (returned as empty strings) until the correct password has been submitted. The response includes an `is_password_protected` boolean field so clients can detect this state and prompt the user. Password verification uses WordPress's native `wp-postpass_*` cookie, set when a user submits the password form on the frontend. The Store API does not accept passwords directly. Other product data (price, images, categories, etc.) remains accessible regardless of password status. ## List Products ```http GET /products GET /products?search=product%20name GET /products?slug=slug-1,slug-2 GET /products?after=2017-03-22&date_column=date GET /products?before=2017-03-22&date_column=date GET /products?exclude=10,44,33 GET /products?include=10,44,33 GET /products?offset=10 GET /products?order=asc&orderby=price GET /products?parent=10 GET /products?parent_exclude=10 GET /products?type=simple GET /products?sku=sku-1,sku-2 GET /products?featured=true GET /products?category=22 GET /products?brand=adidas GET /products?_unstable_tax_my-taxonomy=my-taxonomy-term-id GET /products?tag=special-items GET /products?attributes[0][attribute]=pa_color&attributes[0][slug]=red GET /products?on_sale=true GET /products?min_price=5000 GET /products?max_price=10000 GET /products?stock_status=['outofstock'] GET /products?catalog_visibility=search GET /products?rating=4,5 GET /products?related=34 GET /products?return_price_range=true GET /products?return_attribute_counts=pa_size,pa_color GET /products?return_rating_counts=true ``` | Attribute | Type | Required | Description | | :------------------------------------------ | :------ | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `search` | string | no | Limit results to those matching a string. | | `slug` | string | no | Limit result set to products with specific slug(s). Use commas to separate. | | `after` | string | no | Limit response to resources created after a given ISO8601 compliant date. | | `before` | string | no | Limit response to resources created before a given ISO8601 compliant date. | | `date_column` | string | no | When limiting response using after/before, which date column to compare against. Allowed values: `date`, `date_gmt`, `modified`, `modified_gmt` | | `exclude` | array | no | Ensure result set excludes specific IDs. | | `include` | array | no | Limit result set to specific ids. | | `offset` | integer | no | Offset the result set by a specific number of items. | | `order` | string | no | Order sort attribute ascending or descending. Allowed values: `asc`, `desc` | | `orderby` | string | no | Sort collection by object attribute. Allowed values : `date`, `modified`, `id`, `include`, `title`, `slug`, `price`, `popularity`, `rating`, `menu_order`, `comment_count` | | `parent` | array | no | Limit result set to those of particular parent IDs. | | `parent_exclude` | array | no | Limit result set to all items except those of a particular parent ID. | | `type` | string | no | Limit result set to products assigned a specific type. | | `sku` | string | no | Limit result set to products with specific SKU(s). Use commas to separate. | | `featured` | boolean | no | Limit result set to featured products. | | `category` | string | no | Limit result set to products assigned to categories IDs or slugs, separated by commas. | | `category_operator` | string | no | Operator to compare product category terms. Allowed values: `in`, `not_in`, `and` | | `brand` | string | no | Limit result set to products assigned to brands IDs or slugs, separated by commas. | | `brand_operator` | string | no | Operator to compare product brand terms. Allowed values: `in`, `not_in`, `and` | | `_unstable_tax_[product-taxonomy]` | string | no | Limit result set to products assigned to the term ID of that custom product taxonomy. `[product-taxonomy]` should be the key of the custom product taxonomy registered. | | `_unstable_tax_[product-taxonomy]_operator` | string | no | Operator to compare custom product taxonomy terms. Allowed values: `in`, `not_in`, `and` | | `tag` | string | no | Limit result set to products assigned a specific tag ID. | | `tag_operator` | string | no | Operator to compare product tags. Allowed values: `in`, `not_in`, `and` | | `on_sale` | boolean | no | Limit result set to products on sale. | | `min_price` | string | no | Limit result set to products based on a minimum price, provided using the smallest unit of the currency. E.g. provide 10025 for 100.25 USD, which is a two-decimal currency, and 1025 for 1025 JPY, which is a zero-decimal currency. | | `max_price` | string | no | Limit result set to products based on a maximum price, provided using the smallest unit of the currency. E.g. provide 10025 for 100.25 USD, which is a two-decimal currency, and 1025 for 1025 JPY, which is a zero-decimal currency. | | `stock_status` | array | no | Limit result set to products with specified stock statuses. Expects an array of strings containing 'instock', 'outofstock' or 'onbackorder'. | | `attributes` | array | no | Limit result set to specific attribute terms. Expects an array of objects containing `attribute` (taxonomy), `term_id` or `slug`, and optional `operator` for comparison. | | `attribute_relation` | string | no | The logical relationship between attributes when filtering across multiple at once. | | `catalog_visibility` | string | no | Determines if hidden or visible catalog products are shown. Allowed values: `any`, `visible`, `catalog`, `search`, `hidden` | | `rating` | array | no | Limit result set to products with a certain average rating. Allowed values: `1`, `2`, `3`, `4`, `5`. | | `related` | integer | no | Limit result set to products related to a specific product ID. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products" ``` **Example response:** ```json [ { "id": 34, "name": "WordPress Pennant", "slug": "wordpress-pennant", "variation": "", "permalink": "https://local.wordpress.test/product/wordpress-pennant/", "sku": "wp-pennant", "summary": "

This is an external product.

", "short_description": "

This is an external product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "on_sale": false, "prices": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "price": "1105", "regular_price": "1105", "sale_price": "1105", "price_range": null }, "average_rating": "0", "review_count": 0, "images": [ { "id": 57, "src": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "pennant-1.jpg", "alt": "" } ], "has_options": false, "is_purchasable": true, "is_in_stock": true, "is_password_protected": false, "low_stock_remaining": null, "add_to_cart": { "text": "Add to cart", "description": "Add “WordPress Pennant” to your cart" } } ] ``` ## Single Product by ID Get a single product by id. ```http GET /products/:id ``` | Attribute | Type | Required | Description | | :-------- | :------ | :------: | :--------------------------------- | | `id` | integer | Yes | The ID of the product to retrieve. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/34" ``` **Example response:** ```json { "id": 34, "name": "WordPress Pennant", "slug": "wordpress-pennant", "variation": "", "permalink": "https://local.wordpress.test/product/wordpress-pennant/", "sku": "wp-pennant", "summary": "

This is an external product.

", "short_description": "

This is an external product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "on_sale": false, "prices": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "price": "1105", "regular_price": "1105", "sale_price": "1105", "price_range": null }, "average_rating": "0", "review_count": 0, "images": [ { "id": 57, "src": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "pennant-1.jpg", "alt": "" } ], "has_options": false, "is_purchasable": true, "is_in_stock": true, "is_password_protected": false, "low_stock_remaining": null, "add_to_cart": { "text": "Add to cart", "description": "Add “WordPress Pennant” to your cart" } } ``` ## Single Product by slug Get a single product by slug. ```http GET /products/:slug ``` | Attribute | Type | Required | Description | | :-------- | :----- | :------: | :----------------------------------- | | `slug` | string | Yes | The slug of the product to retrieve. | ```sh curl "https://example-store.com/wp-json/wc/store/v1/products/wordpress-pennant" ``` **Example response:** ```json { "id": 34, "name": "WordPress Pennant", "slug": "wordpress-pennant", "variation": "", "permalink": "https://local.wordpress.test/product/wordpress-pennant/", "sku": "wp-pennant", "summary": "

This is an external product.

", "short_description": "

This is an external product.

", "description": "

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

", "on_sale": false, "prices": { "currency_code": "GBP", "currency_symbol": "£", "currency_minor_unit": 2, "currency_decimal_separator": ".", "currency_thousand_separator": ",", "currency_prefix": "£", "currency_suffix": "", "price": "1105", "regular_price": "1105", "sale_price": "1105", "price_range": null }, "average_rating": "0", "review_count": 0, "images": [ { "id": 57, "src": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg", "thumbnail": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg", "srcset": "https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1.jpg 800w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-324x324.jpg 324w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-100x100.jpg 100w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-416x416.jpg 416w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-300x300.jpg 300w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-150x150.jpg 150w, https://local.wordpress.test/wp-content/uploads/2020/03/pennant-1-768x768.jpg 768w", "sizes": "(max-width: 800px) 100vw, 800px", "name": "pennant-1.jpg", "alt": "" } ], "has_options": false, "is_purchasable": true, "is_in_stock": true, "is_password_protected": false, "low_stock_remaining": null, "add_to_cart": { "text": "Add to cart", "description": "Add “WordPress Pennant” to your cart" } } ``` ## Product Links and Embedding Product responses include `_links` that provide URLs to related resources. When products have upsells, cross-sells, or related products configured, embeddable links are included that can be used with WordPress's `_embed` feature. ### Available Links | Link | Description | Embeddable | | :----------- | :-------------------------------------------------- | :--------: | | `self` | Link to the current product | No | | `collection` | Link to the products collection | No | | `up` | Link to parent product (for variations) | No | | `upsells` | Link to fetch upsell products (if configured) | Yes | | `cross_sells`| Link to fetch cross-sell products (if configured) | Yes | | `related` | Link to fetch related products | Yes | ### Example Response with Links ```json { "id": 34, "name": "WordPress Pennant", "_links": { "self": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products/34"}], "collection": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products"}], "upsells": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products?include=10,20", "embeddable": true}], "cross_sells": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products?include=30", "embeddable": true}], "related": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products?related=34&per_page=10", "embeddable": true}] } } ``` ### Using the `_embed` Parameter Add `?_embed` to any product request to automatically fetch and include the linked resources in an `_embedded` object: ```sh curl "https://local.wordpress.test/wp-json/wc/store/v1/products/34?_embed" ``` **Example response with embedding:** ```json { "id": 34, "name": "WordPress Pennant", "_links": { "self": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products/34"}], "collection": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products"}], "upsells": [{"href": "https://local.wordpress.test/wp-json/wc/store/v1/products?include=10,20", "embeddable": true}] }, "_embedded": { "upsells": [ {"id": 10, "name": "Upsell Product 1", "...": "..."}, {"id": 20, "name": "Upsell Product 2", "...": "..."} ] } } ``` ## Product Variations By default, Store API excludes product variations. You can retrieve the variations for a product by using the `type=variation`. ```sh curl "https://example-store.com/wp-json/wc/store/v1/products?type=variation" ``` --- ## WooCommerce coding standards *Source: best-practices/coding-standards/coding-standards.md* # WooCommerce coding standards Adhering to WooCommerce coding standards is essential for maintaining high code quality, ensuring compatibility, and facilitating easier maintenance and updates. This document outlines the recommended coding practices for developers working within the WooCommerce ecosystem, including the use of hooks, function prefixing, translatable texts, and code structure. ## Position of hooks Position hooks below the function call to align with 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 to avoid conflicts. For the code snippets in this repo, use `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 Ensure all plain texts are translatable and use a consistent text domain, adhering to internationalization best practices. For the code snippets in this repo, use the textdomain `YOUR-TEXTDOMAIN`. Example: ```php /** * Add welcome 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() To prevent errors from potential function redeclaration, wrap all function calls with `function_exists()`. 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' ); ``` ## Code quality standards To ensure the highest standards of code quality, developers are encouraged to adhere to the following practices: ### WooCommerce sniffs and WordPress code standards - **Ensure no code style issues** when code is passed through WooCommerce Sniffs and WordPress Code Standards for PHP_CodeSniffer. ### Automated testing - **Unit tests**: Implement automated unit tests to validate code functionality in isolation. - **E2E tests**: Utilize automated end-to-end tests to verify the integrated operation of components within the application. ### Tracking and managing bugs - **Monitor and aim to minimize** the number of open bugs, ensuring a stable and reliable product. ### Code organization - **Organize code in self-contained classes** to avoid creating "god/super classes" that contain all plugin code. This practice promotes modularity and simplifies maintenance. By following these coding standards and practices, developers can create high-quality, maintainable, and secure WooCommerce extensions that contribute positively to the WordPress ecosystem. --- ## CSS/Sass naming conventions *Source: best-practices/coding-standards/css-sass-naming-conventions.md* # CSS/Sass naming conventions ## Introduction Our guidelines are based on those used in [Calypso](https://github.com/Automattic/wp-calypso), which itself follows the [BEM methodology](https://getbem.com/). Refer to the [Calypso CSS/Sass Coding Guidelines](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md) for full details. Read more about [BEM key concepts](https://en.bem.info/methodology/key-concepts/). There are a few differences in WooCommerce which are outlined below. ## Prefixing As a WordPress plugin WooCommerce has to play nicely with WordPress core and other plugins/themes. To minimize conflict potential, all classes should be prefixed with `.woocommerce-`. ## Class names When naming classes, remember: - **Block** - Standalone entity that is meaningful on its own. Such as the name of a component. - **Element** - Parts of a block and have no standalone meaning. They are semantically tied to its block. - **Modifier** - Flags on blocks or elements. Use them to change appearance or behavior. ### Example ```css /* Block */ .woocommerce-loop {} /* Nested block */ .woocommerce-loop-product {} /* Modifier */ .woocommerce-loop-product--sale {} /* Element */ .woocommerce-loop-product__link {} /* Element */ .woocommerce-loop-product__button-add-to-cart {} /* Modifier */ .woocommerce-loop-product__button-add-to-cart--added {} ``` **Note:** `.woocommerce-loop-product` is not named as such because the block is nested within `.woocommerce-loop`. It's to be specific so that we can have separate classes for single products, cart products, etc. **Nested blocks do not need to inherit their parents full name.** ## TL;DR - Follow the [WordPress Coding standards for CSS](https://make.wordpress.org/core/handbook/best-practices/coding-standards/css/) unless it contradicts anything here. - Follow [Calypso guidelines for CSS](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md). - Use BEM for [class names](https://en.bem.info/methodology/naming-convention/). - Prefix all class names. --- ## WooCommerce grammar, punctuation and capitalization guide *Source: best-practices/coding-standards/grammar-punctuation-capitalization.md* # 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 I'm and there's, give writing an informal and conversational feel, but may be inappropriate if content is being translated. For example, sometimes the not in don't 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.' Don't 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 & Jerry's - 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 Sam's bike. - A teammate borrowed Chris's bike. - Employees hid the office managers' pens. These are possessives: FAQ's questions, HE's 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. - 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 you're 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 I've Done"? - Brandon Flowers of The Killers said, "I was inspired and on a roll when I wrote, 'I got soul, but I'm 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. - 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. Don't 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. Don't 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. Don't italicize. Avoid writing out URLs; omit `http://www` when it's 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. --- ## Naming conventions *Source: best-practices/coding-standards/naming-conventions.md* # Naming conventions ## PHP WooCommerce core generally follows [WordPress PHP naming conventions](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions). There are some additional conventions that apply, depending on the location of the code. ### `/src` Classes defined inside `/src` follow the [PSR-4](https://www.php-fig.org/psr/psr-4/) standard. See the [README for `/src`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/README.md) for more information. The following conventions apply to this directory: - No class name prefix is needed, as all classes in this location live within the `Automattic\WooCommerce` namespace. - Classes are named using `CamelCase` convention. - Functions are named using `snake_case` convention. - Class file names should match the class name. They do not need a `class-` prefix. - The namespace should match the directory structure. - Hooks are prefixed with `woocommerce_`. - Hooks are named using `snake_case` convention. For example, the class defined in `src/Util/StringUtil.php` should be named `StringUtil` and should be in the `Automattic\WooCommerce\Util` namespace. ### `/includes` The `/includes` directory contains legacy code that does not follow the PSR-4 standard. See the [README for `/includes`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/README.md) for more information. The following conventions apply to this directory: - Class names are prefixed with `WC_`. - Classes are named using `Upper_Snake_Case` convention. - Functions are prefixed with `wc_`. - Functions are named using `snake_case` convention. - Hooks are prefixed with `woocommerce_`. - Hooks are named using `snake_case` convention. Class name examples: - `WC_Cache_Helper` - `WC_Cart` Function name examples: - `wc_get_product()` - `wc_is_active_theme()` Hook name examples (actions or filters): - `woocommerce_after_checkout_validation` - `woocommerce_get_formatted_order_total` ## JS WooCommerce core follows [WordPress JS naming conventions](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/#naming-conventions). As with PHP, function, class, and hook names should be prefixed, but the convention for JS is slightly different. - Global class names are prefixed with `WC`. Class names exported from modules are not prefixed. - Classes are named using `UpperCamelCase` convention. - Global function names are prefixed with `wc`. Function names exported from modules are not prefixed. - Functions are named using `camelCase` convention. - Hooks names are prefixed with `woocommerce`. - Hooks are named using `camelCase` convention. Global class name example: - `WCOrdersTable` Global function name example: - `wcSettings()` Hook name example (actions or filters): - `woocommerceTracksEventProperties` ## CSS and SASS See [CSS/Sass Naming Conventions](./css-sass-naming-conventions.md). --- ## How to add a custom field to simple and variable products *Source: best-practices/data-management/adding-a-custom-field-to-variable-products.md* # How to add 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, let's 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 doesn't 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 let's 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, we'll be appending our field right at the end with `woocommerce_product_options_inventory_product_data`. ## Creating our class Let's 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 hooks(); } private function hooks() {} } ``` Then in Terminal we run `composer dump-autoload -o` to regenerate the class map. Once that's 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; ?>
'_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' ) ) ); ?>
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 we're 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' )` Let's 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 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 we're within WP Admin. So to call our new class we'll 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 don't 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, I'm suggesting to use `esc_html()` to force plain text. ```php public function add_stock_info() { global $product; ?>

get_meta( '_new_stock_information' ) ); ?>

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 WooCommerce 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`. --- ## Developing using WooCommerce CRUD objects *Source: best-practices/data-management/crud-objects.md* # Developing using WooCommerce CRUD objects CRUD is an abbreviation of the four basic operations you can do to a database or resource - Create, Read, Update, Delete. [WooCommerce 3.0 introduced CRUD objects](https://woocommerce.wordpress.com/2016/10/27/the-new-crud-classes-in-woocommerce-2-7/) for working with WooCommerce data. **Whenever possible these objects should be used in your code instead of directly updating metadata or using WordPress post objects.** Each of these objects contains a schema for the data it controls (properties), a getter and setter for each property, and a save/delete method which talks to a data store. The data store handles the actual saving/reading from the database. The object itself does not need to be aware of where the data is stored. ## The benefits of CRUD * Structure - Each object has a pre-defined structure and keeps its own data valid. * Control - We control the flow of data, and any validation needed, so we know when changes occur. * Ease of development - As a developer, you don't need to know the internals of the data you're working with, just the names. * Abstraction - The data can be moved elsewhere, e.g. custom tables, without affecting existing code. * Unification - We can use the same code for updating things in admin as we do in the REST API and CLIs. Everything is unified. * Simplified code - Less procedural code to update objects which reduces likelihood of malfunction and adds more unit test coverage. ## CRUD object structure The [`WC_Data`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/abstracts/abstract-wc-data.php) class is the basic implementation for CRUD objects, and all CRUD objects extend it. The most important properties to note are `$data`, which is an array of props supported in each object, and `$id`, which is the object's ID. The [coupon object class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/class-wc-coupon.php) is a good example of extending `WC_Data` and adding CRUD functions to all properties. ### Data `$data` stores the property names, and default values: ```php /** * Data array, with defaults. * @since 3.0.0 * @var array */ protected $data = array( 'code' => '', 'amount' => 0, 'date_created' => '', 'date_modified' => '', 'discount_type' => 'fixed_cart', 'description' => '', 'date_expires' => '', 'usage_count' => 0, 'individual_use' => false, 'product_ids' => array(), 'excluded_product_ids' => array(), 'usage_limit' => 0, 'usage_limit_per_user' => 0, 'limit_usage_to_x_items' => 0, 'free_shipping' => false, 'product_categories' => array(), 'excluded_product_categories' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', 'email_restrictions' => array(), 'used_by' => array(), ); ``` ### Getters and setters Each one of the keys in this array (property) has a getter and setter, e.g. `set_used_by()` and `get_used_by()`. `$data` itself is private, so the getters and setters must be used to access the data. Example getter: ```php /** * Get records of all users who have used the current coupon. * @since 3.0.0 * @param string $context * @return array */ public function get_used_by( $context = 'view' ) { return $this->get_prop( 'used_by', $context ); } ``` Example setter: ```php /** * Set which users have used this coupon. * @since 3.0.0 * @param array $used_by * @throws WC_Data_Exception */ public function set_used_by( $used_by ) { $this->set_prop( 'used_by', array_filter( $used_by ) ); } ``` `set_prop` and `get_prop` are part of `WC_Data`. These apply various filters (based on context) and handle changes, so we can efficiently save only the props that have changed rather than all props. A note on `$context`: when getting data for use on the front end or display, `view` context is used. This applies filters to the data so extensions can change the values dynamically. `edit` context should be used when showing values to edit in the backend, and for saving to the database. Using `edit` context does not apply any filters to the data. ### The constructor The constructor of the CRUD objects facilitates the read from the database. The actual read is not done by the CRUD class, but by its data store. Example: ```php /** * Coupon constructor. Loads coupon data. * @param mixed $data Coupon data, object, ID or code. */ public function __construct( $data = '' ) { parent::__construct( $data ); if ( $data instanceof WC_Coupon ) { $this->set_id( absint( $data->get_id() ) ); } elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) { $this->set_id( $data ); } elseif ( ! empty( $data ) ) { $this->set_id( wc_get_coupon_id_by_code( $data ) ); $this->set_code( $data ); } else { $this->set_object_read( true ); } $this->data_store = WC_Data_Store::load( 'coupon' ); if ( $this->get_id() > 0 ) { $this->data_store->read( $this ); } } ``` Note how it sets the ID based on the data passed to the object, then calls the data store to retrieve the data from the database. Once the data is read via the data store, or if no ID is set, `$this->set_object_read( true );` is set so the data store and CRUD object knows it's read. Once this is set, changes are tracked. ### Saving and deleting Save and delete methods are optional on CRUD child classes because the `WC_Data` parent class can handle it. When `save` is called, the data store is used to store data to the database. Delete removes the object from the database. `save` must be called for changes to persist, otherwise they will be discarded. The save method in `WC_Data` looks like this: ```php /** * Save should create or update based on object existence. * * @since 2.6.0 * @return int */ public function save() { if ( $this->data_store ) { // Trigger action before saving to the DB. Allows you to adjust object props before save. do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); if ( $this->get_id() ) { $this->data_store->update( $this ); } else { $this->data_store->create( $this ); } return $this->get_id(); } } ``` Update/create is used depending on whether the object has an ID yet. The ID will be set after creation. The delete method is like this: ```php /** * Delete an object, set the ID to 0, and return result. * * @since 2.6.0 * @param bool $force_delete * @return bool result */ public function delete( $force_delete = false ) { if ( $this->data_store ) { $this->data_store->delete( $this, array( 'force_delete' => $force_delete ) ); $this->set_id( 0 ); return true; } return false; } ``` ## CRUD usage examples ### Creating a new simple product ```php $product = new WC_Product_Simple(); $product->set_name( 'My Product' ); $product->set_slug( 'myproduct' ); $product->set_description( 'A new simple product' ); $product->set_regular_price( '9.50' ); $product->save(); $product_id = $product->get_id(); ``` ### Updating an existing coupon ```php $coupon = new WC_Coupon( $coupon_id ); $coupon->set_discount_type( 'percent' ); $coupon->set_amount( 25.00 ); $coupon->save(); ``` ### Retrieving a customer ```php $customer = new WC_Customer( $user_id ); $email = $customer->get_email(); $address = $customer->get_billing_address(); $name = $customer->get_first_name() . ' ' . $customer->get_last_name(); ``` --- ## Data storage primer *Source: best-practices/data-management/data-storage.md* # Data storage primer When developing for WordPress and WooCommerce, it's important to consider the nature and permanence of your data. This will help you decide the best way to store it. Here's a quick primer: ## Transients If the data may not always be present (i.e., it expires), use a [transient](https://developer.wordpress.org/apis/handbook/transients/). Transients are a simple and standardized way of storing cached data in the database temporarily by giving it a custom name and a timeframe after which it will expire and be deleted. ## WP Cache If the data is persistent but not always present, consider using the [WP Cache](https://developer.wordpress.org/reference/classes/wp_object_cache/). The WP Cache functions allow you to cache data that is computationally expensive to regenerate, such as complex query results. ## wp_options Table If the data is persistent and always present, consider the [wp_options table](https://developer.wordpress.org/apis/handbook/options/). The Options API is a simple and standardized way of storing data in the wp_options table in the WordPress database. ## Post Types If the data type is an entity with n units, consider a [post type](https://developer.wordpress.org/post_type/). Post types are "types" of content that are stored in the same way, but are easy to distinguish in the code and UI. ## Taxonomies If the data is a means of sorting/categorizing an entity, consider a [taxonomy](https://developer.wordpress.org/taxonomy/). Taxonomies are a way of grouping things together. ## Logging Logs should be written to a file using the [WC_Logger](https://woocommerce.com/wc-apidocs/class-WC_Logger.html) class. This is a simple and standardized way of recording events and errors for debugging purposes. Remember, the best method of data storage depends on the nature of the data and how it will be used in your application. --- ## How to manage WooCommerce Data Stores *Source: best-practices/data-management/data-stores.md* # How to manage WooCommerce Data Stores ## Introduction Data store classes act as a bridge between WooCommerce's data CRUD classes (`WC_Product`, `WC_Order`, `WC_Customer`, etc) and the database layer. With the database logic separate from data, WooCommerce becomes more flexible. The data stores shipped with WooCommerce core (powered by WordPress' custom posts system and some custom tables) can be swapped out for a different database structure, type, or even be powered by an external API. This guide will walk through the structure of a data store class, how to create a new data store, how to replace a core data store, and how to call a data store from your own code. The examples in this guide will look at the [`WC_Coupon`](https://github.com/woocommerce/woocommerce/blob/dcecf0f22890f3cd92fbea13a98c11b2537df2a8/includes/class-wc-coupon.php#L19) CRUD data class and [`WC_Coupon_Data_Store_CPT`](https://github.com/woocommerce/woocommerce/blob/dcecf0f22890f3cd92fbea13a98c11b2537df2a8/includes/data-stores/class-wc-coupon-data-store-cpt.php), an implementation of a coupon data store using WordPress custom post types. This is how coupons are currently stored in WooCommerce. The important thing to know about `WC_Coupon` or any other CRUD data class when working with data stores is which props (properties) they contain. This is defined in the [`data`](https://github.com/woocommerce/woocommerce/blob/dcecf0f22890f3cd92fbea13a98c11b2537df2a8/includes/class-wc-coupon.php#L26) array of each class. ## Structure Every data store for a CRUD object should implement the `WC_Object_Data_Store_Interface` interface. `WC_Object_Data_Store_Interface` includes the following methods: * `create` * `read` * `update` * `delete` * `read_meta` * `delete_meta` * `add_meta` * `update_meta` The `create`, `read`, `update`, and `delete` methods should handle the CRUD logic for your props: * `create` should create a new entry in the database. Example: Create a coupon. * `read` should query a single entry from the database and set properties based on the response. Example: Read a coupon. * `update` should make changes to an existing entry. Example: Update or edit a coupon. * `delete` should remove an entry from the database. Example: Delete a coupon. All data stores must implement handling for these methods. In addition to handling your props, other custom data can be passed. This is considered `meta`. For example, coupons can have custom data provided by plugins. The `read_meta`, `delete_meta`, `add_meta`, and `update_meta` methods should be defined so meta can be read and managed from the correct source. In the case of our WooCommerce core classes, we define them in `WC_Data_Store_WP` and then use the same code for all of our data stores. They all use the WordPress meta system. You can redefine these if meta should come from a different source. Your data store can also implement other methods to replace direct queries. For example, the coupons data store has a public `get_usage_by_user_id` method. Data stores should always define and implement an interface for the methods they expect, so other developers know what methods they need to write. Put another way, in addition to the `WC_Object_Data_Store_Interface` interface, `WC_Coupon_Data_Store_CPT` also implements `WC_Coupon_Data_Store_Interface`. ## Replacing a data store Let's look at how we would replace the `WC_Coupon_Data_Store_CPT` class with a `WC_Coupon_Data_Store_Custom_Table` class. Our examples will just provide stub functions, instead of a full working solution. Imagine that we would like to store coupons in a table named `wc_coupons` with the following columns: ```text id, code, amount, date_created, date_modified, discount_type, description, date_expires, usage_count,individual_use, product_ids, excluded_product_ids, usage_limit, usage_limit_per_user, limit_usage_to_x_items, free_shipping, product_categories, excluded_product_categories, exclude_sale_items, minimum_amount, maximum_amount, email_restrictions, used_by ``` These column names match 1 to 1 with prop names. First we would need to create a new data store class to contain our logic: ```php /** * WC Coupon Data Store: Custom Table. */ class WC_Coupon_Data_Store_Custom_Table extends WC_Data_Store_WP implements WC_Coupon_Data_Store_Interface, WC_Object_Data_Store_Interface { } ``` Note that we implement the main `WC_Object_Data_Store_Interface` interface as well as the ` WC_Coupon_Data_Store_Interface` interface. Together, these represent all the methods we need to provide logic for. We would then define the CRUD handling for these properties: ```php /** * Method to create a new coupon in the database. * * @param WC_Coupon */ public function create( &$coupon ) { $coupon->set_date_created( current_time( 'timestamp' ) ); /** * This is where code for inserting a new coupon would go. * A query would be built using getters: $coupon->get_code(), $coupon->get_description(), etc. * After the INSERT operation, we want to pass the new ID to the coupon object. */ $coupon->set_id( $coupon_id ); // After creating or updating an entry, we need to also cause our 'meta' to save. $coupon->save_meta_data(); // Apply changes let's the object know that the current object reflects the database and no "changes" exist between the two. $coupon->apply_changes(); // It is helpful to provide the same hooks when an action is completed, so that plugins can interact with your data store. do_action( 'woocommerce_new_coupon', $coupon_id ); } /** * Method to read a coupon. * * @param WC_Coupon */ public function read( &$coupon ) { $coupon->set_defaults(); // Read should do a check to see if this is a valid coupon // and otherwise throw an 'Invalid coupon.' exception. // For valid coupons, set $data to contain our database result. // All props should be set using set_props with output from the database. This "hydates" the CRUD data object. $coupon_id = $coupon->get_id(); $coupon->set_props( array( 'code' => $data->code, 'description' => $data->description, // .. ) ); // We also need to read our meta data into the object. $coupon->read_meta_data(); // This flag reports if an object has been hydrated or not. If this ends up false, the database hasn't correctly set the object. $coupon->set_object_read( true ); do_action( 'woocommerce_coupon_loaded', $coupon ); } /** * Updates a coupon in the database. * * @param WC_Coupon */ public function update( &$coupon ) { // Update coupon query, using the getters. $coupon->save_meta_data(); $coupon->apply_changes(); do_action( 'woocommerce_update_coupon', $coupon->get_id() ); } /** * Deletes a coupon from the database. * * @param WC_Coupon * @param array $args Array of args to pass to the delete method. */ public function delete( &$coupon, $args = array() ) { // A lot of objects in WordPress and WooCommerce support // the concept of trashing. This usually is a flag to move the object // to a "recycling bin". Since coupons support trashing, your layer should too. // If an actual delete occurs, set the coupon ID to 0. $args = wp_parse_args( $args, array( 'force_delete' => false, ) ); $id = $coupon->get_id(); if ( $args['force_delete'] ) { // Delete Query $coupon->set_id( 0 ); do_action( 'woocommerce_delete_coupon', $id ); } else { // Trash Query do_action( 'woocommerce_trash_coupon', $id ); } } ``` We are extending `WC_Data_Store_WP` so our classes will continue to use WordPress' meta system. As mentioned in the structure section, we are responsible for implementing the methods defined by `WC_Coupon_Data_Store_Interface`. Each interface describes the methods and parameters it accepts, and what your function should do. A coupons replacement would look like the following: ```php /** * Increase usage count for current coupon. * * @param WC_Coupon * @param string $used_by Either user ID or billing email */ public function increase_usage_count( &$coupon, $used_by = '' ) { } /** * Decrease usage count for current coupon. * * @param WC_Coupon * @param string $used_by Either user ID or billing email */ public function decrease_usage_count( &$coupon, $used_by = '' ) { } /** * Get the number of uses for a coupon by user ID. * * @param WC_Coupon * @param id $user_id * @return int */ public function get_usage_by_user_id( &$coupon, $user_id ) { } /** * Return a coupon code for a specific ID. * @param int $id * @return string Coupon Code */ public function get_code_by_id( $id ) { } /** * Return an array of IDs for for a specific coupon code. * Can return multiple to check for existence. * @param string $code * @return array Array of IDs. */ public function get_ids_by_code( $code ) { } ``` Once all the data store methods are defined and logic written, we need to tell WooCommerce to load our new class instead of the built-in class. This is done using the `woocommerce_data_stores` filter. An array of data store slugs is mapped to default WooCommerce classes. Example: ```php 'coupon' => 'WC_Coupon_Data_Store_CPT', 'customer' => 'WC_Customer_Data_Store', 'customer-download' => 'WC_Customer_Download_Data_Store', 'customer-session' => 'WC_Customer_Data_Store_Session', 'order' => 'WC_Order_Data_Store_CPT', 'order-refund' => 'WC_Order_Refund_Data_Store_CPT', 'order-item' => 'WC_Order_Item_Data_Store', 'order-item-coupon' => 'WC_Order_Item_Coupon_Data_Store', 'order-item-fee' => 'WC_Order_Item_Fee_Data_Store', 'order-item-product' => 'WC_Order_Item_Product_Data_Store', 'order-item-shipping' => 'WC_Order_Item_Shipping_Data_Store', 'order-item-tax' => 'WC_Order_Item_Tax_Data_Store', 'payment-token' => 'WC_Payment_Token_Data_Store', 'product' => 'WC_Product_Data_Store_CPT', 'product-grouped' => 'WC_Product_Grouped_Data_Store_CPT', 'product-variable' => 'WC_Product_Variable_Data_Store_CPT', 'product-variation' => 'WC_Product_Variation_Data_Store_CPT', 'shipping-zone' => 'WC_Shipping_Zone_Data_Store', ``` We specifically want to target the coupon data store, so we would do something like this: ```php function myplugin_set_wc_coupon_data_store( $stores ) { $stores['coupon'] = 'WC_Coupon_Data_Store_Custom_Table'; return $stores; } add_filter( 'woocommerce_data_stores', 'myplugin_set_wc_coupon_data_store' ); ``` Our class would then be loaded by WooCommerce core, instead of `WC_Coupon_Data_Store_CPT`. ## Creating a new data store ### Defining a new product type Does your extension create a new product type? Each product type has a data store in addition to a parent product data store. The parent store handles shared properties like name or description and the child handles more specific data. For example, the external product data store handles "button text" and "external URL". The variable data store handles the relationship between parent products and their variations. Check out [this walkthrough](https://developer.woocommerce.com/2017/02/06/wc-2-7-extension-compatibility-examples-3-bookings/) for more information on this process. ### Data store for custom data If your extension introduces a new database table, new custom post type, or some new form of data not related to products, orders, etc, then you should implement your own data store. Your data store should still implement `WC_Object_Data_Store_Interface` and provide the normal CRUD functions. Your data store should be the main point of entry for interacting with your data, so any other queries or operations should also have methods. The [shipping zone data store](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php) serves as a good example for a "simple" data store using a custom table. The coupons code is a good example for a data store using a custom post type. All you need to do to register your data store is add it to the `woocommerce_data_stores` filter: ```php function myplugin_set_my_custom_data_store( $stores ) { $stores['mycustomdata'] = 'WC_My_Custom_Data_Store'; return $stores; } add_filter( 'woocommerce_data_stores', 'myplugin_set_my_custom_data_store' ); ``` You can then load your data store like any other WooCommerce data store. ## Calling a data store Calling a data store is as simple as using the static `WC_Data_Store::load()` method: ```php // Load the shipping zone data store. $data_store = WC_Data_Store::load( 'shipping-zone' ); // Get the number of shipping methods for zone ID 4. $num_of_methods = $data_store->get_method_count( 4 ); ``` You can also chain methods: ```php // Get the number of shipping methods for zone ID 4. $num_of_methods = WC_Data_Store::load( 'shipping-zone' )->get_method_count( 4 ); ``` The `::load()` method works for any data store registered to `woocommerce_data_stores`, so you could load your custom data store: ```php $data_store = WC_Data_Store::load( 'mycustomdata' ); ``` ## Data store limitations and WP Admin Currently, several WooCommerce screens still rely on WordPress to list objects. Examples of this include coupons and products. If you replace data via a data store, some parts of the existing UI may fail. An example of this may be lists of coupons when using the `type` filter. This filter uses meta data, and is in turn passed to WordPress which runs a query using the `WP_Query` class. This cannot handle data outside of the regular meta tables (Ref #19937). To get around this, usage of `WP_Query` would need to be deprecated and replaced with custom query classes and functions. --- ## Logging in WooCommerce *Source: best-practices/data-management/logging.md* # Logging in WooCommerce WooCommerce has its own robust system for logging, which can be used for debugging during development, catching errors on production, or even sending notifications when specific events occur. By default, WooCommerce uses this logger to record errors, warnings, and other notices that may be useful for troubleshooting problems with a store. Many extensions for WooCommerce also make use of the logger for similar purposes. ## Viewing logs You can view the entries created by the logger by going to **WooCommerce > Status > Logs**. The log viewing interface depends on which log storage method is configured (see the "Configuring the logger" section below). ### File system With the default file system storage method, the first thing you will see is the list of existing log files: ![The default log viewing interface, showing a list of log files](/img/doc_images/file-browse.png) The name of a log file is based on the source of the entries it contains (meaning the extension or the part of the WooCommerce codebase), as well as the date the entries were generated. In this file browsing view, you can sort the files in different ways as well as filtering them to show only those from a specific source. Clicking on a file will take you to a single file view, where you can see the actual log entries: ![The contents of a log file](/img/doc_images/file-view-new.png) Click on a line number in the file to highlight it. This can also be used to link to a specific line in a file from elsewhere. From the file browser view, you can sort and filter a list of log files, and then search for a string within the contents of those files: ![A list of search results](/img/doc_images/search-results.png) Clicking on a search result line number will take you to that line in the single file view. ### Database With the database storage method, you will see a list of log entries, starting with the most recent: ![The log viewing interface when log entries are stored in the database](/img/doc_images/database-logs.png) These entries can be sorted by timestamp, level, and source, as well as filtered to only show a particular source or a minimum level. You can also search for a string within the log entry message fields. ## Configuring the logger From the Logs screen at **WooCommerce > Status > Logs**, click the "Settings" link to make configuration changes: ![The Logs settings screen](/img/doc_images/settings.png) ### Logger Uncheck the box here to turn off all logging. This is not recommended in most circumstances, as logging can provide valuable information about what is happening on your site! ### Log storage Out-of-the-box, WooCommerce has two different log storage methods available: * **File system** - Log entries are recorded to files. Files are differentiated by the `source` value for the log entry (see the "Adding logs" section below), and by the current date. The files are stored in the `wc-logs` subdirectory of the site's `uploads` directory. A custom directory can be defined using the `woocommerce_log_directory` filter hook. Log files can be up to 5 MB in size, after which the log file will rotate. * **Database** - Log entries are recorded to the database, in the `{$wpdb->prefix}woocommerce_log` table. If you change this setting, and you already have some log entries, those entries will not be migrated to the other storage method, but neither will they be deleted. ### Retention period The logger will periodically go through and delete logs that are older than this time period, as a space-saving measure. If log entries are stored in the file system, the entire log file that is beyond the retention period will be deleted, while with database storage, individual log entries will be deleted. ### Level threshold Each log entry has a severity level (see the "Adding logs" section below). This sets a minimum severity level, and any log entries that are generated that are not at or above the minimum level will not be recorded. Use this setting with caution! If this setting is set to "None", it means that all log entries will be recorded, regardless of severity level. ## Adding logs Logs are added via methods in the `WC_Logger` class. The class instance is accessed by using the `wc_get_logger()` function. The basic method for adding a log entry is [`WC_Logger::log( $level, $message, $context )`](https://woocommerce.github.io/code-reference/classes/WC-Logger.html#method_log). There are also shortcut methods for each log severity level, for example `WC_Logger::warning( $message, $context )`. It is preferable to use the shortcut methods rather than the generic `log` method. ### Level Logs have eight different severity levels: * `emergency` * `alert` * `critical` * `error` * `warning` * `notice` * `info` * `debug` Aside from giving a site owner context as to how important a log entry is, these levels also allow logs to be filtered. If you only want log entries to be recorded for `error` severity and higher, you can set the threshold on the Logs Settings screen (see the "Configuring the logger" above). Note that this threshold will apply to all logs, regardless of which log handler is in use. The `WC_Log_Handler_Email` class, for example, has its own threshold setting, but it is secondary to the global threshold. ### Message The message is the main content of a log entry. Make sure it's understandable by anyone who might be viewing the logs! ### Context The context parameter is intended to be used to store additional structured data related to the log entry. For example, in a log entry about an order, you may want to include contents of the related order object. When the logger is generating an entry, the data in the context parameter is converted to JSON before it is stored. So, if you want to add multiple pieces of context data, each should be added as a separate key within the context array. There are two specific keys that can be added to the context array that will cause special behavior: #### `source` It is recommended that every log entry include a `source` value in the context parameter. `source` is intended to provide context about where in the application or codebase the log was generated, and can be used to filter log entries. If a source is not specified, the logger will generate a source value based on the plugin or file where the log entry was generated. #### `backtrace` Setting the `backtrace` key in your context parameter to `true` will cause the logger to generate a backtrace (i.e. stack trace) in array format, which will be included in the context in place of the `true` value. This is useful particularly for error-related logs, so you can see what code was executed that led to the log entry being generated. ![A backtrace displayed in the log file viewer](/img/doc_images/backtrace.png) ### Full example ```php wc_get_logger()->info( 'It is time for lunch.', array( 'source' => 'your_stomach', 'backtrace' => true, 'previous_meal' => $elapsed_time_since_breakfast, 'lunch_options' => array( 'fridge leftovers', 'bahn mi', 'tacos', 'pupusas' ), ) ); ``` ## Logging best practices ### When to use logging * To help troubleshoot an unexpected problem: * An unexpected value or error is received from an API. * Data in the system is incorrect or corrupted. * The application is in an unexpected or broken state. * To keep an audit log of a process: * Transaction details not stored elsewhere, like an order note (but maybe they should be?) * Results of data import or export. * An important setting is changed. * An automated process changes or deletes data. ### When _not_ to use logging * To let a developer know that they're using a method or API incorrectly. This can lead to a large volume of useless log entries, especially if it will get triggered on every page request. Better to give them immediate feedback in the form of an error or exception (e.g. `wc_doing_it_wrong()`). ### Best practices * Rather than using the `WC_Logger`'s `log()` method directly, it's better to use one of the wrapper methods that's specific to the log level. E.g. `info()` or `error()`. * Write a message that is a complete, coherent sentence. This will make it more useful for people who aren't familiar with the codebase. * Log messages should not be translatable. Keeping the message in English makes it easier to search for solutions based on the message contents, and also makes it easier for anyone troubleshooting to understand what's happening, since they may not speak the same language as the site owner. * Ideally, each log entry message should be a single line (i.e. no line breaks within the message string). Additional lines or extra data should be put in the context array. * Avoid outputting structured data in the message string. Put it in a key in the context array instead. The logger will handle converting it to JSON and making it legible in the log viewer. * If you need to include a stack trace, let the logger generate it for you. * Decide on a source for the component or system you are working on and use it for every log call you make. This will make it easier to find all the log entries that are related to the component, and filter them out from other, unrelated log entries. * Consider adding one log entry after a looped process with the aggregated results of the loop, rather than adding a separate entry during each item within a loop. ## Customizing the logger ### The logger class The `WC_Logger` class can be substituted for another class via the `woocommerce_logging_class` filter hook. The alternative class must implement [`WC_Logger_Interface`](https://woocommerce.github.io/code-reference/classes/WC-Logger-Interface.html), or it will not be used. Generally it is better to create a custom log handler (see below) rather than overriding the logger class itself. ### Log handlers In WooCommerce, a log handler is a PHP class that takes the raw log data and transforms it into a log entry that can be stored or dispatched. WooCommerce ships with four different log handler classes: * `Automattic\\WooCommerce\\Internal\\Admin\\Logging\\LogHandlerFileV2`: This is the default handler, representing the "file system" log storage method. It records log entries to files. * `WC_Log_Handler_File`: This is the old default handler that also records log entries to files. It may be deprecated in the future, and it is not recommended to use this class or extend it. * `WC_Log_Handler_DB`: This handler represents the "database" log storage method. It records log entries to the database. * `WC_Log_Handler_Email`: This handler does not store log entries, but instead sends them as email messages. Emails are sent to the site admin email address. This handler has [some limitations](https://github.com/woocommerce/woocommerce/blob/fe81a4cf27601473ad5c394a4f0124c785aaa4e6/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php#L15-L27). #### Changing or adding handlers To switch from the file handler to the database handler, you can simply update the option on the Logs Settings screen. However, in some cases, you may want to have more than one log handler, and/or you might want to modify the settings of a handler. For example, you may want to have most logs saved to files, but log entries that are classified as emergency or critical errors also sent to an email address. For this, you can use the `woocommerce_register_log_handlers` filter hook to create an array of log handler class instances that you want to use. Some handler class constructors have optional parameters that you can use when instantiating the class to change their default behavior. Here's an example of adding the email handler: ```php function my_wc_log_handlers( $handlers ) { $recipients = array( 'wayne@example.com', 'garth@example.com' ); // Send logs to multiple recipients. $threshold = 'critical'; // Only send emails for logs of this level and higher. $handlers[] = new WC_Log_Handler_Email( $recipients, $threshold ); return $handlers; } add_filter( 'woocommerce_register_log_handlers', 'my_wc_log_handlers' ); ``` #### Creating a custom handler You may want to create your own log handler class in order to send logs somewhere else, such as a Slack channel or perhaps an InfluxDB instance. Your class must extend the [`WC_Log_Handler`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler.html) abstract class and implement the [`WC_Log_Handler_Interface`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler-Interface.html) interface. The [`WC_Log_Handler_Email`](https://github.com/woocommerce/woocommerce/blob/6688c60fe47ad42d49deedab8be971288e4786c1/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php) handler class provides a good example of how to set this up. ### Log file storage location When using the "file system" log handler, by default the log files are stored in the `wc-logs` subdirectory of the WordPress `uploads` directory, which means they might be publicly accessible. WooCommerce adds an `.htaccess` file to prevent access to `wc-logs`, but not all web servers recognize that file. If you have the option, you may want to consider storing your log files in a directory outside of the web root. Make sure the directory has the same user/group permissions as the `uploads` directory so that WordPress can access it. Then use the `woocommerce_log_directory` filter hook to set the path to your custom directory. ### Turning off noisy logs If there is a particular log that is recurring frequently and clogging up your log files, you should probably figure out why it keeps getting triggered and resolve the issue. However, if that's not possible, you can add a callback to the `woocommerce_logger_log_message` filter hook to ignore that particular log while still allowing other logs to get through: ```php function my_ignored_logs( $message, $level, $context, $handler ) { if ( false !== strpos( $message, 'Look, a squirrel!' ) ) { return null; } return $message; } add_filter( 'woocommerce_logger_log_message', 'my_ignored_logs', 10, 4 ); ``` ### The Logs UI If you create a custom log handler and you want to build a separate interface for it on the Logs screen, there are a couple of filter hooks that you can work with: * `woocommerce_logger_handler_options`: This filter hook allows you to add your custom log handler to the list of "Log storage" options on the Settings screen. Your handler must be selected for this setting, making it the "default" log handler, before you can render an alternative interface for it. * `wc_logs_render_page`: This is the action to hook into for rendering your own Logs interface. It only fires if the default log handler is set to something that is not one of the built-in handlers. * `wc_logs_load_tab`: This action fires when the Logs tab first starts loading, before any content is output. It's useful for handling form submissions. --- ## Updating countries and subdivisions *Source: best-practices/localization-translation/countries-and-subdivisions.md* # Updating countries and subdivisions WooCommerce comes complete with a comprehensive list of countries and subdivisions (such as provinces or states) that are used in various parts of the user interface. Of course, even countries and their subdivisions periodically change. In these cases, you can certainly file a [bug report](https://github.com/woocommerce/woocommerce/issues/new?template=1-bug-report.yml) or [submit a pull request](/docs/contribution/contributing). However, it is important to understand that our policy is only to accept changes if they align with the current version of the [CLDR project](https://cldr.unicode.org/). Therefore, it is generally best to review that and, if necessary, propose a change there first before asking that it be adopted by WooCommerce. This approach may not be suitable in all cases, because it can take time for CLDR to accept updates. In such cases, you can still modify the lists of countries and subdivisions by using custom snippets like the following: - [Snippet to add a country](/docs/code-snippets/add-a-country) - [Snippet to add or modify states](/docs/code-snippets/add-or-modify-states) --- ## How to translate WooCommerce *Source: best-practices/localization-translation/translating-woocommerce.md* # How to translate WooCommerce WooCommerce is already translated into several languages and is translation-ready right out of the box. All that's needed is a translation file for your language. There are several methods to create a translation, most of which are outlined in the WordPress Codex. In most cases you can contribute to the project on [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/). To create custom translations you can consider using [Poedit](https://poedit.net/). ## Set up WordPress in your language To set your WordPress site's language: 1. Go to `WP Admin > Settings > General` and adjust the `Site Language`. 2. Go to `WP Admin > Dashboard > Updates` and click the `Update Translations` button. Once this has been done, the shop displays in your locale if the language file exists. Otherwise, you need to create the language files (process explained below). ## Contributing your localization to core We encourage contributions to our translations. If you want to add translated strings or start a new translation, simply register at WordPress.org and submit your translations to [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/) for approval. ## Translating WooCommerce into your language Both stable and development versions of WooCommerce are available for translation. When you install or update WooCommerce, WordPress will automatically fetch a 100% complete translation for your language. If such a translation isn't available, you can either download it manually or contribute to complete the translation, benefiting all users. If you're new to translating, check out the [translators handbook](https://make.wordpress.org/polyglots/handbook/tools/glotpress-translate-wordpress-org/) to get started. ### Downloading translations from translate.wordpress.org manually 1. Go to [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/woocommerce) and look for your language in the list. 2. Click the title to be taken to the section for that language. ![screenshot of WooCommerce translation page on wordpress.org](https://developer.woocommerce.com/wp-content/uploads/2023/12/2016-02-17-at-09.57.png) 3. Click the heading under `Set/Sub Project` to view and download a Stable version. ![list of versions available for selected language](https://developer.woocommerce.com/wp-content/uploads/2023/12/2016-02-17-at-09.59.png) 4. Scroll to the bottom for export options. Export a `.mo` file for use on your site. 5. Rename this file to `woocommerce-YOURLANG.mo` (e.g., Great Britain English should be `en_GB`). The corresponding language code can be found by going to [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/) and opening the desired language. The language code is visible in the upper-right corner. ![screenshot of plugin card with associated language code](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-10-17-at-09.44.53.png) 6. Upload to your site under `wp-content/languages/woocommerce/`. Once uploaded, this translation file may be used. ## Creating custom translations WooCommerce includes a language file (`.pot` file) that contains all of the English text. You can find this language file inside the plugin folder in `woocommerce/i18n/languages/`. ## Creating custom translations with PoEdit WooCommerce comes with a `.pot` file that can be imported into PoEdit to translate. To get started: 1. Open PoEdit and select `Create new translation from POT template`. 2. Choose `woocommerce.pot` and PoEdit will show the catalog properties window. ![screenshot](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screen-Shot-2013-05-09-at-10.16.46.png) 3. Enter your name and details, so other translators know who you are, and click `OK`. 4. Save your `.po` file. Name it based on what you are translating to, i.e., a GB translation is saved as `woocommerce-en_GB.po`. Now the strings are listed. ![screenshot](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screen-Shot-2013-05-09-at-10.20.58.png) 5. Save after translating strings. The `.mo` file is generated automatically. 6. Update your `.po` file by opening it and then go to `Catalog > Update from POT file`. 7. Choose the file and it will be updated accordingly. ## Making your translation upgrade safe WooCommerce keeps translations in `wp-content/languages/plugins`, like all other plugins. But if you wish to include a custom translation, you can use the directory `wp-content/languages/woocommerce`, or you can use a snippet to load a custom translation stored elsewhere: ```php // Code to be placed in functions.php of your theme or a custom plugin file. add_filter( 'load_textdomain_mofile', 'load_custom_plugin_translation_file', 10, 2 ); /** * Replace 'textdomain' with your plugin's textdomain. e.g. 'woocommerce'. * File to be named, for example, yourtranslationfile-en_GB.mo * File to be placed, for example, wp-content/languages/textdomain/yourtranslationfile-en_GB.mo */ function load_custom_plugin_translation_file( $mofile, $domain ) { if ( 'textdomain' === $domain ) { $mofile = WP_LANG_DIR . '/textdomain/yourtranslationfile-' . get_locale() . '.mo'; } return $mofile; } ``` ## Other tools There are some other third-party tools that can help with translations. The following list shows a few of them. ### Loco Translate [Loco Translate](https://wordpress.org/plugins/loco-translate/) provides in-browser editing of WordPress translation files and integration with automatic translation services. ### Say what? [Say what?](https://wordpress.org/plugins/say-what/) allows to effortlessly translate or modify specific words without delving into a WordPress theme's `.po` file. ### String locator [String Locator](https://wordpress.org/plugins/string-locator/) enables quick searches within themes, plugins, or the WordPress core, displaying a list of files with the matching text and its line number. ## FAQ ### Why some strings on the Checkout page are not being translated? You may see that some of the strings are not being translated on the Checkout page. For example, in the screenshot below, `Local pickup` shipping method, `Cash on delivery` payment method and a message related to Privacy Policy are not being translated to Russian while the rest of the form is indeed translated: ![checkout page with some strings not translated](https://developer.woocommerce.com/wp-content/uploads/2023/12/not_translated.jpg) This usually happens when you first install WooCommerce and select default site language (English) and later change the site language to another one. In WooCommerce, the strings that have not been translated in the screenshot, are stored in the database after the initial WooCommerce installation. Therefore, if the site language is changed to another one, there is no way for WooCommerce to detect a translatable string since these are database entries. In order to fix it, navigate to WooCommerce settings corresponding to the string you need to change and update the translation there directly. For example, to fix the strings in our case above, you would need to do the following: **Local pickup**: 1. Go to `WooCommerce > Settings > Shipping > Shipping Zones`. 2. Select the shipping zone where "Local pickup" is listed. 3. Open "Local pickup" settings. 4. Rename the method using your translation. 5. Save the setting. **Cash on delivery**: 1. Go to `WooCommerce > Settings > Payments`. 2. Select the "Cash on delivery" payment method. 3. Open its settings. 4. Rename the method title, description, and instructions using your translation. 5. Save the setting. **Privacy policy message**: 1. Go to `WooCommerce > Settings > Accounts & Privacy`. 2. Scroll to the "Privacy policy" section. 3. Edit both the `Registration privacy policy` and `Checkout privacy policy` fields with your translation. 4. Save the settings. Navigate back to the Checkout page - translations should be reflected there. ### I have translated the strings I needed, but some of them don't show up translated on the front end. Why? If some of your translated strings don't show up as expected on your WooCommerce site, the first thing to check is if these strings have both a Single and Plural form in the Source text section. To do so, open the corresponding translation on [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/), e.g. [the translation for Product and Products](https://translate.wordpress.org/projects/wp-plugins/woocommerce/stable/de/default/?filters%5Bstatus%5D=either&filters%5Boriginal_id%5D=577764&filters%5Btranslation_id%5D=24210880). This screenshot shows that the Singular translation is available: ![This screenshot shows that the Singular translation is available:](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-10-17-at-10.10.06.png) While this screenshot shows that the Plural translation is not available: ![this screenshot shows that the Plural translation is not available](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-10-17-at-10.10.21.png) --- ## How to configure caching plugins for WooCommerce *Source: best-practices/performance/configuring-caching-plugins.md* # How to configure caching plugins for WooCommerce ## Excluding pages from the cache Oftentimes if using caching plugins they'll already exclude these pages. Otherwise make sure you exclude the following pages from the cache through your caching systems respective settings. - Cart - My Account - Checkout These pages need to stay dynamic since they display information specific to the current customer and their cart. ## Excluding WooCommerce session from the cache If the caching system you're using offers database caching, it might be helpful to exclude `_wc_session_` from being cached. This will be dependent on the plugin or host caching so refer to the specific instructions or docs for that system. ## Excluding WooCommerce cookies from the cache Cookies in WooCommerce help track the products in your customers cart, can keep their cart in the database if they leave the site, and powers the recently viewed widget. Below is a list of the cookies WooCommerce uses for this, which you can exclude from caching. | COOKIE NAME | DURATION | PURPOSE | | --- | --- | --- | | woocommerce_cart_hash | session | Helps WooCommerce determine when cart contents/data changes. | | woocommerce_items_in_cart | session | Helps WooCommerce determine when cart contents/data changes. | | wp_woocommerce_session_ | 2 days | Contains a unique code for each customer so that it knows where to find the cart data in the database for each customer. | | woocommerce_recently_viewed | session | Powers the Recent Viewed Products widget. | | store_notice[notice id] | session | Allows customers to dismiss the Store Notice. | We're unable to cover all options, but we have added some tips for the popular caching plugins. For more specific support, please reach out to the support team responsible for your caching integration. ### W3 total cache minify settings Ensure you add 'mfunc' to the 'Ignored comment stems' option in the Minify settings. ### WP-Rocket WooCommerce is fully compatible with WP-Rocket. Please ensure that the following pages (Cart, Checkout, My Account) are not to be cached in the plugin's settings. We recommend avoiding JavaScript file minification. ### WP Super Cache WooCommerce is natively compatible with WP Super Cache. WooCommerce sends information to WP Super Cache so that it doesn't cache the Cart, Checkout, or My Account pages by default. ### Varnish ```varnish if (req.url ~ "^/(cart|my-account|checkout|addons)") { return (pass); } if ( req.url ~ "\\?add-to-cart=" ) { return (pass); } ``` ## Troubleshooting ### Why is my Varnish configuration not working in WooCommerce? Check out the following WordPress.org Support forum post on[ how cookies may be affecting your varnish coding](https://wordpress.org/support/topic/varnish-configuration-not-working-in-woocommerce). ```text Add this to vcl_recv above "if (req.http.cookie) {": # Unset Cookies except for WordPress admin and WooCommerce pages if (!(req.url ~ "(wp-login|wp-admin|cart|my-account/*|wc-api*|checkout|addons|logout|lost-password|product/*)")) { unset req.http.cookie; } # Pass through the WooCommerce dynamic pages if (req.url ~ "^/(cart|my-account/*|checkout|wc-api/*|addons|logout|lost-password|product/*)") { return (pass); } # Pass through the WooCommerce add to cart if (req.url ~ "\?add-to-cart=" ) { return (pass); } # Pass through the WooCommerce API if (req.url ~ "\?wc-api=" ) { return (pass); } # Block access to php admin pages via website if (req.url ~ "^/phpmyadmin/.*$" || req.url ~ "^/phppgadmin/.*$" || req.url ~ "^/server-status.*$") { error 403 "For security reasons, this URL is only accessible using localhost (127.0.0.1) as the hostname"; } Add this to vcl_fetch: # Unset Cookies except for WordPress admin and WooCommerce pages if ( (!(req.url ~ "(wp-(login|admin)|login|cart|my-account/*|wc-api*|checkout|addons|logout|lost-password|product/*)")) || (req.request == "GET") ) { unset beresp.http.set-cookie; } ``` ### Why is my Password Reset stuck in a loop? This is due to the My Account page being cached, Some hosts with server-side caching don't prevent my-account.php from being cached. If you're unable to reset your password and keep being returned to the login screen, please speak to your host to make sure this page is being excluded from their caching. --- ## Performance best practices for WooCommerce extensions *Source: best-practices/performance/performance-best-practices.md* # Performance best practices for WooCommerce extensions Optimizing the performance of WooCommerce extensions is vital for ensuring that online stores run smoothly, provide a superior user experience, and rank well in search engine results. This guide is tailored for developers looking to enhance the speed and efficiency of their WooCommerce extensions, with a focus on understanding performance impacts, benchmarking, testing, and implementing strategies for improvement. ## Performance optimization For WooCommerce extensions, performance optimization means ensuring that your code contributes to a fast, responsive user experience without adding unnecessary load times or resource usage to the store. ### Why performance is critical - **User Experience**: Fast-performing extensions contribute to a seamless shopping experience, encouraging customers to complete purchases. - **Store Performance**: Extensions can significantly impact the overall speed of WooCommerce stores; optimized extensions help maintain optimal site performance. - **SEO and Conversion Rates**: Speed is a critical factor for SEO rankings and conversion rates. Efficient extensions support better store rankings and higher conversions. ## Benchmarking performance Setting clear performance benchmarks is essential for development and continuous improvement of WooCommerce extensions. A recommended performance standard is achieving a Chrome Core Web Vitals "Performance" score of 90 or above on a simple Woo site, using tools like the [Chrome Lighthouse](https://developer.chrome.com/docs/lighthouse/overview/). ### Using accessible tools for benchmarking Chrome Lighthouse provides a comprehensive framework for evaluating the performance of web pages, including those impacted by your WooCommerce extension. By integrating Lighthouse testing into your development workflow, you can identify and address performance issues early on. We recommend leveraging tools like this to assess the impact of your extension on a WooCommerce store's performance and to identify areas for improvement. ## Performance improvement strategies Optimizing the performance of WooCommerce extensions can involve several key strategies: - **Optimize asset loading**: Ensure that scripts and styles are loaded conditionally, only on pages where they're needed. - **Efficient database queries**: Optimize database interactions to minimize query times and resource usage. Use indexes appropriately and avoid unnecessary data retrieval. - **Lazy Loading**: Implement lazy loading for images and content loaded by your extension to reduce initial page load times. - **Minification and concatenation**: Minify CSS and JavaScript files and concatenate them where possible to reduce the number of HTTP requests. - **Testing with and without your extension**: Regularly test WooCommerce stores with your extension activated and deactivated to clearly understand its impact on performance. - **Caching support**: Ensure your extension is compatible with popular caching solutions, and avoid actions that might bypass or clear cache unnecessarily. By following these best practices and regularly benchmarking and testing your extension, you can ensure it enhances, rather than detracts from, the performance of WooCommerce stores. Implementing these strategies will lead to more efficient, faster-loading extensions that store owners and their customers will appreciate. --- ## How to optimize performance for WooCommerce stores *Source: best-practices/performance/performance-optimization.md* # How to optimize performance 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. --- ## Review guidelines *Source: best-practices/review-guidelines/README.md* # Review guidelines Reviews are an integral part of the online shopping experience, and people installing software pay attention to them. Prospective users of your extensions will likely consider average ratings when making software choices. Many of today's most popular online review platforms - from Yelp business reviews, to Amazon product reviews - have a range of opinion that can be polarized, with many extremely positive and/or negative reviews, and fewer moderate opinions. This creates a "J-shaped" distribution of reviews that isn't as accurate or as helpful as could be. WooCommerce.com and WordPress.org both feature reviews heavily, and competing extensions having a higher rating likely have the edge in user choice. ## Primary considerations around reviews Requesting more reviews for a extension with major issues will not generate good reviews, and analyzing existing reviews will help surface areas to address before soliciting reviews. It is extremely rare for users of WordPress plugins to leave reviews organically (.2% of users for WordPress.org leave reviews), which means that there's an untapped market of 99.8% of users of the average plugin. These plugins are competing with other plugins on the same search terms in the WordPress.org plugin directory, and ratings are a large factor in the ranking algorithm. This is not usually a factor to the same extent on the WooCommerce Marketplace. For instance, WooCommerce's PayPal extension directly competes on all possible keywords with other PayPal extensions on the WordPress.org repository, while it does not compete with other PayPal payments extensions on the Marketplace. --- ## How to request WooCommerce extension reviews *Source: best-practices/review-guidelines/how-to-request-reviews.md* # How to request WooCommerce extension reviews ## Methods of requesting reviews ### Admin notices Admin notices are an industry standard method of requesting reviews, but bombarding the admin dashboard with admin notices is not effective. We recommend using restraint in the design of a notice, as well as limiting to a single notice at a time. It's very easy to overwhelm merchants with too many notices, or too intrusive notices. #### Recommendations * A good place for an admin notice to review an extension would be to show on the `Plugins` page and extension's settings pages. * Include a snooze option (or multiple) on your notices with a clear expectation of when the notice will reappear. * Admin notices should always be always be completely dismissable. They cannot only have a snooze option. * The options presented in the notice must be phrased carefully to avoid manipulative language. * Use consistently designed notices so the request for reviews feels like a part of your extension, and looks consistent with WooCommerce's design. ### Direct contact #### Recommendations * The most direct route to requesting reviews with the highest chance of being positive is to contact the customer when they are the happiest with the product. * This can be milestone or time based, following the timing guidelines below. * The best method for this is either an email or other direct exchange (support chat, call, etc.). This has the highest conversion rate, especially when timed properly so that the customer is happiest. * This is also extremely effective when you are able to request feedback from specifically qualified merchants, such as merchants that have processed a certain amount with your platform, or who have shipped their first 100 orders using your fulfillment extension, or similar. * Direct outreach is most likely to be successful if you have ways of targeting users for review requests (merchant account / usage info, etc.), as well as ways to gather the reviews, like sales or marketing teams able to email/call/chat with merchants. ## Messaging for requesting reviews One method of requesting feedback that we recommend is using the NPS style of review solicitation. This can allow for an increase positive reviews as well as providing the opportunity to assist merchants that are struggling. NPS-style reviews first ask the user how they rate the product (out of 5 stars), then route them based on their response: * If they click 4 or 5 stars, ask them to leave a review. * If they click 1, 2 or 3, tell them we're here to help & ask them to submit a support ticket. Merchants are significantly more likely to leave a review after a positive support interaction with a support rep who explicitly asked for a review. The language "the best way to thank me is to leave a 5 star review that mentions me in it" or similar tends to work very well - people are more willing to help a person than a produc or company. --- ## Miscellaneous guidelines and advice *Source: best-practices/review-guidelines/misc-guidelines.md* # Miscellaneous guidelines and advice Contributors' names matching search terms directly will rank extremely highly on the WordPress.org plugin repo, which means that having a WordPress.org user named after your business (if that's a search term for your plugin) could tilt the scales over a competing plugin. Constant nags and overwhelming the admin dashboard with unnecessary alerts detract from your user experience. You can request to have reviews that are not actually feedback removed. Where this might be applicable is if the request is a simple support request, where it's obviously in the wrong spot. 1 star reviews, even if aggressive or angry, are not usually removed. Reply to reviews! Thank the giver for offering feedback, acknowledge the issue if needed, ask for more specific feedback, or provide an update when that feedback is addressed! Your reviews are a great window into what your extension's users are actually thinking. Having folks close to the extension's development (think developers and project managers) looking at reviews on a regular basis is a good way to ensure the customers voice is heard. 1 star reviews are among the most useful, as long as the issue is understood and addressed (if needed). --- ## Notifying users about bug fixes and feature requests *Source: best-practices/review-guidelines/notifying-users-about-important-events.md* # Notifying users about bug fixes and feature requests A bug or a missing feature can be a showstopper for merchants. Bugs that pile up or popular features that are not implemented can lead to negative reviews and/or merchants churning and looking at a competitive solution. Bugs are usually reported via support or GitHub, or you can discover them yourselves in testing. When a critical bug is found, resolve it within a couple of days (most critical bugs should be resolved within 24 hours) and release a new plugin version promptly. Part of your release process should be to notify all stakeholders (the support team and the merchant affected) about the upcoming release. Even though a critical bug is a great source of stress for merchants, a quick resolution makes merchants feel heard and supported - having a reliable business partner, who is keen to help in the most difficult situation, helps build a stronger relationship. Therefore, we usually ask merchants for a 5* review when we deliver a fast solution. When you implement a new feature request and ship a new plugin version, you can follow a similar approach to bugs: * Notify all stakeholders. * Update the relevant request in the Feature Requests board, by sharing a public update and marking it as 'Completed'. * For breaking releases: communicating with your marketing/relations teams to publish updates/newsletters before the release. --- ## How to respond to negative WooCommerce extension reviews *Source: best-practices/review-guidelines/responding-to-negative-reviews.md* # How to respond to negative WooCommerce extension reviews An unpleasant event in the merchant's journey can lead them to leave a public, negative review. These events usually are: * a problem with the product, * a missing product feature, * an unhelpful reply, * long wait times to receive a reply, or; * combinations of the above. When receiving a negative review, your goal should always be to turn this review around - this sounds tough, but it is really rewarding. In the majority of cases, merchants who leave a negative review have first tried contacting support for help. This is useful knowledge, as we can read through the conversation history, understand the issue the merchant experienced and share more details with them when we reach out, even from our first reply. The process we have seen work well is: * Create a new response (via email, or on a public review) with subject: Regarding your recent review for xxx. * Start by introducing yourself, for example: "Hey there, This is Andrew from the team that develops xxx". * Use empathetic language and make it clear that this negative review had an impact on you. For example, "I read your recent review for xxx and I am worried to hear that an issue is preventing you from using this plugin as you had in mind. I'd be happy to help you resolve this!". Compare the above sentence with: "I am sorry to hear that you experienced an issue with xxx". "I am sorry to" indicates that you are saddened by an event, but don't necessarily plan to do something about it. In comparison, in "I am worried to hear", worry indicates action. Additionally, "That you experienced an issue" can be interpreted as if the problem is mainly the merchant's fault, whereas language like "an issue is preventing you from using this plugin as you had in mind. I'd be happy to help you resolve this!" implies you and the merchant are on the same team. * Share more details, a solution, an idea, a suggestion or an explanation. * Urge the merchant to update the review, by highlighting how important reviews are for our team and for other merchants. Example language to do this is "We would appreciate it if you could take a couple of minutes to update your review and describe your experience with our product and support. Honest reviews are extremely helpful for other merchants who try to decide if a plugin is a right fit for them. Thank you for your contribution!". * Include a direct link to the reviews section, so merchants can easily navigate there and change their review. * On a follow-up communication, if the merchant has changed the review, consider saying something like: "I shared this with the rest of the team and it made everyone's day". If the above things are true, sharing some of your procedures with merchants (highlighting how your team emphasizes and thrives on feedback) helps ensure merchants feel like you are part of their team and builds a strong relationship with them. Even a merchant that doesn't change their review can offer a mutually beneficial discussion by learning more about their setup and offering some suggestions. These conversations help grow merchants' trust. --- ## Utilizing WooCommerce extension feature requests *Source: best-practices/review-guidelines/utilizing-feature-requests.md* # Utilizing WooCommerce extension feature requests It is important to keep track of all feature requests, and have some sort of system of record where anyone can see what kind of feedback the product is receiving over time. We recommend a daily or bi-daily check-in, where you: * triage new feature requests, * celebrate positive reviews and; * act upon negative reviews. Carefully maintaining feature request boards (or similar system) is key, as the average board contains a lot of duplicate/spam content, requests about features that have been implemented and requests about features that will likely never be implemented. Poorly maintained boards make merchants feel unheard/neglected. This results in more negative reviews on the premise that the product teams were not reading/listening to their feedback. We've seen good results with the following procedures: Starting with the most affected products, go through all open requests, reply to most/all of them and categorize them as: * "Open", for requests that we still want more feedback, * "Planned", for requests that we plan to implement, * "Completed", for requests that have already been implemented and; * "Closed", for requests that we do not plan to implement, as they are not a good fit for the product, for duplicate/spam requests and for requests that were actually support questions. Replying to all "Open" requests is the goal, but if that's not attainable currently, make sure to reply to 100% of the requests that are closed. For new open requests that arrive as a feature request, discuss/triage them, reply promptly, and assign a status to avoid having the board become unmanaged, and ensure merchants feel (and are) heard. In addition to the effect a tidy board has on merchants, it also helps product teams better understand which requests are most wanted and most impactful and then plan work accordingly. --- ## Utilizing your support team to respond to feedback *Source: best-practices/review-guidelines/utilizing-your-support-team.md* # Utilizing your support team to respond to feedback Your support team is usually the primary contact point of merchants when they contact you. Tickets and chats are the best tools we have to converse with merchants, understand pain-points about our software, listen to their feedback and analyze their feature requests. Collectively, support teams have a great understanding of the products and how people use them. This information is essential to be transferred over to product and engineering teams. We recommend that you take the following steps to best utilize your support team: * Create a strict internal SLA where support team requests are answered by product or engineering teams. * Ensure you have a way for your support team to effectively report bugs to your product and engineering teams. * When responding to your support team, avoid super-short answers, and try to explain the answer simply and concisely. This will allow the support agent to copy/paste your answer to the merchant. * Avoid replying with statements like "no, this is not possible" or "no, this feature will not be implemented" without providing additional context about technical or product limitations. * Regularly dedicate additional time to implement a short custom code snippets or to provide in-depth technical details about how a custom project would be implemented so that merchants can reach a solution faster if they decide to hire a dedicated WooCommerce developer. A small effort can go a long way to amaze merchants and reveal an opportunity to request a 5 star review. * Keep support in the loop when they report a bug or request a new feature. When you release a new product version, we always consider the impact it can have on support. * Work closely with your support team. For example, consider having a feedback hangout call every month where you can discuss product feedback and planned improvements. With these kinds of practices in place, support teams are more willing to share feedback, issues, concerns, and questions with us. This helps maintain a closer relationship with merchants and identify pain-points early, before they become a reason for them to churn. --- ## When to request WooCommerce extension reviews *Source: best-practices/review-guidelines/when-to-request-reviews.md* # When to request WooCommerce extension reviews The best approach to increasing our top-star reviews is to identify key moments in the merchant's journey, when they are more likely to leave a review and actively request for it. The most distinct moments for most of our use cases are: * When merchants feel helpless, lost, frustrated and we are able to help, * When merchants find a bug in our code and we quickly ship a fix, * When merchants need a feature and we notify them when it is shipped, * When merchants feel alone and we make them feel heard and; * When merchants contact with a question and we go out of our way to provide them with top-notch support, even if this means slightly stepping outside the official boundaries of a support policy. Think about who is seeing the review request, and what they are doing at that time. Showing a request to a fulfillment worker just trying to ship an order isn't likely going to work well. Outreach after a milestone works really well. Some language we've used before is "Congratulations on your xxth sale! We're delighted that WooPayments facilitated this milestone. Would you consider sharing your experience and encouraging others by reviewing our extension?". Another way to optimally time a review request would be to setup a prompt that aligns with use patterns. For instance, if you know that most of your merchants use your extension daily, you would likely send a review request sooner than a extension that most merchants interact very sparingly with. SaaS/Connector extensions need to be particularly careful about requesting ratings correctly, as they are the most likely to be overlooked unless there is an issue, leading to skewed ratings not representative of the actual extension. Consider requesting feedback at the end of every single support interaction, especially in the WordPress.org support forums. One of the largest barriers to leaving a review is the requirement of a user being logged into WooCommerce.com (or WordPress.org), and the WordPress.org support forums present a good opportunity to gather these reviews. By being highly responsive in the public support forum and solving issues there, users are already logged in and able to immediately leave a review (after being requested to!). --- ## How to Prevent Data Leaks in WooCommerce *Source: best-practices/security/prevent-data-leaks.md* # How to Prevent Data Leaks in WooCommerce Data leaks can expose sensitive information and compromise the security of a WooCommerce site. One common source of data leaks is direct access to PHP files. This tutorial will show you how to prevent these kinds of data leaks. In each PHP file of your WooCommerce extension, you should check if a constant called 'ABSPATH' is defined. This constant is defined by WordPress itself, and it's not defined if a file is being accessed directly. Here's how you can do this: ```php if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } ``` With this code, if someone tries to access the PHP file directly, the 'ABSPATH' constant won't be defined, and the script will exit before any sensitive data can be leaked. Remember, security is a crucial aspect of any WooCommerce site. Always take steps to prevent data leaks and protect your site's information. --- ## WooCommerce security best practices *Source: best-practices/security/security-best-practices.md* # WooCommerce security best practices ## Introduction to security practices Security is extremely important for online stores. A breach can lead to significant financial loss, reputational damage, and erosion of customer trust. Implementing robust security practices is crucial. 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. ## Security standards We recommend that you maintain the following standards: - **PHPCS**: The PHP Code Sniffer tool helps ensure your code adheres to specific coding standards, which can prevent common security issues. Extensions shouldn't have any failure results against PHPCS. - **SemGrep**: Focuses on identifying patterns that may indicate security vulnerabilities, allowing developers to write more secure code. Extensions shouldn't have any failure results against SemGrep. Understanding and implementing these standards can significantly reduce the risk of security vulnerabilities in your extensions and themes. ### Common security pitfalls and how to avoid them Several common security pitfalls can easily be avoided with vigilance and best practices: - **SQL injection**: Ensure all database queries are properly sanitized. - **Cross-site scripting**: Sanitize all user input to prevent malicious code from being executed. - **File inclusions**: Limit file inclusion to prevent unauthorized access to the file system. Awareness and prevention of these common issues are crucial steps in securing your WooCommerce store. For more resources, we recommend reading the [WordPress security documentation](https://developer.wordpress.org/apis/security/). ## Manual testing guidelines Even without access to automated tools like the [Woo Quality Insights Toolkit (QIT)](https://qit.woo.com), manual testing remains a vital component of securing your WooCommerce store. Here are some guidelines on how developers can manually test their extensions for security vulnerabilities: 1. **Code review**: Regularly review your code for security vulnerabilities, focusing on the areas where breaches commonly occur, such as user input fields and database queries. Code reviews should check against the [WordPress security criteria](https://developer.wordpress.org/apis/security/) mentioned above. 2. **Use security plugins**: Plugins like Wordfence, Sucuri, or iThemes Security provide an interface for manual security checks, including file integrity monitoring and malware scanning. Additionally, tooling like [OWASP Zap](https://www.zaproxy.org/) or [GitHub Security Scanning](https://github.com/features/security/) can be used. 3. **Testing environments**: Utilize staging environments to test the security of new features or updates before deploying them live. ## Other security best practices ### 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. ### 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. ### 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. ### 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. ### 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. ### 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. ### 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. ### 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. --- ## Shareable Checkout URLs *Source: best-practices/urls-and-routing/checkout-urls.md* # Shareable Checkout URLs Custom checkout links automatically populate the cart with specific products and redirect customers straight to checkout with a unique session id. The custom checkout link path is `/checkout-link/` and is not customizable. ## Supported parameters ### Products ```plaintext products=123:2,456:1 ``` A comma-separated list of product IDs and quantities. For example, `123:2,456:1`. This feature supports simple products with no additional options. Individual variations can also be added to cart by using the correct variation ID. ### Coupon ```plaintext coupon=SPRING10 ``` A coupon code to apply to the cart. For example, `SPRING10`. ## Example ```plaintext https://yourstore.com/checkout-link/?products=123:2,456:1&coupon=SPRING10 ``` In this link: - Product ID `123` will be added with quantity `2` - Product ID `456` with quantity `1` - The coupon code `SPRING10` will be applied - The customer is taken directly to the checkout page ## Sessions Once the user is redirected to the checkout page, the cart is populated with the products and coupon code. The final URL includes a `session` parameter storing the ID of the session. Future changes to the checkout will be persisted in the session, enabling persistent and shareable carts. --- ## Customizing WooCommerce endpoint URLs *Source: best-practices/urls-and-routing/customizing-endpoint-urls.md* # Customizing WooCommerce endpoint URLs Before you start, check out [WooCommerce Endpoints](./woocommerce-endpoints.md). ## Customizing endpoint URLs The URL for each endpoint can be customized in **WooCommerce > Settings > Advanced** in the Page setup section. ![Endpoints](https://developer.woocommerce.com/wp-content/uploads/2023/12/endpoints.png) Ensure that they are unique to avoid conflicts. If you encounter issues with 404s, go to **Settings > Permalinks** and save to flush the rewrite rules. ## Using endpoints in menus If you want to include an endpoint in your menus, you need to use the Links section: ![The Links section of a menu item in WordPress](https://developer.woocommerce.com/wp-content/uploads/2023/12/2014-02-26-at-14.26.png) Enter the full URL to the endpoint and then insert that into your menu. Remember that some endpoints, such as view-order, require an order ID to work. In general, we don't recommend adding these endpoints to your menus. These pages can instead be accessed via the my-account page. ## Using endpoints in payment gateway plugins WooCommerce provides helper functions in the order class for getting these URLs. They are: `$order->get_checkout_payment_url( $on_checkout = false );` and: `$order->get_checkout_order_received_url();` Gateways need to use these methods for full 2.1+ compatibility. ## Troubleshooting ### Endpoints showing 404 - If you see a 404 error, go to **WordPress Admin** > **Settings > Permalinks** and Save. This ensures that rewrite rules for endpoints exist and are ready to be used. - If using an endpoint such as view-order, ensure that it specifies an order number. /view-order/ is invalid. /view-order/10/ is valid. These types of endpoints should not be in your navigation menus. ### Endpoints are not working On Windows servers, the **web.config** file may not be set correctly to allow for the endpoints to work correctly. In this case, clicking on endpoint links (e.g. /edit-account/ or /customer-logout/) may appear to do nothing except refresh the page. In order to resolve this, try simplifying the **web.config** file on your Windows server. Here's a sample file configuration: ```xml <<>?xml version="1.0" encoding="UTF-8"?> ``` ### Pages direct to wrong place Landing on the wrong page when clicking an endpoint URL is typically caused by incorrect settings. For example, clicking 'Edit address' on your account page takes you to the Shop page instead of the edit address form means you selected the wrong page in settings. Confirm that your pages are correctly configured and that a different page is used for each section. ### How to remove "Downloads" from My Account Sometimes the "Downloads" endpoint on the "My account" page does not need to be displayed. This can be removed by going to **WooCommerce > Settings > Advanced > Account endpoints** and clearing the Downloads endpoint field. ![Account endpoints](https://developer.woocommerce.com/wp-content/uploads/2023/12/Screenshot-2023-04-09-at-11.45.58-PM.png) --- ## Understanding the risks of removing URL bases in WooCommerce *Source: best-practices/urls-and-routing/removing-product-product-category-or-shop-from-the-url.md* # Understanding the risks of removing URL bases in WooCommerce ## In sum Removing `/product/`, `/product-category/`, or `/shop/` from the URLs is not advisable due to the way WordPress resolves its URLs. It uses the `product-category` (or any other text for that matter) base of a URL to detect that it is a URL leading to a product category. There are SEO plugins that allow you to remove this base, but that can lead to a number of problems with performance and duplicate URLs. ## Better to avoid You will make it harder for WordPress to detect what page you are trying to reach when you type in a product category URL. Also, understand that the standard "Page" in WordPress always has no base text in the URL. For example: - `http://yoursite.com/about-page/` (this is the URL of a standard page) - `http://yoursite.com/product-category/category-x/` (this is the URL leading to a product category) What would happen if we remove that 'product-category' part? - `http://yoursite.com/about-page/` - `http://yoursite.com/category-x/` WordPress will have to do much more work to detect what page you are looking for when entering one of the above URLs. That is why we do not recommend using any SEO plugin to achieve this. --- ## Working with webhooks in WooCommerce *Source: best-practices/urls-and-routing/webhooks.md* # Working with webhooks in WooCommerce ## What are webhooks? A [Webhook](http://en.wikipedia.org/wiki/Webhook) is an event notification sent to a URL of your choice. Users can configure them to trigger events on one site to invoke behavior on another. Webhooks are useful for integrating with third-party services and other external API that support them. ## Webhooks in WooCommerce Webhooks were introduced in WooCommerce 2.2 and can trigger events each time you add, edit or delete orders, products, coupons or customers. It's also possible to use webhooks with WooCommerce actions, e.g., Create a webhook to be used every time a product is added to the shopping cart, using the action `woocommerce_add_to_cart`. Webhooks also make it easier for third-party apps to integrate with WooCommerce. ## Creating webhooks ![WebHooks screen](https://woocommerce.com/wp-content/uploads/2013/01/woo-webhooks.png) To create a new webhook: 1/ **Go to**: **WooCommerce > Settings > Advanced > Webhooks**. > **Note:** Webhooks were formerly found under WooCommerce > Settings > API prior to WooCommerce 3.4. 2/ Select **Create a new webhook** (first incident) or **Add webhook**. The **Webhook Data** box appears. ![WebHooks creation](https://woocommerce.com/wp-content/uploads/2013/01/woo-webhooks.png) 3/ **Enter**. - **Name**: The **name** is auto-generated as "Webhook created on [date and time of creation]" as a standard to facilitate creation. Change the name to something else. - **Status**: Set to **Active** (delivers payload), **Paused** (does not deliver), or **Disabled** (does not deliver due delivery failures). - **Topic**: Indicate when the webhook should be triggered - **Order Created**, **Product Deleted**, or **Customer Updated**. There are also **Action** and **Custom** options. - **Action Event**: This option is available when the Topic is a WooCommerce **Action**, such as `woocommerce_add_to_cart` for when customers add products to the shopping cart. - **Custom Topic**: This option is for **advanced users only**. It's possible to introduce new, customized topics with the help of `woocommerce_webhook_topic_hooks` filter. - **Delivery URL**: URL where the webhook payload is delivered. - **Secret**: The Secret Key generates a hash of the delivered webhook and is provided in the request headers. This defaults to the current API user's consumer secret, if nothing is entered. 4/ **Save webhook**. > **Note**: The first time your webhook is saved with the Activated status, it sends a ping to the Delivery URL. Webhooks are disabled after 5 retries by default if the delivery URL returns an unsuccessful status such as `404` or `5xx`. Successful responses are `2xx`, `301` or `302`. To increase the number of retries, you can use the `woocommerce_max_webhook_delivery_failures` filter function. ## Editing and deleting webhooks Webhooks are listed the same way as posts or products. 1. Find the webhook you wish to alter. 2. Hover over the name, and **Edit** and **Delete permanently** options appear. 3. **Delete**, or make **Edits** and **Save changes**. Bulk deletion is also possible with the dropdown. ![WebHooks deletion](https://woocommerce.com/wp-content/uploads/2013/01/editdelete-webhook.png) ## Webhook logs WooCommerce saves logs of all events triggering a webhook. Webhook logs are found at: **WooCommerce > Status > Logs**. ![WebHooks logs](https://woocommerce.com/wp-content/uploads/2022/11/Viewing-WooCommerce-Webhook-Logs.png?w=650) Logs may be reviewed to see delivery and response from the server, making it simpler to integrate and debug. --- ## Understanding WooCommerce endpoints *Source: best-practices/urls-and-routing/woocommerce-endpoints.md* # Understanding WooCommerce endpoints Endpoints are an extra part in the website URL that is detected to show different content when present. For example: You may have a 'my account' page shown at URL **yoursite.com/my-account**. When the endpoint 'edit-account' is appended to this URL, making it '**yoursite.com/my-account/edit-account**' then the **Edit account page** is shown instead of the **My account page**. This allows us to show different content without the need for multiple pages and shortcodes, and reduces the amount of content that needs to be installed. Endpoints are located at **WooCommerce > Settings > Advanced**. ## Checkout endpoints The following endpoints are used for checkout-related functionality and are appended to the URL of the /checkout page: - Pay page - `/order-pay/{ORDER_ID}` - Order received (thanks) - `/order-received/` - Add payment method - `/add-payment-method/` - Delete payment method - `/delete-payment-method/` - Set default payment method - `/set-default-payment-method/` ## Account endpoints The following endpoints are used for account-related functionality and are appended to the URL of the /my-account page: - Orders - `/orders/` - View order - `/view-order/{ORDER_ID}` - Downloads - `/downloads/` - Edit account (and change password) - `/edit-account/` - Addresses - `/edit-address/` - Payment methods - `/payment-methods/` - Lost password - `/lost-password/` - Logout - `/customer-logout/` ## Learn more - [Customizing endpoint URLs](./customizing-endpoint-urls.md) --- ## WooCommerce Block Development *Source: block-development/README.md* # WooCommerce Block Development Welcome to the WooCommerce Block Development documentation! This section is your starting point for learning how to build, customize, and extend blocks for WooCommerce. Whether you’re new to block development or looking to deepen your expertise, this guide will walk you through the process step by step, from the basics to advanced extensibility. ## Start Here - **[Getting Started](/docs/block-development/getting-started/extensibility-overview/)**: Set up your environment and create your first block. - **[Tutorials](/docs/category/tutorials/)**: Follow hands-on guides to build and enhance your blocks. - **[Reference](/docs/category/reference/)**: Look up APIs, filters, and block details as you build. - **[Extensibility](/docs/category/extensible-blocks/)**: Learn to extend and customize blocks for advanced use cases. - **[Cart & Checkout](/docs/block-development/extensible-blocks/cart-and-checkout-blocks/)** and **[Product Collection](/docs/block-development/extensible-blocks/product-collection-block/)**: Explore specialized block documentation. Happy building! --- ## Getting started with Cart and Checkout extensibility *Source: block-development/extensible-blocks/cart-and-checkout-blocks/README.md* # Getting started with Cart and Checkout extensibility This document is a high-level overview of the moving parts required to extend the Cart and Checkout blocks. To get started, it is recommended to first read the [Block Development Environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) documentation from WordPress and follow [Tutorial: Build your first block ](https://developer.wordpress.org/block-editor/getting-started/tutorial/). ## Example block template package There is an example block template in the WooCommerce repository. Having this template set up while reading this document may help to understand some of the concepts discussed. See the [`@woocommerce/extend-cart-checkout-block` package documentation](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/extend-cart-checkout-block/README.md) for how to install and run the example block. (Note: the code in the repository linked above will not be very useful alone; the code there is templating code. It will be transformed into regular JS and PHP after following the README instructions.) ## Front-end extensibility To extend the front-end of the blocks, extensions must use JavaScript. The JavaScript files must be enqueued and loaded on the page before they will have any effect. ### Build system Some extensions may be very simple and include only a single JavaScript file, other extensions may be complex and the code may be split into multiple files. Either way, it is recommended that the files are bundled together and minified into a single output file. If your extension has several distinct parts that only load on specific pages, bundle splitting is recommended, though that is out of scope for this document. To set up the build system, the recommended approach is to align with WordPress and use a JavaScript package called [`@wordpress/scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/). This package contains a script called `build`. By default, this will build your scripts into a single output file that can be enqueued using `wp_enqueue_script`. The base configuration of the `build` script in `@wordpress/scripts` can be overridden by creating a `webpack.config.js` file in the root of your plugin. The example block shows how the base config can be extended. #### `WooCommerceDependencyExtractionWebpackPlugin` See [`WordPress Dependency Extraction Webpack Plugin`](https://github.com/WordPress/gutenberg/tree/trunk/packages/dependency-extraction-webpack-plugin) and [`WooCommerce Dependency Extraction Webpack Plugin`](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/dependency-extraction-webpack-plugin#dependency-extraction-webpack-plugin). This Webpack plugin is used to: - Externalize dependencies that are available as shared scripts or modules on WordPress sites. - This means when you import something from `@woocommerce/blocks-checkout` it resolves that path to `window.wc.wcBlocksCheckout` without you needing to change your code. It makes your code easier to read and allows these packages to be loaded onto the page once. - Add an asset file for each entry point that declares an object with the list of WordPress script or module dependencies for the entry point. The asset file also contains the current version calculated for the current source code. The PHP "asset file" that this plugin outputs contains information your script needs to register itself, such as dependencies and paths. If you have written code that is built by Webpack and using the WooCommerce Dependency Extraction Webpack Plugin, there will be an asset file output for each entry point. This asset file is a PHP file that contains information about your script, specifically dependencies and version, here's an example: ```php array( 'react', 'wc-settings', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '455da4f55e1ac73b6d34' ); ``` When enqueueing your script, using this asset file will ensure the dependencies are loaded correctly and that the client gets the most up-to-date version of your script (the version is used to ensure your script is fetched fresh, rather than from the cache). ```php [], 'version' => $this->get_file_version( $script_path ), ]; wp_register_script( 'example-blocks-integration-handle', $script_url, $script_asset['dependencies'], $script_asset['version'], true ); ``` Please see the [Cart and Checkout – Handling scripts, styles, and data](/docs/block-development/reference/integration-interface/) document for more information about how to correctly register scripts using the `IntegrationInterface`. ### Creating a block In the example block, there is a "checkout-newsletter-subscription-block" directory which contains the files needed to register an inner block in the Checkout. The example block template is only set up to import and build a single block, but the Webpack config can be modified to build multiple blocks. Doing this is not supported as part of this document, refer to the [Webpack documentation](https://webpack.js.org/concepts/) instead. The principles covered in [Tutorial: Build your first block ](https://developer.wordpress.org/block-editor/getting-started/tutorial/) apply here too. ### Modifying existing values on the front-end You may not need to create a block to get your extension working the way you want, for example, if your extension only modifies existing content through filters. In this case, you could remove the block folder from the example block, modify the Webpack config file so it no longer reads from that directory, and include the code you need in the entry JavaScript file. More information about how to use filters can be found in the [Filter Registry](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/checkout/filter-registry/README.md) and [Available Filters](/docs/block-development/extensible-blocks/cart-and-checkout-blocks/filters-in-cart-and-checkout/) documents. ### Importing WooCommerce components into your extension Components can be imported from `@woocommerce/blocks-components` (externalized to `window.wc.blocksComponents` by `@woocommerce/dependency-extraction-webpack-plugin`). The list of available components can be seen in [the WooCommerce Storybook](https://woocommerce.github.io/woocommerce/?path=/docs/woocommerce-blocks_external-components-button--docs), listed under "WooCommerce Blocks -> External components". An example of importing the `Button` component is: ```js import { Button } from '@woocommerce/blocks-components'; const MyComponent = () => { return
; } ``` ### Importing WooCommerce utilities and React hooks Some checkout utilities and React hooks are available for external use from `@woocommerce/blocks-checkout`. See the [Checkout Utilities](/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-utilities/) documentation for available utilities. For accessing store data, using the [`wc/store/...`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/data-store/) data stores is preferred over importing internal hooks like `useStoreCart`. ## Back-end extensibility ### Modifying information during the Checkout process Modifying the server-side part of the Cart and Checkout blocks is possible using PHP. Some actions and filters from the shortcode cart/checkout experience will work too, but not all of them. We have a working document ([Hook alternatives document](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/hook-alternatives.md)) that outlines which hooks are supported as well as alternatives. ### Extending Store API If you need to change how the Store API works, or extend the data in the responses, see [Extending the Store API](/docs/apis/store-api/extending-store-api/). --- ## Additional checkout fields *Source: block-development/extensible-blocks/cart-and-checkout-blocks/additional-checkout-fields.md* # Additional checkout fields A common use-case for developers and merchants is to add a new field to the Checkout form to collect additional data about a customer or their order. This document will outline the steps an extension should take to register some additional checkout fields. ## Available field locations Additional checkout fields can be registered in three different places: | Title | Identifier | | ------------------------------------ | ---------- | | Contact information | **`contact`** | | Addresses (Shipping **and** Billing) | **`address`** | | Order information | **`order`** | A field can only be shown in one location, it is not possible to render the same field in multiple locations in the same registration. ### Contact information The contact information section currently renders at the top of the form. It contains the `email` field and any other additional fields. ![Showing the contact information section with two fields rendered, email and an additional checkout field (optional)](https://github.com/woocommerce/woocommerce/assets/5656702/097c2596-c629-4eab-9604-577ee7a14cfe) Fields rendered here will be saved to the shopper's account. They will be visible and editable render in the shopper's "Account details" section. ### Address The "Address" section currently contains a form for the shipping address and the billing address. Additional checkout fields can be registered to appear within these forms. ![The shipping address form showing the additional checkout field at the bottom](https://github.com/woocommerce/woocommerce/assets/5656702/746d280f-3354-4d37-a78a-a2518eb0e5de) Fields registered here will be saved to both the customer _and_ the order, so returning customers won't need to refill those values again. If a field is registered in the `address` location it will appear in both the shipping **and** the billing address. It is not possible to have the field in only one of the addresses. You will also end up collecting two values for this field, one for shipping and one for billing. ### Order information As part of the additional checkout fields feature, the checkout block has a new inner block called the "Order information block". This block is used to render fields that aren't part of the contact information or address information, for example it may be a "How did you hear about us" field or a "Gift message" field. Fields rendered here will be saved to the order. They will not be part of the customer's saved address or account information. New orders will not have any previously used values pre-filled. ![The order information section containing an additional checkout field](https://github.com/woocommerce/woocommerce/assets/5656702/295b3048-a22a-4225-96b0-6b0371a7cd5f) By default, this block will render as the last step in the Checkout form, however it can be moved using the Gutenberg block controls in the editor. ![The order information block in the post editor"](https://github.com/woocommerce/woocommerce/assets/5656702/05a3d7d9-b3af-4445-9318-443ae2c4d7d8) ## Accessing values Additional fields are saved to individual meta keys in both the customer meta and order meta, you can access them using helper methods, or using the meta keys directly, we recommend using the helper methods, as they're less likely to change, can handle future migrations, and will support future enhancements (e.g. reading from different locations). For address fields, two values are saved: one for shipping, and one for billing. If the customer has selected 'Use same address for billing` then the values will be the same, but still saved independently of each other. For contact and order fields, only one value is saved per field. ### Helper methods `CheckoutFields` provides a function to access values from both customers and orders, it's are `get_field_from_object`. To access a customer billing and/or shipping value: ```php use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; $field_id = 'my-plugin-namespace/my-field'; $customer = wc()->customer; // Or new WC_Customer( $id ) $checkout_fields = Package::container()->get( CheckoutFields::class ); $my_customer_billing_field = $checkout_fields->get_field_from_object( $field_id, $customer, 'billing' ); $my_customer_shipping_field = $checkout_fields->get_field_from_object( $field_id, $customer, 'shipping' ); ``` To access an order field: ```php use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; $field_id = 'my-plugin-namespace/my-field'; $order = wc_get_order( 1234 ); $checkout_fields = Package::container()->get( CheckoutFields::class ); $my_order_billing_field = $checkout_fields->get_field_from_object( $field_id, $order, 'billing' ); $my_order_shipping_field = $checkout_fields->get_field_from_object( $field_id, $order, 'shipping' ); ``` After an order is placed, the data saved to the customer and the data saved to the order will be the same. Customers can change the values for _future_ orders, or from within their My Account page. If you're looking at a customer value at a specific point in time (i.e. when the order was placed), access it from the order object, if you're looking for the most up to date value regardless, access it from the customer object. #### Guest customers When a guest customer places an order with additional fields, those fields will be saved to its session, so as long as the customer still has a valid session going, the values will always be there. #### Logged-in customers For logged-in customers, the value is only persisted once they place an order. Accessing a logged-in customer object during the place order lifecycle will return null or stale data. If you're at a place order hook, doing this will return previous data (not the currently inserted one): ```php $customer = new WC_Customer( $order->customer_id ); // Or new WC_Customer( 1234 ) $my_customer_billing_field = $checkout_fields->get_field_from_object( $field_id, $customer, 'billing' ); ``` Instead, always access the latest data if you want to run some extra validation/data-moving: ```php $customer = wc()->customer // This will return the current customer with its session. $my_customer_billing_field = $checkout_fields->get_field_from_object( $field_id, $customer, 'billing' ); ``` #### Accessing all fields You can use `get_all_fields_from_object` to access all additional fields saved to an order or a customer. ```php use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; $order = wc_get_order( 1234 ); $checkout_fields = Package::container()->get( CheckoutFields::class ); $order_additional_billing_fields = $checkout_fields->get_all_fields_from_object( $order, 'billing' ); $order_additional_shipping_fields = $checkout_fields->get_all_fields_from_object( $order, 'shipping' ); $order_other_additional_fields = $checkout_fields->get_all_fields_from_object( $order, 'other' ); // Contact and Order are saved in the same place under the additional group. ``` This will return an array of all values, it will only include fields currently registered, if you want to include fields no longer registered, you can pass a third `true` parameter. ```php $order = wc_get_order( 1234 ); $checkout_fields = Package::container()->get( CheckoutFields::class ); $order_additional_billing_fields = $checkout_fields->get_all_fields_from_object( $order, 'billing' ); // array( 'my-plugin-namespace/my-field' => 'my-value' ); $order_additional_billing_fields = $checkout_fields->get_all_fields_from_object( $order, 'billing', true ); // array( 'my-plugin-namespace/my-field' => 'my-value', 'old-namespace/old-key' => 'old-value' ); ``` ### Accessing values directly While not recommended, you can use the direct meta key to access certain values, this is useful for external engines or page/email builders who only provide access to meta values. Values are saved under a predefined prefix, this is needed to able to query fields without knowing which ID the field was registered under, for a field with key `'my-plugin-namespace/my-field'`, it's meta key will be the following if it's an address field: - `_wc_billing/my-plugin-namespace/my-field` - `_wc_shipping/my-plugin-namespace/my-field` Or the following if it's a contact/order field: - `_wc_other/my-plugin-namespace/my-field`. Those prefixes are part of `CheckoutFields` class, and can be accessed using the following constants: ```php echo ( CheckoutFields::BILLING_FIELDS_PREFIX ); // _wc_billing/ echo ( CheckoutFields::SHIPPING_FIELDS_PREFIX ); // _wc_shipping/ echo ( CheckoutFields::OTHER_FIELDS_PREFIX ); // _wc_other/ ``` `CheckoutFields` provides a couple of helpers to get the group name or key based on one or the other: ```php CheckoutFields::get_group_name( "_wc_billing" ); // "billing" CheckoutFields::get_group_name( "_wc_billing/" ); // "billing" CheckoutFields::get_group_key( "shipping" ); // "_wc_shipping/" ``` Use cases here would be to build the key name to access the meta directly: ```php $key = CheckoutFields::get_group_key( "other" ) . 'my-plugin/is-opt-in'; $opted_in = get_user_meta( 123, $key, true ) === "1" ? true : false; ``` #### Checkboxes values When accessing a checkbox values directly, it will either return `"1"` for true, `"0"` for false, or `""` if the value doesn't exist, only the provided functions will sanitize that to a boolean. ## Supported field types The following field types are supported: - `select` - `text` - `checkbox` There are plans to expand this list, but for now these are the types available. ## Using the API To register additional checkout fields you must use the `woocommerce_register_additional_checkout_field` function. It is recommended to run this function after the `woocommerce_init` action. The registration function takes an array of options describing your field. Some field types take additional options. ### Options #### General options These options apply to all field types (except in a few circumstances which are noted inline). | Option name | Description | Required? | Example | Default value | |---------------------|-------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `id` | The field's ID. This should be a unique identifier for your field. It is composed of a namespace and field name separated by a `/`. | Yes | `plugin-namespace/how-did-you-hear` | No default - this must be provided. | | `label` | The label shown on your field. This will be the placeholder too. | Yes | `How did you hear about us?` | No default - this must be provided. | | `optionalLabel` | The label shown on your field if it is optional. This will be the placeholder too. | No | `How did you hear about us? (Optional)` | The default value will be the value of `label` with `(optional)` appended. | | `location` | The location to render your field. | Yes | `contact`, `address`, or `order` | No default - this must be provided. | | `type` | The type of field you're rendering. It defaults to `text` and must match one of the supported field types. | No | `text`, `select`, or `checkbox` | `text` | | `attributes` | An array of additional attributes to render on the field's input element. This is _not_ supported for `select` fields. | No | `[ 'data-custom-data' => 'my-custom-data' ]` | `[]` | | `required` | Can be a boolean or a JSON Schema array. If boolean and `true`, the shopper _must_ provide a value for this field during the checkout process. For checkbox fields, the shopper must check the box to place the order. If a JSON Schema array, the field will be required based on the schema conditions. See [Conditional visibility and validation via JSON Schema](#conditional-visibility-and-validation-via-json-schema). | No | `true` or `["type" => "object", "properties" => [...]]` | `false` | | `hidden` | Can be a boolean or a JSON Schema array. Must be `false` when used as a boolean. If a JSON Schema array, the field will be hidden based on the schema conditions. See [Conditional visibility and validation via JSON Schema](#conditional-visibility-and-validation-via-json-schema). | No | `false` or `["type" => "object", "properties" => [...]]` | `false` | | `validation` | An array of JSON Schema objects that define validation rules for the field. See [Conditional visibility and validation via JSON Schema](#conditional-visibility-and-validation-via-json-schema). | No | `[{"type": "object", "properties": {...}}]` | `[]` | | `sanitize_callback` | A function called to sanitize the customer provided value when posted. | No | See example below | By default the field's value is returned unchanged. | | `validate_callback` | A function called to validate the customer provided value when posted. This runs _after_ sanitization. | No | See example below | The default validation function will add an error to the response if the field is required and does not have a value. [See the default validation function.](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php#L270-L281) | ##### Example of `sanitize_callback`. This function will remove spaces from the value ```php 'sanitize_callback' => function( $field_value ) { return str_replace( ' ', '', $field_value ); }, ``` ##### Example of `validate_callback`. This function will check if the value is an email ```php 'validate_callback' => function( $field_value ) { if ( ! is_email( $field_value ) ) { return new WP_Error( 'invalid_alt_email', 'Please ensure your alternative email matches the correct format.' ); } }, ``` #### Options for `text` fields Text fields don't have any additional options beyond the general options listed above. #### Options for `select` fields As well as the options above, select fields must also be registered with an `options` option. This is used to specify what options the shopper can select. Select fields will mount with no value selected by default, if the field is required, the user will be required to select a value. You can set a placeholder to be shown on the select by passing a `placeholder` value when registering the field. This will be the first option in the select and will not be selectable if the field is required. | Option name | Description | Required? | Example | Default value | |-----|-----|-----|----------------|--------------| | `options` | An array of options to show in the select input. Each options must be an array containing a `label` and `value` property. Each entry must have a unique `value`. Any duplicate options will be removed. The `value` is what gets submitted to the server during checkout and the `label` is simply a user-friendly representation of this value. It is not transmitted to the server in any way. | Yes | see below | No default - this must be provided. | | `placeholder` | If this value is set, the shopper will see this option in the select. If the select is required, the shopper cannot select this option. | No | `Select a role` | Select a $label | ##### Example of `options` value ```php [ [ 'value' => 'store_1', 'label' => 'Our London Store' ], [ 'value' => 'store_2', 'label' => 'Our Paris Store' ], [ 'value' => 'store_3', 'label' => 'Our New York Store' ] ] ``` #### Options for `checkbox` fields As well as the options above, checkbox field support showing an error message if it's required and not checked. | Option name | Description | Required? | Example | Default value | |-----------------|------------------------------------------------------------------------------|-----------|--------------------------------------------------------------|---| | `error_message` | A custom message to show if the box is unchecked. | No | `You must confirm you are over 18 before placing the order.` | `Please check this box if you want to proceed.` | ### Attributes Adding additional attributes to checkbox and text fields is supported. Adding them to select fields is **not possible for now**. These attributes have a 1:1 mapping to the HTML attributes on `input` elements (except `pattern` on checkbox). The supported attributes are: - `data-*` attributes - `aria-*` attributes - `autocomplete` - `autocapitalize` - `pattern` (not supported on checkbox fields) - `title` - `maxLength` (equivalent to `maxlength` HTML attribute) - `readOnly` (equivalent to `readonly` HTML attribute) `maxLength` and `readOnly` are in camelCase because the attributes are rendered on a React element which must receive them in this format. Certain attributes are not passed through to the field intentionally, these are `autofocus` and `disabled`. We are welcome to hear feedback and adjust this behaviour if valid use cases are provided. ## Usage examples ### Rendering a text field This example demonstrates rendering a text field in the address section: ```php add_action( 'woocommerce_init', function() { woocommerce_register_additional_checkout_field( array( 'id' => 'namespace/gov-id', 'label' => 'Government ID', 'optionalLabel' => 'Government ID (optional)', 'location' => 'address', 'required' => true, 'attributes' => array( 'autocomplete' => 'government-id', 'aria-describedby' => 'some-element', 'aria-label' => 'custom aria label', 'pattern' => '[A-Z0-9]{5}', // A 5-character string of capital letters and numbers. 'title' => 'Title to show on hover', 'data-custom' => 'custom data', ), ), ); } ); ``` This results in the following address form (the billing form will be the same): ![The shipping address form with the Government ID field rendered at the bottom](https://github.com/woocommerce/woocommerce/assets/5656702/f6eb3c6f-9178-4978-8e74-e6b2ea353192) The rendered markup looks like this: ```html ``` ### Rendering a checkbox field This example demonstrates rendering a checkbox field in the contact information section: ```php add_action( 'woocommerce_init', function() { woocommerce_register_additional_checkout_field( array( 'id' => 'namespace/marketing-opt-in', 'label' => 'Do you want to subscribe to our newsletter?', 'location' => 'contact', 'type' => 'checkbox', ) ); } ); ``` This results in the following contact information section: ![The contact information section with a newsletter subscription checkbox rendered inside it](https://github.com/woocommerce/woocommerce/assets/5656702/7444e41a-97cc-451d-b2c9-4eedfbe05724) Note that because an `optionalLabel` was not supplied, the string `(optional)` is appended to the label. To remove that an `optionalLabel` property should be supplied to override this. ### Rendering a select field This example demonstrates rendering a select field in the order information section: ```php add_action( 'woocommerce_init', function() { woocommerce_register_additional_checkout_field( array( 'id' => 'namespace/how-did-you-hear-about-us', 'label' => 'How did you hear about us?', 'placeholder' => 'Select a source', 'location' => 'order', 'type' => 'select', 'options' => [ [ 'value' => 'google', 'label' => 'Google' ], [ 'value' => 'facebook', 'label' => 'Facebook' ], [ 'value' => 'friend', 'label' => 'From a friend' ], [ 'value' => 'other', 'label' => 'Other' ], ] ) ); } ); ``` This results in the order information section being rendered like so: ### The select input before being focused ![The select input before being focused](https://github.com/woocommerce/woocommerce/assets/5656702/bbe17ad0-7c7d-419a-951d-315f56f8898a) ### The select input when focused ![The select input when focused](https://github.com/woocommerce/woocommerce/assets/5656702/bd943906-621b-404f-aa84-b951323e25d3) If it is undesirable to force the shopper to select a value, mark the select as optional by setting the `required` option to `false`. ## Validation and sanitization It is possible to add custom validation and sanitization for additional checkout fields using WordPress action hooks. These actions happen in two places: 1. Updating and submitting the form during the checkout process and, 2. Updating address/contact information in the "My account" area. ### Sanitization Sanitization is used to ensure the value of a field is in a specific format. An example is when taking a government ID, you may want to format it so that all letters are capitalized and there are no spaces. At this point, the value should **not** be checked for _validity_. That will come later. This step is only intended to set the field up for validation. #### Using the `woocommerce_sanitize_additional_field` filter To run a custom sanitization function for a field you can use the `sanitize_callback` function on registration, or the `woocommerce_sanitize_additional_field` filter. | Argument | Type | Description | |--------------|-------------------|-------------------------------------------------------------------------| | `$field_value` | `boolean\|string` | The value of the field. | | `$field_key` | `string` | The ID of the field. This is the same ID the field was registered with. | ##### Example of sanitization This example shows how to remove whitespace and capitalize all letters in the example Government ID field we added above. ```php add_action( 'woocommerce_sanitize_additional_field', function ( $field_value, $field_key ) { if ( 'namespace/gov-id' === $field_key ) { $field_value = str_replace( ' ', '', $field_value ); $field_value = strtoupper( $field_value ); } return $field_value; }, 10, 2 ); ``` ### Validation There are two phases of validation in the additional checkout fields system. The first is validating a single field based on its key and value. #### Single field validation ##### Using the `woocommerce_validate_additional_field` action When the `woocommerce_validate_additional_field` action is fired the callback receives the field's key, the field's value, and a `WP_Error` object. To add validation errors to the response, use the [`WP_Error::add`](https://developer.wordpress.org/reference/classes/wp_error/add/) method. | Argument | Type | Description | |--------------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `$errors` | `WP_Error` | An error object containing errors that were already encountered while processing the request. If no errors were added yet, it will still be a `WP_Error` object but it will be empty. | | `$field_key` | `string` | The id of the field. This is the ID the field was registered with. | | `$field_value` | `boolean\|string` | The value of the field | ###### The `WP_Error` object When adding your error to the `WP_Error` object, it should have a unique error code. You may want to prefix the error code with the plugin namespace to reduce the chance of collision. Using codes that are already in use across other plugins may result in the error message being overwritten or showing in a different location. ###### Example of single-field validation The below example shows how to apply custom validation to the `namespace/gov-id` text field from above. The code here ensures the field is made up of 5 characters, either upper-case letters or numbers. The sanitization function from the example above ensures that all whitespace is removed and all letters are capitalized, so this check is an extra safety net to ensure the input matches the pattern. ```php add_action( 'woocommerce_validate_additional_field', function ( WP_Error $errors, $field_key, $field_value ) { if ( 'namespace/gov-id' === $field_key ) { $match = preg_match( '/[A-Z0-9]{5}/', $field_value ); if ( 0 === $match || false === $match ) { $errors->add( 'invalid_gov_id', 'Please ensure your government ID matches the correct format.' ); } } }, 10, 3 ); ``` It is important to note that this action must _add_ errors to the `WP_Error` object it receives. Returning a new `WP_Error` object or any other value will result in the errors not showing. If no validation errors are encountered the function can just return void. #### Multiple field validation There are cases where the validity of a field depends on the value of another field, for example validating the format of a government ID based on what country the shopper is in. In this case, validating only single fields (as above) is not sufficient as the country may be unknown during the `woocommerce_validate_additional_field` action. To solve this, it is possible to validate a field in the context of the location it renders in. The other fields in that location will be passed to this action. ##### Using the `woocommerce_blocks_validate_location_{location}_fields` action This action will be fired for each location that additional fields can render in (`address`, `contact`, and `order`). For `address` it fires twice, once for the billing address and once for the shipping address. The callback receives the keys and values of the other additional fields in the same location. It is important to note that any fields rendered in other locations will not be passed to this action, however it might be possible to get those values by accessing the customer or order object, however this is not supported and there are no guarantees regarding backward compatibility in future versions. | Argument | Type | Description | |----------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `$errors` | `WP_Error` | An error object containing errors that were already encountered while processing the request. If no errors were added yet, it will still be a `WP_Error` object but it will be empty. | | `$fields` | `array` | The fields rendered in this locations. | | `$group` | `'billing'\|'shipping'\|'other'` | If the action is for the address location, the type of address will be set here. If it is for contact or order, this will be 'other'. | There are several places where these hooks are fired. - When checking out using the Checkout block or Store API. - `woocommerce_blocks_validate_location_address_fields` (x2) - `woocommerce_blocks_validate_location_contact_fields` - `woocommerce_blocks_validate_location_other_fields` - When updating addresses in the "My account" area - `woocommerce_blocks_validate_location_address_fields` (**x1** - only the address being edited) - When updating the "Account details" section in the "My account" area - `woocommerce_blocks_validate_location_contact_fields` ##### Example of location validation In this example, assume there is another field registered alongside the `namespace/gov-id` called `namespace/confirm-gov-id`. This field will be a confirmation for the Government ID field. The example below illustrates how to verify that the value of the confirmation field matches the value of the main field. ```php add_action( 'woocommerce_blocks_validate_location_address_fields', function ( \WP_Error $errors, $fields, $group ) { if ( $fields['namespace/gov-id'] !== $fields['namespace/confirm-gov-id'] ) { $errors->add( 'gov_id_mismatch', 'Please ensure your government ID matches the confirmation.' ); } }, 10, 3 ); ``` If these fields were rendered in the "contact" location instead, the code would be the same except the hook used would be: `woocommerce_blocks_validate_location_contact_fields`. ## Conditional visibility and validation via JSON Schema The `required`, `hidden`, and `validation` properties accept an `array` of [JSON Schema](https://json-schema.org/understanding-json-schema/about) to create conditional logic for fields. This allows you to dynamically control field visibility, requirement status, and validation rules based on the values of other fields. Schema is evaluated in the frontend in real-time, and on the backend at any update. This ensures fast and responsive UI, and consistent results between the client and server. ### JSON Schema Structure Each schema in the array should be a valid JSON Schema object that defines conditions for when the property should be applied. The schema is evaluated against the current cart and checkout state, which includes all field values and various options (payment, shipping, customer). Basic structure of a JSON Schema object: ```json { "type": "object", "properties": { "fieldId": { "enum": ["value1", "value2"] } }, "required": ["fieldId"] } ``` If you're not familiar with JSON Schema, you can get a quick introduction to it [from the official website](https://json-schema.org/understanding-json-schema/basics), or from one of the libraries used [like AJV](https://ajv.js.org/json-schema.html) or [OPIS.](https://opis.io/json-schema/2.x/examples.html) Checkout builds an abstraction on top of both of them. ### Document object When you're writing your rules, you're writing a partial schema for the document object, essentially describing the ideal state you want for your field to be required or hidden. **Important:** All properties in the document object use snake_case naming convention (e.g., `total_price`, `shipping_rates`, `customer_note`), not camelCase. An example of the document object looks like this:
Document object ```json { "cart": { "coupons": [ "my_coupon" ], "shipping_rates": [ "free_shipping:1" ], "items": [ 27, 27, 68 ], "items_type": [ "simple", "variation" ], "items_count": 3, "items_weight": 0, "needs_shipping": true, "prefers_collection": false, "totals": { "total_price": 6600, "total_tax": 600 }, "extensions": {} }, "checkout": { "create_account": false, "customer_note": "", "additional_fields": { "namespace/myorder-field": "myvalue" }, "payment_method": "bacs" }, "customer": { "id": 1, "billing_address": { "first_name": "First Name", "last_name": "Last Name", "company": "Company", "address_1": "Address 1", "address_2": "Address 2", "city": "City", "state": "State", "postcode": "08000", "country": "US", "email": "email@example.com", "phone": "1234567890", "namespace/myfield": "myvalue" }, "shipping_address": { "first_name": "First Name", "last_name": "Last Name", "company": "Company", "address_1": "Address 1", "address_2": "Address 2", "city": "City", "state": "State", "postcode": "08000", "country": "US", "phone": "1234567890", "namespace/myfield": "myvalue" }, "additional_fields": { "namespace/mycontact-field": "myvalue" }, "address": { "first_name": "First Name", "last_name": "Last Name", "company": "Company", "address_1": "Address 1", "address_2": "Address 2", "city": "City", "state": "State", "postcode": "08000", "country": "US", "phone": "1234567890", "namespace/myfield": "myvalue" } } } ```
It's full schema is this one:
Document schema ```json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cart and Checkout Document Object Schema", "description": "Document object schema for cart, checkout, and customer information, to be used for conditional visibility, requirement, and validation of fields.", "type": "object", "properties": { "cart": { "type": "object", "description": "Information about the shopping cart", "properties": { "coupons": { "type": "array", "description": "List of coupon codes applied to the cart", "items": { "type": "string" } }, "shipping_rates": { "type": "array", "description": "List of currently selected shipping rates", "items": { "type": "string", "description": "Shipping rate identifier using the full shipping rate ID so method_id:instance_id, for example: flat_rate:1" } }, "items": { "type": "array", "description": "List of product IDs in the cart, IDs will be duplicated depending on the quantity of the product in the cart, so if you have 2 of product ID 1, the array will have 2 entries of product ID 1. This only supports integer quantities, not floats (which round up to the nearest integer).", "items": { "type": "integer" } }, "items_type": { "type": "array", "description": "Types of items in the cart, for example: simple, variation, subscription, etc.", "items": { "type": "string" } }, "items_count": { "type": "integer", "description": "Total number of items in the cart", "minimum": 0 }, "items_weight": { "type": "number", "description": "Total weight of items in the cart", "minimum": 0 }, "needs_shipping": { "type": "boolean", "description": "Whether the items in the cart require shipping" }, "prefers_collection": { "type": "boolean", "description": "Whether the customer prefers using Local Pickup" }, "totals": { "type": "object", "description": "Cart totals information", "properties": { "total_price": { "type": "integer", "description": "Total price of the cart in smallest currency unit (e.g., cents), after applying all discounts, shipping, and taxes" }, "total_tax": { "type": "integer", "description": "Total tax amount in smallest currency unit (e.g., cents), after applying all discounts, shipping, and taxes" } }, "additionalProperties": false }, "extensions": { "type": "object", "description": "Additional cart extension data, this is similar to what's passed in Store API's extensions parameter" } }, "additionalProperties": false }, "checkout": { "type": "object", "description": "Checkout preferences and settings", "properties": { "create_account": { "type": "boolean", "description": "Whether the customer checked the create account checkbox, this will be false if the customer is logged in, cannot create an account, or forced to create an account." }, "customer_note": { "type": "string", "description": "Customer's note or special instructions for the order, this will be empty if the customer didn't add a note." }, "additional_fields": { "type": "object", "description": "Additional checkout fields with 'order' location. These fields are rendered in the order information section.", "additionalProperties": { "type": "string" }, "patternProperties": { "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$": { "type": "string", "description": "Custom fields with namespace identifiers" } } }, "payment_method": { "type": "string", "description": "Selected payment method identifier, this will be the payment method ID regardless if the customer selected a saved payment method or new payment method" } }, "additionalProperties": false }, "customer": { "type": "object", "description": "Customer information", "properties": { "id": { "type": "integer", "description": "Customer ID, this will be 0 if the customer is not logged in" }, "billing_address": { "$ref": "#/definitions/address", "description": "Customer's billing address" }, "shipping_address": { "$ref": "#/definitions/address", "description": "Customer's shipping address" }, "additional_fields": { "type": "object", "description": "Additional checkout fields with 'contact' location. These fields are rendered in the contact information section.", "additionalProperties": { "type": "string" }, "patternProperties": { "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$": { "type": "string", "description": "Custom fields with namespace identifiers" } } }, "address": { "$ref": "#/definitions/address", "description": "This is a dynamic field that will be the billing or shipping address depending on the context of the field being evaluated." } }, "additionalProperties": false } }, "additionalProperties": false, "definitions": { "address": { "type": "object", "description": "Customer address information", "properties": { "first_name": { "type": "string", "description": "First name of the recipient" }, "last_name": { "type": "string", "description": "Last name of the recipient" }, "company": { "type": "string", "description": "Company name" }, "address_1": { "type": "string", "description": "Primary address line" }, "address_2": { "type": "string", "description": "Secondary address line" }, "city": { "type": "string", "description": "City name" }, "state": { "type": "string", "description": "State or province, this will be the state code if it's a predefined list, for example: CA, TX, NY, etc, or the field value if it's a freeform state, for example: London." }, "postcode": { "type": "string", "description": "Postal or ZIP code" }, "country": { "type": "string", "description": "Country code (e.g., US, UK)" }, "email": { "type": "string", "format": "email", "description": "Email address" }, "phone": { "type": "string", "description": "Phone number" } }, "additionalProperties": { "type": "string", "description": "Additional fields with 'address' location appear here as properties within the address objects" }, "patternProperties": { "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$": { "type": "string", "description": "Additional fields with 'address' location using namespace identifiers (e.g., 'namespace/field-name')" } } } } } ```
### Examples #### Required and visible field In this example we make the field required and visible only if local pickup is being used. ```php 'required' => [ "type" => "object", "properties" => [ "cart" => [ "properties" => [ "prefers_collection" => [ "const" => true ] ] ] ] ], 'hidden' => [ "type" => "object", "properties" => [ "cart" => [ "properties" => [ "prefers_collection" => [ "const" => false ] ] ] ] ] ``` Notice that for hidden, we inverse the field, meaning, this field should only be hidden if `prefers_collection` is false, which is almost all cases except when it's selected. In the examples above, we used [the keyword `const`](https://ajv.js.org/json-schema.html#const). #### Validation schema example Validation is slightly different from conditional visibility and requirement. In validation, you will pass in a subset of schema (only applicable to your field), and its role is to validate the field and show any errors if there. In this example, we ensure that VAT is made up of a country code and 8-12 numbers. ```php 'validation' => [ "type" => "string", "pattern" => "^[A-Z]{2}[0-9]{8,12}$", "errorMessage" => "Please enter a valid VAT code with 2 letters for country code and 8-12 numbers." ] ``` Validation can also be against other fields, for example, an alternative email field that shouldn't match the current email: ```php 'validation' => [ "type" => "string", "format" => "email", "not" => [ "const" => [ '$data' => "/customer/billing_address/email" ] ], "errorMessage" => "Please enter a valid alternative email." ] ``` In the example above, we used [format keyword](https://github.com/ajv-validator/ajv-formats) and `$data` to refer to the current field value via [JSON pointers](https://ajv.js.org/guide/combining-schemas.html#data-reference). We also used the `errorMessage` property to provide a custom error message. #### `$data` keyword and JSON pointers `$data` keyword is a way in JSON schema to reference another field's value. In the above example, we use it to refer to the billing email via [JSON pointers](https://ajv.js.org/guide/combining-schemas.html#data-reference). When dealing with JSON pointers, there are some things to keep in mind: - The forward slash `/` is used to navigate through the JSON object, so for additional fields, a field named `my-plugin-namespace/my-field` will need to be referenced as `my-plugin-namespace~1my-field`. - Navigation in JSON pointers can be from the current field backward, or from the root. If you have an address field and want to validate say the phone field, this means you will validate 2 values, one for shipping, and one for billing, so you can reference the phone field in 2 ways: - `0/customer/address/phone` which uses root navigation (via the `0/`) prefix, and uses the dynamic `address` group, which will change depending if the billing or shipping value is being validated. - `1/phone` which uses relative pointers to step back, in this case, it will access its sibling field, the `phone` field. Increase the number to step back even further, for example, `2/id` will access the customer ID. ### Keywords and values that are not in spec We support [JSON Schema Draft-07](https://json-schema.org/draft-07), which is simple and doesn't support all the keywords and values that are in the latest spec, but we feel like it covers most of the use cases. On top of that, we introduced some non-standard keywords and values that are not in the spec, their implementation might be different between Opis and AJV (or any future implementation), this is the list of such keywords and values: - `errorMessage`: Custom error message for validation, in AJV, this is `errorMessage` and in Opis, this is `$error`, we only support `errorMessage` and maps that internally for Opis. We also don't support templates in `errorMessage` for now. - `$data`: Refers to the current field value via [JSON pointers](https://ajv.js.org/guide/combining-schemas.html#data-reference), both Opis and AJV use the same implementation. ### Evaluation Logic - For `required`: If any schema in the array matches the current checkout state, the field will be required. - For `hidden`: If any schema in the array matches the current checkout state, the field will be hidden. - For `validation`: The value of the field will be evaluated against the partial schema provided and an error will be shown if it didn't match. ### Performance Considerations Complex JSON Schema conditions can impact checkout performance. Keep your schemas as simple as possible and limit the number of conditions to what's necessary for your use case. ## Backward compatibility Due to technical reasons, it's not yet possible to specify the meta key for fields, as we want them to be prefixed and managed. Plugins with existing fields in shortcode Checkout can be compatible and react to reading and saving fields using hooks. Assuming 2 fields, named `my-plugin-namespace/address-field` in the address step and `my-plugin-namespace/my-other-field` in the order step, you can: ### React to saving fields You can react to those fields being saved by hooking into `woocommerce_set_additional_field_value` action. ```php add_action( 'woocommerce_set_additional_field_value', function ( $key, $value, $group, $wc_object ) { if ( 'my-plugin-namespace/address-field' !== $key ) { return; } if ( 'billing' === $group ) { $my_plugin_address_key = 'existing_billing_address_field_key'; } else { $my_plugin_address_key = 'existing_shipping_address_field_key'; } $wc_object->update_meta_data( $my_plugin_address_key, $value, true ); }, 10, 4 ); add_action( 'woocommerce_set_additional_field_value', function ( $key, $value, $group, $wc_object ) { if ( 'my-plugin-namespace/my-other-field' !== $key ) { return; } $my_plugin_key = 'existing_order_field_key'; $wc_object->update_meta_data( $my_plugin_key, $value, true ); }, 10, 4 ); ``` This way, you can ensure existing systems will continue working and your integration will continue to work. However, ideally, you should migrate your existing data and systems to use the new meta fields. ### React to reading fields You can use the `woocommerce_get_default_value_for_{$key}` filters to provide a different default value (a value coming from another meta field for example): ```php add_filter( "woocommerce_get_default_value_for_my-plugin-namespace/address-field", function ( $value, $group, $wc_object ) { if ( 'billing' === $group ) { $my_plugin_key = 'existing_billing_address_field_key'; } else { $my_plugin_key = 'existing_shipping_address_field_key'; } return $wc_object->get_meta( $my_plugin_key ); }, 10, 3 ); add_filter( "woocommerce_get_default_value_for_my-plugin-namespace/my-other-field", function ( $value, $group, $wc_object ) { $my_plugin_key = 'existing_order_field_key'; return $wc_object->get_meta( $my_plugin_key ); }, 10, 3 ); ``` ## A full example In this full example we will register the Government ID text field and verify that it conforms to a specific pattern. This example is just a combined version of the examples shared above. ```php add_action( 'woocommerce_init', function() { woocommerce_register_additional_checkout_field( array( 'id' => 'namespace/gov-id', 'label' => 'Government ID', 'location' => 'address', 'required' => true, 'attributes' => array( 'autocomplete' => 'government-id', 'pattern' => '[A-Z0-9]{5}', // A 5-character string of capital letters and numbers. 'title' => 'Your 5-digit Government ID', ), ), ); woocommerce_register_additional_checkout_field( array( 'id' => 'namespace/confirm-gov-id', 'label' => 'Confirm government ID', 'location' => 'address', 'required' => true, 'attributes' => array( 'autocomplete' => 'government-id', 'pattern' => '[A-Z0-9]{5}', // A 5-character string of capital letters and numbers. 'title' => 'Confirm your 5-digit Government ID', ), ), ); add_action( 'woocommerce_sanitize_additional_field', function ( $field_value, $field_key ) { if ( 'namespace/gov-id' === $field_key || 'namespace/confirm-gov-id' === $field_key ) { $field_value = str_replace( ' ', '', $field_value ); $field_value = strtoupper( $field_value ); } return $field_value; }, 10, 2 ); add_action( 'woocommerce_validate_additional_field', function ( WP_Error $errors, $field_key, $field_value ) { if ( 'namespace/gov-id' === $field_key ) { $match = preg_match( '/[A-Z0-9]{5}/', $field_value ); if ( 0 === $match || false === $match ) { $errors->add( 'invalid_gov_id', 'Please ensure your government ID matches the correct format.' ); } } return $errors; }, 10, 3 ); } ); add_action( 'woocommerce_blocks_validate_location_address_fields', function ( \WP_Error $errors, $fields, $group ) { if ( $fields['namespace/gov-id'] !== $fields['namespace/confirm-gov-id'] ) { $errors->add( 'gov_id_mismatch', 'Please ensure your government ID matches the confirmation.' ); } }, 10, 3 ); ``` --- ## Available slots *Source: block-development/extensible-blocks/cart-and-checkout-blocks/available-slot-fills.md* # Available slots This document presents the list of available Slots that you can use for adding your custom content (Fill). If you want to add a new SlotFill component, check the [Checkout - Slot Fill document](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/packages/checkout/slot/README.md). To read more about Slot and Fill, check the [Slot and Fill document](/docs/block-development/reference/slot-fills/). **Note About Naming:** Slots that are prefixed with `Experimental` are experimental and subject to change or remove. Once they graduate from the experimental stage, the naming would change and the `Experimental` prefix would be dropped. Check the [Feature Gating document](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md) from more information. ## ExperimentalOrderMeta This Slot renders below the Checkout summary section and above the "Proceed to Checkout" button in the Cart. ```ts const { __ } = window.wp.i18n; const { registerPlugin } = window.wp.plugins; const { ExperimentalOrderMeta } = window.wc.blocksCheckout; const render = () => { return (
{ __( 'Yearly recurring total ...', 'YOUR-TEXTDOMAIN' ) }
); }; registerPlugin( 'slot-and-fill-examples', { render, scope: 'woocommerce-checkout', } ); ``` Cart: ![Example of ExperimentalOrderMeta in the Cart block](https://user-images.githubusercontent.com/1628454/154517779-117bb4e4-568e-413c-904c-855fc3450dfa.png) Checkout: ![Example of ExperimentalOrderMeta in the Checkout block](https://user-images.githubusercontent.com/1628454/154697224-de245182-6783-4914-81ba-1dbcf77292eb.png) ### Passed parameters - `cart`: `wc/store/cart` data but in `camelCase` instead of `snake_case`. [Object breakdown.](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c00da597efe4c16fcf5481c213d8052ec5df3766/assets/js/type-defs/cart.ts#L172-L188) - `extensions`: external data registered by third-party developers using `ExtendSchema`. If you used `ExtendSchema` on `wc/store/cart` you would find your data under your namespace here. - `context`, equal to the name of the Block in which the fill is rendered: `woocommerce/cart` or `woocommerce/checkout` ## ExperimentalOrderShippingPackages This slot renders inside the shipping step of Checkout and inside the shipping options in Cart. ```ts const { __ } = window.wp.i18n; const { registerPlugin } = window.wp.plugins; const { ExperimentalOrderShippingPackages } = window.wc.blocksCheckout; const render = () => { return (
{ __( 'Express Shipping', 'YOUR-TEXTDOMAIN' ) }
); }; registerPlugin( 'slot-and-fill-examples', { render, scope: 'woocommerce-checkout', } ); ``` Cart: ![Example of ExperimentalOrderShippingPackages in the Cart block](https://user-images.githubusercontent.com/6165348/118399054-2b4dec80-b653-11eb-94a0-989e2e6e362a.png) Checkout: ![Example of ExperimentalOrderShippingPackages in the Checkout block](https://user-images.githubusercontent.com/6165348/118399133-90094700-b653-11eb-8ff0-c917947c199f.png) ### Passed parameters - `collapsible`: `Boolean|undefined` If a shipping package panel should be collapsible or not, this is false in Checkout and undefined in Cart. - `collapse`: `Boolean` If a panel should be collapsed by default, this is true if if panels are collapsible. - `showItems`: `Boolean|undefined` If we should show the content of each package, this is undefined in Cart and Checkout and is left to the actual package logic to decide. - `noResultsMessage`: A React element that you can render if there are no shipping options. - `renderOption`: a render function that takes a rate object and returns a render option. - `cart`: `wc/store/cart` data but in `camelCase` instead of `snake_case`. [Object breakdown.](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c00da597efe4c16fcf5481c213d8052ec5df3766/assets/js/type-defs/cart.ts#L172-L188) - `extensions`: external data registered by third-party developers using `ExtendSchema`, if you used `ExtendSchema` on `wc/store/cart` you would find your data under your namespace here. - `components`: an object containing components you can use to render your own shipping rates, it contains `ShippingRatesControlPackage`. - `context`, equal to the name of the Block in which the fill is rendered: `woocommerce/cart` or `woocommerce/checkout` ## ExperimentalOrderLocalPickupPackages This slot renders inside the Checkout Pickup Options block in the Checkout block. It does not render in the Cart block. ```ts const { __ } = window.wp.i18n; const { registerPlugin } = window.wp.plugins; const { ExperimentalOrderLocalPickupPackages } = window.wc.blocksCheckout; const render = () => { return (
{ __( 'By using our convenient local pickup option, you can come to our store and pick up your order. We will send you and email when your order is ready for pickup.', 'YOUR-TEXTDOMAIN' ) }
); }; registerPlugin( 'slot-and-fill-examples', { render, scope: 'woocommerce-checkout', } ); ``` Checkout: ![Example of ExperimentalOrderLocalPickupPackages in the Checkout block](https://user-images.githubusercontent.com/5656702/222814945-a449d016-0621-4a70-b0f4-2ae1ce6487f1.png) ### Passed parameters - `renderPickupLocation`: a render function that renders the address details of a local pickup option. - `cart`: `wc/store/cart` data but in `camelCase` instead of `snake_case`. [Object breakdown.](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c00da597efe4c16fcf5481c213d8052ec5df3766/assets/js/type-defs/cart.ts#L172-L188) - `extensions`: external data registered by third-party developers using `ExtendSchema`, if you used `ExtendSchema` on `wc/store/cart` you would find your data under your namespace here. - `components`: an object containing components you can use to render your own pickup rates, it contains `ShippingRatesControlPackage` and `RadioControl`. ## ExperimentalDiscountsMeta This slot renders below the `CouponCode` input. ```ts const { __ } = window.wp.i18n; const { registerPlugin } = window.wp.plugins; const { ExperimentalDiscountsMeta } = window.wc.blocksCheckout; const render = () => { return (
{ __( 'You have 98683 coins to spend ...', 'YOUR-TEXTDOMAIN' ) }
); }; registerPlugin( 'slot-and-fill-examples', { render, scope: 'woocommerce-checkout', } ); ``` Cart: ![Cart showing ExperimentalDiscountsMeta location](https://user-images.githubusercontent.com/5656702/122774218-ea27a880-d2a0-11eb-9450-11f119567f26.png) Checkout: ![Checkout showing ExperimentalDiscountsMeta location](https://user-images.githubusercontent.com/5656702/122779606-efd3bd00-d2a5-11eb-8c84-6525eca5d704.png) ### Passed parameters - `cart`: `wc/store/cart` data but in `camelCase` instead of `snake_case`. [Object breakdown.](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c00da597efe4c16fcf5481c213d8052ec5df3766/assets/js/type-defs/cart.ts#L172-L188) - `extensions`: external data registered by third-party developers using `ExtendSchema`, if you used `ExtendSchema` on `wc/store/cart` you would find your data under your namespace here. - `context`, equal to the name of the Block in which the fill is rendered: `woocommerce/cart` or `woocommerce/checkout` --- ## Checkout flow and events *Source: block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/checkout-flow-and-events.md* # Checkout flow and events This document gives an overview of the flow for the checkout in the WooCommerce checkout block, and some general architectural overviews. The architecture of the Checkout Block is derived from the following principles: - A single source of truth for data within the checkout flow. - Provide a consistent interface for extension integrations (eg Payment methods). This interface protects the integrity of the checkout process and isolates extension logic from checkout logic. The checkout block handles _all_ communication with the server for processing the order. Extensions are able to react to and communicate with the checkout block via the provided interface. - Checkout flow state is tracked by checkout status. - Extensions are able to interact with the checkout flow via subscribing to emitted events. Here's a high level overview of the flow: ![checkout flow diagram](https://user-images.githubusercontent.com/1628454/113739726-f8c9df00-96f7-11eb-80f1-78e25ccc88cb.png) ## General Concepts ### Tracking flow through status At any point in the checkout lifecycle, components should be able to accurately detect the state of the checkout flow. This includes things like: - Is something loading? What is loading? - Is there an error? What is the error? - is the checkout calculating totals? Using simple booleans can be fine in some cases, but in others it can lead to complicated conditionals and bug prone code (especially for logic behaviour that reacts to various flow state). To surface the flow state, the block uses statuses that are tracked in the various contexts. _As much as possible_ these statuses are set internally in reaction to various actions so there's no implementation needed in children components (components just have to _consume_ the status not set status). The following statuses exist in the Checkout. #### Checkout Data Store Status There are various statuses that are exposed on the Checkout data store via selectors. All the selectors are detailed below and in the [Checkout API docs](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/internal-developers/block-client-apis/checkout/checkout-api.md). You can use them in your component like so ```jsx const { useSelect } = window.wp.data; const { checkoutStore } = window.wc.wcBlocksData; const MyComponent = ( props ) => { const isComplete = useSelect( ( select ) => select( checkoutStore ).isComplete() ); // do something with isComplete }; ``` The following boolean flags available related to status are: **isIdle**: When the checkout status is `IDLE` this flag is true. Checkout will be this status after any change to checkout state after the block is loaded. It will also be this status when retrying a purchase is possible after processing happens with an error. **isBeforeProcessing**: When the checkout status is `BEFORE_PROCESSING` this flag is true. Checkout will be this status when the user submits checkout for processing. **isProcessing**: When the checkout status is `PROCESSING` this flag is true. Checkout will be this status when all the observers on the event emitted with the `BEFORE_PROCESSING` status are completed without error. It is during this status that the block will be sending a request to the server on the checkout endpoint for processing the order. **Note:** there are some checkout payment status changes that happen during this state as well (outlined in the `PaymentProvider` exposed statuses section). **isAfterProcessing**: When the checkout status is `AFTER_PROCESSING` this flag is true. Checkout will have this status after the block receives the response from the server side processing request. **isComplete**: When the checkout status is `COMPLETE` this flag is true. Checkout will have this status after all observers on the events emitted during the `AFTER_PROCESSING` status are completed successfully. When checkout is at this status, the shopper's browser will be redirected to the value of `redirectUrl` at that point (usually the `order-received` route). #### Special States The following booleans are exposed via the checkout provider. They are independent from each other and checkout statuses, but can be used in combination to react to various states in the checkout. ##### **isCalculating** `isCalculating` is true when the total is being re-calculated for the order, or when a plugin is intentionally disabling the checkout using the `disableCheckoutFor` action (covered in the next section). There are numerous things that might trigger a recalculation of the total—coupons being added or removed, shipping rates updating, shipping rates being selected, etc. Instead of having to check each of those individual states, you can reliably check if this boolean is true (calculating) or false (not calculating). What `isCalculating` affects: - Disables the "Place Order" button in the checkout block - Disables the "Proceed to Checkout" button in the cart block - Shows a loading state for Express Payment methods while calculations are pending ###### Controlling `isCalculating` with `disableCheckoutFor` You can programmatically control `isCalculating` using the `disableCheckoutFor` thunk: ```jsx const { dispatch } = window.wp.data; const { checkoutStore } = window.wc.wcBlocksData; // Example: Disable checkout while performing an async operation dispatch( checkoutStore ).disableCheckoutFor( async () => { // Your async operation here, e.g. validating data with an API await myAsyncOperation(); // No need to return anything - we only care about the promise resolving } ); ``` The thunk controls internal state, ensuring that the client won't be able to attempt completing the flow until the provided promise resolves, regardless of whether it succeeds or fails. ##### **hasError** `hasError` is true when anything in the checkout has created an error condition state. This might be validation errors, request errors, coupon application errors, payment processing errors etc. ### `ShippingProvider` Exposed Statuses The shipping context provider exposes everything related to shipping in the checkout. Included in this are a set of error statuses that inform what error state the shipping context is in and the error state is affected by requests to the server on address changes, rate retrieval and selection. Currently the error status may be one of `NONE`, `INVALID_ADDRESS` or `UNKNOWN` (note, this may change in the future). The status is exposed on the `currentErrorStatus` object provided by the `useShippingDataContext` hook. This object has the following properties on it: - `isPristine` and `isValid`: Both of these booleans are connected to the same error status. When the status is `NONE` the values for these booleans will be `true`. It basically means there is no shipping error. - `hasInvalidAddress`: When the address provided for shipping is invalid, this will be true. - `hasError`: This is `true` when the error status for shipping is either `UNKNOWN` or `hasInvalidAddress`. ### Payment Method Data Store Status The status of the payment lives in the payment data store. You can query the status with the following selectors: ```jsx const { select } = window.wp.data; const { paymentStore } = window.wc.wcBlocksData; const MyComponent = ( props ) => { const isPaymentIdle = select( paymentStore ).isPaymentIdle(); const isExpressPaymentStarted = select( paymentStore ).isExpressPaymentStarted(); const isPaymentProcessing = select( paymentStore ).isPaymentProcessing(); const isPaymentReady = select( paymentStore ).isPaymentReady(); const hasPaymentError = select( paymentStore ).hasPaymentError(); // do something with the boolean values }; ``` The status here will help inform the current state of _client side_ processing for the payment and are updated via the store actions at different points throughout the checkout processing cycle. _Client side_ means the state of processing any payments by registered and active payment methods when the checkout form is submitted via those payment methods registered client side components. It's still possible that payment methods might have additional server side processing when the order is being processed but that is not reflected by these statuses (more in the [payment method integration doc](./payment-method-integration.md)). The possible _internal_ statuses that may be set are: - `IDLE`: This is the status when checkout is initialized and there are payment methods that are not doing anything. This status is also set whenever the checkout status is changed to `IDLE`. - `EXPRESS_STARTED`: **Express Payment Methods Only** - This status is used when an express payment method has been triggered by the user clicking it's button. This flow happens before processing, usually in a modal window. - `PROCESSING`: This status is set when the checkout status is `PROCESSING`, checkout `hasError` is false, checkout is not calculating, and the current payment status is not `FINISHED`. When this status is set, it will trigger the payment processing event emitter. - `READY`: This status is set after all the observers hooked into the payment processing event have completed successfully. The `CheckoutProcessor` component uses this along with the checkout `PROCESSING` status to signal things are ready to send the order to the server with data for processing and to take payment - `ERROR`: This status is set after an observer hooked into the payment processing event returns an error response. This in turn will end up causing the checkout `hasError` flag to be set to true. ### Emitting Events Another tricky thing for extensibility, is providing opinionated, yet flexible interfaces for extensions to act and react to specific events in the flow. For stability, it's important that the core checkout flow _controls_ all communication to and from the server specific to checkout/order processing and leave extension specific requirements for the extension to handle. This allows for extensions to predictably interact with the checkout data and flow as needed without impacting other extensions hooking into it. One of the most reliable ways to implement this type of extensibility is via the usage of an events system. Thus the various context providers: - expose subscriber APIs for extensions to subscribe _observers_ to the events they want to react to. - emit events at specific points of the checkout flow that in turn will feed data to the registered observers and, in some cases, react accordingly to the responses from observers. One _**very important rule**_ when it comes to observers registered to any event emitter in this system is that they _cannot_ update context state. Updating state local to a specific component is okay but not any context or global state. The reason for this is that the observer callbacks are run sequentially at a specific point and thus subsequent observers registered to the same event will not react to any change in global/context state in earlier executed observers. ```jsx const unsubscribe = emitter( myCallback ); ``` You could substitute in whatever emitter you are registering for the `emitter` function. So for example if you are registering for the `onCheckoutValidation` event emitter, you'd have something like: ```jsx const unsubscribe = onCheckoutValidation( myCallback ); ``` You can also indicate what priority you want your observer to execute at. Lower priority is run before higher priority, so you can affect when your observer will run in the stack of observers registered to an emitter. You indicate priority via an number on the second argument: ```jsx const unsubscribe = onCheckoutValidation( myCallback, 10 ); ``` In the examples, `myCallback`, is your subscriber function. The subscriber function could receive data from the event emitter (described in the emitter details below) and may be expected to return a response in a specific shape (also described in the specific emitter details). The subscriber function can be a `Promise` and when the event emitter cycles through the registered observers it will await for any registered Promise to resolve. Finally, the return value of the call to the emitter function is an unsubscribe function that can be used to unregister your observer. This is especially useful in a React component context where you need to make sure you unsubscribe the observer on component unmount. An example is usage in a `useEffect` hook: ```jsx const MyComponent = ( { onCheckoutValidation } ) => { useEffect( () => { const unsubscribe = onCheckoutValidation( () => true ); return unsubscribe; }, [ onCheckoutValidation ] ); return null; }; ``` **`Event Emitter Utilities`** There are a bunch of utility methods that can be used related to events. These are available in `assets/js/base/context/event-emit/utils.ts` and can be imported as follows: ```jsx import { noticeContexts, responseTypes, shouldRetry, } from '@woocommerce/base-context'; import { isSuccessResponse, isErrorResponse, isFailResponse, } from '@woocommerce/types'; ``` The helper functions are described below: - `isSuccessResponse`, `isErrorResponse` and `isFailResponse`: These are helper functions that receive a value and report via boolean whether the object is a type of response expected. For event emitters that receive responses from registered observers, a `type` property on the returned object from the observer indicates what type of response it is and event emitters will react according to that type. So for instance if an observer returned `{ type: 'success' }` the emitter could feed that to `isSuccessResponse` and it would return `true`. You can see an example of this being implemented for the [payment processing emitted event here](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js#L281-L307). - `noticeContexts`: This is an object containing properties referencing areas where notices can be targeted in the checkout. The object has the following properties: - `PAYMENTS`: This is a reference to the notice area in the payment methods step. - `EXPRESS_PAYMENTS`: This is a reference to the notice area in the express payment methods step. - `responseTypes`: This is an object containing properties referencing the various response types that can be returned by observers for some event emitters. It makes it easier for autocompleting the types and avoiding typos due to human error. The types are `SUCCESS`, `FAIL`, `ERROR`. The values for these types also correspond to the [payment status types](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/Payments/PaymentResult.php#L21) from the [checkout endpoint response from the server](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L113). - `shouldRetry`: This is a function containing the logic whether the checkout flow should allow the user to retry the payment after a previous payment failed. It receives the `response` object and by default checks whether the `retry` property is true/undefined or false. Refer to the [`onCheckoutSuccess`](#oncheckoutsuccess) documentation for more details. Note: `noticeContexts` and `responseTypes` are exposed to payment methods via the `emitResponse` prop given to their component: ```jsx const MyPaymentMethodComponent = ( { emitResponse } ) => { const { noticeContexts, responseTypes } = emitResponse; // other logic for payment method... }; ``` The following event emitters are available to extensions to register observers to: ### `onCheckoutValidation` Observers registered to this event emitter will receive nothing as an argument. Also, all observers will be executed before the checkout handles the responses from the emitters. Observers registered to this emitter can return `true` if they have nothing to communicate back to checkout, `false` if they want checkout to go back to `IDLE` status state, or an object with any of the following properties: - `errorMessage`: This will be added as an error notice on the checkout context. - `validationErrors`: This will be set as inline validation errors on checkout fields. If your observer wants to trigger validation errors it can use the following shape for the errors: - This is an object where keys are the property names the validation error is for (that correspond to a checkout field, eg `country` or `coupon`) and values are the error message describing the validation problem. This event is emitted when the checkout status is `BEFORE_PROCESSING` (which happens at validation time, after the checkout form submission is triggered by the user - or Express Payment methods). If all observers return `true` for this event, then the checkout status will be changed to `PROCESSING`. This event emitter subscriber can be obtained via the checkout context using the `useCheckoutContext` hook or to payment method extensions as a prop on their registered component: _For internal development:_ ```jsx import { useCheckoutContext } from '@woocommerce/base-contexts'; import { useEffect } from '@wordpress/element'; const Component = () => { const { onCheckoutValidation } = useCheckoutContext(); useEffect( () => { const unsubscribe = onCheckoutValidation( () => true ); return unsubscribe; }, [ onCheckoutValidation ] ); return null; }; ``` _For registered payment method components:_ ```jsx const { useEffect } = window.wp.element; const PaymentMethodComponent = ( { eventRegistration } ) => { const { onCheckoutValidation } = eventRegistration; useEffect( () => { const unsubscribe = onCheckoutValidation( () => true ); return unsubscribe; }, [ onCheckoutValidation ] ); }; ``` _For anything else:_ ```jsx const { onCheckoutValidation } = wc.blocksCheckoutEvents; useEffect( () => { const unsubscribe = onCheckoutValidation( () => true ); return unsubscribe; }, [ onCheckoutValidation ] ); ``` ### ~~`onPaymentProcessing`~~ This is now deprecated and replaced by the `onPaymentSetup` event emitter. ### `onPaymentSetup` This event emitter was fired when the payment method context status is `PROCESSING` and that status is set when the checkout status is `PROCESSING`, checkout `hasError` is false, checkout is not calculating, and the current payment status is not `FINISHED`. This event emitter will execute through each registered observer (passing in nothing as an argument) _until_ an observer returns a non-truthy value at which point it will _abort_ further execution of registered observers. When a payment method returns a non-truthy value, if it returns a valid response type the event emitter will update various internal statuses according to the response. Here's the possible response types that will get handled by the emitter: #### Success A successful response should be given when the user's entered data is correct and the payment checks are successful. A response is considered successful if, at a minimum, it is an object with this shape: ```js const successResponse = { type: 'success' }; ``` When a success response is returned, the payment method context status will be changed to `SUCCESS`. In addition, including any of the additional properties will result in extra actions: - `paymentMethodData`: The contents of this object will be included as the value for `payment_data` when checkout sends a request to the checkout endpoint for processing the order. This is useful if a payment method does additional server side processing. - `billingAddress`: This allows payment methods to update any billing data information in the checkout (typically used by Express payment methods) so it's included in the checkout processing request to the server. This data should be in the [shape outlined here](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/settings/shared/default-fields.ts). - `shippingAddress`: This allows payment methods to update any shipping data information for the order (typically used by Express payment methods) so it's included in the checkout processing request to the server. This data should be in the [shape outlined here](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/assets/js/settings/shared/default-fields.ts). If `billingAddress` or `shippingAddress` properties aren't in the response object, then the state for the data is left alone. #### Fail A fail response should be given when there is an error with the payment processing. A response is considered a fail response when it is an object with this shape: ```js const failResponse = { type: 'failure' }; ``` When a fail response is returned by an observer, the payment method context status will be changed to `FAIL`. In addition, including any of the following properties will result in extra actions: - `message`: The string provided here will be set as an error notice in the checkout. - `messageContext`: If provided, this will target the given area for the error notice (this is where `noticeContexts` mentioned earlier come in to play). Otherwise the notice will be added to the `noticeContexts.PAYMENTS` area. - `paymentMethodData`: (same as for success responses). - `billingAddress`: (same as for success responses). #### Error An error response should be given when there is an error with the user input on the checkout form. A response is considered an error response when it is an object with this shape: ```js const errorResponse = { type: 'error' }; ``` When an error response is returned by an observer, the payment method context status will be changed to `ERROR`. In addition, including any of the following properties will result in extra actions: - `message`: The string provided here will be set as an error notice. - `messageContext`: If provided, this will target the given area for the error notice (this is where `noticeContexts` mentioned earlier come in to play). Otherwise, the notice will be added to the `noticeContexts.PAYMENTS` area. - `validationErrors`: This will be set as inline validation errors on checkout fields. If your observer wants to trigger validation errors it can use the following shape for the errors: - This is an object where keys are the property names the validation error is for (that correspond to a checkout field, eg `country` or `coupon`) and values are the error message describing the validation problem. If the response object doesn't match any of the above conditions, then the fallback is to set the payment status as `SUCCESS`. When the payment status is set to `SUCCESS` and the checkout status is `PROCESSING`, the `CheckoutProcessor` component will trigger the request to the server for processing the order. This event emitter subscriber can be obtained via the checkout context using the `usePaymentEventsContext` hook or to payment method extensions as a prop on their registered component: _For internal development:_ ```jsx import { usePaymentEventsContext } from '@woocommerce/base-contexts'; import { useEffect } from '@wordpress/element'; const Component = () => { const { onPaymentSetup } = usePaymentEventsContext(); useEffect( () => { const unsubscribe = onPaymentSetup( () => true ); return unsubscribe; }, [ onPaymentSetup ] ); return null; }; ``` _For registered payment method components:_ ```jsx const { useEffect } = window.wp.element; const PaymentMethodComponent = ( { eventRegistration } ) => { const { onPaymentSetup } = eventRegistration; useEffect( () => { const unsubscribe = onPaymentSetup( () => true ); return unsubscribe; }, [ onPaymentSetup ] ); }; ``` ### `onCheckoutSuccess` This event emitter is fired when the checkout status is `AFTER_PROCESSING` and the checkout `hasError` state is false. The `AFTER_PROCESSING` status is set by the `CheckoutProcessor` component after receiving a response from the server for the checkout processing request. Observers registered to this event emitter will receive the following object as an argument: ```js const onCheckoutProcessingData = { redirectUrl, orderId, customerId, orderNotes, paymentResult, }; ``` The properties are: - `redirectUrl`: This is a string that is the url the checkout will redirect to as returned by the processing on the server. - `orderId`: Is the id of the current order being processed. - `customerId`: Is the id for the customer making the purchase (that is attached to the order). - `orderNotes`: This will be any custom note the customer left on the order. - `paymentResult`: This is the value of [`payment_result` from the /checkout StoreApi response](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L138). The data exposed on this object is (via the object properties): - `paymentStatus`: Whatever the status is for the payment after it was processed server side. Will be one of `success`, `failure`, `pending`, `error`. - `paymentDetails`: This will be an arbitrary object that contains any data the payment method processing server side sends back to the client in the checkout processing response. Payment methods are able to hook in on the processing server side and set this data for returning. This event emitter will invoke each registered observer until a response from any of the registered observers does not equal `true`. At that point any remaining non-invoked observers will be skipped and the response from the observer triggering the abort will be processed. This emitter will handle a `success` response type (`{ type: success }`) by setting the checkout status to `COMPLETE`. Along with that, if the response includes `redirectUrl` then the checkout will redirect to the given address. This emitter will also handle a `failure` response type or an `error` response type and if no valid type is detected it will treat it as an `error` response type. In all cases, if there are the following properties in the response, additional actions will happen: - `message`: This string will be added as an error notice. - `messageContext`: If present, the notice will be configured to show in the designated notice area (otherwise it will just be a general notice for the checkout block). - `retry`: If this is `true` or not defined, then the checkout status will be set to `IDLE`. This basically means that the error is recoverable (for example try a different payment method) and so checkout will be reset to `IDLE` for another attempt by the shopper. If this is `false`, then the checkout status is set to `COMPLETE` and the checkout will redirect to whatever is currently set as the `redirectUrl`. - `redirectUrl`: If this is present, then the checkout will redirect to this url when the status is `COMPLETE`. If all observers return `true`, then the checkout status will just be set to `COMPLETE`. This event emitter subscriber can be obtained via the checkout context using the `useCheckoutContext` hook or to payment method extensions as a prop on their registered component: _For internal development:_ ```jsx import { useCheckoutContext } from '@woocommerce/base-contexts'; import { useEffect } from '@wordpress/element'; const Component = () => { const { onCheckoutSuccess } = useCheckoutContext(); useEffect( () => { const unsubscribe = onCheckoutSuccess( () => true ); return unsubscribe; }, [ onCheckoutSuccess ] ); return null; }; ``` _For registered payment method components:_ ```jsx const { useEffect } = window.wp.element; const PaymentMethodComponent = ( { eventRegistration } ) => { const { onCheckoutSuccess } = eventRegistration; useEffect( () => { const unsubscribe = onCheckoutSuccess( () => true ); return unsubscribe; }, [ onCheckoutSuccess ] ); }; ``` _For anything else:_ ```jsx const { onCheckoutSuccess } = wc.blocksCheckoutEvents; useEffect( () => { const unsubscribe = onCheckoutSuccess( () => true ); return unsubscribe; }, [ onCheckoutSuccess ] ); ``` ### `onCheckoutFail` This event emitter is fired when the checkout status is `AFTER_PROCESSING` and the checkout `hasError` state is `true`. The `AFTER_PROCESSING` status is set by the `CheckoutProcessor` component after receiving a response from the server for the checkout processing request. Observers registered to this emitter will receive the same data package as those registered to `onCheckoutSuccess`. The response from the first observer returning a value that does not `===` true will be handled similarly as the `onCheckoutSuccess` except it only handles when the type is `error` or `failure`. If all observers return `true`, then the checkout status will just be set to `IDLE` and a default error notice will be shown in the checkout context. This event emitter subscriber can be obtained via the checkout context using the `useCheckoutContext` hook or to payment method extensions as a prop on their registered component: _For internal development:_ ```jsx import { useCheckoutContext } from '@woocommerce/base-contexts'; import { useEffect } from '@wordpress/element'; const Component = () => { const { onCheckoutFail } = useCheckoutContext(); useEffect( () => { const unsubscribe = onCheckoutFail( () => true ); return unsubscribe; }, [ onCheckoutFail ] ); return null; }; ``` _For registered payment method components:_ ```jsx const { useEffect } = window.wp.element; const PaymentMethodComponent = ( { eventRegistration } ) => { const { onCheckoutFail } = eventRegistration; useEffect( () => { const unsubscribe = onCheckoutFail( () => true ); return unsubscribe; }, [ onCheckoutFail ] ); }; ``` _For anything else:_ ```jsx const { onCheckoutFail } = wc.blocksCheckoutEvents; useEffect( () => { const unsubscribe = onCheckoutFail( () => true ); return unsubscribe; }, [ onCheckoutFail ] ); ``` ### `onShippingRateSuccess` This event emitter is fired when shipping rates are not loading and the shipping data context error state is `NONE` and there are shipping rates available. This event emitter doesn't care about any registered observer response and will simply execute all registered observers passing them the current shipping rates retrieved from the server. ### `onShippingRateFail` This event emitter is fired when shipping rates are not loading and the shipping data context error state is `UNKNOWN` or `INVALID_ADDRESS`. This event emitter doesn't care about any registered observer response and will simply execute all registered observers passing them the current error status in the context. ### `onShippingRateSelectSuccess` This event emitter is fired when a shipping rate selection is not being persisted to the server and there are selected rates available and the current error status in the context is `NONE`. This event emitter doesn't care about any registered observer response and will simply execute all registered observers passing them the current selected rates. ### `onShippingRateSelectFail` This event emitter is fired when a shipping rate selection is not being persisted to the server and the shipping data context error state is `UNKNOWN` or `INVALID_ADDRESS`. This event emitter doesn't care about any registered observer response and will simply execute all registered observers passing them the current error status in the context. --- ## Filtering payment methods in the Checkout block *Source: block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/filtering-payment-methods.md* # Filtering payment methods in the Checkout block ## The problem You're an extension developer, and your extension is conditionally hiding payment gateways on the checkout step. You need to be able to hide payment gateways on the Checkout block using a front-end extensibility point. ### The solution WooCommerce Blocks provides a function called `registerPaymentMethodExtensionCallbacks` which allows extensions to register callbacks for specific payment methods to determine if they can make payments. ### Importing #### Aliased import ```js import { registerPaymentMethodExtensionCallbacks } from '@woocommerce/blocks-registry'; ``` #### `wc global` ```js const { registerPaymentMethodExtensionCallbacks } = window.wc.wcBlocksRegistry; ``` ### Signature | Parameter | Description | Type | | ----------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | `namespace` | Unique string to identify your extension. Choose something that eliminates a name collision with another extension. | `string` | | `callbacks` | An object containing callbacks registered for different payment methods | Record< string, CanMakePaymentExtensionCallback > | Read more below about [callbacks](#callbacks-registered-for-payment-methods). #### Extension namespace collision When trying to register callbacks under an extension namespace already used with `registerPaymentMethodExtensionCallbacks`, the registration will be aborted and you will be notified that you are not using a unique namespace. This will be shown in the JavaScript console. ### Usage example ```js registerPaymentMethodExtensionCallbacks( 'my-hypothetical-extension', { cod: ( arg ) => { return arg.shippingAddress.city === 'Berlin'; }, cheque: ( arg ) => { return false; }, } ); ``` ### Callbacks registered for payment methods Extensions can register only one callback per payment method: ```text payment_method_name: ( arg ) => {...} ``` `payment_method_name` is the value of the [name property](/docs/block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration) used when the payment method was registered with WooCommerce Blocks. The registered callbacks are used to determine whether the corresponding payment method should be available as an option for the shopper. The function will be passed an object containing data about the current order. ```ts type CanMakePaymentExtensionCallback = ( cartData: CanMakePaymentArgument ) => boolean; ``` Each callback will have access to the information bellow ```ts interface CanMakePaymentArgument { cart: Cart; cartTotals: CartTotals; cartNeedsShipping: boolean; billingAddress: CartResponseBillingAddress; shippingAddress: CartResponseShippingAddress; selectedShippingMethods: Record< string, unknown >; paymentRequirements: Array< string >; } ``` If you need data that is not available in the parameter received by the callback you can consider [exposing your data in the Store API](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md). ## Filtering payment methods using requirements ### The problem Your extension has added functionality to your store in such a way that only specific payment gateways can process orders that contain certain products. Using the example of `Bookings` if the shopper adds a `Bookable` product to their cart, for example a stay in a hotel, and you, the merchant, want to confirm all bookings before taking payment. You would still need to capture the customer's checkout details but not their payment method at that point. ### The solution To allow the shopper to check out without entering payment details, but still require them to fill in the other checkout details it is possible to create a new payment method which will handle carts containing a `Bookable` item. Using the `supports` configuration of payment methods it is possible to prevent other payment methods (such as credit card, PayPal etc.) from being used to check out, and only allow the one your extension has added to appear in the Checkout block. For more information on how to register a payment method with WooCommerce Blocks, please refer to the [Payment method integration](./payment-method-integration.md) documentation. ### Basic usage Following the documentation for registering payment methods linked above, you should register your payment method with a unique `supports` feature, for example `booking_availability`. This will be used to isolate it and prevent other methods from displaying. First you will need to create a function that will perform the checks on the cart to determine what the specific payment requirements of the cart are. Below is an example of doing this for our `Bookable` products. Then you will need to use the `register_payment_requirements` on the `ExtendSchema` class to tell the Checkout block to execute a callback to check for requirements. ### Putting it all together This code example assumes there is some class called `Pseudo_Booking_Class` that has the `cart_contains_bookable_product` method available. The implementation of this method is not relevant here. ```php /** * Check the content of the cart and add required payment methods. * * * @return array list of features required by cart items. */ function inject_payment_feature_requirements_for_cart_api() { // Cart contains a bookable product, so return an array containing our requirement of booking_availability. if ( Pseudo_Booking_Class::cart_contains_bookable_product() ) { return array( 'booking_availability' ); } // No bookable products in the cart, no need to add anything. return array(); } ``` To summarise the above: if there's a bookable product in the cart then this function will return an array containing `booking_availability`, otherwise it will return an empty array. The next step will tell the `ExtendSchema` class to execute this callback when checking which payment methods to display. To do this you could use the following code: ```php add_action('woocommerce_blocks_loaded', function() { woocommerce_store_api_register_payment_requirements( array( 'data_callback' => 'inject_payment_feature_requirements_for_cart_api', ) ); }); ``` It is important to note the comment in this code block, you must not instantiate your own version of `ExtendSchema`. If you've added your payment method correctly with the correct `supports` values then when you reach the checkout page with a `Bookable` item in your cart, any method that does not `supports` the `booking_availability` requirement should not display, while yours, the one that _does_ support this requirement _will_ display. --- ## Payment method integration *Source: block-development/extensible-blocks/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md* # Payment method integration ## Client Side integration The client side integration consists of an API for registering both _regular_ and _express_ payment methods. In both cases, the client side integration is done using registration methods exposed on the `blocks-registry` API. You can access this via the `wc` global in a WooCommerce environment (`wc.wcBlocksRegistry`). > Note: In your build process, you could do something similar to what is done in the blocks repository which [aliases this API as an external on `@woocommerce/blocks-registry`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e089ae17043fa525e8397d605f0f470959f2ae95/bin/webpack-helpers.js#L16-L35). ## Express Payment Methods Express payment methods are payment methods that consist of a one-button payment process initiated by the shopper such as Stripe, ApplePay, or GooglePay. ![Express Payment Area](https://user-images.githubusercontent.com/1429108/79565636-17fed500-807f-11ea-8e5d-9af32e43b71d.png) ### Registration To register an express payment method, you use the `registerExpressPaymentMethod` function from the blocks registry. ```js const { registerExpressPaymentMethod } = window.wc.wcBlocksRegistry; ``` If you're using an aliased import for `@woocommerce/blocks-registry`, you can import the function like this: ```js import { registerExpressPaymentMethod } from '@woocommerce/blocks-registry'; ``` The registry function expects a JavaScript object with options specific to the payment method: ```js registerExpressPaymentMethod( options ); ``` The options you feed the configuration instance should be an object in this shape (see `ExpressPaymentMethodConfiguration` typedef): ```js const options = { name: 'my_payment_method', title: 'My Mayment Method', description: 'A setence or two about your payment method', gatewayId: 'gateway-id', label: , content: , edit: , canMakePayment: () => true, paymentMethodId: 'my_payment_method', supports: { features: [], style: [], }, }; ``` #### `ExpressPaymentMethodConfiguration` | Option | Type | Description | Required | | --- | --- | --- | --- | | `name` | String | Unique identifier for the gateway client side. | Yes | | `title` | String | Human readable name of your payment method. Displayed to the merchant in the editor. | No | | `description` | String | One or two sentences describing your payment gateway. Displayed to the merchant in the editor. | No | | `gatewayId` | String | ID of the Payment Gateway registered server side. Used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page. | No | | `content` | ReactNode | React node output in the express payment method area when the block is rendered in the frontend. Receives props from the checkout payment method interface. | Yes | | `edit` | ReactNode | React node output in the express payment method area when the block is rendered in the editor. Receives props from the payment method interface to checkout (with preview data). | Yes | | `canMakePayment` | Function | Callback to determine whether the payment method should be available for the shopper. | Yes | | `paymentMethodId` | String | Identifier accompanying the checkout processing request to the server. Used to identify the payment method gateway class for processing the payment. | No | | `supports:features` | Array | Array of payment features supported by the gateway. Used to crosscheck if the payment method can be used for the cart content. Defaults to `['products']` if no value is provided. | No | | `supports:style` | Array | This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`. | No | #### The `canMakePayment` option `canMakePayment` is a callback to determine whether the payment method should be available as an option for the shopper. The function will be passed an object containing data about the current order. ```ts canMakePayment( { cart: Cart, cartTotals: CartTotals, cartNeedsShipping: boolean, shippingAddress: CartShippingAddress, billingAddress: CartBillingAddress, selectedShippingMethods: Record, paymentRequirements: string[], } ) ``` `canMakePayment` returns a boolean value. If your gateway needs to perform async initialization to determine availability, you can return a promise (resolving to boolean). This allows a payment method to be hidden based on the cart, e.g. if the cart has physical/shippable products (example: [`Cash on delivery`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e089ae17043fa525e8397d605f0f470959f2ae95/assets/js/payment-method-extensions/payment-methods/cod/index.js#L48-L70)); or for payment methods to control whether they are available depending on other conditions. `canMakePayment` only runs on the frontend of the Store. In editor context, rather than use `canMakePayment`, the editor will assume the payment method is available (true) so that the defined `edit` component is shown to the merchant. **Keep in mind this function could be invoked multiple times in the lifecycle of the checkout and thus any expensive logic in the callback provided on this property should be memoized.** ### Button Attributes for Express Payment Methods This API provides a way to synchronise the look and feel of the express payment buttons for a coherent shopper experience. Express Payment Methods must prefer the values provided in the `buttonAttributes`, and use it's own configuration settings as backup when the buttons are rendered somewhere other than the Cart or Checkout block. For example, in your button component, you would do something like this: ```js // Get your extension specific settings and set defaults if not available let { borderRadius = '4', height = '48', } = getButtonSettingsFromConfig(); // In a cart & checkout block context, we receive `buttonAttributes` as a prop which overwrite the extension specific settings if ( typeof buttonAttributes !== 'undefined' ) { height = buttonAttributes.height; borderRadius = buttonAttributes.borderRadius; } ... return ); } return ( ); }; registerPaymentMethod( { name: 'my-custom-payment', label:
My Custom Payment
, content:
Payment method description
, edit:
Payment method description
, placeOrderButton: CustomButton, canMakePayment: () => true, supports: { features: [ 'products' ], }, } ); ``` **Note:** The custom button is only shown when the payment method is selected from the list. When a saved payment token is selected, the default "Place Order" button is used instead. ## Props Fed to Payment Method Nodes A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts). | Property | Type | Description | | --- | --- | --- | | `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | | `billing` | Object with billingAddress, cartTotal, currency, cartTotalItems, displayPricesIncludingTax, appliedCoupons, customerId properties | Contains everything related to billing. | | `cartData` | Object with cartItems, cartFees, extensions properties | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | | `checkoutStatus` | Object with isCalculating, isComplete, isIdle, isProcessing properties | The current checkout status exposed as various boolean state. | | `components` | Object with ValidationInputError, PaymentMethodLabel, PaymentMethodIcons, LoadingMask properties | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. | | `emitResponse` | Object with noticeContexts and responseTypes properties | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. | | `eventRegistration` | Object with onCheckoutValidation, onCheckoutSuccess, onCheckoutFail, onPaymentSetup, onShippingRateSuccess, onShippingRateFail, onShippingRateSelectSuccess, onShippingRateSelectFail properties | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | | `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | | `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | | `onSubmit` | Function | Submits the checkout and begins processing | | `validate` | Function | Async function that validates the checkout form without submitting. Returns a promise resolving to `{ hasError: boolean }`. Useful when you need to validate before showing a payment sheet. | | `buttonAttributes` | Object with height, borderRadius properties | Styles set by the merchant that should be respected by all express payment buttons | | `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | | `paymentStatus.isPristine` | Boolean | This is true when the current payment status is `PRISTINE`. | | `paymentStatus.isStarted` | Boolean | This is true when the current payment status is `EXPRESS_STARTED`. | | `paymentStatus.isProcessing` | Boolean | This is true when the current payment status is `PROCESSING`. | | `paymentStatus.isFinished` | Boolean | This is true when the current payment status is one of `ERROR`, `FAILED`, or `SUCCESS`. | | `paymentStatus.hasError` | Boolean | This is true when the current payment status is `ERROR`. | | `paymentStatus.hasFailed` | Boolean | This is true when the current payment status is `FAILED`. | | `paymentStatus.isSuccessful` | Boolean | This is true when the current payment status is `SUCCESS`. | | `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | | `shippingData` | Object with shippingRates, shippingRatesLoading, selectedRates, setSelectedRates, isSelectingRate, shippingAddress, setShippingAddress, needsShipping properties | Contains all shipping related data (outside of the shipping status). | | `shippingStatus` | Object with shippingErrorStatus, shippingErrorTypes properties | Various shipping status helpers. | | `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security). ## Server Side Integration For the server side integration, you need to create a class that extends the `Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType` class. This class is the server side representation of your payment method. It is used to handle the registration of your payment methods assets with the Store API and Checkout block at the correct time. It is not the same as the [Payment Gateway API](/features/payments/payment-gateway-api.md) that you need to implement separately for payment processing. ### Example Payment Method Integration Class ```php settings = get_option( 'woocommerce_my_payment_method_settings', [] ); } /** * This should return whether the payment method is active or not. * * If false, the scripts will not be enqueued. * * @return boolean */ public function is_active() { return filter_var( $this->get_setting( 'enabled', false ), FILTER_VALIDATE_BOOLEAN ); } /** * Returns an array of scripts/handles to be registered for this payment method. * * In this function you should register your payment method scripts (using `wp_register_script`) and then return the * script handles you registered with. This will be used to add your payment method as a dependency of the checkout script * and thus take sure of loading it correctly. * * Note that you should still make sure any other asset dependencies your script has are registered properly here, if * you're using Webpack to build your assets, you may want to use the WooCommerce Webpack Dependency Extraction Plugin * (https://www.npmjs.com/package/@woocommerce/dependency-extraction-webpack-plugin) to make this easier for you. * * @return array */ public function get_payment_method_script_handles() { wp_register_script( 'my-payment-method', 'path/to/your/script/my-payment-method.js', [], '1.0.0', true ); return [ 'my-payment-method' ]; } /** * Returns an array of script handles to be enqueued for the admin. * * Include this if your payment method has a script you _only_ want to load in the editor context for the checkout block. * Include here any script from `get_payment_method_script_handles` that is also needed in the admin. */ public function get_payment_method_script_handles_for_admin() { return $this->get_payment_method_script_handles(); } /** * Returns an array of key=>value pairs of data made available to the payment methods script client side. * * This data will be available client side via `wc.wcSettings.getSetting`. So for instance if you assigned `stripe` as the * value of the `name` property for this class, client side you can access any data via: * `wc.wcSettings.getSetting( 'stripe_data' )`. That would return an object matching the shape of the associative array * you returned from this function. * * @return array */ public function get_payment_method_data() { return [ 'title' => $this->get_setting( 'title' ), 'description' => $this->get_setting( 'description' ), 'supports' => $this->get_supported_features(), ]; } } ``` ### Registering the Payment Method Integration After creating a class that extends `Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType`, you need to register it with the server side handling of payment methods. You can do this by using the `register` method on the `PaymentMethodRegistry` class. ```php use MyPlugin\MyPaymentMethod\MyPaymentMethodType; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; add_action( 'woocommerce_blocks_payment_method_type_registration', function( PaymentMethodRegistry $payment_method_registry ) { $payment_method_registry->register( new MyPaymentMethodType() ); } ); ``` ## Processing Payments (legacy support) Payments are still handled via the [Payment Gateway API](/features/payments/payment-gateway-api.md). This is a separate API from the one used for the payment methods integration above. The checkout block converts incoming `payment_data` provided by the client-side script to `$_POST` and calls the Payment Gateway `process_payment` method. _If you already have a WooCommerce Payment method extension integrated with the shortcode checkout flow, the legacy handling will take care of processing your payment for you on the server side._ ## Processing Payments via the Store API There may be more advanced cases where the legacy payment processing mentioned earlier doesn't work for your existing payment method integration. For these cases, there is also an action hook you can use to handle the server side processing of the order which provides more context and is specific to the Store API. This hook is the _preferred_ place to hook in your payment processing: ```php do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$result ] ); ``` > Note: A good place to register your callback on this hook is in the `initialize` method of the payment method type class you created earlier A callback on this hook will receive: - A `PaymentContext` object which contains the chosen `payment_method` (this is the same as the `paymentMethodId` value defined when registering your payment method), the `order` being placed, and any additional `payment_data` provided by the payment method client. - A `PaymentResult` object which you can use to set the status, redirect url, and any additional payment details back to the client via the Store API. If you set a status on the provided `PaymentResult` object, legacy payment processing will be ignored for your payment method. If there is an error, your callback can throw an exception which will be handled by the Store API. Here is an example callback: ```php add_action( 'woocommerce_rest_checkout_process_payment_with_context', function( $context, $result ) { if ( $context->payment_method === 'my_payment_method' ) { // Order processing would happen here! // $context->order contains the order object if needed // ... // If the logic above was successful, we can set the status to success. $result->set_status( 'success' ); $result->set_payment_details( array_merge( $result->payment_details, [ 'custom-data' => '12345', ] ) ); $result->set_redirect_url( 'some/url/to/redirect/to' ); } }, 10, 2 ); ``` ## Passing values from the client to the server side payment processing In this example, lets pass some data from the BACS payment method to the server. Registration of BACS looks something like this: ```js // Get our settings that were provided when the payment method was registered const settings = window.wc.wcSettings.getSetting( 'bacs_data' ); // This is a component that would be rendered in the checkout block when the BACS payment method is selected const Content = () => { return decodeEntities( settings?.description || '' ); }; // This is the label for the payment method const Label = ( props ) => { const { PaymentMethodLabel } = props.components; return ; }; // Register the payment method const bankTransferPaymentMethod = { name: 'BACS', label: