Merge branch 'trunk' into in-memory-order-cache

This commit is contained in:
Michael Pretty 2024-08-09 12:37:06 -04:00 committed by GitHub
commit 32b35e879a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
665 changed files with 16278 additions and 10567 deletions

View File

@ -0,0 +1,78 @@
# How to update
## Source code
This action is extracted and bundled version of the following:
Repository: https://github.com/WordPress/gutenberg/tree/trunk/packages/report-flaky-tests
Commit ID: ce803384250671d01fde6c7d6d2aa83075fcc726
## How to build
After checking out the repository, navigate to packages/report-flaky-tests and do some modifications:
### package.json file
Add the following dependency: `"ts-loader": "^9.5.1",`.
### tsconfig.json file
The file context should be updated to following state:
```
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"moduleResolution": "node",
"declarationDir": "build-types",
"rootDir": "src",
"emitDeclarationOnly": false,
},
"include": [ "src/**/*" ],
"exclude": [ "src/__tests__/**/*", "src/__fixtures__/**/*" ]
}
```
### webpack.config.js file
The file should be added with the following content:
```
const path = require( 'path' );
const buildMode = process.env.NODE_ENV || 'production';
module.exports = {
entry: './src/index.ts',
target: 'node',
mode: buildMode,
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
plugins: [],
output: {
filename: 'index.js',
path: path.resolve( __dirname, 'dist' ),
clean: true,
},
};
```
### Build
Run `webpack --config webpack.config.js` (don't forget about `npm install` before that).
Use the generated files under `packages/report-flaky-tests/dist` to update the bundled distribution in this repository.

View File

@ -22,14 +22,14 @@ inputs:
runs:
using: 'composite'
steps:
- name: 'Read PNPM Version'
id: 'read-pnpm-version'
shell: 'bash'
run: 'echo "version=$(./.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh package.json)" >> $GITHUB_OUTPUT'
- name: 'Setup PNPM'
uses: 'pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d'
with:
version: ${{ steps.read-pnpm-version.outputs.version }}
# Next step is rudimentary - fixes a know composite action bug during post-actions:
# Error: Index was out of range. Must be non-negative and less than the size of the collection.
- name: 'Read PNPM version'
id: 'read-pnpm-version'
shell: 'bash'
run: 'echo "version=$(pnpm --version)" >> $GITHUB_OUTPUT'
- name: 'Setup Node'
uses: 'actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65'
with:

View File

@ -1,9 +0,0 @@
#!/usr/bin/env bash
PACKAGE_FILE=$1
if [[ -z "$PACKAGE_FILE" ]]; then
echo "Usage: $0 <package.json>"
exit 1
fi
awk -F'"' '/"pnpm": ".+"/{ print $4; exit; }' $PACKAGE_FILE

View File

@ -16,7 +16,8 @@ on:
type: string
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
# Cancel concurrent jobs but not for push event. For push use the run_id to have a unique group.
group: ci-${{ github.event_name == 'push' && github.run_id || github.event_name }}-${{ github.ref }}-${{ inputs.trigger }}
cancel-in-progress: true
env:
@ -38,16 +39,20 @@ jobs:
- uses: 'actions/checkout@v4'
name: 'Checkout'
with:
fetch-depth: 0
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
with:
php-version: false # We don't want to waste time installing PHP since we aren't using it in this job.
# If 'base_ref' is available, the 'Build Matrix' step requires non-shallow git-history to identify changed files.
fetch-depth: ${{ ( ( github.base_ref && '0' ) || '1' ) }}
- name: 'Setup PNPM'
uses: 'pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d'
- uses: actions/github-script@v7
name: 'Build Matrix'
id: 'project-jobs'
with:
script: |
// Intended behaviour of the jobs generation:
// - PRs: run CI jobs aiming PRs and filter out jobs based on the content changes
// - Pushes: run CI jobs aiming pushes without filtering based on the content changes
// github.base_ref is only available for pull_request events
let baseRef = ${{ toJson( github.base_ref ) }};
if ( baseRef ) {
baseRef = `--base-ref origin/${ baseRef }`;
@ -71,6 +76,12 @@ jobs:
githubEvent = trigger;
}
// `pre-release` should trigger `release-checks`, but without a 'tag' ref.
// This will run all release-checks against the branch the workflow targeted, instead of a release artifact.
if ( trigger === 'pre-release' ) {
githubEvent = 'release-checks';
}
const child_process = require( 'node:child_process' );
child_process.execSync( `pnpm utils ci-jobs ${ baseRef } --event ${ githubEvent }` );
@ -87,7 +98,8 @@ jobs:
- uses: 'actions/checkout@v4'
name: 'Checkout'
with:
fetch-depth: 0
# the WooCommerce plugin package uses phpcs-changed for linting, which requires non-shallow git-history.
fetch-depth: ${{ ( ( matrix.projectName == '@woocommerce/plugin-woocommerce' && '0' ) || '1' ) }}
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
@ -138,37 +150,92 @@ jobs:
env: ${{ matrix.testEnv.envVars }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}'
- name: 'Get commit message'
id: 'get_commit_message'
- name: 'Determine BuildKite Analytics Message'
env:
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
COMMIT_MESSAGE=`echo "$HEAD_COMMIT_MESSAGE" | head -1`
MESSAGE=`echo "$HEAD_COMMIT_MESSAGE" | head -1`
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
COMMIT_MESSAGE="$PR_TITLE"
MESSAGE="$PR_TITLE"
else
COMMIT_MESSAGE="${{ github.event_name }}"
MESSAGE="${{ github.event_name }}"
fi
echo "COMMIT_MESSAGE=$COMMIT_MESSAGE" >> "$GITHUB_OUTPUT"
echo "BUILDKITE_ANALYTICS_MESSAGE=$MESSAGE" >> "$GITHUB_OUTPUT"
shell: bash
- name: 'Run tests (${{ matrix.testType }})'
env:
E2E_ENV_KEY: ${{ secrets.E2E_ENV_KEY }}
BUILDKITE_ANALYTICS_TOKEN: ${{ secrets.BUILDKITE_CORE_E2E_TOKEN }}
BUILDKITE_ANALYTICS_MESSAGE: ${{ steps.get_commit_message.outputs.COMMIT_MESSAGE }}
CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} # required by Metrics tests
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
- name: 'Resolve artifacts path'
if: ${{ always() && matrix.report.resultsPath != '' }}
# Blocks e2e use a relative path which is not supported by actions/upload-artifact@v4
# https://github.com/actions/upload-artifact/issues/176
env:
ARTIFACTS_PATH: '${{ matrix.projectPath }}/${{ matrix.report.resultsPath }}'
run: echo "ARTIFACTS_PATH=$(realpath $ARTIFACTS_PATH)" >> $GITHUB_ENV
run: |
# first runs will probably not have the directory, so we need to create it so that realpath doesn't fail
mkdir -p $ARTIFACTS_PATH
echo "ARTIFACTS_PATH=$(realpath $ARTIFACTS_PATH)" >> $GITHUB_ENV
- name: 'Download Playwright last run info'
id: 'download-last-run-info'
if: ${{ always() && matrix.report.resultsPath != '' && matrix.testType == 'e2e' }}
uses: actions/download-artifact@v4
with:
pattern: 'last-run__${{ strategy.job-index }}'
- name: 'Run tests (${{ matrix.testType }})'
env:
E2E_ENV_KEY: ${{ secrets.E2E_ENV_KEY }}
BUILDKITE_ANALYTICS_TOKEN: ${{ secrets.BUILDKITE_CORE_E2E_TOKEN }}
CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} # required by Metrics tests
LAST_FAILED_RUN: ${{ vars.LAST_FAILED_RUN }}
run: |
lastRunFile="${{ steps.download-last-run-info.outputs.download-path }}/last-run__${{ strategy.job-index }}/.last-run.json"
lastRunFileDest="$ARTIFACTS_PATH/.last-run.json"
if [ -f "$lastRunFile" ]; then
echo "Found last run info file: \"$lastRunFile\""
echo "Moving to destination: \"$lastRunFileDest\""
mkdir -p "$ARTIFACTS_PATH"
mv "$lastRunFile" "$lastRunFileDest"
else
echo "No last run info file found. Searched for: \"$lastRunFile\""
fi
lastRunFlag=""
if [ -f "$lastRunFileDest" ]; then
# Playwright last run info is available, parse the file and check if there are failed tests
cat "$lastRunFileDest"
failedTests=$(jq '.failedTests | length' "$lastRunFileDest")
# Only if there are failed tests, we want to use the --last-failed flag.
# The run will fail if we're using the flag and there are no failed tests.
if [ "$failedTests" -gt 0 ]; then
if [ "$LAST_FAILED_RUN" == "1" ]; then
echo "Found failed tests, running only failed tests"
# Add shard 1/1 to override the default shard value. No tests will run for shards > 1.
# The clean way would be to replace the shard flag from the command, but this also works.
lastRunFlag="--last-failed --shard=1/1"
else
echo "Found failed tests, but LAST_FAILED_RUN is switched off. Running all tests."
fi
else
echo "No failed tests found, running all tests"
fi
fi
# Finally, run the tests
pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }} $lastRunFlag
- name: 'Upload Playwright last run info'
# always upload the last run info, even if the test run passed
if: ${{ always() && matrix.report.resultsPath != '' }}
uses: actions/upload-artifact@v4
with:
name: 'last-run__${{ strategy.job-index }}'
path: '${{ env.ARTIFACTS_PATH }}/.last-run.json'
if-no-files-found: ignore
overwrite: true
- name: 'Upload artifacts'
if: ${{ always() && matrix.report.resultsPath != '' }}
@ -184,15 +251,6 @@ jobs:
path: ${{ env.ARTIFACTS_PATH }}/flaky-tests
if-no-files-found: ignore
- name: 'Archive metrics results'
if: ${{ success() && startsWith(matrix.name, 'Metrics') }} # this seems too fragile, we should update the reporting path and use the generic upload step above
uses: actions/upload-artifact@v4
env:
WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts
with:
name: metrics-results
path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results*.json
evaluate-project-jobs:
# In order to add a required status check we need a consistent job that we can grab onto.
# Since we are dynamically generating a matrix for the project jobs, however, we can't
@ -241,10 +299,8 @@ jobs:
- uses: 'actions/checkout@v4'
name: 'Checkout'
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
with:
php-version: false
- name: 'Setup PNPM'
uses: 'pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d'
- name: 'Send messages for failed jobs'
env:

View File

@ -41,9 +41,7 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
# Both install and build are handled by compressed-size-action.
install: false
build: false
php-version: false
pull-package-deps: '@woocommerce/plugin-woocommerce'
- uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c
@ -52,7 +50,7 @@ jobs:
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './{packages/js/!(*e2e*|*internal*|*test*|*plugin*|*create*),plugins/woocommerce-blocks}/{build,build-style}/**/*.{js,css}'
install-script: 'pnpm install --filter="@woocommerce/plugin-woocommerce..." --frozen-lockfile --config.dedupe-peer-dependents=false'
install-script: 'pnpm install --filter="@woocommerce/plugin-woocommerce..." --frozen-lockfile --config.dedupe-peer-dependents=false --ignore-scripts'
build-script: '--filter="@woocommerce/plugin-woocommerce" build'
clean-script: '--if-present buildclean'
minimum-change-threshold: 100

View File

@ -3,6 +3,7 @@ on:
pull_request:
paths:
- 'plugins/woocommerce/**'
- '!plugins/woocommerce/templates/templates/**'
jobs:
analyze:
name: 'Analyze Branch Changes'
@ -26,7 +27,7 @@ jobs:
GIT_CLONE_PROTECTION_ACTIVE: false
run: |
HEAD_REF=$(git rev-parse HEAD)
exclude="plugins/woocommerce/tests plugins/woocommerce-admin/tests plugins/woocommerce-blocks/tests"
exclude="plugins/woocommerce/tests plugins/woocommerce/templates/templates plugins/woocommerce-admin/tests plugins/woocommerce-blocks/tests"
version=$(pnpm analyzer major-minor "$HEAD_REF" "plugins/woocommerce/woocommerce.php" | tail -n 1)
pnpm analyzer "$HEAD_REF" $version -o "github" -e $exclude
- uses: 'actions/github-script@v6'

View File

@ -38,9 +38,7 @@ jobs:
docs/docs-manifest.json
- name: Setup PNPM
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '8.6.7'
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c

View File

@ -31,7 +31,6 @@ jobs:
issues: write
pull-requests: write
outputs:
pnpmVersion: ${{ steps.read-pnpm-version.outputs.version }}
isTodayAcceleratedFreeze: ${{ steps.get-versions.outputs.isTodayAcceleratedFreeze }}
isTodayMonthlyFreeze: ${{ steps.get-versions.outputs.isTodayMonthlyFreeze }}
acceleratedVersion: ${{ steps.get-versions.outputs.acceleratedVersion }}
@ -47,18 +46,8 @@ jobs:
with:
fetch-depth: 0
- name: Read PNPM Version
id: read-pnpm-version
shell: bash
run: |
version=$(./.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh package.json)
echo "version=$version" >> $GITHUB_OUTPUT
echo "PNPM Version: $version"
- name: Setup PNPM
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: ${{ steps.read-pnpm-version.outputs.version }}
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
@ -149,9 +138,7 @@ jobs:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: ${{ needs.code-freeze-prep.outputs.pnpmVersion }}
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
@ -275,9 +262,7 @@ jobs:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: ${{ needs.code-freeze-prep.outputs.pnpmVersion }}
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
@ -346,9 +331,7 @@ jobs:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: ${{ needs.code-freeze-prep.outputs.pnpmVersion }}
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c

View File

@ -20,14 +20,10 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
install: '@woocommerce/plugin-woocommerce'
build: '@woocommerce/plugin-woocommerce'
install: '@woocommerce/plugin-woocommerce-beta-tester...'
build: '@woocommerce/plugin-woocommerce-beta-tester...'
pull-package-deps: '@woocommerce/plugin-woocommerce-beta-tester'
- name: Lint
working-directory: plugins/woocommerce-beta-tester
run: composer run phpcs
- name: Build WooCommerce Beta Tester Zip
working-directory: plugins/woocommerce-beta-tester
run: pnpm build:zip

View File

@ -7,25 +7,34 @@ if [[ -z "$GITHUB_EVENT_NAME" ]]; then
exit 1
fi
echo "Installing dependencies"
pnpm install --filter="compare-perf"
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
echo "Comparing performance with trunk"
pnpm --filter="compare-perf" run compare perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA
elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then
echo "Comparing performance with base branch"
# The base hash used here need to be a commit that is compatible with the current WP version
# The current one is 19f3d0884617d7ecdcf37664f648a51e2987cada
# it needs to be updated every time it becomes unsupported by the current wp-env (WP version).
# It is used as a base comparison point to avoid fluctuation in the performance metrics.
WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt)
# Updating the WP version used for performance jobs means theres a high
# chance that the reference commit used for performance test stability
# becomes incompatible with the WP version. So, every time the "Tested up
# to" flag is updated in the readme.txt, we also have to update the
# reference commit below (BASE_SHA). The new reference needs to meet the
# following requirements:
# - Be compatible with the new WP version used in the “Tested up to” flag.
# - Be tracked on https://www.codevitals.run/project/woo for all existing
# metrics.
BASE_SHA=3d7d7f02017383937f1a4158d433d0e5d44b3dc9
echo "WP_VERSION: $WP_VERSION"
IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION"
WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}"
pnpm --filter="compare-perf" run compare perf $GITHUB_SHA 19f3d0884617d7ecdcf37664f648a51e2987cada --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR"
pnpm --filter="compare-perf" run compare perf $GITHUB_SHA $BASE_SHA --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR"
echo "Publish results to CodeVitals"
COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI")
pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA 19f3d0884617d7ecdcf37664f648a51e2987cada $COMMITTED_AT
pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA $BASE_SHA $COMMITTED_AT
else
echo "Unsupported event: $GITHUB_EVENT_NAME"
fi

View File

@ -4,14 +4,35 @@ on:
workflow_dispatch:
inputs:
trigger:
description: 'Event name. It will be used to filter the jobs to run in ci.yml. It can be anything, as long as the job you want to run has this event configured in the `events` list. Example: daily-checks, pull_request, on-demand, etc.'
type: choice
description: 'Event name: it will be used to filter the jobs to run in ci.yml.'
required: true
default: ''
options:
- push
- daily-checks
- pre-release
- on-demand
- custom
default: on-demand
custom-trigger:
type: string
description: 'Custom event name: In case the `Event name` choice is `custom`, this field is required.'
required: false
jobs:
validate-input:
runs-on: ubuntu-latest
steps:
- name: 'Validate input'
run: |
if [ "${{ inputs.trigger }}" == "custom" ] && [ -z "${{ inputs.custom-trigger }}" ]; then
echo "Custom event name is required when event name choice `custom`."
exit 1
fi
run-tests:
name: 'Run tests'
uses: ./.github/workflows/ci.yml
with:
trigger: ${{ inputs.trigger }}
trigger: ${{ inputs.trigger == 'custom' && inputs.custom-trigger || inputs.trigger }}
secrets: inherit

View File

@ -1,4 +1,10 @@
#!/bin/sh
#!/usr/bin/env bash
. "$(dirname "$0")/_/husky.sh"
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
if [ -n "$changedManifests" ]; then
printf "It was a change in the following file(s) - refreshing dependencies:\n"
printf " %s\n" $changedManifests
pnpm install --frozen-lockfile
fi

View File

@ -253,7 +253,7 @@
"packages": [
"**"
],
"pinVersion": "wp-6.4"
"pinVersion": "wp-6.6"
},
{
"dependencies": [

View File

@ -1,6 +1,6 @@
---
post_title: How to add a custom field to simple and variable products
menu_title: Add custom fields to products
menu_title: Add Custom Fields to Products
tags: how-to
---

View File

@ -0,0 +1,5 @@
---
category_title: Cart and Checkout Blocks
category_slug: cart-and-checkout-blocks
post_title: Cart and Checkout blocks - Extensibility
---

View File

@ -1,55 +1,9 @@
---
post_title: Cart and Checkout - Additional Checkout Fields
post_title: Cart and Checkout - Additional checkout fields
menu_title: Additional Checkout Fields
tags: reference
---
# Additional Checkout Fields <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [Available field locations](#available-field-locations)
- [Contact information](#contact-information)
- [Address](#address)
- [Order information](#order-information)
- [Accessing values](#accessing-values)
- [Helper methods](#helper-methods)
- [Guest customers](#guest-customers)
- [Logged-in customers](#logged-in-customers)
- [Accessing all fields](#accessing-all-fields)
- [Accessing values directly](#accessing-values-directly)
- [Checkboxes values](#checkboxes-values)
- [Supported field types](#supported-field-types)
- [Using the API](#using-the-api)
- [Options](#options)
- [General options](#general-options)
- [Options for `text` fields](#options-for-text-fields)
- [Options for `select` fields](#options-for-select-fields)
- [Example of `options` value](#example-of-options-value)
- [Options for `checkbox` fields](#options-for-checkbox-fields)
- [Attributes](#attributes)
- [Usage examples](#usage-examples)
- [Rendering a text field](#rendering-a-text-field)
- [Rendering a checkbox field](#rendering-a-checkbox-field)
- [Rendering a select field](#rendering-a-select-field)
- [The select input before being focused](#the-select-input-before-being-focused)
- [The select input when focused](#the-select-input-when-focused)
- [Validation and sanitization](#validation-and-sanitization)
- [Sanitization](#sanitization)
- [Using the `woocommerce_sanitize_additional_field` filter](#using-the-woocommerce_sanitize_additional_field-filter)
- [Example of sanitization](#example-of-sanitization)
- [Validation](#validation)
- [Single field validation](#single-field-validation)
- [Using the `woocommerce_validate_additional_field` action](#using-the-woocommerce_validate_additional_field-action)
- [The `WP_Error` object](#the-wp_error-object)
- [Example of single-field validation](#example-of-single-field-validation)
- [Multiple field validation](#multiple-field-validation)
- [Using the `woocommerce_blocks_validate_location_{location}_fields` action](#using-the-woocommerce_blocks_validate_location_location_fields-action)
- [Example of location validation](#example-of-location-validation)
- [Backward compatibility](#backward-compatibility)
- [React to to saving fields](#react-to-to-saving-fields)
- [React to reading fields](#react-to-reading-fields)
- [A full example](#a-full-example)
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.

View File

@ -0,0 +1,118 @@
---
category_title: Available Filters
category_slug: cart-and-checkout-available-filters
post_title: Cart and Checkout - Available Filters
---
This document lists the filters that are currently available to extensions and offers usage information for each one of them. Information on registering filters can be found on the [Checkout - Filter Registry](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/packages/checkout/filter-registry/README.md) page.
## Cart Line Items filters
The following [Cart Line Items filters](./cart-line-items.md) are available:
- `cartItemClass`
- `cartItemPrice`
- `itemName`
- `saleBadgePriceFormat`
- `showRemoveItemLink`
- `subtotalPriceFormat`
The following screenshot shows which parts the individual filters affect:
![Cart Line Items](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-26-at-13.12.33.png)
## Order Summary Items filters
The following [Order Summary Items filters](./order-summary-items.md) are available:
- `cartItemClass`
- `cartItemPrice`
- `itemName`
- `subtotalPriceFormat`
The following screenshot shows which parts the individual filters affect:
![Order Summary Items](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-26-at-16.29.45.png)
## Totals Footer Item filter
The following [Totals Footer Item filter](./totals-footer-item.md) is available:
- `totalLabel`
- `totalValue`
## Checkout and place order button filters
The following [Checkout and place order button filters](./checkout-and-place-order-button.md) are available:
- `proceedToCheckoutButtonLabel`
- `proceedToCheckoutButtonLink`
- `placeOrderButtonLabel`
## Coupon filters
The following [Coupon filters](./coupons.md) are available:
- `coupons`
- `showApplyCouponNotice`
- `showRemoveCouponNotice`
## Additional Cart and Checkout inner block types filter
The following [Additional Cart and Checkout inner block types filter](./additional-cart-checkout-inner-block-types.md) is available:
- `additionalCartCheckoutInnerBlockTypes`
## Combined filters
Filters can also be combined. The following example shows how to combine some of the available filters.
```tsx
const { registerCheckoutFilters } = window.wc.blocksCheckout;
const isOrderSummaryContext = ( args ) => args?.context === 'summary';
const modifyCartItemClass = ( defaultValue, extensions, args ) => {
if ( isOrderSummaryContext( args ) ) {
return 'my-custom-class';
}
return defaultValue;
};
const modifyCartItemPrice = ( defaultValue, extensions, args ) => {
if ( isOrderSummaryContext( args ) ) {
return '<price/> for all items';
}
return defaultValue;
};
const modifyItemName = ( defaultValue, extensions, args ) => {
if ( isOrderSummaryContext( args ) ) {
return `${ defaultValue }`;
}
return defaultValue;
};
const modifySubtotalPriceFormat = ( defaultValue, extensions, args ) => {
if ( isOrderSummaryContext( args ) ) {
return '<price/> per item';
}
return defaultValue;
};
registerCheckoutFilters( 'example-extension', {
cartItemClass: modifyCartItemClass,
cartItemPrice: modifyCartItemPrice,
itemName: modifyItemName,
subtotalPriceFormat: modifySubtotalPriceFormat,
} );
```
## Troubleshooting
If you are logged in to the store as an administrator, you should be shown an error like this if your filter is not
working correctly. The error will also be shown in your console.
![Troubleshooting](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-30-at-10.52.53.png)

View File

@ -1,15 +1,12 @@
---
post_title: Cart and Checkout - Inner Block Types
post_title: Cart and Checkout Filters - Inner block types
menu_title: Inner Block Types
tags: reference, checkout-available-filters
tags: reference
---
# Additional Cart and Checkout inner block types
The following Additional Cart and Checkout inner block types filter is available:
- [`additionalCartCheckoutInnerBlockTypes`](#additionalcartcheckoutinnerblocktypes)
- `additionalCartCheckoutInnerBlockTypes`
## `additionalCartCheckoutInnerBlockTypes`
@ -69,7 +66,7 @@ document.addEventListener( 'DOMContentLoaded', function () {
To call this filter within the editor, wrap the filter registration in a `DOMContentLoaded` event listener and ensure the code runs in the admin panel.
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->

View File

@ -1,26 +1,24 @@
---
post_title: Cart and Checkout - Cart Line Items
post_title: Cart and Checkout Filters - Cart line items
menu_title: Cart Line Items
tags: reference, checkout-available-filters
tags: reference
---
<!-- markdownlint-disable MD024 -->
# Cart Line Items
The following Cart Line Items filters are available:
- [`cartItemClass`](#cartitemclass)
- [`cartItemPrice`](#cartitemprice)
- [`itemName`](#itemname)
- [`saleBadgePriceFormat`](#salebadgepriceformat)
- [`showRemoveItemLink`](#showremoveitemlink)
- [`subtotalPriceFormat`](#subtotalpriceformat)
- `cartItemClass`
- `cartItemPrice`
- `itemName`
- `saleBadgePriceFormat`
- `showRemoveItemLink`
- `subtotalPriceFormat`
The following objects are shared between the filters:
- [Cart object](#cart-object)
- [Cart Item object](#cart-item-object)
- Cart object
- Cart Item object
The following screenshot shows which parts the individual filters affect:
@ -37,8 +35,8 @@ The `cartItemClass` filter allows to change the cart item class.
- _defaultValue_ `object` (default: `''`) - The default cart item class.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object).
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see [Cart Item object](#cart-item-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see Cart object.
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see Cart Item object.
- _context_ `string` (allowed values: `cart` or `summary`) - The context of the item.
### Returns <!-- omit in toc -->
@ -95,7 +93,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -112,17 +110,17 @@ The `cartItemPrice` filter allows to format the cart item price.
### Parameters <!-- omit in toc -->
- _defaultValue_ `string` (default: `<price/>`) - The default cart item price.
- _defaultValue_ `string` (default: `&lt;price/&gt;`) - The default cart item price.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object).
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see [Cart Item object](#cart-item-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see Cart object.
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see Cart Item object.
- _context_ `string` (allowed values: `cart` or `summary`) - The context of the item.
- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`.
- _validation_ `boolean` - Checks if the return value contains the substring `&lt;price/&gt;`.
### Returns <!-- omit in toc -->
- `string` - The modified format of the cart item price, which must contain the substring `<price/>`, or the original price format.
- `string` - The modified format of the cart item price, which must contain the substring `&lt;price/&gt;`, or the original price format.
### Code examples <!-- omit in toc -->
@ -138,7 +136,7 @@ const modifyCartItemPrice = ( defaultValue, extensions, args, validation ) => {
return defaultValue;
}
return '<price/> for all items';
return '&lt;price/&gt; for all items';
};
registerCheckoutFilters( 'example-extension', {
@ -159,14 +157,14 @@ const modifyCartItemPrice = ( defaultValue, extensions, args, validation ) => {
}
if ( args?.cartItem?.name === 'Beanie with Logo' ) {
return '<price/> to keep you ☀️';
return '&lt;price/&gt; to keep you warm';
}
if ( args?.cartItem?.name === 'Sunglasses' ) {
return '<price/> to keep you ❄️';
return '&lt;price/&gt; to keep you cool';
}
return '<price/> for all items';
return '&lt;price/&gt; for all items';
};
registerCheckoutFilters( 'example-extension', {
@ -174,7 +172,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -193,8 +191,8 @@ The `itemName` filter allows to change the cart item name.
- _defaultValue_ `string` - The default cart item name.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object).
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see [Cart Item object](#cart-item-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see Cart object.
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see Cart Item object.
- _context_ `string` (allowed values: `cart` or `summary`) - The context of the item.
### Returns <!-- omit in toc -->
@ -251,7 +249,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -267,17 +265,17 @@ The `saleBadgePriceFormat` filter allows to format the cart item sale badge pric
### Parameters <!-- omit in toc -->
- _defaultValue_ `string` (default: `<price/>`) - The default cart item sale badge price.
- _defaultValue_ `string` (default: `&lt;price/&gt;`) - The default cart item sale badge price.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object).
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see [Cart Item object](#cart-item-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see Cart object.
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see Cart Item object.
- _context_ `string` (allowed values: `cart` or `summary`) - The context of the item.
- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`.
- _validation_ `boolean` - Checks if the return value contains the substring `&lt;price/&gt;`.
### Returns <!-- omit in toc -->
- `string` - The modified format of the cart item sale badge price, which must contain the substring `<price/>`, or the original price format.
- `string` - The modified format of the cart item sale badge price, which must contain the substring `&lt;price/&gt;`, or the original price format.
### Code examples <!-- omit in toc -->
@ -296,7 +294,7 @@ const modifySaleBadgePriceFormat = (
return defaultValue;
}
return '<price/> per item';
return '&lt;price/&gt; per item';
};
registerCheckoutFilters( 'example-extension', {
@ -322,14 +320,14 @@ const modifySaleBadgePriceFormat = (
}
if ( args?.cartItem?.name === 'Beanie with Logo' ) {
return '<price/> per item while keeping warm';
return '&lt;price/&gt; per item while keeping warm';
}
if ( args?.cartItem?.name === 'Sunglasses' ) {
return '<price/> per item while looking cool';
return '&lt;price/&gt; per item while looking cool';
}
return '<price/> per item';
return '&lt;price/&gt; per item';
};
registerCheckoutFilters( 'example-extension', {
@ -337,7 +335,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -357,8 +355,8 @@ The `showRemoveItemLink` is used to show or hide the cart item remove link.
- _defaultValue_ (type: `boolean`, default: `true`) - The default value of the remove link.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object).
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see [Cart Item object](#cart-item-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see Cart object.
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see Cart Item object.
- _context_ `string` (allowed values: `cart` or `summary`) - The context of the item.
### Returns <!-- omit in toc -->
@ -415,7 +413,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -433,17 +431,17 @@ The `subtotalPriceFormat` filter allows to format the cart item subtotal price.
### Parameters <!-- omit in toc -->
- _defaultValue_ `string` (default: `<price/>`) - The default cart item subtotal price.
- _defaultValue_ `string` (default: `&lt;price/&gt;`) - The default cart item subtotal price.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](#cart-object).
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see [Cart Item object](#cart-item-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see Cart object.
- _cartItem_ `object` - The cart item object from `wc/store/cart`, see Cart Item object.
- _context_ `string` (allowed values: `cart` or `summary`) - The context of the item.
- _validation_ `boolean` - Checks if the return value contains the substring `<price/>`.
- _validation_ `boolean` - Checks if the return value contains the substring `&lt;price/&gt;`.
### Returns <!-- omit in toc -->
- `string` - The modified format of the cart item subtotal price, which must contain the substring `<price/>`, or the original price format.
- `string` - The modified format of the cart item subtotal price, which must contain the substring `&lt;price/&gt;`, or the original price format.
### Code examples <!-- omit in toc -->
@ -464,7 +462,7 @@ const modifySubtotalPriceFormat = (
return defaultValue;
}
return '<price/> per item';
return '&lt;price/&gt; per item';
};
registerCheckoutFilters( 'example-extension', {
@ -490,14 +488,14 @@ const modifySubtotalPriceFormat = (
}
if ( args?.cartItem?.name === 'Beanie with Logo' ) {
return '<price/> per warm beanie';
return '&lt;price/&gt; per warm beanie';
}
if ( args?.cartItem?.name === 'Sunglasses' ) {
return '<price/> per cool sunglasses';
return '&lt;price/&gt; per cool sunglasses';
}
return '<price/> per item';
return '&lt;price/&gt; per item';
};
registerCheckoutFilters( 'example-extension', {
@ -505,7 +503,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -536,7 +534,7 @@ The Cart object of the filters above has the following keys:
- _cartHasCalculatedShipping_ `boolean` - Whether the cart has calculated shipping.
- _cartIsLoading_ `boolean` - Whether the cart is loading.
- _cartItemErrors_ `array` - The cart item errors array.
- _cartItems_ `array` - The cart items array with cart item objects, see [Cart Item object](#cart-item-object).
- _cartItems_ `array` - The cart items array with cart item objects, see Cart Item object.
- _cartItemsCount_ `number` - The cart items count.
- _cartItemsWeight_ `number` - The cart items weight.
- _cartNeedsPayment_ `boolean` - Whether the cart needs payment.

View File

@ -1,21 +1,20 @@
---
post_title: Cart and Checkout - Checkout and Place Order Button
post_title: Cart and Checkout Filters - Checkout and place order button
menu_title: Checkout and Place Order Button
tags: reference, checkout-available-filters
tags: reference
---
<!-- markdownlint-disable MD024 -->
# Checkout and place order button
The following Checkout and place order button filters are available:
- [`proceedToCheckoutButtonLabel`](#proceedtocheckoutbuttonlabel)
- [`proceedToCheckoutButtonLink`](#proceedtocheckoutbuttonlink)
- [`placeOrderButtonLabel`](#placeorderbuttonlabel)
- `proceedToCheckoutButtonLabel`
- `proceedToCheckoutButtonLink`
- `placeOrderButtonLabel`
The following objects are shared between the filters:
- [Cart object](#cart-object)
- [Cart Item object](#cart-item-object)
- Cart object
- Cart Item object
## `proceedToCheckoutButtonLabel`
@ -88,7 +87,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -107,7 +106,7 @@ The `proceedToCheckoutButtonLink` filter allows change the link of the "Proceed
- _defaultValue_ `string` (default: `/checkout`) - The link of the "Proceed to checkout" button.
- _extensions_ `object` (default: `{}`) - The extensions object.
- _args_ `object` - The arguments object with the following keys:
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](../available-filters.md#cart-object).
- _cart_ `object` - The cart object from `wc/store/cart`, see [Cart object](./category/cart-and-checkout-blocks/available-filters/).
### Returns <!-- omit in toc -->
@ -167,7 +166,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -204,7 +203,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->

View File

@ -1,18 +1,16 @@
---
post_title: Cart and Checkout Filters - Coupons
menu_title: Coupons
tags: reference, checkout-available-filters
tags: reference
---
<!-- markdownlint-disable MD024 -->
# Coupons
The following Coupon filters are available:
- [`coupons`](#coupons-1)
- [`showApplyCouponNotice`](#showapplycouponnotice)
- [`showRemoveCouponNotice`](#showremovecouponnotice)
- `coupons`
- `showApplyCouponNotice`
- `showRemoveCouponNotice`
## `coupons`
@ -66,7 +64,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -124,7 +122,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -182,7 +180,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->

View File

@ -1,22 +1,21 @@
---
post_title: Cart and Checkout Filters - Order Summary Items
post_title: Cart and Checkout Filters - Order summary items
menu_title: Order Summary Items
tags: reference, checkout-available-filters
tags: reference
---
<!-- markdownlint-disable MD024 -->
# Order Summary Items
The following Order Summary Items filters are available:
- [`cartItemClass`](#cartitemclass)
- [`cartItemPrice`](#cartitemprice)
- [`itemName`](#itemname)
- [`subtotalPriceFormat`](#subtotalpriceformat)
- `cartItemClass`
- `cartItemPrice`
- `itemName`
- `subtotalPriceFormat`
The following objects are shared between the filters:
- [Cart object](#cart-object)
- [Cart Item object](#cart-item-object)
- Cart object
- Cart Item object
The following screenshot shows which parts the individual filters affect:
@ -91,7 +90,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -169,7 +168,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -246,7 +245,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -334,7 +333,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->

View File

@ -1,17 +1,15 @@
---
post_title: Cart and Checkout - Totals Footer Item
post_title: Cart and Checkout Filters - Totals footer item
menu_title: Totals Footer Item
tags: reference, checkout-available-filters
tags: reference
---
<!-- markdownlint-disable MD024 -->
# Totals Footer Item
The following Totals Footer Item filter are available:
- [`totalLabel`](#totallabel)
- [`totalValue`](#totalvalue)
- `totalLabel`
- `totalValue`
## `totalLabel`
@ -48,7 +46,7 @@ registerCheckoutFilters( 'example-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->
@ -93,7 +91,7 @@ registerCheckoutFilters( 'my-extension', {
} );
```
> 💡 Filters can be also combined. See [Combined filters](../available-filters.md#combined-filters) for an example.
> Filters can be also combined. See [Combined filters](./category/cart-and-checkout-blocks/available-filters/) for an example.
### Screenshots <!-- omit in toc -->

View File

@ -1,27 +1,15 @@
---
post_title: Cart and Checkout - Available Slots
post_title: Cart and Checkout - Available slots
menu_title: Available Slots
tags: reference
---
<!-- markdownlint-disable MD024 -->
# Available Slots <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [ExperimentalOrderMeta](#experimentalordermeta)
- [Passed parameters](#passed-parameters)
- [ExperimentalOrderShippingPackages](#experimentalordershippingpackages)
- [Passed parameters](#passed-parameters-1)
- [ExperimentalOrderLocalPickupPackages](#experimentalorderlocalpickuppackages)
- [Passed parameters](#passed-parameters-2)
- [ExperimentalDiscountsMeta](#experimentaldiscountsmeta)
- [Passed parameters](#passed-parameters-3)
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](../../../../packages/checkout/slot/README.md). To read more about Slot and Fill, check the [Slot and Fill document](./slot-fills.md).
If you want to add a new SlotFill component, check the [Checkout - Slot Fill document](https://github.com/woocommerce/woocommerce/blob/1675c63bba94c59703f57c7ef06e7deff8fd6bba/plugins/woocommerce-blocks/packages/checkout/slot/README.md). To read more about Slot and Fill, check the [Slot and Fill document](./cart-and-checkout-slot-and-fill/).
**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](../../../internal-developers/blocks/feature-flags-and-experimental-interfaces.md) from more information.
**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/1675c63bba94c59703f57c7ef06e7deff8fd6bba/plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md) from more information.
## ExperimentalOrderMeta

View File

@ -0,0 +1,8 @@
---
category_title: Payment Methods
category_slug: checkout-payment-methods
post_title: Checkout payment methods
---

View File

@ -0,0 +1,469 @@
---
post_title: Cart and Checkout - Checkout flow and events
menu_title: Checkout Flow and Events
tags: reference
---
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-blocks/blob/trunk/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 { CHECKOUT_STORE_KEY } = window.wc.wcBlocksData;
const MyComponent = ( props ) => {
const isComplete = useSelect( ( select ) =>
select( CHECKOUT_STORE_KEY ).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 are booleans exposed via the checkout provider that are independent from each other and checkout statuses but can be used in combination to react to various state in the checkout.
**isCalculating:** This is true when the total is being re-calculated for the order. There are numerous things that might trigger a recalculation of the total: coupons being added or removed, shipping rates updated, shipping rate selected etc. This flag consolidates all activity that might be occurring (including requests to the server that potentially affect calculation of totals). So instead of having to check each of those individual states you can reliably just check if this boolean is true (calculating) or false (not calculating).
**hasError:** This 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 { PAYMENT_STORE_KEY } = window.wc.wcBlocksData;
const MyComponent = ( props ) => {
const isPaymentIdle = select( PAYMENT_STORE_KEY ).isPaymentIdle();
const isExpressPaymentStarted =
select( PAYMENT_STORE_KEY ).isExpressPaymentStarted();
const isPaymentProcessing =
select( PAYMENT_STORE_KEY ).isPaymentProcessing();
const isPaymentReady = select( PAYMENT_STORE_KEY ).isPaymentReady();
const hasPaymentError = select( PAYMENT_STORE_KEY ).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 {
isSuccessResponse,
isErrorResponse,
isFailResponse,
noticeContexts,
responseTypes,
shouldRetry,
} from '@woocommerce/base-context';
};
```
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 ] );
};
```
### ~~`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-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-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 ] );
};
```
### `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 ] );
};
```
### `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.

View File

@ -1,30 +1,11 @@
---
post_title: Cart and Checkout - Filtering payment methods in the Checkout block
menu_title: Filtering Payment Methods
tags: how-to, checkout-payment-methods
tags: how-to
---
<!-- markdownlint-disable MD024 -->
# Filtering Payment Methods in the Checkout block <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [The problem](#the-problem)
- [The solution](#the-solution)
- [Importing](#importing)
- [Aliased import](#aliased-import)
- [`wc global`](#wc-global)
- [Signature](#signature)
- [Extension namespace collision](#extension-namespace-collision)
- [Usage example](#usage-example)
- [Callbacks registered for payment methods](#callbacks-registered-for-payment-methods)
- [Filtering payment methods using requirements](#filtering-payment-methods-using-requirements)
- [The problem](#the-problem-1)
- [The solution](#the-solution-1)
- [Basic usage](#basic-usage)
- [Putting it all together](#putting-it-all-together)
## 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.
@ -105,7 +86,8 @@ interface CanMakePaymentArgument {
}
```
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](../rest-api/extend-rest-api-add-data.md).
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/1675c63bba94c59703f57c7ef06e7deff8fd6bba/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md).
## Filtering payment methods using requirements
@ -121,7 +103,7 @@ To allow the shopper to check out without entering payment details, but still re
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](../checkout-payment-methods/payment-method-integration.md) documentation.
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

View File

@ -1,37 +1,9 @@
---
post_title: Cart and Checkout - Payment method integration for the Checkout block
menu_title: Payment Method Integration
tags: reference, checkout-payment-methods
tags: reference
---
# Payment Method Integration for the Checkout Block <!-- omit in toc -->
The checkout block has an API interface for payment methods to integrate that consists of both a server side and client side implementation.
## Table of Contents <!-- omit in toc -->
- [Client Side integration](#client-side-integration)
- [Express payment methods - `registerExpressPaymentMethod( options )`](#express-payment-methods---registerexpresspaymentmethod-options-)
- [Aliased import](#registerexpresspaymentmethod-aliased-import)
- [`wc global`](#registerexpresspaymentmethod-on-the-wc-global)
- [The registration options](#the-registerexpresspaymentmethod-registration-options)
- [`name` (required)](#name-required)
- [`content` (required)](#content-required)
- [`edit` (required)](#edit-required)
- [`canMakePayment` (required)](#canmakepayment-required)
- [`paymentMethodId`](#paymentmethodid)
- [`supports:features`](#supportsfeatures)
- [Payment Methods - `registerPaymentMethod( options )`](#payment-methods---registerpaymentmethod-options-)
- [Aliased import](#registerpaymentmethod-aliased-import)
- [`wc global`](#registerpaymentmethod-on-the-wc-global)
- [The registration options](#the-registerpaymentmethod-registration-options)
- [Props Fed to Payment Method Nodes](#props-fed-to-payment-method-nodes)
- [Server Side Integration](#server-side-integration)
- [Processing Payment](#processing-payment)
- [Registering Assets](#registering-assets)
- [Hooking into the Checkout processing by the Store API](#hooking-into-the-checkout-processing-by-the-store-api)
- [Putting it all together](#putting-it-all-together)
## Client Side integration
The client side integration consists of an API for registering both _express_ payment methods (those that consist of a one-button payment process initiated by the shopper such as Stripe, ApplePay, or GooglePay), and payment methods such as _cheque_, PayPal Standard, or Stripe Credit Card.
@ -89,7 +61,7 @@ This should be a unique string (wise to try to pick something unique for your ga
#### `content` (required)
This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](./payment-method-integration.md#props-fed-to-payment-method-nodes)).
This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](#props-fed-to-payment-method-nodes)).
#### `edit` (required)
@ -165,7 +137,7 @@ The options you feed the configuration instance are the same as those for expres
### 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](../../../../assets/js/types/type-defs/payment-method-interface.ts).
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 | Values |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

View File

@ -1,20 +1,9 @@
---
post_title: Cart and Checkout - Dom Events
menu_title: Dom Events
post_title: Cart and Checkout - DOM events
menu_title: DOM Events
tags: reference
---
# DOM Events <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [WooCommerce core events in WooCommerce Blocks](#woocommerce-core-events-in-woocommerce-blocks)
- [WooCommerce Blocks events](#woocommerce-blocks-events)
- [`wc-blocks_adding_to_cart`](#wc-blocks_adding_to_cart)
- [`wc-blocks_added_to_cart`](#wc-blocks_added_to_cart)
- [`detail` parameters:](#detail-parameters)
- [`wc-blocks_removed_from_cart`](#wc-blocks_removed_from_cart)
Some blocks need to react to certain events in order to display the most up to date data or behave in a certain way. That's the case of the Cart block, for example, that must listen to 'add to cart' events in order to update the cart contents; or the Mini-Cart block, that gets opened every time a product is added to the cart.
## WooCommerce core events in WooCommerce Blocks

View File

@ -0,0 +1,8 @@
---
category_title: Hooks
category_slug: cart-and-checkout-hooks
post_title: Cart and Checkout - Hooks
---

View File

@ -0,0 +1,50 @@
---
post_title: Cart and Checkout - Legacy hooks
menu_title: Legacy Hooks
tags: reference
---
Below are the hooks that exist in WooCommerce core and that were brough over to WooCommerce Blocks.
Please note that the actions and filters here run on the server side. The client-side blocks won't necessarily change based on a callback added to a server side hook. [Please see our documentation relating to APIs for manipulating the blocks on the client-side](./an-introduction-to-extensiblity-in-woocommerce-blocks/).
## Legacy Filters
- [loop_shop_per_page](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#loop_shop_per_page)
- [wc_session_expiration](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#wc_session_expiration)
- [woocommerce_add_cart_item](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_cart_item)
- [woocommerce_add_cart_item_data](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_cart_item_data)
- [woocommerce_add_to_cart_quantity](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_quantity)
- [woocommerce_add_to_cart_sold_individually_quantity](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_sold_individually_quantity)
- [woocommerce_add_to_cart_validation](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_add_to_cart_validation)
- [woocommerce_adjust_non_base_location_prices](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_adjust_non_base_location_prices)
- [woocommerce_apply_base_tax_for_local_pickup](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_base_tax_for_local_pickup)
- [woocommerce_apply_individual_use_coupon](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_individual_use_coupon)
- [woocommerce_apply_with_individual_use_coupon](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_apply_with_individual_use_coupon)
- [woocommerce_cart_contents_changed](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_cart_contents_changed)
- [woocommerce_cart_item_permalink](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_cart_item_permalink)
- [woocommerce_get_item_data](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_get_item_data)
- [woocommerce_loop_add_to_cart_args](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_loop_add_to_cart_args)
- [woocommerce_loop_add_to_cart_link](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_loop_add_to_cart_link)
- [woocommerce_new_customer_data](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_new_customer_data)
- [woocommerce_pay_order_product_has_enough_stock](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_pay_order_product_has_enough_stock)
- [woocommerce_pay_order_product_in_stock](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_pay_order_product_in_stock)
- [woocommerce_registration_errors](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_registration_errors)
- [woocommerce_shipping_package_name](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_shipping_package_name)
- [woocommerce_show_page_title](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_show_page_title)
- [woocommerce_single_product_image_thumbnail_html](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/filters.md#woocommerce_single_product_image_thumbnail_html)
## Legacy Actions
- [woocommerce_add_to_cart](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_add_to_cart)
- [woocommerce_after_main_content](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_after_main_content)
- [woocommerce_after_shop_loop](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_after_shop_loop)
- [woocommerce_applied_coupon](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_applied_coupon)
- [woocommerce_archive_description](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_archive_description)
- [woocommerce_before_main_content](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_before_main_content)
- [woocommerce_before_shop_loop](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_before_shop_loop)
- [woocommerce_check_cart_items](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_check_cart_items)
- [woocommerce_created_customer](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_created_customer)
- [woocommerce_no_products_found](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_no_products_found)
- [woocommerce_register_post](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_register_post)
- [woocommerce_shop_loop](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/docs/third-party-developers/extensibility/hooks/actions.md#woocommerce_shop_loop)

View File

@ -1,24 +1,22 @@
---
post_title: Cart and Checkout - How the Checkout block processes an order
menu_title: Processing an order
post_title: Cart and Checkout - How the checkout block processes an order
menu_title: Processing an Order
tags: reference
---
# How The Checkout Block Processes an Order
This document will shed some light on the inner workings of the Checkout flow. More specifically, what happens after a user hits the “Place Order” button.
This document will shed some light on the inner workings of the Checkout flow. More specifically, what happens after a user hits the "Place Order" button.
## Structure
The following areas are associated with processing the checkout for a user.
### The Payment Registry [:file_folder:](https://href.li/?https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/blocks-registry/payment-methods/registry.ts#L1)
### The Payment Registry [(file)](https://href.li/?https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/blocks-registry/payment-methods/registry.ts#L1)
The payment registry stores all the configuration information for each payment method. We can register a new payment method here with the `registerPaymentMethod` and `registerExpressPaymentMethod `functions, also available to other plugins.
### Data Stores
Data stores are used to keep track of data that is likely to change during a users session, such as the active payment method, whether the checkout has an error, etc. We split these data stores by areas of concern, so we have 2 data stores related to the checkout: `wc/store/checkout` [:file_folder:](https://href.li/?https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/index.ts#L1) and `wc/store/payment` [:file_folder:](https://href.li/?https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/payment-methods/index.ts#L1) . Data stores live in the `assets/js/data` folder.
Data stores are used to keep track of data that is likely to change during a user's session, such as the active payment method, whether the checkout has an error, etc. We split these data stores by areas of concern, so we have 2 data stores related to the checkout: `wc/store/checkout` [(file)](https://href.li/?https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/index.ts#L1) and `wc/store/payment` [(file)](https://href.li/?https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/payment-methods/index.ts#L1) . Data stores live in the `assets/js/data` folder.
### Contexts
@ -59,25 +57,25 @@ Below is the complete checkout flow
The checkout process starts when a user clicks the button
### 2\. Checkout status is set to `before_processing` [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx#L167)
### 2\. Checkout status is set to `before_processing` [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx#L167)
As soon as the user clicks the "Place Order" button, we change the checkout status to _"before_processing"_. This is where we handle the validation of the checkout information.
### 3\. Emit the `checkout_validation_before_processing` event [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx#L113)
### 3\. Emit the `checkout_validation_before_processing` event [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx#L113)
This is where the WooCommerce Blocks plugin and other plugins can register event listeners for dealing with validation. The event listeners for this event will run and if there are errors, we set checkout status to `idle` and display the errors to the user.
If there are no errors, we move to step 4 below.
### 4\. Checkout status is set to `processing` [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L76)
### 4\. Checkout status is set to `processing` [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L76)
The processing status is used by step 5 below to change the payment status
### 5\. Payment status is set to `processing` [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-events-context.tsx#L94)
### 5\. Payment status is set to `processing` [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-events-context.tsx#L94)
Once all the checkout processing is done and if there are no errors, the payment processing begins
### 6\. Emit the `payment_processing` event [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/payment-methods/thunks.ts#L42)
### 6\. Emit the `payment_processing` event [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/payment-methods/thunks.ts#L42)
The `payment_processing` event is emitted. Otherplugins can register event listeners for this event and run their own code.
@ -85,19 +83,19 @@ For example, the Stripe plugin checks the address and payment details here, and
**Important note: The actual payment is not taken here**. **It acts like a pre-payment hook to run any payment plugin code before the actual payment is attempted.**
### 7\. Execute the `payment_processing` event listeners and set the payment and checkout states accordingly [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/payment-methods/thunks.ts#L54-L132)
### 7\. Execute the `payment_processing` event listeners and set the payment and checkout states accordingly [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/payment-methods/thunks.ts#L54-L132)
If the registered event listeners return errors, we will display this to the user.
If the event listeners are considered successful, we sync up the address of the checkout with the payment address, store the `paymentMethodData` in the payment store, and set the payment status property `{ isProcessing: true }`.
### 8\. POST to `/wc/store/v1/checkout` [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-processor.js#L234)
### 8\. POST to `/wc/store/v1/checkout` [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-processor.js#L234)
The `/checkout` StoreApi endpoint is called if there are no payment errors. This will take the final customer addresses and chosen payment method, and any additional payment data, then attempts payment and returns the result.
**Important: payment is attempted through the StoreApi, NOT through the `payment_processing` event that is sent from the client**
### 9\. The `processCheckoutResponse` action is triggered on the checkout store [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L33)
### 9\. The `processCheckoutResponse` action is triggered on the checkout store [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L33)
Once the fetch to the StoreApi `/checkout` endpoint returns a response, we pass this to the `processCheckoutResponse` action on the `checkout` data store.
@ -107,25 +105,25 @@ It will perform the following actions:
- It stores the payment result in the `checkout` data store.
- It changes the checkout status to `after_processing` (step 10)
### 10\. Checkout status is set to `after_processing` [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L42)
### 10\. Checkout status is set to `after_processing` [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L42)
The `after_processing` status indicates that we've finished the main checkout processing step. In this step, we run cleanup actions and display any errors that have been triggered during the last step.
### 11\. Emit the `checkout_after_processing_with_success` event [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L118-L128)
### 11\. Emit the `checkout_after_processing_with_success` event [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L118-L128)
If there are no errors, the `checkout_after_processing_with_success` event is triggered. This is where other plugins can run some code after the checkout process has been successful.
Any event listeners registered on the `checkout_after_processing_with_success` event will be executed. If there are no errors from the event listeners, `setComplete` action is called on the `checkout` data store to set the status to `complete` (step 13). An event listener can also return an error here, which will be displayed to the user.
### 12\. Emit the `checkout_after_processing_with_error` event [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L104-L116)
### 12\. Emit the `checkout_after_processing_with_error` event [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/thunks.ts#L104-L116)
If there has been an error from step 5, the `checkout_after_processing_with_error` event will be emitted. Other plugins can register event listeners to run here to display an error to the user. The event listeners are processed and display any errors to the user.
### 13\. Set checkout status to `complete` [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/utils.ts#L146)
### 13\. Set checkout status to `complete` [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/data/checkout/utils.ts#L146)
If there are no errors, the `status` property changes to `complete` in the checkout data store. This indicates that the checkout process is complete.
### 14\. Redirect [:file_folder:](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-processor.js#L193-L197)
### 14\. Redirect [(file)](https://github.com/woocommerce/woocommerce-blocks/blob/4af2c0916a936369be8a4f0044683b90b3af4f0d/assets/js/base/context/providers/cart-checkout/checkout-processor.js#L193-L197)
Once the checkout status is set to `complete`, if there is a `redirectUrl` in the checkout data store, then we redirect the user to the URL.

View File

@ -1,24 +1,9 @@
---
post_title: Cart and Checkout - Handling scripts, styles, and data
menu_title: Script, styles, and data handling
menu_title: Script, Styles, and Data Handling
tags: how-to
---
# `IntegrationRegistry` and `IntegrationInterface` <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [The problem](#the-problem)
- [The solution](#the-solution)
- [`IntegrationInterface` methods](#integrationinterface-methods)
- [`get_name()`](#get_name)
- [`initialize()`](#initialize)
- [`get_script_handles()`](#get_script_handles)
- [`get_editor_script_handles()`](#get_editor_script_handles)
- [`get_script_data()`](#get_script_data)
- [Usage example](#usage-example)
- [Getting data added in `get_script_data`](#getting-data-added-in-get_script_data)
## The problem
You are an extension developer, and to allow users to interact with your extension on the client-side, you have written some CSS and JavaScript that you would like to be included on the page. Your JavaScript also relies on some server-side data, and you'd like this to be available to your scripts.

View File

@ -1,22 +1,12 @@
---
post_title: Cart and Checkout - Slot and Fill
post_title: Cart and Checkout - Slot and fill
menu_title: Slot and Fill
tags: reference
---
# Slot and Fill <!-- omit in toc -->
## Table of Contents <!-- omit in toc -->
- [The problem](#the-problem)
- [Solution](#solution)
- [Basic Usage](#basic-usage)
- [registerPlugin](#registerplugin)
- [Requirements](#requirements)
## The problem
You added custom data to the [Store API](../rest-api/extend-rest-api-add-data.md). You changed several strings using [Checkout filters](./available-filters.md). Now you want to render your own components in specific places in the Cart and Checkout.
You added custom data to the [Store API](https://github.com/woocommerce/woocommerce/blob/1675c63bba94c59703f57c7ef06e7deff8fd6bba/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md). You changed several strings using [Checkout filters](./category/cart-and-checkout-blocks/available-filters/). Now you want to render your own components in specific places in the Cart and Checkout.
## Solution

View File

@ -0,0 +1,44 @@
---
post_title: Check if a Payment Method Support Refunds, Subscriptions or Pre-orders
menu_title: Payment method support for refunds, subscriptions, pre-orders
tags: payment-methods
current wccom url: https://woocommerce.com/document/check-if-payment-gateway-supports-refunds-subscriptions-preorders/
---
# Check if a Payment Method Support Refunds, Subscriptions or Pre-orders
If a payment method's documentation doesnt clearly outline the supported features, you can often find what features are supported by looking at payment methods code.
Payment methods can add support for certain features from WooCommerce and its extensions. For example, a payment method can support refunds, subscriptions or pre-orders functionality.
## Simplify Commerce example
Taking the Simplify Commerce payment method as an example, open the plugin files in your favorite editor and search for `$this->supports`. You'll find the supported features:
```php
class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway {
/** * Constructor */
public function __construct() {
$this->id
= 'simplify_commerce';
$this->method_title
= __( 'Simplify Commerce', 'woocommerce' );
$this->method_description = __( 'Take payments via Simplify Commerce - uses simplify.js to create card tokens and the Simplify Commerce SDK. Requires SSL when sandbox is disabled.', 'woocommerce' );
$this->has_fields = true;
$this->supports = array(
'subscriptions',
'products',
'subscription_cancellation',
'subscription_reactivation',
'subscription_suspension',
'subscription_amount_changes',
'subscription_payment_method_change',
'subscription_date_changes',
'default_credit_card_form',
'refunds',
'pre-orders'
);
```
If you dont find `$this->supports` in the plugin files, that may mean that the payment method isnt correctly declaring support for refunds, subscripts or pre-orders.

View File

@ -0,0 +1,81 @@
---
post_title: Code snippets for configuring special tax scenarios
menu_title: Configuring special tax scenarios
tags: code-snippet, tax
current wccom url: https://woocommerce.com/document/setting-up-taxes-in-woocommerce/configuring-specific-tax-setups-in-woocommerce/#configuring-special-tax-setups
---
# Code snippets for configuring special tax scenarios
## Scenario A: Charge the same price regardless of location and taxes
Scenario A: Charge the same price regardless of location and taxes
If a store enters product prices including taxes, but levies various location-based tax rates, the prices will appear to change depending on which tax rate is applied. In reality, the base price remains the same, but the taxes influence the total. [Follow this link for a detailed explanation](https://woocommerce.com/document/how-taxes-work-in-woocommerce/#cross-border-taxes).
Some merchants prefer to dynamically change product base prices to account for the changes in taxes and so keep the total price consistent regardless of tax rate. Enable that functionality by adding the following snippet to your child themes functions.php file or via a code snippet plugin.
```php
<?php
add_filter( 'woocommerce_adjust_non_base_location_prices', '__return_false' );
```
## Scenario B: Charge tax based on the subtotal amount
The following snippet is useful in case where a store only ads taxes when the subtotal reaches a specified minimum. In the code snippet below that minimum is 110 of the stores currency. Adjust the snippet according to your requirements.
```php
<?php
add_filter( 'woocommerce_product_get_tax_class', 'big_apple_get_tax_class', 1, 2 );
function big_apple_get_tax_class( $tax_class, $product ) {
if ( WC()->cart->subtotal <= 110 )
$tax_class = 'Zero Rate';
return $tax_class;
}
```
## Scenario C: Apply different tax rates based on the customer role
Some merchants may require different tax rates to be applied based on a customer role to accommodate for wholesale status or tax exemption.
To enable this functionality, add the following snippet to your child themes functions.php file or via a code snippet plugin. In this snippet, users with “administrator” capabilities will be assigned the **Zero rate tax class**. Adjust it according to your requirements.
```php
<?php
/**
* Apply a different tax rate based on the user role.
*/
function wc_diff_rate_for_user( $tax_class, $product ) {
if ( is_user_logged_in() && current_user_can( 'administrator' ) ) {
$tax_class = 'Zero Rate';
}
return $tax_class;
}
add_filter( 'woocommerce_product_get_tax_class', 'wc_diff_rate_for_user', 1, 2 );
add_filter( 'woocommerce_product_variation_get_tax_class', 'wc_diff_rate_for_user', 1, 2 );
```
## Scenario D: Show 0 value taxes
Taxes that have 0-value are hidden by default. To show them regardless, add the following snippet to your themes functions.php file or via a code snippet plugins:
```php
add_filter( 'woocommerce_order_hide_zero_taxes', '__return_false' );
```
## Scenario E: Suffixes on the main variable product
One of the tax settings for WooCommerce enables the use of suffixes to add additional information to product prices. Its available for use with the variations of a variable product, but is disabled at the main variation level as it can impact website performance when there are many variations.
The method responsible for the related price output can be customized via filter hooks if needed for variable products. This will require customization that can be implemented via this filter:
```php
add_filter( 'woocommerce_show_variation_price', '__return_true' );
```

View File

@ -0,0 +1,13 @@
---
post_title: Disabling Marketplace Suggestions Programmatically
menu_title: Disabling marketplace suggestions
current wccom url: https://woocommerce.com/document/woocommerce-marketplace-suggestions-settings/#section-6
---
## Disabling Marketplace Suggestions Programmatically
For those who prefer to programmatically disable marketplace suggestions that are fetched from woocommerce.com, add the `woocommerce_allow_marketplace_suggestions` filter to your themes `functions.php` or a custom plugin.
For example:
This filter will completely remove Marketplace Suggestions from your WooCommerce admin.

View File

@ -0,0 +1,41 @@
---
post_title: Displaying Custom Fields in Your Theme or Site
menu_title: Displaying custom fields in theme
tags: code-snippet
current wccom url: https://woocommerce.com/document/custom-product-fields/
---
## Displaying Custom Fields in Your Theme or Site
You can use the metadata from custom fields you add to your products to display the added information within your theme or site.
To display the custom fields for each product, you have to edit your themes files. Heres an example of how you might display a custom field within the single product pages after the short description:
![image](https://github.com/woocommerce/woocommerce-developer-advocacy/assets/15178758/ed417ed8-4462-45b9-96b6-c0141afaeb2b)
```php
<?php
// Display a product custom field within single product pages after the short description
function woocommerce_custom_field_example() {
if ( ! is_product() ) {
return;
}
global $product;
if ( ! is_object( $product ) ) {
$product = wc_get_product( get_the_ID() );
}
$custom_field_value = get_post_meta( $product->get_id(), 'woo_custom_field', true );
if ( ! empty( $custom_field_value ) ) {
echo '<div class="custom-field">' . esc_html( $custom_field_value ) . '</div>';
}
}
add_action( 'woocommerce_before_add_to_cart_form', 'woocommerce_custom_field_example', 10 );
```

View File

@ -0,0 +1,147 @@
---
post_title: Free Shipping Customizations
menu_title: Free shipping customizations
tags: code-snippets
current wccom url: https://woocommerce.com/document/free-shipping/#advanced-settings-customization
combined with: https://woocommerce.com/document/hide-other-shipping-methods-when-free-shipping-is-available/#use-a-plugin
---
## Free Shipping: Advanced Settings/Customization
### Overview
By default, WooCommerce shows all shipping methods that match the customer and the cart contents. This means Free Shipping also shows along with Flat Rate and other Shipping Methods.
The functionality to hide all other methods, and only show Free Shipping, requires either custom PHP code or a plugin/extension.
### Adding code
Before adding snippets, clear your WooCommerce cache. Go to WooCommerce > System Status > Tools > WooCommerce Transients > Clear transients.
Add this code to your child themes `functions.php`, or via a plugin that allows custom functions to be added. Please dont add custom code directly to a parent themes `functions.php` as changes are entirely erased when a parent theme updates.
## Code Snippets
### Enabling or Disabling Free Shipping via Hooks
You can hook into the `is_available` function of the free shipping method.
```php
return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available );
```
This means you can use `add_filter()` on `woocommerce_shipping_free_shipping_is_available` and return `true` or `false`.
### How do I only show Free Shipping?
The following snippet hides everything but `free_shipping`, if its available and the customer's cart qualifies.
```php
/**
* Hide shipping rates when free shipping is available.
* Updated to support WooCommerce 2.6 Shipping Zones.
*
* @param array $rates Array of rates found for the package.
* @return array
*/
function my_hide_shipping_when_free_is_available( $rates ) {
$free = array();
foreach ( $rates as $rate_id => $rate ) {
if ( 'free_shipping' === $rate->method_id ) {
$free[ $rate_id ] = $rate;
break;
}
}
return ! empty( $free ) ? $free : $rates;
}
add_filter( 'woocommerce_package_rates', 'my_hide_shipping_when_free_is_available', 100 );
```
### How do I only show Local Pickup and Free Shipping?
The snippet below hides everything but `free_shipping` and `local_pickup`, if its available and the customer's cart qualifies.
```php
/**
* Hide shipping rates when free shipping is available, but keep "Local pickup"
* Updated to support WooCommerce 2.6 Shipping Zones
*/
function hide_shipping_when_free_is_available( $rates, $package ) {
$new_rates = array();
foreach ( $rates as $rate_id => $rate ) {
// Only modify rates if free_shipping is present.
if ( 'free_shipping' === $rate->method_id ) {
$new_rates[ $rate_id ] = $rate;
break;
}
}
if ( ! empty( $new_rates ) ) {
//Save local pickup if it's present.
foreach ( $rates as $rate_id => $rate ) {
if ('local_pickup' === $rate->method_id ) {
$new_rates[ $rate_id ] = $rate;
break;
}
}
return $new_rates;
}
return $rates;
}
add_filter( 'woocommerce_package_rates', 'hide_shipping_when_free_is_available', 10, 2 );
```
### Only show free shipping in all states except…
This snippet results in showing only free shipping in all states except the exclusion list. It hides free shipping if the customer is in one of the states listed:
```php
/**
* Hide ALL shipping options when free shipping is available and customer is NOT in certain states
*
* Change $excluded_states = array( 'AK','HI','GU','PR' ); to include all the states that DO NOT have free shipping
*/
add_filter( 'woocommerce_package_rates', 'hide_all_shipping_when_free_is_available' , 10, 2 );
/**
* Hide ALL Shipping option when free shipping is available
*
* @param array $available_methods
*/
function hide_all_shipping_when_free_is_available( $rates, $package ) {
$excluded_states = array( 'AK','HI','GU','PR' );
if( isset( $rates['free_shipping'] ) AND !in_array( WC()->customer->shipping_state, $excluded_states ) ) :
// Get Free Shipping array into a new array
$freeshipping = array();
$freeshipping = $rates['free_shipping'];
// Empty the $available_methods array
unset( $rates );
// Add Free Shipping back into $avaialble_methods
$rates = array();
$rates[] = $freeshipping;
endif;
if( isset( $rates['free_shipping'] ) AND in_array( WC()->customer->shipping_state, $excluded_states ) ) {
// remove free shipping option
unset( $rates['free_shipping'] );
}
return $rates;
}
```
### Enable Shipping Methods on a per Class / Product Basis, split orders, or other scenarios?
Need more flexibility? Take a look at our [premium Shipping Method extensions](https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/).

View File

@ -0,0 +1,37 @@
---
post_title: Legacy Local Pickup Advanced Settings and Customization
tags: code-snippet
current wccom url: https://woocommerce.com/document/local-pickup/#advanced-settings-customization
note: Docs links out to Skyverge's site for howto add a custom email - do we have our own alternative?
---
# Advanced settings and customization for legacy Local Pickup
## Disable local taxes when using local pickup
Local Pickup calculates taxes based on your stores location (address) by default, and not the customers address. Add this snippet at the end of your theme's `functions.php` to use your standard tax configuration instead:
```php
add_filter( 'woocommerce_apply_base_tax_for_local_pickup', '__return_false' );
```
Regular taxes is then used when local pickup is selected, instead of store-location-based taxes.
## Changing the location for local taxes
To charge local taxes based on the postcode and city of the local pickup location, you need to define the shops base city and post code using this example code:
```php
add_filter( 'woocommerce_countries_base_postcode', create_function( '', 'return "80903";' ) );
add_filter( 'woocommerce_countries_base_city', create_function( '', 'return "COLORADO SPRINGS";' ) );
```
Update `80903` to reflect your preferred postcode/zip, and `COLORADO SPRINGS` with your preferred town or city.
## Custom emails for local pickup
_Shipping Address_ is not displayed on the admin order emails when Local Pickup is used as the shipping method.
Since all core shipping options use the standard order flow, customers receive the same order confirmation email whether they select local pickup or any other shipping option.
Use this guide to create custom emails for local pickup if youd like to send a separate email for local pickup orders: [How to Add a Custom WooCommerce Email](https://www.skyverge.com/blog/how-to-add-a-custom-woocommerce-email/).

View File

@ -0,0 +1,29 @@
---
post_title: Making your translation upgrade safe
menu_title: Translation upgrade safety
tags: code-snippet
current wccom url: https://woocommerce.com/document/woocommerce-localization/#making-your-translation-upgrade-safe
---
# Making your translation upgrade safe
Like all other plugins, WooCommerce keeps translations in `wp-content/languages/plugins`.
However, if you want to include a custom translation, you can add them to `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/lanaguages/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;
}
```

View File

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

View File

@ -0,0 +1,22 @@
---
post_title: SSL and HTTPS and WooCommerce
menu_title: SSL and HTTPS and WooCommerce
tags: code-snippet
current wccom url: https://woocommerce.com/document/ssl-and-https/#websites-behind-load-balancers-or-reverse-proxies
---
## Websites behind load balancers or reverse proxies
WooCommerce uses the `is_ssl()` WordPress function to verify if your website using SSL or not.
`is_ssl()` checks if the connection is via HTTPS or on Port 443. However, this wont work for websites behind load balancers, especially websites hosted at Network Solutions. For details, read [WordPress is_ssl() function reference notes](https://codex.wordpress.org/Function_Reference/is_ssl#Notes).
Websites behind load balancers or reverse proxies that support `HTTP_X_FORWARDED_PROTO` can be fixed by adding the following code to the `wp-config.php` file, above the require_once call:
```php
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
```
**Note:** If you use CloudFlare, you need to configure it. Check their documentation.

View File

@ -0,0 +1,26 @@
---
post_title: Uninstall and remove all WooCommerce Data
menu_title: Uninstalling and removing data
tags: code-snippet
current wccom url: https://woocommerce.com/document/installing-uninstalling-woocommerce/#uninstalling-woocommerce
---
# Uninstall and remove all WooCommerce Data
The WooCommerce plugin can be uninstalled like any other WordPress plugin. By default, the WooCommerce data is left in place though.
If you need to remove *all* WooCommerce data as well, including products, order data, coupons, etc., you need to to modify the sites `wp-config.php` *before* deactivating and deleting the WooCommerce plugin.
As this action is destructive and permanent, the information is provided as is. WooCommerce Support cannot help with this process or anything that happens as a result.
To fully remove all WooCommerce data from your WordPress site, open `wp-config.php`, scroll down to the bottom of the file, and add the following constant on its own line above `/* Thats all, stop editing. */`.
```php
define( 'WC_REMOVE_ALL_DATA', true );
/* Thats all, stop editing! Happy publishing. */
```
Then, once the changes are saved to the file, when you deactivate and delete WooCommerce, all of its data is removed from your WordPress site database.
![Uninstall WooCommerce WPConfig](https://woocommerce.com/wp-content/uploads/2020/03/uninstall_wocommerce_plugin_wpconfig.png)

View File

@ -0,0 +1,35 @@
---
post_title: Using NGINX server to protect your upload directory
menu_title: NGINX server to protect upload directory
tags: code-snippet
current wccom url: https://woocommerce.com/document/digital-downloadable-product-handling/#protecting-your-uploads-directory
---
## Using NGINX server to protect your upload directory
If you using NGINX server for your site along with **X-Accel-Redirect/X-Sendfile** or **Force Downloads** download method, it is necessary that you add this configuration for better security:
```php
# Protect WooCommerce upload folder from being accessed directly.
# You may want to change this config if you are using "X-Accel-Redirect/X-Sendfile" or "Force Downloads" method for downloadable products.
# Place this config towards the end of "server" block in NGINX configuration.
location ~* /wp-content/uploads/woocommerce_uploads/ {
if ( $upstream_http_x_accel_redirect = "" ) {
return 403;
}
internal;
}
```
And this the configuration in case you are using **Redirect only** download method:
```php
# Protect WooCommerce upload folder from being accessed directly.
# You may want to change this config if you are using "Redirect Only" method for downloadable products.
# Place this config towards the end of "server" block in NGINX configuration.
location ~* /wp-content/uploads/woocommerce_uploads/ {
autoindex off;
}
```
If you do not know which web server you are using, please reach out to your host along with a link to this support page.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,64 @@
---
category_title: Extensibility in Blocks
category_slug: extensibility-in-blocks
post_title: Extensibility in blocks
---
These documents are all dealing with extensibility in the various WooCommerce Blocks.
## Imports and dependency extration
The documentation in this section will use window globals in code examples, for example:
```js
const { registerCheckoutFilters } = window.wc.blocksCheckout;
```
However, if you're using `@woocommerce/dependency-extraction-webpack-plugin` for enhanced dependency management you can instead use ES module syntax:
```js
import { registerCheckoutFilters } from '@woocommerce/blocks-checkout';
```
See <https://www.npmjs.com/package/@woocommerce/dependency-extraction-webpack-plugin> for more information.
## Hooks (actions and filters)
| Document | Description |
| ----------------------------- | ------------------------------------------------------- |
| [Actions](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/actions.md) | Documentation covering action hooks on the server side. |
| [Filters](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/hooks/filters.md) | Documentation covering filter hooks on the server side. |
| [Migrated Hooks](/docs/cart-and-checkout-legacy-hooks/) | Documentation covering the migrated WooCommerce core hooks. |
## REST API
| Document | Description |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [Exposing your data in the Store API.](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md) | Explains how you can add additional data to Store API endpoints. |
| [Available endpoints to extend with ExtendSchema](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/available-endpoints-to-extend.md) | A list of all available endpoints to extend. |
| [Available Formatters](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-formatters.md) | Available `Formatters` to format data for use in the Store API. |
| [Updating the cart with the Store API](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-update-cart.md) | Update the server-side cart following an action from the front-end. |
## Checkout Payment Methods
| Document | Description |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| [Checkout Flow and Events](/docs/cart-and-checkout-checkout-flow-and-events/) | All about the checkout flow in the checkout block and the various emitted events that can be subscribed to. |
| [Payment Method Integration](/docs/cart-and-checkout-payment-method-integration-for-the-checkout-block/) | Information about implementing payment methods. |
| [Filtering Payment Methods](/docs/cart-and-checkout-filtering-payment-methods-in-the-checkout-block/) | Information about filtering the payment methods available in the Checkout Block. |
## Checkout Block
In addition to the reference material below, [please see the `block-checkout` package documentation](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/packages/checkout/README.md) which is used to extend checkout with Filters, Slot Fills, and Inner Blocks.
| Document | Description |
|--------------------------------------------------------------------------------------------------| ----------------------------------------------------------------------------------------------------------------- |
| [How the Checkout Block processes an order](/docs/cart-and-checkout-how-the-checkout-block-processes-an-order/) | The detailed inner workings of the Checkout Flow. |
| [IntegrationInterface](/docs/cart-and-checkout-handling-scripts-styles-and-data/) | The `IntegrationInterface` class and how to use it to register scripts, styles, and data with WooCommerce Blocks. |
| [Available Filters](/docs/category/cart-and-checkout-blocks/available-filters/) | All about the filters that you may use to change values of certain elements of WooCommerce Blocks. |
| [Slots and Fills](/docs/cart-and-checkout-slot-and-fill/) | Explains Slot Fills and how to use them to render your own components in Cart and Checkout. |
| [Available Slot Fills](/docs/cart-and-checkout-available-slots/) | Available Slots that you can use and their positions in Cart and Checkout. |
| [DOM Events](/docs/cart-and-checkout-dom-events/) | A list of DOM Events used by some blocks to communicate between them and with other parts of WooCommerce. |
| [Filter Registry](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/packages/checkout/filter-registry/README.md) | The filter registry allows callbacks to be registered to manipulate certain values. |
| [Additional Checkout Fields](/docs/cart-and-checkout-additional-checkout-fields/) | The filter registry allows callbacks to be registered to manipulate certain values. |

View File

@ -0,0 +1,5 @@
---
category_title: Product Collection Block
category_slug: product-collection
post_title: Product collection block
---

View File

@ -1,6 +1,12 @@
---
post_title: Registering custom collections in product collection block
menu_title: Registering custom collections
tags: how-to
---
# Register Product Collection
The `__experimentalRegisterProductCollection` function is part of the `@woocommerce/blocks-registry` package. This function allows 3PDs to register a new collection. This function accepts most of the arguments that are accepted by [Block Variation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/#defining-a-block-variation).
The `__experimentalRegisterProductCollection` function is part of the `@woocommerce/blocks-registry` package. This function allows third party developers to register a new collection. This function accepts most of the arguments that are accepted by [Block Variation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/#defining-a-block-variation).
> [!WARNING]
> It's experimental and may change in the future. Please use it with caution.
@ -34,8 +40,9 @@ A Collection is defined by an object that can contain the following fields:
- `title` (type `string`): The title of the collection, which will be displayed in various places including the block inserter and collection chooser.
- `description` (optional, type `string`): A human-readable description of the collection.
- `innerBlocks` (optional, type `Array[]`): An array of inner blocks that will be added to the collection. If not provided, the default inner blocks will be used.
- `isDefault`: ⚠️ It's set to `false` for all collections. 3PDs doesn't need to pass this argument.
- `isActive`: ⚠️ It will be managed by us. 3PDs doesn't need to pass this argument.
- `isDefault`: It's set to `false` for all collections. Third party developers don't need to pass this argument.
- `isActive`: It will be managed by us. Third party developers don't need to pass this argument.
- `usesReference` (optional, type `Array[]`): An array of strings specifying the required reference for the collection. Acceptable values are `product`, `archive`, `cart`, and `order`. When the required reference isn't available on Editor side but will be available in Frontend, we will show a preview label.
### Attributes
@ -90,7 +97,7 @@ The `preview` attribute is optional, and it is used to set the preview state of
- `attributes` (type `object`): The current attributes of the collection.
- `location` (type `object`): The location of the collection. Accepted values are `product`, `archive`, `cart`, `order`, `site`.
For more info, you may check PR #46369, in which the Preview feature was added
For more info, you may check [PR #46369](https://github.com/woocommerce/woocommerce/pull/46369), in which the Preview feature was added
## Examples
@ -239,5 +246,37 @@ This will create a collection with a heading, product image, and product price.
> [!TIP]
> You can learn more about inner blocks template in the [Inner Blocks](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/#template) documentation.
### Example 5: Collection with `usesReference` argument
When a collection requires a reference to work properly, you can specify it using the `usesReference` argument. In the example below, we are defining a collection that requires a `product` reference.
```tsx
__experimentalRegisterProductCollection({
name: "your-plugin-name/product-collection/my-custom-collection",
title: "My Custom Collection",
icon: "games",
description: "This is a custom collection.",
keywords: ["custom collection", "product collection"],
usesReference: ["product"],
});
```
This will create a collection that requires a `product` reference. If the required reference isn't available on the Editor side but will be available in the Frontend, we will show a preview label.
When a collection need one of the multiple references, you can specify it using the `usesReference` argument. In the example below, we are defining a collection that requires either a `product` or `order` reference.
```tsx
__experimentalRegisterProductCollection({
name: "your-plugin-name/product-collection/my-custom-collection",
title: "My Custom Collection",
icon: "games",
description: "This is a custom collection.",
keywords: ["custom collection", "product collection"],
usesReference: ["product", "order"],
});
```
---
> [!TIP]
> You can also take a look at how we are defining our core collections at `plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections` directory. Our core collections will also evolve over time.

View File

@ -0,0 +1,14 @@
# How-to Guides for the Product form
There are several ways to extend and modify the new product form. Below are links to different tutorials.
## Generic fields
One way to extend the new product form is by making use of generic fields. This allows you to easily modify the form using PHP only. We have a wide variety of generic fields that you can use, like a text, checkbox, pricing or a select field. You can find the full list [here](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/blocks/generic/README.md).
To see how you can make use of these fields you can follow the [generic fields tutorial](https://github.com/woocommerce/woocommerce/blob/trunk/docs/product-editor-development/how-to-guides/generic-fields-tutorial.md).
## Writing a custom field
It is also possible to write your own custom field and render those within the product form. This is helpful if the generic fields don't quite fit your use case.
To see an example of how to create a basic dropdown field in the product form you can follow [this tutorial](https://github.com/woocommerce/woocommerce/blob/trunk/docs/product-editor-development/how-to-guides/custom-field-tutorial.md).

View File

@ -0,0 +1,263 @@
# Extending the product form with custom fields
Aside from extending the product form using generic fields it is also possible to use custom fields. This does require knowledge of JavaScript and React.
If you are already familiar with writing blocks for the WordPress site editor this will feel very similar.
## Getting started
To get started we would recommend reading through the [fundamentals of block development docs](https://developer.wordpress.org/block-editor/getting-started/fundamentals/) in WordPress. This gives a good overview of working with blocks, the block structure, and the [JavaScript build process](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor/).
This tutorial will use vanilla JavaScript to render a new field in the product form for those that already have a plugin and may not have a JavaScript build process set up yet.
If you want to create a plugin from scratch with the necessary build tools, we recommend using the `@wordpress/create-block` script. We also have a specific template for the product form: [README](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/create-product-editor-block/README.md).
## Creating a custom field
### Adding and registering our custom field
Adding and registering our custom field is very similar as creating a brand new block.
Inside a new folder within your plugin, let's create a `block.json` file with the contents below. The only main difference between this `block.json` and a `block.json` for the site editor is that we will set `supports.inserter` to false, so it doesn't show up there. We will also be registering this slightly different.
```json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "tutorial/new-product-form-field",
"title": "Product form field",
"category": "woocommerce",
"description": "A sample field for the product form",
"keywords": [ "products" ],
"attributes": {},
"supports": {
"html": false,
"multiple": true,
// Setting inserter to false is important so that it doesn't show in the site editor.
"inserter": false
},
"textdomain": "woocommerce",
"editorScript": "file:./index.js"
}
```
In the same directory, create a `index.js` file, which we can keep simple by just outputting a hello world.
In this case the `edit` function is the part that will get rendered in the form. We are wrapping it with the `createElement` function to keep support for React.
```javascript
( function ( wp ) {
var el = wp.element.createElement;
wp.blocks.registerBlockType( 'tutorial/new-product-form-field', {
title: 'Product form field',
attributes: {},
edit: function () {
return el( 'p', {}, 'Hello World (from the editor).' );
},
} );
} )( window.wp );
```
In React:
```jsx
import { registerBlockType } from '@wordpress/blocks';
function Edit() {
return <p>Hello World (from the editor).</p>;
}
registerBlockType( 'tutorial/new-product-form-field', {
title: 'Product form field',
attributes: {},
edit: Edit,
} );
```
Lastly, in order to make this work the block registration needs to know about the JavaScript dependencies, we can do so by adding a `index.asset.php` file with the below contents:
```php
<?php return array('dependencies' => array('react', 'wc-product-editor', 'wp-blocks' ) );
```
Now that we have all the for the field we need to register it and add it to the template.
Registering can be done on `init` by calling `BlockRegistry::get_instance()->register_block_type_from_metadata` like so:
```php
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\BlockRegistry;
function example_custom_product_form_init() {
if ( isset( $_GET['page'] ) && $_GET['page'] === 'wc-admin' ) {
// This points to the directory that contains your block.json.
BlockRegistry::get_instance()->register_block_type_from_metadata( __DIR__ . '/js/sample-block' );
}
}
add_action( 'init', 'example_custom_product_form_init' );
```
We can add it to the product form by hooking into the `woocommerce_layout_template_after_instantiation` action ( see [block addition and removal](https://github.com/woocommerce/woocommerce/blob/trunk/docs/product-editor-development/block-template-lifecycle.md#block-addition-and-removal) ).
What we did was the following ( see [here](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md#usage) for more related functions ):
- Get a group by the `general` id, this is the General tab.
- Create a new section on the general tab called `Tutorial Section`
- Add our custom field to the `Tutorial Section`
```php
add_action(
'woocommerce_layout_template_after_instantiation',
function( $layout_template_id, $layout_template_area, $layout_template ) {
$general = $layout_template->get_group_by_id( 'general' );
if ( $general ) {
// Creating a new section, this is optional.
$tutorial_section = $general->add_section(
array(
'id' => 'tutorial-section',
'order' => 15,
'attributes' => array(
'title' => __( 'Tutorial Section', 'woocommerce' ),
'description' => __( 'Fields related to the tutorial', 'woocommerce' ),
),
)
);
$tutorial_section->add_block(
[
'id' => 'example-new-product-form-field',
'blockName' => 'tutorial/new-product-form-field',
'attributes' => [],
]
);
}
},
10,
3
);
```
### Turn field into a dropdown
We recommend using components from `@wordpress/components` as this will also keep the styling consistent. We will use the [ComboboxControl](https://wordpress.github.io/gutenberg/?path=/docs/components-comboboxcontrol--docs) core component in this field.
We can add it to our `edit` function pretty easily by making use of `wp.components`. We will also add a constant for the filter options.
**Note:** I also added the `blockProps` to the top element, we still recommend using this as some of these props are being used in the product form. When we add the block props we need to also let the form know it is an interactive element. We do this by adding at-least one attribute with the `__experimentalRole` set to `content`.
So lets add this to our `index.js` attributes:
```javascript
attributes: {
"message": {
"type": "string",
"__experimentalRole": "content",
"source": "text",
"selector": "div"
}
},
```
Dropdown options, these can live outside of the `edit` function:
```javascript
const DROPDOWN_OPTIONS = [
{
value: 'small',
label: 'Small',
},
{
value: 'normal',
label: 'Normal',
},
{
value: 'large',
label: 'Large',
},
];
```
The updated `edit` function:
```javascript
// edit function.
function ( { attributes } ) {
// useState is a React specific function.
const [ value, setValue ] = wp.element.useState();
const [ filteredOptions, setFilteredOptions ] = wp.element.useState( DROPDOWN_OPTIONS );
const blockProps = window.wc.blockTemplates.useWooBlockProps( attributes );
return el( 'div', { ...blockProps }, [
el( wp.components.ComboboxControl, {
label: "Example dropdown",
value: value,
onChange: setValue,
options: filteredOptions,
onFilterValueChange: function( inputValue ) {
setFilteredOptions(
DROPDOWN_OPTIONS.filter( ( option ) =>
option.label
.toLowerCase()
.startsWith( inputValue.toLowerCase() )
)
)
}
} )
] );
},
```
In React:
```jsx
import { createElement, useState } from '@wordpress/element';
import { ComboboxControl } from '@wordpress/components';
import { useWooBlockProps } from '@woocommerce/block-templates';
function Edit( { attributes } ) {
const [ value, setValue ] = useState();
const [ filteredOptions, setFilteredOptions ] =
useState( DROPDOWN_OPTIONS );
const blockProps = useWooBlockProps( attributes );
return (
<div { ...blockProps }>
<ComboboxControl
label="Example dropdown"
value={ value }
onChange={ setValue }
options={ filteredOptions }
onFilterValueChange={ ( inputValue ) =>
setFilteredOptions(
DROPDOWN_OPTIONS.filter( ( option ) =>
option.label
.toLowerCase()
.startsWith( inputValue.toLowerCase() )
)
)
}
/>
</div>
);
}
```
### Save field data to the product data
We can make use of the `__experimentalUseProductEntityProp` for saving the field input to the product.
The function does rely on `postType`, we can hardcode this to `product`, but the `postType` is also exposed through a context. We can do so by adding `"usesContext": [ "postType" ],` to the `block.json` and getting it from the `context` passed into the `edit` function props.
So the top part of the edit function will look like this, where we also replace the `value, setValue` `useState` line:
```javascript
// edit function.
function ( { attributes, context } ) {
const [ value, setValue ] = window.wc.productEditor.__experimentalUseProductEntityProp(
'meta_data.animal_type',
{
postType: context.postType,
fallbackValue: '',
}
);
// .... Rest of edit function
```
Now if you select small, medium, or large from the dropdown and save your product, the value should persist correctly.
Note, the above function supports the use of `meta_data` by dot notation, but you can also target other fields like `regular_price` or `summary`.

View File

@ -0,0 +1,58 @@
# Extending the product form with generic fields
We have large list of generic fields that a plugin can use to extend the new product form. You can find the full list [here](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/blocks/generic/README.md). Each field contains documentation for what attributes the field supports.
## Using a generic block
Using a generic block is pretty easy. We have created an template API that allows you to add new fields, the API refers to them as `blocks`. There are a couple actions that allow us to interact with these templates. There is the `woocommerce_layout_template_after_instantiation` that is triggered when a new template is registered. There are also other actions triggered when a specific field/block is added ( see [block addition and removal](https://github.com/woocommerce/woocommerce/blob/trunk/docs/product-editor-development/block-template-lifecycle.md#block-addition-and-removal) ).
Let's say we want to add something to the basic details section, we can do so by making use of the above mentioned hook:
This will add a number field called **Animal age** to each template that has a `basic-details` section.
```php
add_action(
'woocommerce_layout_template_after_instantiation',
function( $layout_template_id, $layout_template_area, $layout_template ) {
$basic_details = $layout_template->get_section_by_id( 'basic-details' );
if ( $basic_details ) {
$basic_details->add_block(
[
'id' => 'example-tutorial-animal-age',
// This orders the field, core fields are seperated by sums of 10.
'order' => 40,
'blockName' => 'woocommerce/product-number-field',
'attributes' => [
// Attributes specific for the product-number-field.
'label' => 'Animal age',
'property' => 'meta_data.animal_age',
'suffix' => 'Yrs',
'placeholder' => 'Age of animal',
'required' => true,
'min' => 1,
'max' => 20
],
]
);
}
},
10,
3
);
```
### Dynamically hiding or showing the generic field
It is also possible to dynamically hide or show your field if data on the product form changes.
We can do this by adding a `hideCondition` ( plural ). For example if we wanted to hide our field if the product price is higher than 20, we can do so by adding this expression:
```php
'hideConditions' => array(
array(
'expression' => 'editedProduct.regular_price >= 20',
),
),
```
The `hideConditions` also support targeting meta data by using dot notation. You can do so by writing an expression like this: `! editedProduct.meta_data.animal_type` that will hide a field if the `animal_type` meta data value doesn't exist.

View File

@ -37,6 +37,7 @@ Please note that this check is currently not being enforced: the product editor
## Related documentation
- [Examples on Template API usage](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md/)
- [How to guides](https://github.com/woocommerce/woocommerce/blob/trunk/docs/product-editor-development/how-to-guides/README.md)
- [Related hooks and Template API documentation](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/BlockTemplates/README.md)
- [Generic blocks documentation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/blocks/generic/README.md)
- [Validations and error handling](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/contexts/validation-context/README.md)

View File

@ -3,6 +3,7 @@
"title": "WooCommerce Monorepo",
"description": "Monorepo for the WooCommerce ecosystem",
"homepage": "https://woocommerce.com/",
"packageManager": "pnpm@9.1.3",
"engines": {
"node": "^20.11.1",
"pnpm": "^9.1.0"

View File

@ -10,9 +10,9 @@ features:
_\* TypeScript Definitions and Repositories are currently only supported for [Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products), and partially supported for [Orders](https://woocommerce.github.io/woocommerce-rest-api-docs/#orders)._
## Differences from @woocommerce/woocomerce-rest-api
## Differences from @woocommerce/woocommerce-rest-api
WooCommerce has two API clients in JavaScript for interacting with a WooCommerce installation's RESTful API. This package, and the [@woocommerce/woocomerce-rest-api](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) package.
WooCommerce has two API clients in JavaScript for interacting with a WooCommerce installation's RESTful API. This package, and the [@woocommerce/woocommerce-rest-api](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) package.
The main difference between them is the Repositories and the TypeScript definitions for the supported endpoints. When using Axios directly, as you can do with both libraries, you query the WooCommerce API in a raw object format, following the [API documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction) parameters. Comparatively, with the Repositories provided in this package, you have the parameters as properties of an object, which gives you the benefits of auto-complete and strict types, for instance.
@ -104,7 +104,7 @@ The following methods are available on all repositories if the corresponding met
- `read( objectId )` - Read a single object of the model type
- `update( objectId, {...properties} )` - Update a single object of the model type
#### Child Repositories
#### Child Repositories Use
In child model repositories, each method requires the `parentId` as the first parameter:

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Fix typo (README.md)

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Removed defaultProps from React functional components since they will be deprecated for React 19

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Fix image gallery state conflict with external consumer state

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update SelectTree and Tree controls to allow highlighting items without focus

View File

@ -9,17 +9,17 @@ import { uniqueId, noop } from 'lodash';
import PropTypes from 'prop-types';
const DateInput = ( {
disabled,
disabled = false,
value,
onChange,
dateFormat,
label,
describedBy,
error,
onFocus,
onBlur,
onKeyDown,
errorPosition,
onFocus = () => {},
onBlur = () => {},
onKeyDown = noop,
errorPosition = 'bottom center',
} ) => {
const classes = classnames( 'woocommerce-calendar__input', {
'is-empty': value.length === 0,
@ -73,12 +73,4 @@ DateInput.propTypes = {
onKeyDown: PropTypes.func,
};
DateInput.defaultProps = {
disabled: false,
onFocus: () => {},
onBlur: () => {},
errorPosition: 'bottom center',
onKeyDown: noop,
};
export default DateInput;

View File

@ -15,7 +15,12 @@ import { createElement } from '@wordpress/element';
* @param {string} props.visibleFormat
* @return {Object} -
*/
const Date = ( { date, machineFormat, screenReaderFormat, visibleFormat } ) => {
const Date = ( {
date,
machineFormat = 'Y-m-d H:i:s',
screenReaderFormat = 'F j, Y',
visibleFormat = 'Y-m-d',
} ) => {
return (
<time dateTime={ formatDate( machineFormat, date ) }>
<span aria-hidden="true">
@ -48,10 +53,4 @@ Date.propTypes = {
visibleFormat: PropTypes.string,
};
Date.defaultProps = {
machineFormat: 'Y-m-d H:i:s',
screenReaderFormat: 'F j, Y',
visibleFormat: 'Y-m-d',
};
export default Date;

View File

@ -77,6 +77,25 @@ const PrivateSelectedItems = < ItemType, >(
);
}
const focusSibling = ( event: React.KeyboardEvent< HTMLDivElement > ) => {
const selectedItem = ( event.target as HTMLElement ).closest(
'.woocommerce-experimental-select-control__selected-item'
);
const sibling =
event.key === 'ArrowLeft' || event.key === 'Backspace'
? selectedItem?.previousSibling
: selectedItem?.nextSibling;
if ( sibling ) {
(
( sibling as HTMLElement ).querySelector(
'.woocommerce-tag__remove'
) as HTMLElement
)?.focus();
return true;
}
return false;
};
return (
<div className={ classes }>
{ items.map( ( item, index ) => {
@ -102,24 +121,9 @@ const PrivateSelectedItems = < ItemType, >(
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight'
) {
const selectedItem = (
event.target as HTMLElement
).closest(
'.woocommerce-experimental-select-control__selected-item'
);
const sibling =
event.key === 'ArrowLeft'
? selectedItem?.previousSibling
: selectedItem?.nextSibling;
if ( sibling ) {
(
(
sibling as HTMLElement
).querySelector(
'.woocommerce-tag__remove'
) as HTMLElement
)?.focus();
} else if (
const focused = focusSibling( event );
if (
! focused &&
event.key === 'ArrowRight' &&
onSelectedItemsEnd
) {
@ -130,6 +134,9 @@ const PrivateSelectedItems = < ItemType, >(
event.key === 'ArrowDown'
) {
event.preventDefault(); // prevent unwanted scroll
} else if ( event.key === 'Backspace' ) {
onRemove( item );
focusSibling( event );
}
} }
onBlur={ onBlur }

View File

@ -10,6 +10,7 @@ import {
useLayoutEffect,
useState,
} from '@wordpress/element';
import { escapeRegExp } from 'lodash';
/**
* Internal dependencies
@ -26,6 +27,7 @@ type MenuProps = {
isLoading?: boolean;
position?: Popover.Position;
scrollIntoViewOnOpen?: boolean;
highlightedIndex?: number;
items: LinkedTree[];
treeRef?: React.ForwardedRef< HTMLOListElement >;
onClose?: () => void;
@ -44,6 +46,7 @@ export const SelectTreeMenu = ( {
onEscape,
shouldShowCreateButton,
onFirstItemLoop,
onExpand,
...props
}: MenuProps ) => {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
@ -66,7 +69,7 @@ export const SelectTreeMenu = ( {
// Scroll the selected item into view when the menu opens.
useEffect( () => {
if ( isOpen && scrollIntoViewOnOpen ) {
selectControlMenuRef.current?.scrollIntoView();
selectControlMenuRef.current?.scrollIntoView?.();
}
}, [ isOpen, scrollIntoViewOnOpen ] );
@ -74,9 +77,10 @@ export const SelectTreeMenu = ( {
if ( ! props.createValue || ! item.children?.length ) return false;
return item.children.some( ( child ) => {
if (
new RegExp( props.createValue || '', 'ig' ).test(
child.data.label
)
new RegExp(
escapeRegExp( props.createValue || '' ),
'ig'
).test( child.data.label )
) {
return true;
}
@ -130,6 +134,7 @@ export const SelectTreeMenu = ( {
ref={ ref }
items={ items }
onTreeBlur={ onClose }
onExpand={ onExpand }
shouldItemBeExpanded={
shouldItemBeExpanded
}

View File

@ -19,8 +19,17 @@ import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
*/
import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree';
import { Item, TreeControlProps } from '../experimental-tree-control/types';
import {
toggleNode,
createLinkedTree,
getVisibleNodeIndex as getVisibleNodeIndex,
getNodeDataByIndex,
} from '../experimental-tree-control/linked-tree-utils';
import {
Item,
LinkedTree,
TreeControlProps,
} from '../experimental-tree-control/types';
import { SelectedItems } from '../experimental-select-control/selected-items';
import { ComboBox } from '../experimental-select-control/combo-box';
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
@ -55,7 +64,17 @@ export const SelectTree = function SelectTree( {
onClear = () => {},
...props
}: SelectTreeProps ) {
const linkedTree = useLinkedTree( items );
const [ linkedTree, setLinkedTree ] = useState< LinkedTree[] >( [] );
const [ highlightedIndex, setHighlightedIndex ] = useState( -1 );
// whenever the items change, the linked tree needs to be recalculated
useEffect( () => {
setLinkedTree( createLinkedTree( items, props.createValue ) );
}, [ items.length ] );
// reset highlighted index when the input value changes
useEffect( () => setHighlightedIndex( -1 ), [ props.createValue ] );
const selectTreeInstanceId = useInstanceId(
SelectTree,
'woocommerce-experimental-select-tree-control__dropdown'
@ -111,6 +130,19 @@ export const SelectTree = function SelectTree( {
}
}, [ isFocused ] );
// Scroll the newly highlighted item into view
useEffect(
() =>
document
.querySelector(
'.experimental-woocommerce-tree-item--highlighted'
)
?.scrollIntoView?.( {
block: 'nearest',
} ),
[ highlightedIndex ]
);
let placeholder: string | undefined = '';
if ( Array.isArray( props.selected ) ) {
placeholder = props.selected.length === 0 ? props.placeholder : '';
@ -118,12 +150,30 @@ export const SelectTree = function SelectTree( {
placeholder = props.placeholder;
}
// reset highlighted index when the input value changes
useEffect( () => {
if (
highlightedIndex === items.length &&
! shouldShowCreateButton?.( props.createValue )
) {
setHighlightedIndex( items.length - 1 );
}
}, [ props.createValue ] );
const inputProps: React.InputHTMLAttributes< HTMLInputElement > = {
className: 'woocommerce-experimental-select-control__input',
id: `${ props.id }-input`,
'aria-autocomplete': 'list',
'aria-controls': `${ props.id }-menu`,
'aria-activedescendant':
highlightedIndex >= 0
? `woocommerce-experimental-tree-control__menu-item-${ highlightedIndex }`
: undefined,
'aria-controls': menuInstanceId,
'aria-owns': menuInstanceId,
role: 'combobox',
autoComplete: 'off',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
disabled,
onFocus: ( event ) => {
if ( props.multiple ) {
@ -159,40 +209,121 @@ export const SelectTree = function SelectTree( {
setIsOpen( true );
if ( event.key === 'ArrowDown' ) {
event.preventDefault();
// focus on the first element from the Popover
(
document.querySelector(
`#${ menuInstanceId } input, #${ menuInstanceId } button`
) as HTMLInputElement | HTMLButtonElement
)?.focus();
if (
// is advancing from the last menu item to the create button
highlightedIndex === items.length - 1 &&
shouldShowCreateButton?.( props.createValue )
) {
setHighlightedIndex( items.length );
} else {
const visibleNodeIndex = getVisibleNodeIndex(
linkedTree,
Math.min( highlightedIndex + 1, items.length ),
'down'
);
if ( visibleNodeIndex !== undefined ) {
setHighlightedIndex( visibleNodeIndex );
}
}
} else if ( event.key === 'ArrowUp' ) {
event.preventDefault();
if ( highlightedIndex > 0 ) {
const visibleNodeIndex = getVisibleNodeIndex(
linkedTree,
Math.max( highlightedIndex - 1, -1 ),
'up'
);
if ( visibleNodeIndex !== undefined ) {
setHighlightedIndex( visibleNodeIndex );
}
} else {
setHighlightedIndex( -1 );
}
} else if ( event.key === 'Tab' || event.key === 'Escape' ) {
setIsOpen( false );
recalculateInputValue();
} else if ( event.key === ',' || event.key === 'Enter' ) {
} else if ( event.key === 'Enter' || event.key === ',' ) {
event.preventDefault();
if (
highlightedIndex === items.length &&
shouldShowCreateButton
) {
props.onCreateNew?.();
} else if (
// is selecting an item
highlightedIndex !== -1
) {
const nodeData = getNodeDataByIndex(
linkedTree,
highlightedIndex
);
if ( ! nodeData ) {
return;
}
if ( props.multiple && Array.isArray( props.selected ) ) {
if (
! Boolean(
props.selected.find(
( i ) => i.label === nodeData.label
)
)
) {
if ( props.onSelect ) {
props.onSelect( nodeData );
}
} else if ( props.onRemove ) {
props.onRemove( nodeData );
}
setInputValue( '' );
} else {
onInputChange?.( nodeData.label );
props.onSelect?.( nodeData );
setIsOpen( false );
setIsFocused( false );
focusOnInput();
}
} else if ( inputValue ) {
// no highlighted item, but there is an input value, check if it matches any item
const item = items.find(
( i ) => i.label === escapeHTML( inputValue )
);
const isAlreadySelected =
Array.isArray( props.selected ) &&
Boolean(
const isAlreadySelected = Array.isArray( props.selected )
? Boolean(
props.selected.find(
( i ) => i.label === escapeHTML( inputValue )
( i ) =>
i.label === escapeHTML( inputValue )
)
);
if ( props.onSelect && item && ! isAlreadySelected ) {
props.onSelect( item );
)
: props.selected?.label === escapeHTML( inputValue );
if ( item && ! isAlreadySelected ) {
props.onSelect?.( item );
setInputValue( '' );
recalculateInputValue();
}
}
} else if (
( event.key === 'ArrowLeft' || event.key === 'Backspace' ) &&
event.key === 'Backspace' &&
// test if the cursor is at the beginning of the input with nothing selected
( event.target as HTMLInputElement ).selectionStart === 0 &&
( event.target as HTMLInputElement ).selectionEnd === 0 &&
selectedItemsFocusHandle.current
) {
selectedItemsFocusHandle.current();
} else if ( event.key === 'ArrowRight' ) {
setLinkedTree(
toggleNode( linkedTree, highlightedIndex, true )
);
} else if ( event.key === 'ArrowLeft' ) {
setLinkedTree(
toggleNode( linkedTree, highlightedIndex, false )
);
} else if ( event.key === 'Home' ) {
event.preventDefault();
setHighlightedIndex( 0 );
} else if ( event.key === 'End' ) {
event.preventDefault();
setHighlightedIndex( items.length - 1 );
}
},
onChange: ( event ) => {
@ -248,10 +379,6 @@ export const SelectTree = function SelectTree( {
comboBoxProps={ {
className:
'woocommerce-experimental-select-control__combo-box-wrapper',
role: 'combobox',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
'aria-owns': `${ props.id }-menu`,
} }
inputProps={ inputProps }
suffix={
@ -281,7 +408,11 @@ export const SelectTree = function SelectTree( {
<SelectedItems
isReadOnly={ isReadOnly }
ref={ selectedItemsFocusHandle }
items={ ( props.selected as Item[] ) || [] }
items={
! Array.isArray( props.selected )
? [ props.selected ]
: props.selected
}
getItemLabel={ ( item ) =>
item?.label || ''
}
@ -290,6 +421,7 @@ export const SelectTree = function SelectTree( {
}
onRemove={ ( item ) => {
if (
item &&
! Array.isArray( item ) &&
props.onRemove
) {
@ -346,6 +478,12 @@ export const SelectTree = function SelectTree( {
isEventOutside={ isEventOutside }
isLoading={ isLoading }
isOpen={ isOpen }
highlightedIndex={ highlightedIndex }
onExpand={ ( index, value ) => {
setLinkedTree(
toggleNode( linkedTree, index, value )
);
} }
items={ linkedTree }
shouldShowCreateButton={ shouldShowCreateButton }
onEscape={ () => {

View File

@ -1,4 +1,6 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import React, { createElement } from '@wordpress/element';
import { SelectTree } from '../select-tree';
import { Item } from '../../experimental-tree-control';
@ -26,6 +28,44 @@ const DEFAULT_PROPS = {
placeholder: 'Type here',
};
const TestComponent = ( { multiple }: { multiple?: boolean } ) => {
const [ typedValue, setTypedValue ] = useState( '' );
const [ selected, setSelected ] = useState< any >( [] );
return createElement( SelectTree, {
...DEFAULT_PROPS,
multiple,
shouldShowCreateButton: () => true,
onInputChange: ( value ) => {
setTypedValue( value || '' );
},
createValue: typedValue,
selected: Array.isArray( selected )
? selected.map( ( i ) => ( {
value: String( i.id ),
label: i.name,
} ) )
: {
value: String( selected.id ),
label: selected.name,
},
onSelect: ( item: Item | Item[] ) =>
item && Array.isArray( item )
? setSelected(
item.map( ( i ) => ( {
id: +i.value,
name: i.label,
parent: i.parent ? +i.parent : 0,
} ) )
)
: setSelected( {
id: +item.value,
name: item.label,
parent: item.parent ? +item.parent : 0,
} ),
} );
};
describe( 'SelectTree', () => {
beforeEach( () => {
jest.clearAllMocks();
@ -36,7 +76,7 @@ describe( 'SelectTree', () => {
<SelectTree { ...DEFAULT_PROPS } />
);
expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument();
queryByRole( 'textbox' )?.focus();
queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
} );
@ -47,20 +87,21 @@ describe( 'SelectTree', () => {
shouldShowCreateButton={ () => true }
/>
);
queryByRole( 'textbox' )?.focus();
queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create new' ) ).toBeInTheDocument();
} );
it( 'should not show create button when callback is false or no callback', () => {
const { queryByText, queryByRole } = render(
<SelectTree { ...DEFAULT_PROPS } />
);
queryByRole( 'textbox' )?.focus();
queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create new' ) ).not.toBeInTheDocument();
} );
it( 'should show a root item when focused and child when expand button is clicked', () => {
const { queryByText, queryByLabelText, queryByRole } =
render( <SelectTree { ...DEFAULT_PROPS } /> );
queryByRole( 'textbox' )?.focus();
const { queryByText, queryByLabelText, queryByRole } = render(
<SelectTree { ...DEFAULT_PROPS } />
);
queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument();
@ -72,7 +113,7 @@ describe( 'SelectTree', () => {
const { queryAllByRole, queryByRole } = render(
<SelectTree { ...DEFAULT_PROPS } selected={ [ mockItems[ 0 ] ] } />
);
queryByRole( 'textbox' )?.focus();
queryByRole( 'combobox' )?.focus();
expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute(
'aria-selected',
'true'
@ -87,7 +128,7 @@ describe( 'SelectTree', () => {
shouldShowCreateButton={ () => true }
/>
);
queryByRole( 'textbox' )?.focus();
queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument();
} );
it( 'should call onCreateNew when Create "<createValue>" button is clicked', () => {
@ -100,8 +141,34 @@ describe( 'SelectTree', () => {
onCreateNew={ mockFn }
/>
);
queryByRole( 'textbox' )?.focus();
queryByRole( 'combobox' )?.focus();
queryByText( 'Create "new item"' )?.click();
expect( mockFn ).toBeCalledTimes( 1 );
} );
it( 'correctly selects existing item in single mode with arrow keys', async () => {
const { findByRole } = render( <TestComponent /> );
const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
combobox.focus();
userEvent.keyboard( '{arrowdown}{enter}' );
expect( combobox.value ).toBe( 'Item 1' );
} );
it( 'correctly selects existing item in single mode by typing and pressing Enter', async () => {
const { findByRole } = render( <TestComponent /> );
const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
combobox.focus();
userEvent.keyboard( 'Item 1{enter}' );
userEvent.tab();
expect( combobox.value ).toBe( 'Item 1' );
} );
it( 'correctly selects existing item in multiple mode by typing and pressing Enter', async () => {
const { findByRole, getAllByText } = render(
<TestComponent multiple />
);
const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
combobox.focus();
userEvent.keyboard( 'Item 1' );
userEvent.keyboard( '{enter}' );
expect( combobox.value ).toBe( '' ); // input is cleared
expect( getAllByText( 'Item 1' )[ 0 ] ).toBeInTheDocument(); // item is selected (turns into a token)
} );
} );

View File

@ -1,50 +0,0 @@
/**
* External dependencies
*/
import { useMemo } from 'react';
/**
* Internal dependencies
*/
import { Item, LinkedTree } from '../types';
type MemoItems = {
[ value: Item[ 'value' ] ]: LinkedTree;
};
function findChildren(
items: Item[],
parent?: Item[ 'parent' ],
memo: MemoItems = {}
): LinkedTree[] {
const children: Item[] = [];
const others: Item[] = [];
items.forEach( ( item ) => {
if ( item.parent === parent ) {
children.push( item );
} else {
others.push( item );
}
memo[ item.value ] = {
parent: undefined,
data: item,
children: [],
};
} );
return children.map( ( child ) => {
const linkedTree = memo[ child.value ];
linkedTree.parent = child.parent ? memo[ child.parent ] : undefined;
linkedTree.children = findChildren( others, child.value, memo );
return linkedTree;
} );
}
export function useLinkedTree( items: Item[] ): LinkedTree[] {
const linkedTree = useMemo( () => {
return findChildren( items, undefined, {} );
}, [ items ] );
return linkedTree;
}

View File

@ -32,6 +32,9 @@ export function useTreeItem( {
onFirstItemLoop,
onTreeBlur,
onEscape,
highlightedIndex,
isHighlighted,
onExpand,
...props
}: TreeItemProps ) {
const nextLevel = level + 1;
@ -79,16 +82,19 @@ export function useTreeItem( {
getLabel,
treeItemProps: {
...props,
role: 'none',
id:
'woocommerce-experimental-tree-control__menu-item-' +
item.index,
role: 'option',
},
headingProps: {
role: 'treeitem',
'aria-selected': selection.checkedStatus !== 'unchecked',
'aria-expanded': item.children.length
? expander.isExpanded
? item.data.isExpanded
: undefined,
'aria-owns':
item.children.length && expander.isExpanded
item.children.length && item.data.isExpanded
? subTreeId
: undefined,
style: {

View File

@ -10,7 +10,7 @@ import { TreeProps } from '../types';
export function useTree( {
items,
level = 1,
role = 'tree',
role = 'listbox',
multiple,
selected,
getItemLabel,
@ -25,6 +25,8 @@ export function useTree( {
shouldShowCreateButton,
onFirstItemLoop,
onEscape,
highlightedIndex,
onExpand,
...props
}: TreeProps ) {
return {

View File

@ -0,0 +1,211 @@
/**
* Internal dependencies
*/
import { AugmentedItem, Item, LinkedTree } from './types';
type MemoItems = {
[ value: AugmentedItem[ 'value' ] ]: LinkedTree;
};
const shouldItemBeExpanded = (
item: LinkedTree,
createValue: string | undefined
): boolean => {
if ( ! createValue || ! item.children?.length ) return false;
return item.children.some( ( child ) => {
if ( new RegExp( createValue || '', 'ig' ).test( child.data.label ) ) {
return true;
}
return shouldItemBeExpanded( child, createValue );
} );
};
function findChildren(
items: AugmentedItem[],
memo: MemoItems = {},
parent?: AugmentedItem[ 'parent' ],
createValue?: string | undefined
): LinkedTree[] {
const children: AugmentedItem[] = [];
const others: AugmentedItem[] = [];
items.forEach( ( item ) => {
if ( item.parent === parent ) {
children.push( item );
} else {
others.push( item );
}
memo[ item.value ] = {
parent: undefined,
data: item,
children: [],
};
} );
return children.map( ( child ) => {
const linkedTree = memo[ child.value ];
linkedTree.parent = child.parent ? memo[ child.parent ] : undefined;
linkedTree.children = findChildren(
others,
memo,
child.value,
createValue
);
linkedTree.data.isExpanded =
linkedTree.children.length === 0
? true
: shouldItemBeExpanded( linkedTree, createValue );
return linkedTree;
} );
}
function populateIndexes(
linkedTree: LinkedTree[],
startCount = 0
): LinkedTree[] {
let count = startCount;
function populate( tree: LinkedTree[] ): number {
for ( const node of tree ) {
node.index = count;
count++;
if ( node.children ) {
count = populate( node.children );
}
}
return count;
}
populate( linkedTree );
return linkedTree;
}
// creates a linked tree from an array of Items
export function createLinkedTree(
items: Item[],
value: string | undefined
): LinkedTree[] {
const augmentedItems = items.map( ( i ) => ( {
...i,
isExpanded: false,
} ) );
return populateIndexes(
findChildren( augmentedItems, {}, undefined, value )
);
}
// Toggles the expanded state of a node in a linked tree
export function toggleNode(
tree: LinkedTree[],
number: number,
value: boolean
): LinkedTree[] {
return tree.map( ( node ) => {
return {
...node,
children: node.children
? toggleNode( node.children, number, value )
: node.children,
data: {
...node.data,
isExpanded:
node.index === number ? value : node.data.isExpanded,
},
...( node.parent
? {
parent: {
...node.parent,
data: {
...node.parent.data,
isExpanded:
node.parent.index === number
? value
: node.parent.data.isExpanded,
},
},
}
: {} ),
};
} );
}
// Gets the index of the next/previous visible node in the linked tree
export function getVisibleNodeIndex(
tree: LinkedTree[],
highlightedIndex: number,
direction: 'up' | 'down'
): number | undefined {
if ( direction === 'down' ) {
for ( const node of tree ) {
if ( ! node.parent || node.parent.data.isExpanded ) {
if (
node.index !== undefined &&
node.index >= highlightedIndex
) {
return node.index;
}
const visibleNodeIndex = getVisibleNodeIndex(
node.children,
highlightedIndex,
direction
);
if ( visibleNodeIndex !== undefined ) {
return visibleNodeIndex;
}
}
}
} else {
for ( let i = tree.length - 1; i >= 0; i-- ) {
const node = tree[ i ];
if ( ! node.parent || node.parent.data.isExpanded ) {
const visibleNodeIndex = getVisibleNodeIndex(
node.children,
highlightedIndex,
direction
);
if ( visibleNodeIndex !== undefined ) {
return visibleNodeIndex;
}
if (
node.index !== undefined &&
node.index <= highlightedIndex
) {
return node.index;
}
}
}
}
return undefined;
}
// Counts the number of nodes in a LinkedTree
export function countNumberOfNodes( linkedTree: LinkedTree[] ) {
let count = 0;
for ( const node of linkedTree ) {
count++;
if ( node.children ) {
count += countNumberOfNodes( node.children );
}
}
return count;
}
// Gets the data of a node by its index
export function getNodeDataByIndex(
linkedTree: LinkedTree[],
index: number
): Item | undefined {
for ( const node of linkedTree ) {
if ( node.index === index ) {
return node.data;
}
if ( node.children ) {
const child = getNodeDataByIndex( node.children, index );
if ( child ) {
return child;
}
}
}
return undefined;
}

View File

@ -6,7 +6,7 @@ import { createElement, forwardRef } from 'react';
/**
* Internal dependencies
*/
import { useLinkedTree } from './hooks/use-linked-tree';
import { createLinkedTree } from './linked-tree-utils';
import { Tree } from './tree';
import { TreeControlProps } from './types';
@ -14,7 +14,7 @@ export const TreeControl = forwardRef( function ForwardedTree(
{ items, ...props }: TreeControlProps,
ref: React.ForwardedRef< HTMLOListElement >
) {
const linkedTree = useLinkedTree( items );
const linkedTree = createLinkedTree( items, props.createValue );
return <Tree { ...props } ref={ ref } items={ linkedTree } />;
} );

View File

@ -6,6 +6,8 @@ $control-size: $gap-large;
&--highlighted {
> .experimental-woocommerce-tree-item__heading {
background-color: $gray-100;
outline: 1.5px solid var( --wp-admin-theme-color );
outline-offset: -1.5px;
}
}

View File

@ -24,21 +24,25 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
treeItemProps,
headingProps,
treeProps,
expander: { isExpanded, onToggleExpand },
selection,
highlighter: { isHighlighted },
getLabel,
} = useTreeItem( {
...props,
ref,
} );
function handleEscapePress(
event: React.KeyboardEvent< HTMLInputElement >
) {
function handleKeyDown( event: React.KeyboardEvent< HTMLElement > ) {
if ( event.key === 'Escape' && props.onEscape ) {
event.preventDefault();
props.onEscape();
} else if ( event.key === 'ArrowLeft' ) {
if ( item.index !== undefined ) {
props.onExpand?.( item.index, false );
}
} else if ( event.key === 'ArrowRight' ) {
if ( item.index !== undefined ) {
props.onExpand?.( item.index, true );
}
}
}
@ -50,7 +54,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
'experimental-woocommerce-tree-item',
{
'experimental-woocommerce-tree-item--highlighted':
isHighlighted,
props.isHighlighted,
}
) }
>
@ -67,7 +71,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
}
checked={ selection.checkedStatus === 'checked' }
onChange={ selection.onSelectChild }
onKeyDown={ handleEscapePress }
onKeyDown={ handleKeyDown }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore __nextHasNoMarginBottom is a valid prop
__nextHasNoMarginBottom={ true }
@ -80,7 +84,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
onChange={ ( event ) =>
selection.onSelectChild( event.target.checked )
}
onKeyDown={ handleEscapePress }
onKeyDown={ handleKeyDown }
/>
) }
@ -94,11 +98,21 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
{ Boolean( item.children?.length ) && (
<div className="experimental-woocommerce-tree-item__expander">
<Button
icon={ isExpanded ? chevronUp : chevronDown }
onClick={ onToggleExpand }
icon={
item.data.isExpanded ? chevronUp : chevronDown
}
onClick={ () => {
if ( item.index !== undefined ) {
props.onExpand?.(
item.index,
! item.data.isExpanded
);
}
} }
onKeyDown={ handleKeyDown }
className="experimental-woocommerce-tree-item__expander"
aria-label={
isExpanded
item.data.isExpanded
? __( 'Collapse', 'woocommerce' )
: __( 'Expand', 'woocommerce' )
}
@ -107,8 +121,13 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
) }
</div>
{ Boolean( item.children.length ) && isExpanded && (
<Tree { ...treeProps } onEscape={ props.onEscape } />
{ Boolean( item.children.length ) && item.data.isExpanded && (
<Tree
{ ...treeProps }
highlightedIndex={ props.highlightedIndex }
onExpand={ props.onExpand }
onEscape={ props.onEscape }
/>
) }
</li>
);

View File

@ -16,7 +16,8 @@
width: 100%;
cursor: default;
&:hover,
&:focus-within {
&:focus-within,
&--highlighted {
outline: 1.5px solid var(--wp-admin-theme-color);
outline-offset: -1.5px;
background-color: $gray-100;

View File

@ -14,6 +14,7 @@ import { useMergeRefs } from '@wordpress/compose';
import { useTree } from './hooks/use-tree';
import { TreeItem } from './tree-item';
import { TreeProps } from './types';
import { countNumberOfNodes } from './linked-tree-utils';
export const Tree = forwardRef( function ForwardedTree(
props: TreeProps,
@ -27,6 +28,8 @@ export const Tree = forwardRef( function ForwardedTree(
ref,
} );
const numberOfItems = countNumberOfNodes( items );
const isCreateButtonVisible =
props.shouldShowCreateButton &&
props.shouldShowCreateButton( props.createValue );
@ -45,7 +48,12 @@ export const Tree = forwardRef( function ForwardedTree(
{ items.map( ( child, index ) => (
<TreeItem
{ ...treeItemProps }
isExpanded={ props.isExpanded }
isHighlighted={
props.highlightedIndex === child.index
}
onExpand={ props.onExpand }
highlightedIndex={ props.highlightedIndex }
isExpanded={ child.data.isExpanded }
key={ child.data.value }
item={ child }
index={ index }
@ -53,7 +61,7 @@ export const Tree = forwardRef( function ForwardedTree(
onLastItemLoop={ () => {
(
rootListRef.current
?.closest( 'ol[role="tree"]' )
?.closest( 'ol[role="listbox"]' )
?.parentElement?.querySelector(
'.experimental-woocommerce-tree__button'
) as HTMLButtonElement
@ -67,7 +75,17 @@ export const Tree = forwardRef( function ForwardedTree(
) : null }
{ isCreateButtonVisible && (
<Button
className="experimental-woocommerce-tree__button"
id={
'woocommerce-experimental-tree-control__menu-item-' +
numberOfItems
}
className={ classNames(
'experimental-woocommerce-tree__button',
{
'experimental-woocommerce-tree__button--highlighted':
props.highlightedIndex === numberOfItems,
}
) }
onClick={ () => {
if ( props.onCreateNew ) {
props.onCreateNew();

View File

@ -4,10 +4,15 @@ export interface Item {
label: string;
}
export type AugmentedItem = Item & {
isExpanded: boolean;
};
export interface LinkedTree {
parent?: LinkedTree;
data: Item;
data: AugmentedItem;
children: LinkedTree[];
index?: number;
}
export type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate';
@ -18,6 +23,11 @@ type BaseTreeProps = {
* a list of items if it is true.
*/
selected?: Item | Item[];
onExpand?( index: number, value: boolean ): void;
highlightedIndex?: number;
/**
* Whether the tree items are single or multiple selected.
*/
@ -137,6 +147,7 @@ export type TreeItemProps = BaseTreeProps &
item: LinkedTree;
index: number;
isFocused?: boolean;
isHighlighted?: boolean;
getLabel?( item: LinkedTree ): JSX.Element;
shouldItemBeExpanded?( item: LinkedTree ): boolean;
onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void;

View File

@ -1,13 +1,14 @@
/**
* External dependencies
*/
import type { DragEventHandler } from 'react';
import {
Children,
createElement,
cloneElement,
useState,
useEffect,
useMemo,
} from '@wordpress/element';
import { DragEventHandler } from 'react';
import classnames from 'classnames';
import { MediaItem, MediaUpload } from '@wordpress/media-utils';
@ -15,10 +16,9 @@ import { MediaItem, MediaUpload } from '@wordpress/media-utils';
* Internal dependencies
*/
import { moveIndex } from '../sortable';
import { ImageGalleryToolbar } from './index';
import { ImageGalleryChild, MediaUploadComponentType } from './types';
import { removeItem, replaceItem } from './utils';
import { ImageGalleryWrapper } from './image-gallery-wrapper';
import { ImageGalleryToolbar } from './index';
import type { ImageGalleryChild, MediaUploadComponentType } from './types';
export type ImageGalleryProps = {
children: ImageGalleryChild | ImageGalleryChild[];
@ -57,26 +57,86 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
null
);
const [ isDragging, setIsDragging ] = useState< boolean >( false );
const [ orderedChildren, setOrderedChildren ] = useState<
ImageGalleryChild[]
>( [] );
const childElements = useMemo(
() => Children.toArray( children ) as JSX.Element[],
[ children ]
);
useEffect( () => {
if ( ! children ) {
function cloneChild( child: JSX.Element, childIndex: number ) {
const key = child.key || String( childIndex );
const isToolbarVisible = key === activeToolbarKey;
return cloneElement(
child,
{
key,
isDraggable: allowDragging && ! child.props.isCover,
className: classnames( {
'is-toolbar-visible': isToolbarVisible,
} ),
onClick() {
setActiveToolbarKey( isToolbarVisible ? null : key );
},
onBlur( event: React.FocusEvent< HTMLDivElement > ) {
if (
isDragging ||
event.currentTarget.contains( event.relatedTarget ) ||
( event.relatedTarget &&
( event.relatedTarget as Element ).closest(
'.media-modal, .components-modal__frame'
) ) ||
( event.relatedTarget &&
// Check if not a button within the toolbar is clicked, to prevent hiding the toolbar.
( event.relatedTarget as Element ).closest(
'.woocommerce-image-gallery__toolbar'
) ) ||
( event.relatedTarget &&
// Prevent toolbar from hiding if the dropdown is clicked within the toolbar.
( event.relatedTarget as Element ).closest(
'.woocommerce-image-gallery__toolbar-dropdown-popover'
) )
) {
return;
}
setOrderedChildren(
( Array.isArray( children ) ? children : [ children ] ).map(
( child, index ) =>
cloneElement( child, { key: child.key || String( index ) } )
setActiveToolbarKey( null );
},
},
isToolbarVisible && (
<ImageGalleryToolbar
value={ child.props.id }
allowDragging={ allowDragging }
childIndex={ childIndex }
lastChild={ childIndex === childElements.length - 1 }
moveItem={ ( fromIndex: number, toIndex: number ) => {
onOrderChange(
moveIndex< ImageGalleryChild >(
fromIndex,
toIndex,
childElements
)
);
}, [ children ] );
const updateOrderedChildren = ( items: ImageGalleryChild[] ) => {
setOrderedChildren( items );
onOrderChange( items );
};
} }
removeItem={ ( removeIndex: number ) => {
onRemove( {
removeIndex,
removedItem: childElements[ removeIndex ],
} );
} }
replaceItem={ (
replaceIndex: number,
media: { id: number } & MediaItem
) => {
onReplace( { replaceIndex, media } );
} }
setToolBarItem={ ( toolBarItem ) => {
onSelectAsCover( activeToolbarKey );
setActiveToolbarKey( toolBarItem );
} }
MediaUploadComponent={ MediaUploadComponent }
/>
)
);
}
return (
<div
@ -87,7 +147,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
>
<ImageGalleryWrapper
allowDragging={ allowDragging }
updateOrderedChildren={ updateOrderedChildren }
updateOrderedChildren={ onOrderChange }
onDragStart={ ( event ) => {
setIsDragging( true );
onDragStart( event );
@ -98,114 +158,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
} }
onDragOver={ onDragOver }
>
{ orderedChildren.map( ( child, childIndex ) => {
const isToolbarVisible = child.key === activeToolbarKey;
return cloneElement(
child,
{
isDraggable: allowDragging && ! child.props.isCover,
className: classnames( {
'is-toolbar-visible': isToolbarVisible,
} ),
onClick: () => {
setActiveToolbarKey(
isToolbarVisible
? null
: ( child.key as string )
);
},
onBlur: (
event: React.FocusEvent< HTMLDivElement >
) => {
if (
isDragging ||
event.currentTarget.contains(
event.relatedTarget
) ||
( event.relatedTarget &&
(
event.relatedTarget as Element
).closest(
'.media-modal, .components-modal__frame'
) ) ||
( event.relatedTarget &&
// Check if not a button within the toolbar is clicked, to prevent hiding the toolbar.
(
event.relatedTarget as Element
).closest(
'.woocommerce-image-gallery__toolbar'
) ) ||
( event.relatedTarget &&
// Prevent toolbar from hiding if the dropdown is clicked within the toolbar.
(
event.relatedTarget as Element
).closest(
'.woocommerce-image-gallery__toolbar-dropdown-popover'
) )
) {
return;
}
setActiveToolbarKey( null );
},
},
isToolbarVisible ? (
<ImageGalleryToolbar
value={ child.props.id }
allowDragging={ allowDragging }
childIndex={ childIndex }
lastChild={
childIndex === orderedChildren.length - 1
}
moveItem={ (
fromIndex: number,
toIndex: number
) => {
updateOrderedChildren(
moveIndex< ImageGalleryChild >(
fromIndex,
toIndex,
orderedChildren
)
);
} }
removeItem={ ( removeIndex: number ) => {
onRemove( {
removeIndex,
removedItem:
orderedChildren[ removeIndex ],
} );
updateOrderedChildren(
removeItem(
orderedChildren,
removeIndex
)
);
} }
replaceItem={ (
replaceIndex: number,
media: { id: number } & MediaItem
) => {
onReplace( { replaceIndex, media } );
setOrderedChildren(
replaceItem< {
src: string;
alt: string;
} >( orderedChildren, replaceIndex, {
src: media.url as string,
alt: media.alt as string,
} )
);
} }
setToolBarItem={ ( toolBarItem ) => {
onSelectAsCover( activeToolbarKey );
setActiveToolbarKey( toolBarItem );
} }
MediaUploadComponent={ MediaUploadComponent }
/>
) : null
);
} ) }
{ childElements.map( cloneChild ) }
</ImageGalleryWrapper>
</div>
);

View File

@ -22,7 +22,11 @@ import Menu from './menu';
* @param {string} props.label
* @return {Object} -
*/
const SummaryList = ( { children, isDropdownBreakpoint, label } ) => {
const SummaryList = ( {
children,
isDropdownBreakpoint,
label = __( 'Performance Indicators', 'woocommerce' ),
} ) => {
const items = children( {} );
// We default to "one" because we can't have empty children.
const itemCount = Children.count( items ) || 1;
@ -79,10 +83,6 @@ SummaryList.propTypes = {
label: PropTypes.string,
};
SummaryList.defaultProps = {
label: __( 'Performance Indicators', 'woocommerce' ),
};
export default withViewportMatch( {
isDropdownBreakpoint: '< large',
} )( SummaryList );

View File

@ -39,18 +39,18 @@ import { Text } from '../experimental';
const SummaryNumber = ( {
children,
delta,
href,
hrefType,
isOpen,
href = '',
hrefType = 'wc-admin',
isOpen = false,
label,
labelTooltipText,
onToggle,
prevLabel,
prevLabel = __( 'Previous period:', 'woocommerce' ),
prevValue,
reverseTrend,
selected,
reverseTrend = false,
selected = false,
value,
onLinkClickCallback,
onLinkClickCallback = noop,
} ) => {
const liClasses = classnames( 'woocommerce-summary__item-container', {
'is-dropdown-button': onToggle,
@ -238,14 +238,4 @@ SummaryNumber.propTypes = {
onLinkClickCallback: PropTypes.func,
};
SummaryNumber.defaultProps = {
href: '',
hrefType: 'wc-admin',
isOpen: false,
prevLabel: __( 'Previous period:', 'woocommerce' ),
reverseTrend: false,
selected: false,
onLinkClickCallback: noop,
};
export default SummaryNumber;

View File

@ -13,9 +13,16 @@ import { createElement } from '@wordpress/element';
import TimelineGroup from './timeline-group';
import { sortByDateUsing, groupItemsUsing } from './util';
const Timeline = ( props ) => {
const { className, items, groupBy, orderBy, dateFormat, clockFormat } =
props;
const Timeline = ( {
className = '',
items = [],
groupBy = 'day',
orderBy = 'desc',
/* translators: PHP date format string used to display dates, see php.net/date. */
dateFormat = __( 'F j, Y', 'woocommerce' ),
/* translators: PHP clock format string used to display times, see php.net/date. */
clockFormat = __( 'g:ia', 'woocommerce' ),
} ) => {
const timelineClassName = classnames( 'woocommerce-timeline', className );
// Early return in case no data was passed to the component.
@ -111,16 +118,5 @@ Timeline.propTypes = {
clockFormat: PropTypes.string,
};
Timeline.defaultProps = {
className: '',
items: [],
groupBy: 'day',
orderBy: 'desc',
/* translators: PHP date format string used to display dates, see php.net/date. */
dateFormat: __( 'F j, Y', 'woocommerce' ),
/* translators: PHP clock format string used to display times, see php.net/date. */
clockFormat: __( 'g:ia', 'woocommerce' ),
};
export { orderByOptions, groupByOptions } from './util';
export default Timeline;

View File

@ -11,8 +11,12 @@ import { createElement } from '@wordpress/element';
import TimelineItem from './timeline-item';
import { sortByDateUsing } from './util';
const TimelineGroup = ( props ) => {
const { group, className, orderBy, clockFormat } = props;
const TimelineGroup = ( {
group = { title: '', items: [] },
className = '',
orderBy = 'desc',
clockFormat,
} ) => {
const groupClassName = classnames(
'woocommerce-timeline-group',
className
@ -102,13 +106,4 @@ TimelineGroup.propTypes = {
clockFormat: PropTypes.string,
};
TimelineGroup.defaultProps = {
className: '',
group: {
title: '',
items: [],
},
orderBy: 'desc',
};
export default TimelineGroup;

View File

@ -6,9 +6,7 @@ import { format } from '@wordpress/date';
import PropTypes from 'prop-types';
import { createElement } from '@wordpress/element';
const TimelineItem = ( props ) => {
const { item, className, clockFormat } = props;
const TimelineItem = ( { item = {}, className = '', clockFormat } ) => {
const itemClassName = classnames( 'woocommerce-timeline-item', className );
const itemTimeString = format( clockFormat, item.date );
@ -74,9 +72,4 @@ TimelineItem.propTypes = {
} ).isRequired,
};
TimelineItem.defaultProps = {
className: '',
item: {},
};
export default TimelineItem;

View File

@ -17,7 +17,7 @@ import Tag from '../tag';
* @param {Array} props.items
* @return {Object} -
*/
const ViewMoreList = ( { items } ) => {
const ViewMoreList = ( { items = [] } ) => {
return (
<Tag
className="woocommerce-view-more-list"
@ -49,8 +49,4 @@ ViewMoreList.propTypes = {
items: PropTypes.arrayOf( PropTypes.node ),
};
ViewMoreList.defaultProps = {
items: [],
};
export default ViewMoreList;

View File

@ -1,6 +1,6 @@
# {{title}}
A WooCommmerce Extension inspired by [Create Woo Extension](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/create-woo-extension/README.md).
A WooCommerce Extension inspired by [Create Woo Extension](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/create-woo-extension/README.md).
## Getting Started

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Fixed typo WooCommmerce

View File

@ -18,6 +18,7 @@ module.exports = [
'@woocommerce/number',
'@woocommerce/product-editor',
'@woocommerce/tracks',
'@woocommerce/remote-logging',
// wc-blocks packages
'@woocommerce/blocks-checkout',
'@woocommerce/blocks-components',

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add JS remote logging package

View File

@ -1,6 +1,6 @@
# WooCommerce End to End Test Utilities
This package contains utilities to simplify writing e2e tests specific to WooCommmerce.
This package contains utilities to simplify writing e2e tests specific to WooCommerce.
## Installation

View File

@ -263,23 +263,23 @@ const shopper = {
await quantityInput.type( quantityValue.toString() );
},
searchForProduct: async ( prouductName ) => {
searchForProduct: async ( productName ) => {
const searchFieldSelector = 'input.wp-block-search__input';
await page.waitForSelector( searchFieldSelector, { timeout: 100000 } );
await expect( page ).toFill( searchFieldSelector, prouductName );
await expect( page ).toFill( searchFieldSelector, productName );
await expect( page ).toClick( '.wp-block-search__button' );
// Single search results may go directly to product page
if ( await page.waitForSelector( 'h2.entry-title' ) ) {
await expect( page ).toMatchElement( 'h2.entry-title', {
text: prouductName,
text: productName,
} );
await expect( page ).toClick( 'h2.entry-title > a', {
text: prouductName,
text: productName,
} );
}
await page.waitForSelector( 'h1.entry-title' );
await expect( page.title() ).resolves.toMatch( prouductName );
await expect( page ).toMatchElement( 'h1.entry-title', prouductName );
await expect( page.title() ).resolves.toMatch( productName );
await expect( page ).toMatchElement( 'h1.entry-title', productName );
},
/*

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Add bumpStat to woocommerce-tracks mock

1059
packages/js/internal-js-tests/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,4 +3,5 @@
module.exports = {
recordEvent: jest.fn(),
recordPageView: jest.fn(),
bumpStat: jest.fn(),
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove WooCommerce Navigation client side feature and deprecate PHP classes.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Remove deprecated Navigation SlotFill

View File

@ -1,17 +1,11 @@
/**
* External dependencies
*/
import {
createElement,
useState,
useEffect,
useLayoutEffect,
} from '@wordpress/element';
import { useState, useEffect, useLayoutEffect } from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url';
import { parse } from 'qs';
import { pick } from 'lodash';
import { applyFilters } from '@wordpress/hooks';
import { Slot, Fill } from '@wordpress/components';
import { getAdminLink } from '@woocommerce/settings';
/**
@ -343,19 +337,3 @@ export const navigateTo = ( { url } ) => {
window.location.href = String( parsedUrl );
};
/**
* A Fill for extensions to add client facing custom Navigation Items.
*
* @slotFill WooNavigationItem
* @scope woocommerce-navigation
* @param {Object} props React props.
* @param {Array} props.children Node children.
* @param {string} props.item Navigation item slug.
*/
export const WooNavigationItem = ( { children, item } ) => {
return <Fill name={ 'woocommerce_navigation_' + item }>{ children }</Fill>;
};
WooNavigationItem.Slot = ( { name } ) => (
<Slot name={ 'woocommerce_navigation_' + name } />
);

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
[E2E tests]: Add aria-expanded and rename ToolbarItem label #50232

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Use the validatorId to get access to the field instead of the context #50035

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Rename and move errorHandler method #50277

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix clicking outside of the attribute modal triggers an error

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix the image gallery removal conflict now that the image gallery is stateless

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Always count variations without price #50129

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