Merge branch 'trunk' into add/wooexpress-rin-rule

This commit is contained in:
Panos (Panagiotis) Synetos 2023-11-02 13:10:57 +02:00
commit 33d37275b3
No known key found for this signature in database
GPG Key ID: 0404F1D7F00137F9
1091 changed files with 37685 additions and 11137 deletions

View File

@ -3,6 +3,9 @@ description: Handles the installation, building, and caching of the projects wit
permissions: {}
inputs:
install:
description: Indicates whether or not the action should install any projects.
default: 'true'
install-filters:
description: The PNPM filter used to decide what projects to install. Supports multiline strings for multiple filters.
default: ''
@ -54,6 +57,9 @@ runs:
- name: Install Node and PHP Dependencies
shell: bash
if: ${{ inputs.install == 'true' }}
env:
PUPPETEER_SKIP_DOWNLOAD: 'true'
run: |
pnpm -w install turbo
pnpm install ${{ steps.parse-input.outputs.INSTALL_FILTERS }}
@ -70,6 +76,7 @@ runs:
fi
- name: Cache Build Output
if: ${{ inputs.build == 'true' }}
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8
with:
path: .turbo
@ -79,6 +86,6 @@ runs:
${{ runner.os }}-build-output
- name: Build
if: ${{ inputs.build == 'true' }}
if: ${{ inputs.install == 'true' && inputs.build == 'true' }}
shell: bash
run: pnpm -w exec turbo run turbo:build --cache-dir=".turbo" ${{ steps.parse-input.outputs.BUILD_FILTERS }}

View File

@ -1,6 +1,5 @@
name: Run API tests
description: Runs the WooCommerce Core API tests and generates Allure report.
permissions: {}
inputs:
report-name:
@ -8,6 +7,9 @@ inputs:
required: true
tests:
description: Specific tests to run, separated by single whitespace. See https://playwright.dev/docs/test-cli
playwright-config:
description: Playwright config file to be used
default: playwright.config.js
outputs:
result:
@ -17,13 +19,18 @@ outputs:
runs:
using: composite
steps:
- name: Download and install Chromium browser.
working-directory: plugins/woocommerce
shell: bash
run: pnpm exec playwright install chromium
- name: Run API tests.
id: run-api-tests
working-directory: plugins/woocommerce
shell: bash
run: |
pnpm exec playwright test \
--config=tests/api-core-tests/playwright.config.js \
--config=tests/api-core-tests/${{ inputs.playwright-config }} \
${{ inputs.tests }}
- name: Generate Test report.

View File

@ -67,11 +67,11 @@
'plugin: woo-ai':
- plugins/woo-ai/**/*
'focus: performance tests [team:Solaris]':
'focus: performance tests':
- plugins/woocommerce/tests/performance/**/*
'focus: api tests [team:Solaris]':
'focus: api tests':
- plugins/woocommerce/tests/api-core-tests/**/*
'focus: e2e tests [team:Solaris]':
'focus: e2e tests':
- plugins/woocommerce/tests/e2e-pw/**/*

View File

@ -294,6 +294,15 @@ jobs:
core.setOutput( 'cherry-pick-pr', pr.data.html_url )
// label PR
const label = await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.data.number,
labels: ["metric: code freeze exception"],
});
- name: Checkout trunk branch
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
run: git checkout trunk

View File

@ -1,90 +1,98 @@
name: Run CI
on:
push:
branches:
- trunk
- 'release/**'
workflow_dispatch:
defaults:
run:
shell: bash
name: 'CI'
on:
pull_request:
push:
branches:
- 'trunk'
- 'release/*'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
jobs:
test:
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
continue-on-error: ${{ matrix.wp == 'nightly' }}
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0']
wp: ['latest']
unittests: ['shard1', 'shard2']
include:
- wp: nightly
php: '7.4'
unittests: 'shard1'
- wp: nightly
php: '7.4'
unittests: 'shard2'
- wp: '6.1'
php: 7.4
unittests: 'shard1'
- wp: '6.1'
php: 7.4
unittests: 'shard2'
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build-filters: woocommerce
- name: Tool versions
run: |
php --version
composer --version
- name: Build Admin feature config
working-directory: plugins/woocommerce
run: pnpm run build:feature-config
- id: parseMatrix
name: Parse Matrix Variables
uses: actions/github-script@v6
with:
script: |
const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' );
parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => {
core.setOutput( 'wpVersion', version );
} );
- name: Prepare Testing Environment
env:
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
WP_ENV_PHP_VERSION: ${{ matrix.php }}
run: pnpm --filter=woocommerce env:test
- name: Run Tests
env:
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
WP_ENV_PHP_VERSION: ${{ matrix.php }}
run: pnpm --filter=woocommerce test:unit:env --testsuite ${{ matrix.unittests }}
project-matrix:
# Since this is a monorepo, not every pull request or change is going to impact every project.
# Instead of running CI tasks on all projects indiscriminately, we use a script to detect
# which projects have changed and what kind of change occurred. This lets us build a
# matrix that we can use to run CI tasks only on the projects that need them.
name: 'Build Project Matrix'
runs-on: 'ubuntu-20.04'
outputs:
matrix: ${{ steps.project-matrix.outputs.matrix }}
steps:
- uses: 'actions/checkout@v3'
name: 'Checkout'
with:
fetch-depth: 0
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
with:
install: false
- uses: actions/github-script@v6
id: 'project-matrix'
name: 'Build Matrix'
with:
script: |
let baseRef = ${{ toJson( github.base_ref ) }};
if ( baseRef ) {
baseRef = 'origin/' + baseRef;
}
const buildCIMatrix = require( './.github/workflows/scripts/build-ci-matrix' );
core.setOutput( 'matrix', JSON.stringify( await buildCIMatrix( baseRef ) ) );
project-task-matrix:
# This is the actual CI job that will be ran against every project with applicable changes.
# Note that we only run the tasks that have commands set. Our script will set them if
# they are needed and so all the workflow needs to do is run them.
name: '${{ matrix.projectName }} - ${{ matrix.taskName }}' # Note: GitHub doesn't process expressions for skipped jobs so when there's no matrix the name will literally be this.
runs-on: 'ubuntu-20.04'
needs: 'project-matrix'
if: ${{ needs.project-matrix.outputs.matrix != '[]' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON( needs.project-matrix.outputs.matrix ) }}
steps:
- uses: 'actions/checkout@v3'
name: 'Checkout'
with:
fetch-depth: 0
- uses: './.github/actions/setup-woocommerce-monorepo'
id: 'setup-monorepo'
name: 'Setup Monorepo'
with:
# install-filters: '${{ matrix.projectName }}...'
build-filters: '${{ matrix.projectName }}'
- name: 'Lint'
if: ${{ !cancelled() && matrix.lintCommand && steps.setup-monorepo.conclusion == 'success' }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.lintCommand }}'
- name: 'Prepare Test Environment'
id: 'prepare-test-environment'
if: ${{ !cancelled() && matrix.testEnvCommand && steps.setup-monorepo.conclusion == 'success' }}
env: ${{ matrix.testEnvVars }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnvCommand }}'
- name: 'Test - JS'
if: ${{ !cancelled() && matrix.jsTestCommand && steps.setup-monorepo.conclusion == 'success' && ( ! matrix.testEnvCommand || steps.prepare-test-environment.conclusion == 'success' ) }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.jsTestCommand }}'
- name: 'Test - PHP'
if: ${{ !cancelled() && matrix.phpTestCommand && steps.setup-monorepo.conclusion == 'success' && ( ! matrix.testEnvCommand || steps.prepare-test-environment.conclusion == 'success' ) }}
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.phpTestCommand }}'
project-task-matrix-evaluation:
# In order to add a required status check we need a consistent job that we can grab onto.
# Since we are dynamically generating a project matrix, however, we can't rely on
# on any specific job being present. We can get around this limitation by using
# a job that runs after all the others and either passes or fails based on the
# results of the other jobs in the workflow.
name: 'Evaluate Project Matrix'
runs-on: 'ubuntu-20.04'
needs: 'project-task-matrix'
if: ${{ always() }}
steps:
- name: 'Check Matrix Success'
run: |
result="${{ needs.project-task-matrix.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
echo "The matrix has completed successfully."
exit 0
else
echo "One or more jobs in the matrix has failed."
exit 1
fi

View File

@ -1,4 +1,4 @@
name: Run tests against PR in an environment with COT enabled
name: Run tests against PR in an environment with HPOS disabled
on:
pull_request:
workflow_dispatch:
@ -10,8 +10,8 @@ concurrency:
permissions: {}
jobs:
cot-e2e-tests-run:
name: Runs E2E tests with COT enabled.
non-hpos-e2e-tests-run:
name: Runs E2E tests with HPOS disabled.
runs-on: ubuntu-20.04
permissions:
contents: read
@ -24,11 +24,9 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
- name: Load docker images and start containers with COT enabled.
- name: Load docker images and start containers
working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 1
run: pnpm env:test:cot --filter=woocommerce
run: pnpm env:test --filter=woocommerce
- name: Download and install Chromium browser.
working-directory: plugins/woocommerce
@ -39,6 +37,7 @@ jobs:
id: run_playwright_e2e_tests
env:
USE_WP_ENV: 1
ENABLE_HPOS: 0
working-directory: plugins/woocommerce
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
@ -66,8 +65,8 @@ jobs:
if-no-files-found: ignore
retention-days: 5
cot-api-tests-run:
name: Runs API tests with COT enabled.
non-hpos-api-tests-run:
name: Runs API tests with HPOS disabled.
runs-on: ubuntu-20.04
permissions:
contents: read
@ -80,11 +79,9 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
- name: Load docker images and start containers with COT enabled.
- name: Load docker images and start containers
working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 1
run: pnpm env:test:cot --filter=woocommerce
run: pnpm env:test --filter=woocommerce
- name: Run Playwright API tests.
id: run_playwright_api_tests
@ -93,6 +90,7 @@ jobs:
BASE_URL: http://localhost:8086
USER_KEY: admin
USER_SECRET: password
ENABLE_HPOS: 0
run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js
- name: Generate Playwright API Test report.
@ -117,4 +115,4 @@ jobs:
${{ env.ALLURE_RESULTS_DIR }}
${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore
retention-days: 5
retention-days: 5

View File

@ -31,7 +31,6 @@ jobs:
- name: Load docker images and start containers.
working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 0
WP_ENV_PHP_VERSION: 7.4
run: pnpm run env:test
@ -265,4 +264,4 @@ jobs:
-f pr_number=$PR_NUMBER \
-f commit_sha=$COMMIT_SHA \
-f s3_root=public \
--repo woocommerce/woocommerce-test-reports
--repo woocommerce/woocommerce-test-reports

View File

@ -1,51 +0,0 @@
name: Run code sniff on PR
on:
pull_request:
paths-ignore:
- '**/changelog/**'
defaults:
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: Code sniff (PHP 7.4, WP Latest)
timeout-minutes: 15
runs-on: ubuntu-20.04
permissions:
contents: read
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Changed Files
id: changed-files
uses: tj-actions/changed-files@v39
with:
path: plugins/woocommerce
files: "**/*.php"
- name: Setup WooCommerce Monorepo
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Tool versions
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: plugins/woocommerce
run: |
php --version
composer --version
phpcs-changed --version
- name: Run PHPCS
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: plugins/woocommerce
run: phpcs-changed -s --git --git-base ${{ github.event.pull_request.base.sha }} ${{ steps.changed-files.outputs.all_changed_files }}

View File

@ -1,54 +0,0 @@
name: Lint packages
on:
pull_request:
paths-ignore:
- '**/changelog/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
lint-test-js:
name: Lint
runs-on: ubuntu-20.04
permissions:
contents: read
# This is required to allow the action to annotate the PR with the linting results.
checks: write
pull-requests: read
steps:
- uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
with:
version: '8.6.7'
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
with:
node-version-file: .nvmrc
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Install prerequisites
run: |
# ignore scripts is faster, and postinstall should not be needed for lint.
pnpm install --ignore-scripts
- name: Lint JS and CSS
run: pnpm run -r --filter='release-posts' --filter='woocommerce/client/admin...' --filter='@woocommerce/monorepo-utils' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' --color lint
- name: Collect and Combine Eslint Reports
if: ${{ github.event.pull_request.head.repo.fork != true && always() }}
run: node ./.github/workflows/scripts/collect-eslint-reports.js
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@a1bf7cb320a18aa53cb848a267ce9b7417221526
if: ${{ github.event.pull_request.head.repo.fork != true && always() }}
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
report-json: 'combined_eslint_report.json'

View File

@ -1,40 +0,0 @@
name: Run tests for JS packages and woocommerce-admin/client
on:
pull_request:
paths-ignore:
- '**/changelog/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
test-js:
name: Run JS Tests
runs-on: ubuntu-20.04
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
with:
version: '8.6.7'
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
with:
node-version-file: .nvmrc
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Install prerequisites
run: |
# ignore scripts is faster, and postinstall should not be needed for tests.
pnpm install --ignore-scripts
- name: Test
run: pnpm run test --filter='woocommerce/client/admin...' --filter='@woocommerce/monorepo-utils' --filter='!@woocommerce/e2e*' --filter='@woocommerce/monorepo-utils' --filter='!@woocommerce/api' --color

View File

@ -1,24 +0,0 @@
name: Run unit for other PHP plugins
on:
pull_request:
paths-ignore:
- '**/changelog/**'
permissions: {}
jobs:
test:
runs-on: ubuntu-20.04
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
php-version: '8.0'
- name: Run WooCommerce Docs Tests
run: pnpm test:unit
working-directory: ./plugins/woocommerce-docs

View File

@ -1,89 +0,0 @@
name: Run unit tests on PR
on:
pull_request:
paths-ignore:
- '**/changelog/**'
defaults:
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
test:
if: ${{ github.event.pull_request.user.login != 'github-actions[bot]' }}
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} - ${{ matrix.unittests }} ${{ matrix.hpos && 'HPOS' || '' }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
continue-on-error: ${{ matrix.wp == 'nightly' }}
env:
HPOS: ${{ matrix.hpos }}
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0']
wp: ['latest']
unittests: ['shard1', 'shard2']
include:
- wp: nightly
php: '7.4'
unittests: 'shard1'
- wp: nightly
php: '7.4'
unittests: 'shard2'
- wp: '6.1'
php: 7.4
unittests: 'shard1'
- wp: '6.1'
php: 7.4
unittests: 'shard2'
- wp: 'latest'
php: '7.4'
hpos: true
unittests: 'shard1'
- wp: 'latest'
php: '7.4'
hpos: true
unittests: 'shard2'
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
php-version: ${{ matrix.php }}
- id: parseMatrix
name: Parse Matrix Variables
uses: actions/github-script@v6
with:
script: |
const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' );
parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => {
core.setOutput( 'wpVersion', version );
} );
- name: Prepare Testing Environment
env:
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
WP_ENV_PHP_VERSION: ${{ matrix.php }}
run: pnpm --filter=woocommerce env:test
- name: Run Tests
env:
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
WP_ENV_PHP_VERSION: ${{ matrix.php }}
run: pnpm --filter=woocommerce test:unit:env --testsuite ${{ matrix.unittests }}

View File

@ -1,24 +0,0 @@
name: Prime caches against trunk
on:
push:
branches:
- trunk
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
prime:
name: Prime cache
runs-on: ubuntu-20.04
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo

View File

@ -0,0 +1,973 @@
/**
* External dependencies.
*/
const child_process = require( 'child_process' );
const fs = require( 'fs' );
const https = require( 'http' );
/**
* Uses the WordPress API to get the downlod URL to the latest version of an X.X version line. This
* also accepts "latest-X" to get an offset from the latest version of WordPress.
*
* @param {string} wpVersion The version of WordPress to look for.
* @return {Promise.<string>} The precise WP version download URL.
*/
async function getPreciseWPVersionURL( wpVersion ) {
return new Promise( ( resolve, reject ) => {
// We're going to use the WordPress.org API to get information about available versions of WordPress.
const request = https.get(
'http://api.wordpress.org/core/stable-check/1.0/',
( response ) => {
// Listen for the response data.
let responseData = '';
response.on( 'data', ( chunk ) => {
responseData += chunk;
} );
// Once we have the entire response we can process it.
response.on( 'end', () =>
resolve( JSON.parse( responseData ) )
);
}
);
request.on( 'error', ( error ) => {
reject( error );
} );
} ).then( ( allVersions ) => {
// Note: allVersions is an object where the keys are the version and the value is information about the version's status.
// If we're requesting a "latest" offset then we need to figure out what version line we're offsetting from.
const latestSubMatch = wpVersion.match( /^latest(?:-([0-9]+))?$/i );
if ( latestSubMatch ) {
for ( const version in allVersions ) {
if ( allVersions[ version ] !== 'latest' ) {
continue;
}
// We don't care about the patch version because we will
// the latest version from the version line below.
const versionParts = version.match( /^([0-9]+)\.([0-9]+)/ );
// We're going to subtract the offset to figure out the right version.
let offset = parseInt( latestSubMatch[ 1 ] ?? 0, 10 );
let majorVersion = parseInt( versionParts[ 1 ], 10 );
let minorVersion = parseInt( versionParts[ 2 ], 10 );
while ( offset > 0 ) {
minorVersion--;
if ( minorVersion < 0 ) {
majorVersion--;
minorVersion = 9;
}
offset--;
}
// Set the version that we found in the offset.
wpVersion = majorVersion + '.' + minorVersion;
}
}
// Scan through all of the versions to find the latest version in the version line.
let latestVersion = null;
let latestPatch = -1;
for ( const v in allVersions ) {
// Parse the version so we can make sure we're looking for the latest.
const matches = v.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ );
// We only care about the correct minor version.
const minor = `${ matches[ 1 ] }.${ matches[ 2 ] }`;
if ( minor !== wpVersion ) {
continue;
}
// Track the latest version in the line.
const patch =
matches[ 3 ] === undefined ? 0 : parseInt( matches[ 3 ], 10 );
if ( patch > latestPatch ) {
latestPatch = patch;
latestVersion = v;
}
}
if ( ! latestVersion ) {
throw new Error(
`Unable to find latest version for version line ${ wpVersion }.`
);
}
return `https://wordpress.org/wordpress-${ latestVersion }.zip`;
} );
}
/**
* Parses a display-friendly WordPress version and returns a link to download the given version.
*
* @param {string} wpVersion A display-friendly WordPress version. Supports ("master", "trunk", "nightly", "latest", "latest-X", "X.X" for version lines, and "X.X.X" for specific versions)
* @return {Promise.<string>} A link to download the given version of WordPress.
*/
async function parseWPVersion( wpVersion ) {
// Allow for download URLs in place of a version.
if ( wpVersion.match( /[a-z]+:\/\//i ) ) {
return wpVersion;
}
// Start with versions we can infer immediately.
switch ( wpVersion ) {
case 'master':
case 'trunk': {
return 'WordPress/WordPress#master';
}
case 'nightly': {
return 'https://wordpress.org/nightly-builds/wordpress-latest.zip';
}
case 'latest': {
return 'https://wordpress.org/latest.zip';
}
}
// We can also infer X.X.X versions immediately.
const parsedVersion = wpVersion.match( /^([0-9]+)\.([0-9]+)\.([0-9]+)$/ );
if ( parsedVersion ) {
// Note that X.X.0 versions use a X.X download URL.
let urlVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`;
if ( parsedVersion[ 3 ] !== '0' ) {
urlVersion += `.${ parsedVersion[ 3 ] }`;
}
return `https://wordpress.org/wordpress-${ urlVersion }.zip`;
}
// Since we haven't found a URL yet we're going to use the WordPress.org API to try and infer one.
return getPreciseWPVersionURL( wpVersion );
}
/**
* Given a path within a project,
*
* @param {string} absolutePath An absolute path to a project or a project file.
* @return {string} The path to the project.
*/
function getProjectPathFromAbsolutePath( absolutePath ) {
const matches = absolutePath.match(
// Note the special handling for `plugins/woocommerce/client/*` packages.
/((?:plugins\/woocommerce\/client\/[a-z0-9\-_.]+|plugins\/|packages\/[a-z0-9\-_.]+\/|tools\/)[a-z0-9\-_.]+)\/?/i
);
if ( ! matches ) {
return null;
}
return matches[ 1 ];
}
/**
* A record for a project and all of the changes that have occurred to it.
*
* @typedef {Object} ProjectChanges
* @property {string} path The path to the project.
* @property {boolean} phpSourceChanges Whether or not the project has changes to PHP source files.
* @property {boolean} jsSourceChanges Whether or not the project has changes to JS source files.
* @property {boolean} assetSourceChanges Whether or not the project has changed to asset source files.
* @property {boolean} documentationChanges Whether or not the project has documentation changes.
* @property {boolean} phpTestChanges Whether or not the project has changes to PHP test files.
* @property {boolean} jsTestChanges Whether or not the project has changes to JS test files.
* @property {boolean} e2eTestChanges Whether or not the project has changes to e2e test files.
*/
/**
* Scans through the files that have been changed since baseRef and returns information about the projects that have
* changes and the kind of changes that have taken place.
*
* @param {string} baseRef The base branch to check for changes against.
* @return {Array.<ProjectChanges>} An array of projects and the kinds of changes that have occurred.
*/
function detectProjectChanges( baseRef ) {
// Using a diff will not only allow us to find the projects that have changed but we can also identify the nature of the change.
const output = child_process.execSync(
`git diff --relative --name-only ${ baseRef }`,
{ encoding: 'utf8' }
);
const changedFilePaths = output.split( '\n' );
// Scan all of the changed files into the projects they belong to.
const projectsWithChanges = {};
for ( const filePath of changedFilePaths ) {
if ( ! filePath ) {
continue;
}
const projectPath = getProjectPathFromAbsolutePath( filePath );
if ( ! projectPath ) {
console.log(
`${ filePath }: ignoring change because it is not part of a project.`
);
continue;
}
if ( ! projectsWithChanges[ projectPath ] ) {
projectsWithChanges[ projectPath ] = [];
}
projectsWithChanges[ projectPath ].push( filePath );
console.log(
`${ filePath }: marked as a change in project "${ projectPath }".`
);
}
// Scan through the projects that have changes and identify the type of changes that have occurred.
const projectChanges = [];
for ( const projectPath in projectsWithChanges ) {
// We are only interested in projects that are part of our workspace.
if ( ! fs.existsSync( `${ projectPath }/package.json` ) ) {
console.error( `${ projectPath }: no "package.json" file found.` );
continue;
}
// Keep track of the kind of changes that have occurred.
let phpTestChanges = false;
let jsTestChanges = false;
let e2eTestChanges = false;
let phpSourceChanges = false;
let jsSourceChanges = false;
let assetSourceChanges = false;
let documentationChanges = false;
// Now we can look through all of the files that have changed and figure out the type of changes that have occurred.
const fileChanges = projectsWithChanges[ projectPath ];
for ( const filePath of fileChanges ) {
// Some types of changes are not interesting and should be ignored completely.
if ( filePath.match( /\/changelog\//i ) ) {
console.log(
`${ projectPath }: ignoring changelog file "${ filePath }".`
);
continue;
}
// As a preface, the detection of changes here is likely not absolutely perfect. We're going to be making some assumptions
// about file extensions and paths in order to decide whether or not something is a type of change. This should still
// be okay though since we have other cases where we check everything without looking at any changes to filter.
// We can identify PHP test files using PSR-4 or WordPress file naming conventions. We also have
// a fallback to any PHP files in a "tests" directory or its children.
// Note: We need to check for this before we check for source files, otherwise we will
// consider test file changes to be PHP source file changes.
if (
filePath.match( /(?:[a-z]+Test|-test|\/tests?\/[^\.]+)\.php$/i )
) {
phpTestChanges = true;
console.log(
`${ projectPath }: detected PHP test file change in "${ filePath }".`
);
continue;
}
// We can identify JS test files using Jest file file naming conventions. We also have
// a fallback to any JS files in a "tests" directory or its children, but we need to
// avoid picking up E2E test files in the process.
// Note: We need to check for this before we check for source files, otherwise we will
// consider test file changes to be JS source file changes.
if (
filePath.match(
/(?:(?<!e2e[^\.]+)\.(?:spec|test)|\/tests?\/(?!e2e)[^\.]+)\.(?:t|j)sx?$/i
)
) {
jsTestChanges = true;
console.log(
`${ projectPath }: detected JS test file change in "${ filePath }".`
);
continue;
}
// We're going to make an assumption about where E2E test files live based on what seems typical.
if ( filePath.match( /\/test?\/e2e/i ) ) {
e2eTestChanges = true;
console.log(
`${ projectPath }: detected E2E test file change in "${ filePath }".`
);
continue;
}
// Generally speaking, PHP files and changes to Composer dependencies affect PHP source code.
if (
filePath.match( /\.(?:php|html)$|composer\.(?:json|lock)$/i )
) {
phpSourceChanges = true;
console.log(
`${ projectPath }: detected PHP source file change in "${ filePath }".`
);
continue;
}
// JS changes should also include JSX and TS files.
if (
filePath.match( /\.(?:(?:t|j)sx?|json|html)$|package\.json$/i )
) {
jsSourceChanges = true;
console.log(
`${ projectPath }: detected JS source file change in "${ filePath }".`
);
continue;
}
// We should also keep an eye on asset file changes since these may affect
// presentation in different tests that have expectations about this data.
if (
filePath.match(
/\.(?:png|jpg|gif|scss|css|ttf|svg|eot|woff|xml|csv|txt|ya?ml)$/i
)
) {
assetSourceChanges = true;
console.log(
`${ projectPath }: detected asset file change in "${ filePath }".`
);
continue;
}
// We can be a strict with documentation changes because they are only ever going to be markdown files.
if ( filePath.match( /\.md$/i ) ) {
documentationChanges = true;
console.log(
`${ projectPath }: detected documentation change in "${ filePath }".`
);
continue;
}
}
// We only want to track a changed project when we have encountered file changes that we care about.
if (
! phpSourceChanges &&
! jsSourceChanges &&
! assetSourceChanges &&
! documentationChanges &&
! phpTestChanges &&
! jsSourceChanges &&
! e2eTestChanges
) {
console.log( `${ projectPath }: no changes detected.` );
continue;
}
// We can use the information we've collected to generate the project change object.
projectChanges.push( {
path: projectPath,
phpSourceChanges,
jsSourceChanges,
assetSourceChanges,
documentationChanges,
phpTestChanges,
jsTestChanges,
e2eTestChanges,
} );
}
return projectChanges;
}
/**
* Check the changes that occurred in each project and add any projects that are affected by those changes.
*
* @param {Array.<ProjectChanges>} projectChanges The project changes to cascade.
* @return {Array.<ProjectChanges>} The project changes with any cascading changes.
*/
function cascadeProjectChanges( projectChanges ) {
const cascadedChanges = {};
// Scan through all of the changes and add any other projects that are affected by the changes.
for ( const changes of projectChanges ) {
// Populate the change object for the project if it doesn't already exist.
// It might exist if the project has been affected by another project.
if ( ! cascadedChanges[ changes.path ] ) {
cascadedChanges[ changes.path ] = changes;
}
// Make sure that we are recording any "true" changes that have occurred either in the project itself or as a result of another project.
for ( const property in changes ) {
// We're going to assume the only properties on this object are "path" and the change flags.
if ( property === 'path' ) {
continue;
}
cascadedChanges[ changes.path ][ property ] =
changes[ property ] ||
cascadedChanges[ changes.path ][ property ];
}
// Use PNPM to get a list of dependent packages that may have been affected.
// Note: This is actually a pretty slow way of doing this. If we find it is
// taking too long we can instead use `--depth="Infinity" --json` and then
// traverse the dependency tree ourselves.
const output = child_process.execSync(
`pnpm list --filter='...{./${ changes.path }}' --only-projects --depth='-1' --parseable`,
{ encoding: 'utf8' }
);
// The `--parseable` flag returns a list of package directories separated by newlines.
const affectedProjects = output.split( '\n' );
// At the VERY least PNPM will return the path to the project if it exists. The only way
// this will happen is if the project isn't part of the workspace and we can ignore it.
// We expect this to happen and thus haven't use the caret in the filter above.
if ( ! affectedProjects ) {
continue;
}
// Run through and decide whether or not the project has been affected by the changes.
for ( const affected of affectedProjects ) {
const affectedProjectPath =
getProjectPathFromAbsolutePath( affected );
if ( ! affectedProjectPath ) {
continue;
}
// Skip the project we're checking against since it'll be in the results.
if ( affectedProjectPath === changes.path ) {
continue;
}
// Only changes to source files will impact other projects.
if (
! changes.phpSourceChanges &&
! changes.jsSourceChanges &&
! changes.assetSourceChanges
) {
continue;
}
console.log(
`${ changes.path }: cascading source file changes to ${ affectedProjectPath }.`
);
// Populate the change object for the affected project if it doesn't already exist.
if ( ! cascadedChanges[ affectedProjectPath ] ) {
cascadedChanges[ affectedProjectPath ] = {
path: affectedProjectPath,
phpSourceChanges: false,
jsSourceChanges: false,
assetSourceChanges: false,
documentationChanges: false,
phpTestChanges: false,
jsTestChanges: false,
e2eTestChanges: false,
};
}
// Consider the source files to have changed in the affected project because they are dependent on the source files in the changed project.
if ( changes.phpSourceChanges ) {
cascadedChanges[ affectedProjectPath ].phpSourceChanges = true;
}
if ( changes.jsSourceChanges ) {
cascadedChanges[ affectedProjectPath ].jsSourceChanges = true;
}
if ( changes.assetSourceChanges ) {
cascadedChanges[
affectedProjectPath
].assetSourceChanges = true;
}
}
}
return Object.values( cascadedChanges );
}
/**
* The valid commands that we can execute.
*
* @typedef {string} CommandType
* @enum {CommandType}
*/
const COMMAND_TYPE = {
Lint: 'lint',
TestPHP: 'test:php',
TestJS: 'test:js',
E2E: 'e2e',
};
/**
* Checks a command to see whether or not it is valid.
*
* @param {CommandType} command The command to check.
* @return {boolean} Whether or not the command is valid.T
*/
function isValidCommand( command ) {
for ( const commandType in COMMAND_TYPE ) {
if ( COMMAND_TYPE[ commandType ] === command ) {
return true;
}
}
return false;
}
/**
* Indicates whether or not the command is a test command.
*
* @param {CommandType} command The command to check.
* @return {boolean} Whether or not the command is a test command.
*/
function isTestCommand( command ) {
return (
command === COMMAND_TYPE.TestPHP ||
command === COMMAND_TYPE.TestJS ||
command === COMMAND_TYPE.E2E
);
}
/**
* Details about a task that should be run for a project.
*
* @typedef {Object} ProjectTask
* @property {string} name The name of the task.
* @property {Array.<CommandType>} commandsToRun The commands that the project should run.
* @property {Object.<string,string>} customCommands Any commands that should be run in place of the default commands.
* @property {string|null} testEnvCommand The command that should be run to start the test environment if one is needed.
* @property {Object.<string,string>} testEnvConfig Any configuration for the test environment if one is needed.
*/
/**
* Parses the task configuration from the package.json file and returns a task object.
*
* @param {Object} packageFile The package file for the project.
* @param {Object} config The taw task configuration.
* @param {Array.<CommandType>} commandsForChanges The commands that we should run for the project.
* @param {ProjectTask|null} parentTask The task that this task is a child of.
* @return {ProjectTask|null} The parsed task.
*/
function parseTaskConfig(
packageFile,
config,
commandsForChanges,
parentTask
) {
// Child tasks are required to have a name because otherwise
// every task for a project would be named "default".
let taskName = 'default';
if ( parentTask ) {
taskName = config.name;
if ( ! taskName ) {
throw new Error( `${ packageFile.name }: missing name for task.` );
}
}
// When the config object declares a command filter we should remove any
// of the commands it contains from the list of commands to run.
if ( config?.commandFilter ) {
// Check for invalid commands being used since they won't do anything.
for ( const command of config.commandFilter ) {
if ( ! isValidCommand( command ) ) {
throw new Error(
`${ packageFile.name }: invalid command filter type of "${ command }" for task "${ taskName }".`
);
}
}
// Apply the command filter.
commandsForChanges = commandsForChanges.filter( ( command ) =>
config.commandFilter.includes( command )
);
}
// Custom commands developers to support a command without having to use the
// standardized script name for it. For ease of use we will add parent task
// custom commands to children and allow the children to override any
// specific tasks they want.
const customCommands = Object.assign(
{},
parentTask?.customCommands ?? {}
);
if ( config?.customCommands ) {
for ( const customCommandType in config.customCommands ) {
// Check for invalid commands being mapped since they won't do anything.
if ( ! isValidCommand( customCommandType ) ) {
throw new Error(
`${ packageFile.name }: invalid custom command type "${ customCommandType } for task "${ taskName }".`
);
}
// Custom commands may have tokens that we need to remove in order to check them for existence.
const split =
config.customCommands[ customCommandType ].split( ' ' );
const customCommand = split[ 0 ];
if ( ! packageFile.scripts?.[ customCommand ] ) {
throw new Error(
`${ packageFile.name }: unknown custom "${ customCommandType }" command "${ customCommand }" for task "${ taskName }".`
);
}
// We only need to bother with commands we can actually run.
if ( commandsForChanges.includes( customCommandType ) ) {
customCommands[ customCommandType ] =
config.customCommands[ customCommandType ];
}
}
}
// Our goal is to run only the commands that have changes, however, not all
// projects will have scripts for all of the commands we want to run.
const commandsToRun = [];
for ( const command of commandsForChanges ) {
// We have already filtered and confirmed custom commands.
if ( customCommands[ command ] ) {
commandsToRun.push( command );
continue;
}
// Commands that don't have a script to run should be ignored.
if ( ! packageFile.scripts?.[ command ] ) {
continue;
}
commandsToRun.push( command );
}
// We don't want to create a task if there aren't any commands to run.
if ( ! commandsToRun.length ) {
return null;
}
// The test environment command only needs to be set when a test environment is needed.
let testEnvCommand = null;
if ( commandsToRun.some( ( command ) => isTestCommand( command ) ) ) {
if ( config?.testEnvCommand ) {
// Make sure that a developer hasn't put in a test command that doesn't exist.
if ( ! packageFile.scripts?.[ config.testEnvCommand ] ) {
throw new Error(
`${ packageFile.name }: unknown test environment command "${ config.testEnvCommand }" for task "${ taskName }".`
);
}
testEnvCommand =
config?.testEnvCommand ?? parentTask?.testEnvCommand;
} else if ( packageFile.scripts?.[ 'test:env:start' ] ) {
testEnvCommand = 'test:env:start';
}
}
// The test environment configuration should also cascade from parent task to child task.
const testEnvConfig = Object.assign(
{},
parentTask?.testEnvConfig ?? {},
config?.testEnvConfig ?? {}
);
return {
name: taskName,
commandsToRun,
customCommands,
testEnvCommand,
testEnvConfig,
};
}
/**
* Details about a project and the tasks that should be run for it.
*
* @typedef {Object} ProjectTasks
* @property {string} name The name of the project.
* @property {Array.<ProjectTask>} tasks The tasks that should be run for the project.
*/
/**
* Evaluates the given changes against the possible commands and returns those that should run as
* a result of the change criteria being met.
*
* @param {ProjectChanges|null} changes Any changes that have occurred to the project.
* @return {Array.<string>} The commands that can be run for the project.
*/
function getCommandsForChanges( changes ) {
// Here are all of the commands that we support and the change criteria that they require to execute.
// We treat the command's criteria as passing if any of the properties are true.
const commandCriteria = {
[ COMMAND_TYPE.Lint ]: [
'phpSourceChanges',
'jsSourceChanges',
'assetSourceChanges',
'phpTestChanges',
'jsTestChanges',
],
[ COMMAND_TYPE.TestPHP ]: [ 'phpSourceChanges', 'phpTestChanges' ],
[ COMMAND_TYPE.TestJS ]: [ 'jsSourceChanges', 'jsTestChanges' ],
//[ COMMAND_TYPE.E2E ]: [ 'phpSourceChanges', 'jsSourceChanges', 'assetSourceChanges', 'e2eTestFileChanges' ],
};
// We only want the list of possible commands to contain those that
// the project actually has and meet the criteria for execution.
const commandsForChanges = [];
for ( const command in commandCriteria ) {
// The criteria only needs to be checked if there is a change object to evaluate.
if ( changes ) {
let commandCriteriaMet = false;
for ( const criteria of commandCriteria[ command ] ) {
// Confidence check to make sure the criteria wasn't misspelled.
if ( ! changes.hasOwnProperty( criteria ) ) {
throw new Error(
`Invalid criteria "${ criteria }" for command "${ command }".`
);
}
if ( changes[ criteria ] ) {
commandCriteriaMet = true;
break;
}
}
// As long as we meet one of the criteria requirements we can add the command.
if ( ! commandCriteriaMet ) {
continue;
}
console.log( `${ changes.path }: command "${ command }" added based on given changes.` );
}
commandsForChanges.push( command );
}
return commandsForChanges;
}
/**
* Builds a task object for the project with support for limiting the tasks to only those that have changed.
*
* @param {string} projectPath The path to the project.
* @param {ProjectChanges|null} changes Any changes that have occurred to the project.
* @return {ProjectTasks|null} The tasks that should be run for the project.
*/
function buildTasksForProject( projectPath, changes ) {
// There's nothing to do if the project has no tasks.
const commandsForChanges = getCommandsForChanges( changes );
if ( ! commandsForChanges.length ) {
return null;
}
// Load the package file so we can check for task existence before adding them.
const rawPackageFile = fs.readFileSync(
`${ projectPath }/package.json`,
'utf8'
);
const packageFile = JSON.parse( rawPackageFile );
// We're going to parse each of the projects and add them to the list of tasks if necessary.
const projectTasks = [];
// Parse the task configuration from the package.json file.
const parentTask = parseTaskConfig(
packageFile,
packageFile.config?.ci,
commandsForChanges,
null
);
if ( parentTask ) {
projectTasks.push( parentTask );
}
if ( packageFile.config?.ci?.additionalTasks ) {
for ( const additionalTask of packageFile.config.ci.additionalTasks ) {
const task = parseTaskConfig(
packageFile,
additionalTask,
commandsForChanges,
parentTask
);
if ( task ) {
projectTasks.push( task );
}
}
}
if ( ! projectTasks.length ) {
return null;
}
return {
name: packageFile.name,
tasks: projectTasks,
};
}
/**
* This function takes a list of project changes and generates a list of tasks that should be run for each project.
*
* @param {Array.<ProjectChanges>} projectChanges The project changes to generate tasks for.
* @return {Array.<ProjectTasks>} All of the projects and the tasks that they should undertake.
*/
function generateProjectTasksForChanges( projectChanges ) {
const projectTasks = [];
// Scan through all of the changes and generate task objects for them.
for ( const changes of projectChanges ) {
const tasks = buildTasksForProject( changes.path, changes );
if ( tasks ) {
projectTasks.push( tasks );
}
}
return projectTasks;
}
/**
* Generates a list of tasks that should be run for each project in the workspace.
*
* @return {Array.<ProjectTasks>} All of the projects and the tasks that they should undertake.
*/
function generateProjectTasksForWorkspace() {
// We can use PNPM to quickly get a list of every project in the workspace.
const output = child_process.execSync(
"pnpm list --filter='*' --only-projects --depth='-1' --parseable",
{ encoding: 'utf8' }
);
// The `--parseable` flag returns a list of package directories separated by newlines.
const workspaceProjects = output.split( '\n' );
const projectTasks = [];
for ( const project of workspaceProjects ) {
const projectPath = getProjectPathFromAbsolutePath( project );
if ( ! projectPath ) {
continue;
}
const tasks = buildTasksForProject( projectPath, null );
if ( tasks ) {
projectTasks.push( tasks );
}
}
return projectTasks;
}
/**
* A CI matrix for the GitHub workflow.
*
* @typedef {Object} CIMatrix
* @property {string} projectName The name of the project.
* @property {string} taskName The name of the task.
* @property {Object.<string,string>} testEnvVars The environment variables for the test environment.
* @property {string|null} lintCommand The command to run if linting is necessary.
* @property {string|null} phpTestCommand The command to run if PHP tests are necessary.
* @property {string|null} jsTestCommand The command to run if JS tests are necessary.
* @property {string|null} e2eCommand The command to run if E2E is necessary.
*/
/**
* Parses the test environment's configuration and returns any environment variables that
* should be set.
*
* @param {Object} testEnvConfig The test environment configuration.
* @return {Promise.<Object>} The environment variables for the test environment.
*/
async function parseTestEnvConfig( testEnvConfig ) {
const envVars = {};
// Convert `wp-env` configuration options to environment variables.
if ( testEnvConfig.wpVersion ) {
try {
envVars.WP_ENV_CORE = await parseWPVersion(
testEnvConfig.wpVersion
);
} catch ( error ) {
throw new Error(
`Failed to parse WP version: ${ error.message }.`
);
}
}
if ( testEnvConfig.phpVersion ) {
envVars.WP_ENV_PHP_VERSION = testEnvConfig.phpVersion;
}
return envVars;
}
/**
* Generates a command for the task that can be executed in the CI matrix. This will check the task
* for the command, apply any command override, and replace any valid tokens with their values.
*
* @param {ProjectTask} task The task to get the command for.
* @param {CommandType} command The command to run.
* @param {Object.<string,string>} tokenValues Any tokens that should be replaced and their associated values.
* @return {string|null} The command that should be run for the task or null if the command should not be run.
*/
function getCommandForMatrix( task, command, tokenValues ) {
if ( ! task.commandsToRun.includes( command ) ) {
return null;
}
// Support overriding the default command with a custom one.
command = task.customCommands[ command ] ?? command;
// Replace any of the tokens that are used in commands with their values if one exists.
let matrixCommand = command;
const matches = command.matchAll( /\${([a-z0-9_\-]+)}/gi );
if ( matches ) {
for ( const match of matches ) {
if ( ! tokenValues.hasOwnProperty( match[ 1 ] ) ) {
throw new Error(
`Command "${ command }" contains unknown token "${ match[ 1 ] }".`
);
}
matrixCommand = matrixCommand.replace(
match[ 0 ],
tokenValues[ match[ 1 ] ]
);
}
}
return matrixCommand;
}
/**
* Generates a matrix for the CI GitHub Workflow.
*
* @param {string} baseRef The base branch to check for changes against. If empty we check for everything.
* @return {Promise.<Array.<CIMatrix>>} The CI matrix to be used in the CI workflows.
*/
async function buildCIMatrix( baseRef ) {
const matrix = [];
// Build the project tasks based on the branch we are comparing against.
let projectTasks = [];
if ( baseRef ) {
const projectChanges = detectProjectChanges( baseRef );
const cascadedProjectChanges = cascadeProjectChanges( projectChanges );
projectTasks = generateProjectTasksForChanges( cascadedProjectChanges );
} else {
projectTasks = generateProjectTasksForWorkspace();
}
// Prepare the tokens that are able to be replaced in commands.
const commandTokens = {
baseRef: baseRef ?? '',
};
// Parse the tasks and generate matrix entries for each of them.
for ( const project of projectTasks ) {
for ( const task of project.tasks ) {
matrix.push( {
projectName: project.name,
taskName: task.name,
testEnvCommand: task.testEnvCommand,
testEnvVars: await parseTestEnvConfig( task.testEnvConfig ),
lintCommand: getCommandForMatrix(
task,
COMMAND_TYPE.Lint,
commandTokens
),
phpTestCommand: getCommandForMatrix(
task,
COMMAND_TYPE.TestPHP,
commandTokens
),
jsTestCommand: getCommandForMatrix(
task,
COMMAND_TYPE.TestJS,
commandTokens
),
e2eCommand: getCommandForMatrix(
task,
COMMAND_TYPE.E2E,
commandTokens
),
} );
}
}
return matrix;
}
module.exports = buildCIMatrix;

View File

@ -0,0 +1,19 @@
module.exports = async ( { github, context, core } ) => {
const { ASSET_ID: asset_id } = process.env;
const { owner, repo } = context.repo;
const fs = require( 'fs' );
const path = require( 'path' );
const response = await github.rest.repos.getReleaseAsset( {
owner,
repo,
asset_id,
headers: { accept: 'application/octet-stream' },
} );
const zipPath = path.resolve( 'tmp', 'woocommerce.zip' );
fs.mkdirSync( 'tmp' );
fs.writeFileSync( zipPath, Buffer.from( response.data ) );
core.setOutput( 'zip-path', zipPath );
};

View File

@ -1,89 +0,0 @@
const https = require( 'http' );
/**
For convenience, this method will convert between a display-friendly version format and one used
internally by wp-env. We lean towards using WordPress.org ZIPs which requires us to reference
the full URL to the archive. For instance, instead of needing the action to fully define the
URL to the nightly build we can pass "nightly" to this function and retrieve it.
@param {string} wpVersion The display-friendly version. Supports ("master", "trunk", "nightly",
"latest", "X.X" for version lines, and "X.X.X" for specific versions)
@return {Promise.<string>} The wp-env "core" property".
**/
module.exports = async function parseWPVersion( wpVersion ) {
// Start with versions we can infer immediately.
switch ( wpVersion ) {
case 'master':
case 'trunk': {
return 'WordPress/WordPress#master';
}
case 'nightly': {
return 'https://wordpress.org/nightly-builds/wordpress-latest.zip';
}
case 'latest': {
return 'https://wordpress.org/latest.zip';
}
}
return new Promise( ( resolve, reject ) => {
// We're going to download the correct zip archive based on the version they're requesting.
const parsedVersion = wpVersion.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ );
if ( ! parsedVersion ) {
throw new Error( `Invalid 'wp-version': ${ wpVersion } must be 'trunk', 'nightly', 'latest', 'X.X', or 'X.X.X'.` );
}
// When they've provided a specific version we can just provide that.
if ( parsedVersion[ 3 ] !== undefined ) {
let zipVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`;
// .0 versions do not have a patch.
if ( parsedVersion[ 3 ] !== '0' ) {
zipVersion += `.${ parsedVersion[ 3 ] }`;
}
resolve( `https://wordpress.org/wordpress-${ zipVersion }.zip` );
}
const request = https.get(
'http://api.wordpress.org/core/stable-check/1.0/',
( response ) => {
// Listen for the response data.
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
// Once we have the entire response we can process it.
response.on('end', () => {
// Parse the response and find the latest version of every minor release.
const latestVersions = {};
const rawVersions = JSON.parse( data );
for ( const v in rawVersions ) {
// Parse the version so we can find the latest.
const matches = v.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ );
const minor = `${ matches[1] }.${ matches[2] }`;
const patch = matches[ 3 ] === undefined ? 0 : parseInt( matches[ 3 ] );
// We will only be keeping the latest release of each minor.
if ( latestVersions[ minor ] === undefined || patch > latestVersions[ minor ] ) {
latestVersions[ minor ] = patch;
}
}
let zipVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`;
// .0 versions do not have a patch.
if ( latestVersions[ zipVersion ] !== 0 ) {
zipVersion += `.${ latestVersions[ zipVersion ]}`;
}
resolve( `https://wordpress.org/wordpress-${ zipVersion }.zip` );
});
},
);
request.on( 'error', ( error ) => {
reject( error );
} );
} );
}

View File

@ -0,0 +1,51 @@
module.exports = async ( { github, context, core } ) => {
const { RELEASE_VERSION, GITHUB_EVENT_NAME } = process.env;
async function findRelease() {
const { owner, repo } = context.repo;
const list = await github.rest.repos.listReleases( {
owner,
repo,
per_page: 100,
} );
const match = list.data.find( ( { tag_name, name } ) =>
[ tag_name, name ].includes( RELEASE_VERSION )
);
return match;
}
async function handleWorkflowDispatch() {
const match = await findRelease();
if ( match ) {
return match;
}
throw new Error(
`"${ RELEASE_VERSION }" is not a valid release version!`
);
}
function findWooCommerceZipAsset() {
const match = release.assets.find(
( { name } ) => name === 'woocommerce.zip'
);
if ( ! match ) {
throw new Error(
`Release ${ RELEASE_VERSION } does not contain a woocommerce.zip asset!`
);
}
return match;
}
const release =
GITHUB_EVENT_NAME === 'release'
? await findRelease()
: await handleWorkflowDispatch();
const asset = findWooCommerceZipAsset();
core.setOutput( 'version', RELEASE_VERSION );
core.setOutput( 'created', release.created_at );
core.setOutput( 'asset-id', asset.id );
};

View File

@ -45,15 +45,6 @@ jobs:
install-filters: woocommerce
build: false
- name: Update site to nightly version
uses: ./.github/actions/tests/run-e2e-tests
with:
report-name: ${{ env.API_ARTIFACT }}
tests: update-woocommerce.spec.js
env:
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
UPDATE_WC: nightly
- name: Run API tests
id: run-api-composite-action
uses: ./.github/actions/tests/run-api-tests
@ -62,6 +53,8 @@ jobs:
env:
USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
UPDATE_WC: nightly
e2e-tests:
name: E2E tests on nightly build

View File

@ -5,7 +5,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'WooCommerce Release Tag'
description: 'WooCommerce release version'
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.release.tag_name || inputs.tag }}
@ -16,55 +16,33 @@ env:
E2E_UPDATE_WC_ARTIFACT: WooCommerce version update test on release smoke test site (run ${{ github.run_number }})
SLACK_BLOCKS_ARTIFACT: slack-blocks
jobs:
get-tag:
name: Get WooCommerce release tag
validate-version:
name: Validate release version
permissions:
contents: read
runs-on: ubuntu-20.04
outputs:
tag: ${{ steps.get-tag.outputs.tag }}
created: ${{ steps.created-at.outputs.created }}
version: ${{ steps.validate-version.outputs.version }}
created: ${{ steps.validate-version.outputs.created }}
asset-id: ${{ steps.validate-version.outputs.asset-id }}
steps:
- name: Validate tag
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release view "${{ inputs.tag }}" --repo=woocommerce/woocommerce
- uses: actions/checkout@v3
- name: Get tag from triggered event
id: get-tag
- name: Validate release version
id: validate-version
uses: actions/github-script@v6
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
echo "Triggered event: ${{ github.event_name }}"
echo "Tag from event: $RELEASE_TAG"
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
- name: Verify woocommerce.zip asset
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
RELEASE_TAG: ${{ steps.get-tag.outputs.tag }}
run: |
ASSET_NAMES=$(gh release view $RELEASE_TAG --repo woocommerce/woocommerce --json assets --jq ".assets[].name")
if [[ $ASSET_NAMES == *"woocommerce.zip"* ]]
then
echo "$RELEASE_TAG has a valid woocommerce.zip asset."
exit 0
fi
echo "$RELEASE_TAG does not have a valid woocommerce.zip asset."
exit 1
- name: Get 'created-at' of WooCommerce zip
id: created-at
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: echo "created=$(gh release view ${{ steps.get-tag.outputs.tag }} --json assets --jq .assets[0].createdAt --repo woocommerce/woocommerce)" >> $GITHUB_OUTPUT
RELEASE_VERSION: ${{ inputs.tag }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/validate-release-version.js');
await script({ github, context, core });
e2e-update-wc:
name: Test WooCommerce update
runs-on: ubuntu-20.04
needs: [get-tag]
needs: [validate-version]
permissions:
contents: read
env:
@ -94,7 +72,7 @@ jobs:
CUSTOMER_USER: ${{ secrets.RELEASE_TEST_CUSTOMER_USER }}
DEFAULT_TIMEOUT_OVERRIDE: 120000
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
UPDATE_WC: ${{ needs.get-tag.outputs.tag }}
UPDATE_WC: ${{ needs.validate-version.outputs.version }}
- name: Upload Allure artifacts to bucket
if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' )
@ -113,10 +91,10 @@ jobs:
ENV_DESCRIPTION: wp-latest
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_WP_LATEST_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -132,12 +110,12 @@ jobs:
test-name: WC Update test
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: wp-latest
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
api-wp-latest:
name: API on WP Latest
runs-on: ubuntu-20.04
needs: [get-tag, e2e-update-wc]
needs: [validate-version, e2e-update-wc]
permissions:
contents: read
outputs:
@ -155,16 +133,23 @@ jobs:
install-filters: woocommerce
build: false
- name: Download and install Chromium browser.
working-directory: plugins/woocommerce
run: pnpm exec playwright install chromium
- name: Run API tests
id: run-api-composite-action
uses: ./.github/actions/tests/run-api-tests
with:
report-name: ${{ env.API_WP_LATEST_ARTIFACT }}
tests: hello
playwright-config: ci-release.playwright.config.js
env:
API_BASE_URL: ${{ secrets.RELEASE_TEST_URL }}
USER_KEY: ${{ secrets.RELEASE_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }}
UPDATE_WC: ${{ needs.validate-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
- name: Upload Allure artifacts to bucket
if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' )
@ -183,10 +168,10 @@ jobs:
ENV_DESCRIPTION: wp-latest
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.API_WP_LATEST_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="api" \
@ -202,12 +187,12 @@ jobs:
test-name: WP Latest
api-result: ${{ steps.run-api-composite-action.outputs.result }}
env-slug: wp-latest
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
e2e-wp-latest:
name: E2E on WP Latest
runs-on: ubuntu-20.04
needs: [get-tag, api-wp-latest]
needs: [validate-version, api-wp-latest]
permissions:
contents: read
env:
@ -285,10 +270,10 @@ jobs:
ENV_DESCRIPTION: wp-latest
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_WP_LATEST_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -305,12 +290,12 @@ jobs:
api-result: ${{ needs.api-wp-latest.outputs.result }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: wp-latest
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
test-wp-latest-1:
name: Test against WP Latest-1
runs-on: ubuntu-20.04
needs: [ get-tag ]
needs: [validate-version]
env:
API_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-report
API_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-results
@ -331,7 +316,7 @@ jobs:
script: |
const { getVersionWPLatestMinusOne } = require( './plugins/woocommerce/tests/e2e-pw/utils/wordpress' );
await getVersionWPLatestMinusOne( { core, github } );
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
@ -341,15 +326,23 @@ jobs:
working-directory: plugins/woocommerce
run: pnpm run env:test
env:
WP_ENV_CORE: WordPress/WordPress#${{ steps.get-wp-latest-1.outputs.version }}
WP_ENV_CORE: WordPress/WordPress#${{ steps.get-wp-latest-1.outputs.version }}
- name: Download release zip
id: download-zip
uses: actions/github-script@v6
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release download ${{ steps.get-wp-latest-1.outputs.version }} --dir tmp
ASSET_ID: ${{ needs.validate-version.outputs.asset-id }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/download-release-zip.js');
await script({ github, context, core });
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
run: unzip -d plugins -o tmp/woocommerce.zip
run: unzip -d plugins -o ${{ env.ZIP_PATH }}
env:
ZIP_PATH: ${{ steps.download-zip.outputs.zip-path }}
- name: Run API tests
id: run-api-composite-action
@ -381,10 +374,10 @@ jobs:
ENV_DESCRIPTION: wp-latest-1
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.API_WP_LATEST_X_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="api" \
@ -422,10 +415,10 @@ jobs:
ENV_DESCRIPTION: wp-latest-1
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_WP_LATEST_X_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -445,12 +438,12 @@ jobs:
api-result: ${{ steps.run-api-composite-action.outputs.result }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: wp-latest-1
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
test-php-versions:
name: Test against PHP ${{ matrix.php_version }}
runs-on: ubuntu-20.04
needs: [get-tag]
needs: [validate-version]
strategy:
fail-fast: false
matrix:
@ -484,12 +477,20 @@ jobs:
run: bash verify-php-version.sh
- name: Download release zip
id: download-zip
uses: actions/github-script@v6
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release download ${{ needs.get-tag.outputs.tag }} --dir tmp
ASSET_ID: ${{ needs.validate-version.outputs.asset-id }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/download-release-zip.js');
await script({ github, context, core });
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
run: unzip -d plugins -o tmp/woocommerce.zip
run: unzip -d plugins -o ${{ env.ZIP_PATH }}
env:
ZIP_PATH: ${{ steps.download-zip.outputs.zip-path }}
- name: Run API tests
id: run-api-composite-action
@ -521,10 +522,10 @@ jobs:
ENV_DESCRIPTION: php-${{ matrix.php_version }}
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.API_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="api" \
@ -562,10 +563,10 @@ jobs:
ENV_DESCRIPTION: php-${{ matrix.php_version }}
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -585,12 +586,12 @@ jobs:
api-result: ${{ steps.run-api-composite-action.outputs.result }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: php-${{ matrix.php_version }}
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
test-plugins:
name: With ${{ matrix.plugin }}
runs-on: ubuntu-20.04
needs: [get-tag]
needs: [validate-version]
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
@ -632,12 +633,20 @@ jobs:
run: pnpm run env:test
- name: Download release zip
id: download-zip
uses: actions/github-script@v6
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release download ${{ needs.get-tag.outputs.tag }} --dir tmp
ASSET_ID: ${{ needs.validate-version.outputs.asset-id }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/download-release-zip.js');
await script({ github, context, core });
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
run: unzip -d plugins -o tmp/woocommerce.zip
run: unzip -d plugins -o ${{ env.ZIP_PATH }}
env:
ZIP_PATH: ${{ steps.download-zip.outputs.zip-path }}
- name: Run 'Upload plugin' test
id: run-upload-test
@ -680,10 +689,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.ARTIFACT_NAME }}" \
-f env_description="${{ matrix.env_description }}" \
-f test_type="e2e" \
@ -698,7 +707,7 @@ jobs:
test-name: With ${{ matrix.plugin }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: ${{ matrix.env_description }}
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
post-slack-summary:
name: Post Slack summary
@ -711,7 +720,7 @@ jobs:
)
needs:
- e2e-wp-latest
- get-tag
- validate-version
- test-php-versions
- test-plugins
- test-wp-latest-1
@ -729,7 +738,7 @@ jobs:
id: run-payload-action
uses: ./.github/actions/tests/slack-summary-on-release/slack-payload
with:
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
blocks-dir: ${{ steps.download-slack-blocks.outputs.download-path }}
- name: Send Slack message

View File

@ -13,9 +13,10 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v3
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 40
stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed."
close-issue-message: 'This issue was closed because it has been 14 days with no activity.'
days-before-issue-stale: 7

View File

@ -0,0 +1,88 @@
name: Send a Slack notification when a PR contains rest api changes
on:
pull_request_target:
types: [labeled]
permissions: {}
jobs:
send-slack-notification-when-pr-contains-rest-api-changes:
if: "${{ github.event.label.name == 'contains: rest api change' && (github.event.pull_request.state == 'open' || github.event.pull_request.merged) }}"
runs-on: ubuntu-20.04
steps:
- name: Wait 2 minutes for other labelling jobs to finish
run: sleep 2m
shell: bash
- name: Calculate test date
id: calculate_date
run: |
#!/bin/bash
# Get the day of the week of the merged PR (0 for Sunday, 1 for Monday, etc.)
MERGE_DAY_OF_WEEK=$(date -u -d "${{ github.event.pull_request.merged_at }}" +"%u")
# Calculate days until the next Thursday after the merge
# If the merge is on Thursday, this will give 7 (i.e., next week's Thursday)
DAYS_UNTIL_NEXT_THURSDAY=$(( (4 + 7 - MERGE_DAY_OF_WEEK) % 7 ))
# If DAYS_UNTIL_NEXT_THURSDAY is 0, the merge was on a Thursday, so we set it to 7 to get the next Thursday
if [ $DAYS_UNTIL_NEXT_THURSDAY -eq 0 ]; then
DAYS_UNTIL_NEXT_THURSDAY=7
fi
# Calculate the date for the next Thursday after the merge
THURSDAY_AFTER_MERGE=$(date -u -d "${{ github.event.pull_request.merged_at }} + $DAYS_UNTIL_NEXT_THURSDAY days" +"%Y-%m-%d")
WOOAF_RELEASE_DATE=$(date -u -d "${THURSDAY_AFTER_MERGE} + 6 days" +"%Y-%m-%d")
TEST_DATE_MESSAGE="Thursday, $THURSDAY_AFTER_MERGE. (Targeting release on $WOOAF_RELEASE_DATE)"
echo "TEST_DATE_MESSAGE=${TEST_DATE_MESSAGE}" >> $GITHUB_ENV
- name: Determine Milestone Date
id: get_milestone_date
run: |
#!/bin/bash
MILESTONE_TITLE="${{ github.event.pull_request.milestone.title }}"
MILESTONE_DATE="Undefined"
# Mapping of milestone titles to release dates
declare -A MILESTONE_DATES
MILESTONE_DATES=(
["8.0.0"]="2023-08-08"
["8.1.0"]="2023-09-12"
["8.2.0"]="2023-10-10"
["8.3.0"]="2023-11-14"
["8.4.0"]="2023-12-12"
["8.5.0"]="2024-01-09"
["8.6.0"]="2024-02-13"
["8.7.0"]="2024-03-12"
["8.8.0"]="2024-04-09"
["8.9.0"]="2024-05-14"
["9.0.0"]="2024-06-11"
)
# Check if the milestone title exists in our predefined list and get the date
if [[ -v "MILESTONE_DATES[${MILESTONE_TITLE}]" ]]; then
MILESTONE_DATE=${MILESTONE_DATES[${MILESTONE_TITLE}]}
fi
# Export for later steps
echo "MILESTONE_DATE=${MILESTONE_DATE}" >> $GITHUB_ENV
# Notify Slack Step
- name: Notify Slack
uses: archive/github-actions-slack@d9dae40827adf93bddf939db6552d1e392259d7d
id: notify
with:
slack-bot-user-oauth-access-token: ${{ secrets.TEST_ASSISTANCE_BOT_TOKEN }}
slack-channel: ${{ secrets.WOO_CORE_REST_API_CHANGES_SLACK_CHANNEL }}
slack-text: |
<${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>
*Labels:* ${{ join(github.event.pull_request.labels.*.name, ', ') }}
*Monthly Release Milestone:* <${{ github.event.pull_request.milestone.html_url }}|${{ github.event.pull_request.milestone.title }}> (Release Date: ${{ env.MILESTONE_DATE }})
*WooAF (weekly) Timeline: this PR can be tested from:* ${{ env.TEST_DATE_MESSAGE }}
Please visit the <#${{ secrets.WOO_CORE_RELEASES_SLACK_CHANNEL }}> to obtain the latest WooAF build for testing.
slack-optional-unfurl_links: false
slack-optional-unfurl_media: false

View File

@ -1,11 +1,8 @@
name: Add Triage Label
on:
issues:
types: opened
permissions: {}
jobs:
add_label:
runs-on: ubuntu-20.04
@ -14,7 +11,22 @@ jobs:
issues: write
steps:
- uses: actions/checkout@v3
# We want to delay the labeling of the issue so that the author has a change to add labels after issue creation.
- name: 'Delay Labeling'
run: sleep 3m
# Make sure that the latest issue is pulled from the database rather than relying on the payload. This is
# because the payload won't include any labels that were added after the issue was created.
- uses: actions/github-script@v6
id: latest-issue
with:
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
core.setOutput('hasLabels', issue.data.labels.length > 0);
- uses: actions-ecosystem/action-add-labels@v1
if: github.event.issue.labels[0] == null
if: ${{ steps.latest-issue.outputs.hasLabels == 'false'}}
with:
labels: 'status: awaiting triage'

View File

@ -4,6 +4,7 @@
"MD007": { "indent": 4 },
"MD013": { "line_length": 9999 },
"MD024": { "allow_different_nesting": true },
"MD033": { "allowed_elements": ["video"] },
"no-hard-tabs": false,
"whitespace": false
}

View File

@ -0,0 +1,131 @@
#!/usr/bin/env node
const https = require( 'https' );
const vm = require( 'vm' );
const fs = require( 'fs' );
const path = require( 'path' );
const intlUrl =
'https://raw.githubusercontent.com/jackocnr/intl-tel-input/master/src/js/data.js';
const phoneUrl =
'https://raw.githubusercontent.com/AfterShip/phone/master/src/data/country_phone_data.ts';
const fetch = ( url ) =>
new Promise( ( resolve, reject ) => {
https
.get( url, ( res ) => {
let body = '';
res.on( 'data', ( chunk ) => {
body += chunk;
} );
res.on( 'end', () => {
resolve( body );
} );
} )
.on( 'error', reject );
} );
const numberOrString = ( str ) =>
Number( str ).toString().length !== str.length ? str : Number( str );
const evaluate = ( code ) => {
const script = new vm.Script( code );
const context = vm.createContext();
script.runInContext( context );
return context;
};
const parse = ( data /*: any[]*/ ) /*: DataType*/ =>
data.reduce(
( acc, item ) => ( {
...acc,
[ item[ 0 ] ]: {
alpha2: item[ 0 ],
code: item[ 1 ].toString(),
priority: item[ 2 ] || 0,
start: item[ 3 ]?.map( String ),
lengths: item[ 4 ],
},
} ),
{}
);
const saveToFile = ( data ) => {
const dataString = JSON.stringify( data ).replace( /null/g, '' );
const parseString = parse.toString().replace( / \/\*(.+?)\*\//g, '$1' );
const code = [
'// Do not edit this file directly.',
'// Generated by /bin/packages/js/components/phone-number-input/build-data.js',
'',
'/* eslint-disable */',
'',
'import type { DataType } from "./types";',
'',
`const parse = ${ parseString }`,
'',
`const data = ${ dataString }`,
'',
'export default parse(data);',
].join( '\n' );
const filePath = path.resolve(
'packages/js/components/src/phone-number-input/data.ts'
);
fs.writeFileSync( filePath, code );
};
( async () => {
const intlData = await fetch( intlUrl ).then( evaluate );
const phoneData = await fetch( phoneUrl )
.then( ( data ) => 'var data = ' + data.substring( 15 ) )
.then( evaluate );
// Convert phoneData array to object
const phoneCountries = phoneData.data.reduce(
( acc, item ) => ( {
...acc,
[ item.alpha2.toLowerCase() ]: item,
} ),
{}
);
// Traverse intlData to create a new array with required fields
const countries = intlData.allCountries.map( ( item ) => {
const phoneCountry = phoneCountries[ item.iso2 ];
const result = [
item.iso2.toUpperCase(), // alpha2
Number( item.dialCode ), // code
/* [2] priority */
/* [3] start */
/* [4] lengths */
,
,
,
];
if ( item.priority ) {
result[ 2 ] = item.priority;
}
const areaCodes = item.areaCodes || [];
const beginWith = phoneCountry?.mobile_begin_with || [];
if ( areaCodes.length || beginWith.length ) {
result[ 3 ] = [ ...new Set( [ ...areaCodes, ...beginWith ] ) ].map(
numberOrString
);
}
if ( phoneCountry?.phone_number_lengths ) {
result[ 4 ] = phoneCountry.phone_number_lengths;
}
return result;
} );
saveToFile( countries );
} )();

View File

@ -1,5 +1,141 @@
== Changelog ==
= 8.2.1 2023-10-16 =
**WooCommerce**
* Fix - Prevent global attribute terms from being automatically selected [#40729](https://github.com/woocommerce/woocommerce/pull/40729)
= 8.2.0 2023-10-13 =
**WooCommerce**
* Fix - Correctly set 'created_via' for HPOS orders created on the admin. [#40469](https://github.com/woocommerce/woocommerce/pull/40469)
* Fix - Fix backwards compatibility issue with `wc_get_orders()` when HPOS is active and the pagination bit is set. [#40551](https://github.com/woocommerce/woocommerce/pull/40551)
* Fix - Save hpos order data before clearing the order from cache [#40282](https://github.com/woocommerce/woocommerce/pull/40282)
* Fix - Disable WP's post lock on HPOS order edit screen. [#40355](https://github.com/woocommerce/woocommerce/pull/40355)
* Fix - Enqueue media scripts for Images block within the product editor, as is required for Images block. [#40356](https://github.com/woocommerce/woocommerce/pull/40356)
* Fix - Addressed visual tweaks for CYS in response to feedback from 12th Sept [#40155](https://github.com/woocommerce/woocommerce/pull/40155)
* Fix - Address missing order type handling in HPOS compatibility mode sync. [#40279](https://github.com/woocommerce/woocommerce/pull/40279)
* Fix - Add Variation options section back to the product blocks template [#39914](https://github.com/woocommerce/woocommerce/pull/39914)
* Fix - Avoid a fatal error on the order received page if the order ID is not for a valid order. [#39876](https://github.com/woocommerce/woocommerce/pull/39876)
* Fix - Avoid string<>int comparison in products bought query to avoid results with customer_id = 0. [#40030](https://github.com/woocommerce/woocommerce/pull/40030)
* Fix - Changed Tax task completion criteria so that it considers both boolean and stringly typed values as expected [#39983](https://github.com/woocommerce/woocommerce/pull/39983)
* Fix - Display search results subtitle in HPOS list table view. [#40270](https://github.com/woocommerce/woocommerce/pull/40270)
* Fix - Eliminate an unnecessary redirect when the geo hash isalready set to the correct value. [#39634](https://github.com/woocommerce/woocommerce/pull/39634)
* Fix - Fix a bug where updating store location doesn't update store currency. [#40142](https://github.com/woocommerce/woocommerce/pull/40142)
* Fix - Fix cached refund not deleted when the refund is deleted with HPOS active [#40197](https://github.com/woocommerce/woocommerce/pull/40197)
* Fix - Fix changes in order custom fields made from admin not being applied when using the order Update button with HPOS active. [#40278](https://github.com/woocommerce/woocommerce/pull/40278)
* Fix - Fix customize store white screen bug in WP 6.3 [#40031](https://github.com/woocommerce/woocommerce/pull/40031)
* Fix - Fix customize your store task header button [#40031](https://github.com/woocommerce/woocommerce/pull/40031)
* Fix - Fix CYS UI issues [#40269](https://github.com/woocommerce/woocommerce/pull/40269)
* Fix - Fix CYS `__experimentalReapplyBlockTypeFilters` is not a function [#40104](https://github.com/woocommerce/woocommerce/pull/40104)
* Fix - Fixed missed lint error in Assembler Hub [#39964](https://github.com/woocommerce/woocommerce/pull/39964)
* Fix - Fix minor layout shift in the core profiler. [#39898](https://github.com/woocommerce/woocommerce/pull/39898)
* Fix - Fix product e2e tests [#39823](https://github.com/woocommerce/woocommerce/pull/39823)
* Fix - Fix undismissable notice when using localization for certain messages like "Coupon management has moved" [#39913](https://github.com/woocommerce/woocommerce/pull/39913)
* Fix - FIx WC Admin pages are empty for WP 6.2 and below. [#39995](https://github.com/woocommerce/woocommerce/pull/39995)
* Fix - Properly convert local time date queries to UTC in the HPOS datastore. [#40146](https://github.com/woocommerce/woocommerce/pull/40146)
* Fix - Redirect to Jetpack connect when jetpack-boost is selected. [#40261](https://github.com/woocommerce/woocommerce/pull/40261)
* Fix - Remove COT enable requirement from sync and verify command. [#39998](https://github.com/woocommerce/woocommerce/pull/39998)
* Fix - Removed references to the un-used Purchase task item in the onboarding task list. [#40121](https://github.com/woocommerce/woocommerce/pull/40121)
* Fix - Remove use of woocommerce-page class within WooCommerce Admin pages, replaced with woocommerce-admin-page. [#40218](https://github.com/woocommerce/woocommerce/pull/40218)
* Fix - Restore moving to trash functionality within HPOS order edit screen. [#39693](https://github.com/woocommerce/woocommerce/pull/39693)
* Fix - update the SqlQuery filter prefix in data.md [#39319](https://github.com/woocommerce/woocommerce/pull/39319)
* Fix - Use correct object reference when cloning a cart [#39282](https://github.com/woocommerce/woocommerce/pull/39282)
* Fix - [HPOS]Fix duplicate meta handling by passing meta_value|unique in post calls [#40088](https://github.com/woocommerce/woocommerce/pull/40088)
* Fix - [HPOS] Modify query to have less characters before the `FROM` keyword. [#40109](https://github.com/woocommerce/woocommerce/pull/40109)
* Fix - [HPOS] Support deleting metadata just by meta id. [#40064](https://github.com/woocommerce/woocommerce/pull/40064)
* Fix - [HPOS] Use objects method instead of calling datastore directly. [#40158](https://github.com/woocommerce/woocommerce/pull/40158)
* Add - Add ability to remove blocks from templates. [#39900](https://github.com/woocommerce/woocommerce/pull/39900)
* Add - Add a filter to OrdersTableQuery to allow overriding of HPOS queries. [#39945](https://github.com/woocommerce/woocommerce/pull/39945)
* Add - Add after_add_block and after_remove block hooks to the block template API. [#40139](https://github.com/woocommerce/woocommerce/pull/40139)
* Add - Add AI wizard business info step for Customize Your Store task [#39979](https://github.com/woocommerce/woocommerce/pull/39979)
* Add - Add component to Customize Your Store task. [#40140](https://github.com/woocommerce/woocommerce/pull/40140)
* Add - Add customize store - fonts [#40082](https://github.com/woocommerce/woocommerce/pull/40082)
* Add - Add customize store AI wizard call for best colour palette suggestions. [#40295](https://github.com/woocommerce/woocommerce/pull/40295)
* Add - Add customize store AI wizard call for color palette suggestion [#40237](https://github.com/woocommerce/woocommerce/pull/40237)
* Add - Add customize store AI wizard call for font pairing suggestion [#40240](https://github.com/woocommerce/woocommerce/pull/40240)
* Add - Add customize store assembler hub onboarding tour [#39981](https://github.com/woocommerce/woocommerce/pull/39981)
* Add - Add customize store assembler hub [#39843](https://github.com/woocommerce/woocommerce/pull/39843)
* Add - Add customize store color palettes [#40051](https://github.com/woocommerce/woocommerce/pull/40051)
* Add - Add customize store transitional screen [#40122](https://github.com/woocommerce/woocommerce/pull/40122)
* Add - Added URL navigation support to customize-store feature [#40068](https://github.com/woocommerce/woocommerce/pull/40068)
* Add - Add filter woocommerce_hpos_enable_sync_on_read to disable sync on read with HPOS sync enabled. [#40039](https://github.com/woocommerce/woocommerce/pull/40039)
* Add - Add has_price param to the variations REST API query. [#40281](https://github.com/woocommerce/woocommerce/pull/40281)
* Add - Add header customization to the Assembler Hub [#40107](https://github.com/woocommerce/woocommerce/pull/40107)
* Add - Add help text to Name field in Create new category modal [#40059](https://github.com/woocommerce/woocommerce/pull/40059)
* Add - Add new e2e test for Shopper My Account Downloads section [#40100](https://github.com/woocommerce/woocommerce/pull/40100)
* Add - Add new e2e test to cover My Account Addresses section [#40114](https://github.com/woocommerce/woocommerce/pull/40114)
* Add - Add sidebar to customize your store task. [#40136](https://github.com/woocommerce/woocommerce/pull/40136)
* Add - Adds new action hook `woocommerce_pay_order_before_payment` to the `checkout/form-pay.php` template. [#37588](https://github.com/woocommerce/woocommerce/pull/37588)
* Add - Add support for slug auto generation to the create attribute endpoint [#39827](https://github.com/woocommerce/woocommerce/pull/39827)
* Add - Add tags (or general taxonomy ) block [#39966](https://github.com/woocommerce/woocommerce/pull/39966)
* Add - Add track events to customize store AI wizard [#40144](https://github.com/woocommerce/woocommerce/pull/40144)
* Add - Add track events to customize store transitional page [#40143](https://github.com/woocommerce/woocommerce/pull/40143)
* Add - Add Tracks events to Appearance > Themes screen [#40193](https://github.com/woocommerce/woocommerce/pull/40193)
* Add - Add tracks to CYS assembler-hub and hide pages sidebar screen [#40156](https://github.com/woocommerce/woocommerce/pull/40156)
* Add - Add variable product experiment [#40177](https://github.com/woocommerce/woocommerce/pull/40177)
* Add - Add woocommerce_block_template_register action. [#39915](https://github.com/woocommerce/woocommerce/pull/39915)
* Add - Create a plugin to enable Variations feature #40027 [#40027](https://github.com/woocommerce/woocommerce/pull/40027)
* Add - Implement customize store assembler hub - logo feature [#39932](https://github.com/woocommerce/woocommerce/pull/39932)
* Add - Implemented loader design for Customize your store - Design with AI [#40083](https://github.com/woocommerce/woocommerce/pull/40083)
* Add - Made ai completion for look and tone more robust and added tracks [#40052](https://github.com/woocommerce/woocommerce/pull/40052)
* Add - Records plugin API requests and installation errors to coreprofiler_install_plugin_error separately for the core profiler. [#39899](https://github.com/woocommerce/woocommerce/pull/39899)
* Update - Added Marketplace class as basis for Reactified marketplace. [#39121](https://github.com/woocommerce/woocommerce/pull/39121)
* Update - Added xstate scaffolding for AI Wizard in customize your store feature [#39863](https://github.com/woocommerce/woocommerce/pull/39863)
* Update - Display a custom WooPayments onboarding task header content, when an incentive with server based header is available. [#40034](https://github.com/woocommerce/woocommerce/pull/40034)
* Update - Implement customize your store task completion logic [#40267](https://github.com/woocommerce/woocommerce/pull/40267)
* Update - Optimize customize store preview frame resize performance [#39930](https://github.com/woocommerce/woocommerce/pull/39930)
* Update - Remove core-profiler checks from the tests -- core profiler is enabled by default now. [#40260](https://github.com/woocommerce/woocommerce/pull/40260)
* Update - Replace Personalize Your Store task with Choose Your Theme [#40239](https://github.com/woocommerce/woocommerce/pull/40239)
* Update - Track coreprofiler_store_extensions_installed_and_activated when async installation is complete [#39921](https://github.com/woocommerce/woocommerce/pull/39921)
* Update - Turn off the experimental flag for HPOS. [#39846](https://github.com/woocommerce/woocommerce/pull/39846)
* Update - Update Action Scheduler to 3.6.3 [#40147](https://github.com/woocommerce/woocommerce/pull/40147)
* Update - Update intro screen for the new Customize Your Store task [#40293](https://github.com/woocommerce/woocommerce/pull/40293)
* Update - Update Remote Inbox Notifications to add in and !in comparison operators for comparing values against arrays [#40084](https://github.com/woocommerce/woocommerce/pull/40084)
* Update - Update the simple product template implementation to use the product form template API. [#39814](https://github.com/woocommerce/woocommerce/pull/39814)
* Update - Update use of preventLeavingProductForm with new function changes. [#40225](https://github.com/woocommerce/woocommerce/pull/40225)
* Update - Update WooCommerce Blocks to 10.9.3 [#39895](https://github.com/woocommerce/woocommerce/pull/39895)
* Update - Update WooCommerce Blocks to 11.0.0 [#39971](https://github.com/woocommerce/woocommerce/pull/39971)
* Update - Update WooCommerce Blocks to 11.1.0 [#40141](https://github.com/woocommerce/woocommerce/pull/40141)
* Update - Update WooCommerce Blocks to 11.1.1 [#40300](https://github.com/woocommerce/woocommerce/pull/40300)
* Update - Update WooCommerce Blocks to 11.1.2 [#40475](https://github.com/woocommerce/woocommerce/pull/40475)
* Update - We have completely redesigned the In-app Marketplace. [#39121](https://github.com/woocommerce/woocommerce/pull/39121)
* Dev - Added documentation for the Core Profiler [#39963](https://github.com/woocommerce/woocommerce/pull/39963)
* Dev - Add job to post Slack summary of plugin test results in "Smoke test daily" workflow. [#39838](https://github.com/woocommerce/woocommerce/pull/39838)
* Dev - Add new E2E test covering shopper product page and make Product-related tests granular (separated test files) [#40132](https://github.com/woocommerce/woocommerce/pull/40132)
* Dev - Add notice to "track inventory" toggle #40011 [#40011](https://github.com/woocommerce/woocommerce/pull/40011)
* Dev - Add some basic E2E tests for Assembler Hub [#40235](https://github.com/woocommerce/woocommerce/pull/40235)
* Dev - Adds test to check required fields on checkout [#40099](https://github.com/woocommerce/woocommerce/pull/40099)
* Dev - Bump required PHP version to 7.4 [#39820](https://github.com/woocommerce/woocommerce/pull/39820)
* Dev - Cleanup: remove the unused is_feature_visible and show_feature methods. [#39931](https://github.com/woocommerce/woocommerce/pull/39931)
* Dev - Fixes and enables API test suite to run on daily CI run against alternate host [#39858](https://github.com/woocommerce/woocommerce/pull/39858)
* Dev - Fix flakiness around the `Turn off the new product form` menu item. [#39957](https://github.com/woocommerce/woocommerce/pull/39957)
* Dev - Fix for a couple of flaky API tests on daily runs [#39918](https://github.com/woocommerce/woocommerce/pull/39918)
* Dev - Improve documentation for the `is_checkout()` function. [#40258](https://github.com/woocommerce/woocommerce/pull/40258)
* Dev - Refactored core profiler loader to be more generalizable and moved to @woocommerce/onboarding [#39735](https://github.com/woocommerce/woocommerce/pull/39735)
* Dev - Remove "WP Latest-2" from release tests. [#40012](https://github.com/woocommerce/woocommerce/pull/40012)
* Dev - Remove legacy PHP version update checks [#39845](https://github.com/woocommerce/woocommerce/pull/39845)
* Dev - Run a full reset on API daily test site [#40061](https://github.com/woocommerce/woocommerce/pull/40061)
* Dev - Updates Playwright from 1.33 to 1.37.1 [#39815](https://github.com/woocommerce/woocommerce/pull/39815)
* Tweak - Add order property to every block in SimpleProductTemplate [#39946](https://github.com/woocommerce/woocommerce/pull/39946)
* Tweak - Adds an informative tooltip to the Account Details section of the Direct Bank Transfer settings. [#39860](https://github.com/woocommerce/woocommerce/pull/39860)
* Tweak - Fix a minor code typo, no change in functionality [#36402](https://github.com/woocommerce/woocommerce/pull/36402)
* Tweak - Make it easier to disable email verification checks for the order confirmation and order pay pages. [#40050](https://github.com/woocommerce/woocommerce/pull/40050)
* Tweak - Migrate category field to woocommerce/product-taxonomy-field block [#40021](https://github.com/woocommerce/woocommerce/pull/40021)
* Tweak - tweak some of the HPOS Settings UI. [#39912](https://github.com/woocommerce/woocommerce/pull/39912)
* Tweak - Tweak tasklist description color to darker [#39903](https://github.com/woocommerce/woocommerce/pull/39903)
* Enhancement - Add CLI commands to enable or disable HPOS. [#39865](https://github.com/woocommerce/woocommerce/pull/39865)
* Enhancement - Design enhancements for the Attributes tab. [#39987](https://github.com/woocommerce/woocommerce/pull/39987)
* Enhancement - Design enhancements for the Inventory tab. [#39962](https://github.com/woocommerce/woocommerce/pull/39962)
* Enhancement - Enable HPOS by default for new installs. [#40296](https://github.com/woocommerce/woocommerce/pull/40296)
* Enhancement - Improve the existing E2E test to verify one more element on each page load. [#40008](https://github.com/woocommerce/woocommerce/pull/40008)
* Enhancement - Update the default setting for the task list progress bar from 0 to 0.25, which gives the progress better visual context when no tasks have been completed. [#39369](https://github.com/woocommerce/woocommerce/pull/39369)
* Enhancement - Update Venezuelan currency: Bolívar (Bs.). [#29380](https://github.com/woocommerce/woocommerce/pull/29380)
= 8.1.1 2023-09-18 =
**WooCommerce**

4
changelog/39948-patch-4 Normal file
View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Do not remove sale date from when the sale is still active

View File

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

View File

@ -1,7 +1,5 @@
# Adjust the quantity input values
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our  [Support Policy](http://www.woocommerce.com/support-policy/).
Set the starting value, maximum value, minimum value, and increment amount for quantity input fields on product pages.
Add this code to your child themes `functions.php` file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file, as this will be wiped entirely when you update the theme.
@ -34,7 +32,7 @@ if ( ! function_exists( 'YOUR_PREFIX_woocommerce_available_variation' ) ) {
function YOUR_PREFIX_woocommerce_available_variation( $args ) {
$args['max_qty'] = 20; // Maximum value (variations)
$args['min_qty'] = 2; // Minimum value (variations)
// Note: the starting value and step for variations is controlled
// from the 'woocommerce_quantity_input_args' filter shown above for
// simple products

View File

@ -1,9 +1,9 @@
# Add a message above the login / register form
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
This code will add a custom message above the login/register form on the users my-account page.
Add this code to your child themes `functions.php` file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file, as this will be wiped entirely when you update the theme.
```php
if ( ! function_exists( 'YOUR_PREFIX_login_message' ) ) {
/**
@ -30,5 +30,5 @@ if ( ! function_exists( 'YOUR_PREFIX_login_message' ) ) {
Please note that for this code to work, the following options must be checked in the WooCommerce “Accounts & Privacy” settings:
- Allow customers to create an account during checkout.
- Allow customers to create an account on the "My Account" page.
- Allow customers to create an account during checkout.
- Allow customers to create an account on the "My Account" page.

View File

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

View File

@ -1,7 +1,5 @@
# Change number of related products output
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/?).
Add code to your child themes functions.php file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file as this will be wiped entirely when you update the theme.
Please note that it does not work for all themes because of the way theyre coded.

View File

@ -1,7 +1,5 @@
# Unhook and remove WooCommerce emails
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our  [Support Policy](http://www.woocommerce.com/support-policy/).
This code allows you to unhook and remove the default WooCommerce emails.
Add this code to your child themes `functions.php` file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file, as this will be wiped entirely when you update the theme.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

449
docs/docs-manifest.json Normal file
View File

@ -0,0 +1,449 @@
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/style-guide.md",
"hash": "e81f1f926568e1792b6814c735c0321dee1356b4f8c053b7a5ee1770440e8052",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/style-guide.md",
"id": "211490a474b1b2ce6a6fc670b159f3497d6a9054"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/README.md",
"hash": "b1a13c53d16ba2a1b089a0cf19c5c37a8181f33bdbe0c6ee106cd1c80aef9fa0",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/README.md",
"id": "2c20e49a2bfd5bfcbb318dedbec96ada77784f14",
"links": {
"extension-development/building-your-first-extension.md": "bfb30a2379ec5e7c7960192b0a9175191e39aef7",
"extension-development/how-to-design-a-simple-extension.md": "c2f4612f31cc3892bfb19c6e9859f2daccdb6fa0",
"style-guide.md": "211490a474b1b2ce6a6fc670b159f3497d6a9054",
"../plugins/woocommerce/README.md": "827066d08695e5002bf059cd2e9d4d2a0fa58df8",
"../plugins/woocommerce/i18n/languages/README.md": "826a4400174812dfabb978c87ff2742bfdf28d62",
"../plugins/woocommerce/includes/README.md": "3d07aabeb0926e4c675e5770e78a0cfa537d2f56",
"../plugins/woocommerce/lib/README.md": "7c7e05959e4e9dcde4ac0e3e2a13258d7521e731",
"../plugins/woocommerce/packages/README.md": "740c206346a48e9dcb2e70efd5a3221992c389dc",
"../plugins/woocommerce/src/README.md": "c7444c322c5bb1ff755b2bf3e961babf3a879f4e",
"../plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md": "a46d35b9e8a8c15b89082e0c2c04d83c852d545f",
"../plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md": "bbccc83ae18c86679a3b632e7203f6c395987967",
"../plugins/woocommerce/src/Blocks/README.md": "532a6602e970797759269b4c588c724551379214",
"../plugins/woocommerce/src/Internal/README.md": "67d669be90b641e2273194796e3d9bdfdbd69a7e",
"../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md": "1d691e34fd20dd268e8e9f8283f46a595758c33b",
"../plugins/woocommerce/tests/README.md": "722532b12b14b2f5c0b8efdd96cca1854bba38c4",
"../plugins/woocommerce/tests/api-core-tests/README.md": "b000db6a9a2807a49a3bb47c57bb78326a865c78",
"../plugins/woocommerce/tests/e2e/README.md": "c15296a46be7331ed23d791073cdebeb0f4c48c4",
"../plugins/woocommerce/tests/e2e-pw/README.md": "103a9a613a34a031b34ca48f0895640dc9fc2b10",
"../plugins/woocommerce/tests/performance/README.md": "8c14e3b9fac89bced565b78e96d3a3a89e0a568e",
"../plugins/woocommerce/tests/Tools/CodeHacking/README.md": "231d9fc132423f1ecc391aaf1bcb57a3cd749d24",
"../plugins/woocommerce-admin/README.md": "9ba7a7e2c4411e01c70d866f8b8b5604484368d8",
"../plugins/woocommerce-admin/client/activity-panel/README.md": "2de9812a153a9c9ed90588feacd9ece41ecff93c",
"../plugins/woocommerce-admin/client/activity-panel/activity-card/README.md": "c34672cf87665e3ff157c6df3ddf1993fd33e7a4",
"../plugins/woocommerce-admin/client/activity-panel/activity-header/README.md": "13e379fd37082d3cba36e76046acfc63ff70ab8a",
"../plugins/woocommerce-admin/client/analytics/report/README.md": "84430b89912cb95d31240cd2d00a400b2d904ec4",
"../plugins/woocommerce-admin/client/analytics/settings/README.md": "421781cef9f3d0160c1e3892488b21b3f4a2a7f5",
"../plugins/woocommerce-admin/client/dashboard/README.md": "f7d6ff3c0f18554161afaa94698fb127f098986f",
"../plugins/woocommerce-admin/client/header/README.md": "39d3152fc1be21beee488dddd3f5a90e6af2502a",
"../plugins/woocommerce-admin/client/marketing/README.md": "54f1eb4cf5ec67713ce2fdf2b2f1dc28b03e35ef",
"../plugins/woocommerce-admin/client/marketing/components/product-icon/README.md": "3fa5081872b79078ad320dd0d9e218ed4148dc4e",
"../plugins/woocommerce-admin/client/utils/README.md": "0486d6a8f01b89bf171e21678df5dae9b5791a99",
"../plugins/woocommerce-admin/client/wp-admin-scripts/README.md": "641ab0c131d812f0969631b44ed9e8fe3fdb0cba",
"../plugins/woocommerce-admin/docs/README.md": "25155a38fc7599a01bfb30eb31a1edb098b59219",
"../plugins/woocommerce-admin/docs/examples/README.md": "b5ca126b81199d9da6ae62d5d26c093f88405a06",
"../plugins/woocommerce-admin/docs/examples/extensions/README.md": "42a90d07061074d0de1b15aeaf80531f83b2a258",
"../plugins/woocommerce-admin/docs/features/README.md": "1ee849cb8dc013ffbfe8ebcb54b8cc83f39d72e4",
"../plugins/woocommerce-admin/docs/woocommerce.com/README.md": "ee127fc0f256c3ae546869c602e94b4a6e258be8",
"../plugins/woocommerce-beta-tester/README.md": "081da0cc3caba15e926b606082120c2692b71ad4",
"../plugins/woocommerce-beta-tester/src/tools/README.md": "aedc38bbe8b29e676a15d252bcb1be38acc86f3c",
"../plugins/woocommerce-beta-tester/userscripts/README.md": "d3d94172608c29c4968ee73858ee3575a2cc11ce"
}
}
],
"categories": [
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md",
"hash": "a22ec5c7c7c670e97e34a438d5e13dc3709716484efbecbf49e5917b6da9079d",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md",
"id": "517b5bdeb798c1832c3ba76670e728b7b922d5d1"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/rename-a-country.md",
"hash": "3929e2145dcaad30a83fda6d82d973028e4463eeb2f6a6ba6ff9cb70c6b52fd5",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/rename-a-country.md",
"id": "2197550f7fdfbce39155865ba36f1f396a35e4a6"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/number-of-products-per-row.md",
"hash": "507e7be407716b56fe577109b296b55cdcb48cb2b2e339a761446b4df2d730e9",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/number-of-products-per-row.md",
"id": "7da8ddcdbc3f427d14b040015fb1de9efde79d19"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/change-a-currency-symbol.md",
"hash": "4485b212ff7bdf7fd8c23dc589dbbb6263a530623b939b384297f2377b11056f",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/change-a-currency-symbol.md",
"id": "de0e6701b0e15a209ac6ed3c100b34f67237ea96"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/before-login--register-form.md",
"hash": "3435f3859ec047566696ae5474df697c99a6e1ab04168ae8f1ca831abc519d67",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/before-login--register-form.md",
"id": "5acf55bdb90174bc00a10816ca71a8846bd7040f"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/adjust-quantity-input-values.md",
"hash": "056fb597c07e0e82b92ca8a2c484166f807d3d513fa950cb5c46a8db087fe986",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/adjust-quantity-input-values.md",
"id": "06d0ddda56838dc22ad94716aca95a1f2a9ad67c"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/add-or-modify-states.md",
"hash": "d61d5051439b0cbf949be64958c8101a7c6487672a7b79dd3f528e9c1a5b408e",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/add-or-modify-states.md",
"id": "10568c46b0baa2f8ea01a8cd9dd6b2d65d35aafc"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/add-a-currency-symbol.md",
"hash": "0053a87328ee04a168c5657df26cf18f219d30669b000300ace4284ea2333186",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/add-a-currency-symbol.md",
"id": "6d63d431fa1d66af7b3b47861ccc940775eba397"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/add-a-country.md",
"hash": "9305a5ed51cc0174e19e68ebc94801c5e7d6e62e820d106f35175540b0531d10",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/add-a-country.md",
"id": "7c591c3bd31cb2942fdcb4f77da167bc9ec36207"
}
],
"categories": []
},
{
"category_title": "Contributing",
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/contributing/deciding-pr-high-impact.md",
"hash": "6d9ddea0e44e1ab9f7b12183069c7fde51df26c9bcad15d8b6f9d3ab17ee1fe0",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/contributing/deciding-pr-high-impact.md",
"id": "cee3a746887c88f92966bb72ec1847aa5d97cebd"
}
],
"categories": []
},
{
"category_title": "Data-management",
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/data-management/data-stores.md",
"hash": "7af9424c5b3fdfa5b30b658f0c3a452ac12d67390b58e9c23ef3bc71412b2a92",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/data-management/data-stores.md",
"id": "72609f6c4fecccf2881c76bb8f21fc6a2d3cfef5"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/data-management/crud-objects.md",
"hash": "9b687cd323893ed0fc012ad82e67a329ea0f41c312017bfdf3015ab7ef51a23a",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/data-management/crud-objects.md",
"id": "0214be4dc9e0ddcc6d6a175fcd2f2b9fd4c8b042"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/tools-for-low-code-development.md",
"hash": "5989c60346c29c019332b7e2faee0dfcddf063db2d12f22f7755525e99c8ba4a",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/tools-for-low-code-development.md",
"id": "9db13086ff88d75fc5d5cd7bd36820b2d372b288"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/readme.md",
"hash": "e22a09b0d39adf37cd19fa57263e1195646f766396a36571411c54c64c6ba5a1",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/readme.md",
"id": "4946a26f3f2ba63d5db65033a90b6dda3a43a4ee"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/how-to-design-a-simple-extension.md",
"hash": "893c9361b6a1f6cd8a83dc9a4500c9b3050d66ef6d567fddf56d68de01d182a1",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/how-to-design-a-simple-extension.md",
"id": "c2f4612f31cc3892bfb19c6e9859f2daccdb6fa0"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/how-to-add-your-own-store-management-links.md",
"hash": "59dfa1f941a2fafaccedf2dd9be5bc1ee8c34b7320c6589c860ad98403bcd273",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/how-to-add-your-own-store-management-links.md",
"id": "461423141956d0653582aa15956a7b0401301cf6"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-merchant-onboarding.md",
"hash": "e4a8358de815e378beb07426fd5687a0e91792f70a697574edd5031bf073c08d",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-merchant-onboarding.md",
"id": "878874b744cea792a439935c67ac833a5f7d8796"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-deactivation-and-uninstallation.md",
"hash": "cfc8cdf9e9a41e69cf40dcc1548cf7b0231c51c33c504ee800dd7525afe6d483",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-deactivation-and-uninstallation.md",
"id": "93a623f5b7ff808c92541941787bdc2101518f5a"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/extension-developer-handbook.md",
"hash": "ff87298651dff8672b0c0da8693b53cb19d1fe7e65f04a947029aeec9cc9896f",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/extension-developer-handbook.md",
"id": "9f22e410b1ff4f6e5c22f6b4567c1a676d037295"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/development-environment.md",
"hash": "811f43ef03b40b48ad72a9d787031cb10d5a17b5f4ef4a2d5c0fa4977b2d43df",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/development-environment.md",
"id": "188b18434a7e844978d4d25a4b1fbc74004294d5"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/building-your-first-extension.md",
"hash": "2aad1fe83142ff55197f3bc560fafbe2c1279ccf9bb727fea5257eed447ba694",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/building-your-first-extension.md",
"id": "bfb30a2379ec5e7c7960192b0a9175191e39aef7"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/readme.md",
"hash": "8aa7cd66683478dff1f37c767f154d35e6b78a738b604d0bc2ae31a10f920ba6",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/readme.md",
"id": "2145ad4acea5e37b1ff4354bf44a8af9d10c750a"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/developer-tools.md",
"hash": "860d227350f472b610351bf416953bf9401bf0df296ee8ebe2af9c7a9d20fc57",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/developer-tools.md",
"id": "3d8c7c1a9187dd7c96d2561bccfcfa88a8d0102a"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/developer-resources.md",
"hash": "e5417528667f6afb2de5ed1b4f7073a63268e72e163bd67a8a1daeda1e623eb0",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/developer-resources.md",
"id": "0923cb0175b15041bcecf8f9dbc60828052e1fec"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/debugging.md",
"hash": "e8407732d22ffa183db83e5875e6709bdc8dd7ac47a05280590c4b5f1021fea9",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/debugging.md",
"id": "b28ccc6fa293efebd716b510df0331fb222b8a1b",
"links": {
"../utilities/logging.md": "f673f50bdd83d5d0d7fd8d9976828f8736809510"
}
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/high-performance-order-storage/readme.md",
"hash": "aada6bfad51c9f68ddb61e6475aae137e6b2d91839afa001aa3862ecd651093b",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/high-performance-order-storage/readme.md",
"id": "214c00235e4efed0a786fd704c4bfde25952a669"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/high-performance-order-storage/hpos-overview.md",
"hash": "a7504d828aed93aa68fbaff338406682fa6169f9401b2c4de6e750d7f51c7416",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/high-performance-order-storage/hpos-overview.md",
"id": "380a11e440eb17bc76fb606fecce4ce82fc9b0cb"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-editor-development/common-tasks.md",
"hash": "a7725479ff8b34f8069f3a8262502dc82354ea50d6ccd3760c5c7f7a9ae58fe2",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-editor-development/common-tasks.md",
"id": "c09542004d78254458250704f3a68cfeb9fa7a7c"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/readme.md",
"hash": "7afc464833ea0eb33127e98cb92f951bcb4a5d9006898e56db9d1f36006970cf",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/readme.md",
"id": "f12729345d42760bd61a4a4cfdcebf6dbf2daac4"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/performance-optimization.md",
"hash": "cc54a9af29f80e413536f46e70bc17512c92de536375bded23e4893a3e95e262",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/performance-optimization.md",
"id": "280d9d16382ef5374226629751cff7fceefbfa62"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/grammar-punctuation-capitalization.md",
"hash": "cea26a41342f5e67bb3313c935726da68647827a60c2fb034f3e64dd783496bd",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/grammar-punctuation-capitalization.md",
"id": "85d910730d1daa03c86c42f2d4cbd2e146cec83c"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/coding-standards.md",
"hash": "332efc3cccdde767237087abba302fcc371d510e5f287d439de49fe99a5445cc",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/coding-standards.md",
"id": "3a47d9e0da6c415606b7ee87bb65ed24f4630be1"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reference-code/readme.md",
"hash": "49a289229235989dcb8fa990af7e5017ae37569d6103dfd044ee2d95f250f596",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reference-code/readme.md",
"id": "1934f96989f9b62e2caf7fd0ac7deb2292f161ce"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/readme.md",
"hash": "117ccaf1bf931f7a98486ac0b506c48dd4834a2f2acd76f8936a9bf6ba93171b",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/readme.md",
"id": "fc4ef6d8e26fc46ef6c9da5e0d860cf051928e12"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/extending-woocommerce-admin-reports.md",
"hash": "860c37ab2d1de9744d838e6d3b11eeadb6e6cb11afffa8dfc11021240661d843",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/extending-woocommerce-admin-reports.md",
"id": "9960520d4548fd3647c5bbc7ed4d4fcc6a76cfce"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md",
"hash": "e8265405615834b04a02f5406c38e3be8acdd01116b66f08f25f99a86a580948",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md",
"id": "10cabd954ae223ac83f84223611f6d82361f9d9d",
"links": {
"extending-woocommerce-admin-reports.md": "9960520d4548fd3647c5bbc7ed4d4fcc6a76cfce"
}
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/rest-api/readme.md",
"hash": "895a1986fd30f437c1bc2aaa618d6a06a98c65511cd257077302c2d1225787d9",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/rest-api/readme.md",
"id": "128b41ed5e9a0ed13a51de3196bd06c0297b2f33"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/rest-api/getting-started.md",
"hash": "b0bb28f6eed57c7d57dde75e8093771450db4666cbfab16d35387c39cd8d1e20",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/rest-api/getting-started.md",
"id": "10f52db2464ea2f373eff9a45ce9e8ff40214122",
"links": {
"_media/permalinks.webp": "5ac10f14cd0ca3ae45a9c089d659fee3ea4d9f22",
"_media/keys.png": "7a29fcb3f0b565f4f6983f1e12200d2c4d62907b",
"_media/postman.png": "b100bbf70f8889c26fcde6a6daca29be796071de",
"_media/insomnia.png": "a50683984998ab518b4b05433ffb1803f2ac135f",
"_media/sslerror.png": "5b6825fb0aba7ed55e1641880701c2afd1744ce1",
"_media/postman-ssl.png": "7e3523b0340acf908e98ac320f822d00a0710ac5",
"_media/insomnia-ssl.png": "84e6406407498b9936d6ef1df5e4c4cc78f75b00"
}
}
],
"categories": [
{
"category_title": "_media",
"categories": []
}
]
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/security/security-best-practices.md",
"hash": "ded14bf9c1b5402c9896c795c4c4cbe6165d465de56009c32559f854446bafda",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/security/security-best-practices.md",
"id": "ee399e729785fec50b90d73b231d321b34ab5ff9"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/security/reporting-security-issues.md",
"hash": "4a748990a0e3207c9f0828c585c1d2edce14bafee9afc331d43bda0b8ee09aa6",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/security/reporting-security-issues.md",
"id": "48caba58b4e5b44fc418bb80cf03aa48f87e4cc8"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/security/readme.md",
"hash": "b484a4886068a33cd8114066262e95a2c8036f1e11d09f0dc63141cd36d85e95",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/security/readme.md",
"id": "8b4aa089616cbeeedfec301eee17b5a00f669154"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/theme-design-ux-guidelines.md",
"hash": "bcab4deb86172df46ff775a23b921f8fb4aadd5b5c96085451609a3863642efc",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/theme-design-ux-guidelines.md",
"id": "1abe0b1ff5e3990387c7da7fa5a147430b3282f7"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/marketplace-guidelines.md",
"hash": "5ca562b2f4cabefe47b0d08334f39d56e8d3e6dc31ea347000d3bb221e51d0a1",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/marketplace-guidelines.md",
"id": "f39a348d787fd0cf4a390e70e6f0cbd36b27a283"
}
],
"categories": []
},
{
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/tutorials/readme.md",
"hash": "2ca52b9289180adad5d59865ca45c50098119b420662d727d739243e6a98131a",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/tutorials/readme.md",
"id": "0741650e7ad7567b84f776db7ddad2b8f5a598ea"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/tutorials/adding-actions-and-filters.md",
"hash": "fa6bc021c918aa168023a90e683832455dfa35abbb2561470b08002f1281356f",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/tutorials/adding-actions-and-filters.md",
"id": "3f4d6dcdbc181b11c97c438132ec13a0a8484524"
},
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/tutorials/adding-a-custom-field-to-variable-products.md",
"hash": "a6b654234e7c7f1fc3f9be49b7d00187d6f271e2d699971bade3cf9a728a780a",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/tutorials/adding-a-custom-field-to-variable-products.md",
"id": "f0a7a4a194a6e0aee6f939791f3eaebf36aebb27"
}
],
"categories": []
},
{
"category_title": "Utilities",
"posts": [
{
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/utilities/logging.md",
"hash": "8912094df57a66c047fe2177be815a40024b461a10ddd61ed297c4b2c2919d64",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/utilities/logging.md",
"id": "f673f50bdd83d5d0d7fd8d9976828f8736809510",
"links": {
"_media/log-critical.jpg": "cc802f94ac4cf125a66bc6bb77bf95b80bf4fbed"
}
}
],
"categories": [
{
"category_title": "_media",
"categories": []
}
]
}
],
"hash": "fc6ee378adfa92b706b9d0813f541bf6e8029634c0dd6623000f76047316f0af"
}

View File

@ -0,0 +1,138 @@
# Adding a Section to a Settings Tab
When youre adding building an extension for WooCommerce that requires settings of some kind, its important to ask yourself: **Where do they belong?** If your extension just has a couple of simple settings, do you really need to create a new tab specifically for it? Most likely the answer is no.
## [When to Create a Section](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-1)
Lets say we had an extension that adds a slider to a single product page. This extension doesnt have many options, just a couple:
- Auto-insert into single product page (checkbox)
- Slider Title (text field)
Thats only two options, specifically related to **Products**. We could quite easily just append them onto the core WooCommerce Products Settings (**WooCommerce > Settings > Products**), but that wouldnt be very user friendly. Users wouldnt know where to look initially so theyd have to scan all of the Products options and it would be difficult / impossible to link the options directly. Fortunately, as of WooCommerce 2.2.2, there is a new filter in place that allows you add a new **section**, beneath one of the core settings tabs.
> **Note:** This is a **Developer level** doc. If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
## [How to Create a Section](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-2)
Well go over doing this through individual functions, but you should probably create a Class that stores all of your settings methods.
The first thing you need to is add the section, which can be done like this by hooking into the `woocommerce_get_sections_products` filter:
```php
/**
* Create the section beneath the products tab
**/
add_filter( 'woocommerce_get_sections_products', 'wcslider_add_section' );
function wcslider_add_section( $sections ) {
$sections['wcslider'] = __( 'WC Slider', 'text-domain' );
return $sections;
}
```
_[wc-create-section-beneath-products.php](https://gist.github.com/woogists/2964ec01c8bea50fcce62adf2f5c1232/raw/da5348343cf3664c0bc8b6b132d8105bfcf9ca51/wc-create-section-beneath-products.php)_
Make sure you change the **wcslider** parts to suit your extensions name / text-domain. The important thing about the `woocommerce_get_sections_products` filter, is that the last part **products**, is the tab youd like to add a section to. So if you want to add a new tab to accounts section, you would hook into the `woocommerce_get_sections_accounts` filter.
## [How to Add Settings to a Section](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-3)
Now that youve got the tab, you need to filter the output of `woocommerce_get_sections_products` (or similar). You would add the settings like usual using the [**WooCommerce Settings API**](https://github.com/woocommerce/woocommerce/blob/trunk/docs/settings-api/), but check for the current section before adding the settings to the tabs settings array. For example, lets add the sample settings we discussed above to the new **wcslider** section we just created:
```php
/**
* Add settings to the specific section we created before
*/
add_filter( 'woocommerce_get_settings_products', 'wcslider_all_settings', 10, 2 );
function wcslider_all_settings( $settings, $current_section ) {
/**
* Check the current section is what we want
**/
if ( $current_section == 'wcslider' ) {
$settings_slider = array();
// Add Title to the Settings
$settings_slider[] = array( 'name' => __( 'WC Slider Settings', 'text-domain' ), 'type' => 'title', 'desc' => __( 'The following options are used to configure WC Slider', 'text-domain' ), 'id' => 'wcslider' );
// Add first checkbox option
$settings_slider[] = array(
'name' => __( 'Auto-insert into single product page', 'text-domain' ),
'desc_tip' => __( 'This will automatically insert your slider into the single product page', 'text-domain' ),
'id' => 'wcslider_auto_insert',
'type' => 'checkbox',
'css' => 'min-width:300px;',
'desc' => __( 'Enable Auto-Insert', 'text-domain' ),
);
// Add second text field option
$settings_slider[] = array(
'name' => __( 'Slider Title', 'text-domain' ),
'desc_tip' => __( 'This will add a title to your slider', 'text-domain' ),
'id' => 'wcslider_title',
'type' => 'text',
'desc' => __( 'Any title you want can be added to your slider with this option!', 'text-domain' ),
);
$settings_slider[] = array( 'type' => 'sectionend', 'id' => 'wcslider' );
return $settings_slider;
/**
* If not, return the standard settings
**/
} else {
return $settings;
}
}
```
_[wc-add-settings-section.php](https://gist.github.com/woogists/4038b83900508806c57a193a2534b845#file-wc-add-settings-section-php)_
Were hooking into the same `woocommerce_get_sections_products` filter, but this time doing a check that the `$current_section` matches our earlier defined custom section (wcslider), before adding in our new settings.
## [Using the New Settings](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-4)
You would now just use your newly created settings like you would any other WordPress / WooCommerce setting, through the [**get_option**](http://codex.wordpress.org/Function_Reference/get_option) function and the defined ID of the setting. For example, to use the previously created **wcslider_auto_insert** option, simply use the following code: `get_option( 'wcslider_auto_insert' )`
## [Conclusion](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-5)
When creating an extension for WooCommerce, think about where your settings belong before you create them. The key to building a useful product is making it easy to use for the end user, so appropriate setting placement is crucially important. For more specific information on adding settings to WooCommerce, check out the [**Settings API documentation**](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/settings-api/).

View File

@ -0,0 +1,54 @@
# Classes in WooCommerce
## [List of Classes in WooCommerce](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-1)
For a list of Classes in WooCommerce, please see the [WooCommerce Code Reference](https://woocommerce.github.io/code-reference/packages/WooCommerce-Classes.html).
## [Common Classes](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-2)
### [WooCommerce](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-3)
The main class is `woocommerce` which is available globally via the `$woocommerce` variable. This handles the main functions of WooCommerce and inits other classes, stores site-wide variables, and handles error/success messages. The woocommerce class initializes the following classes when constructed:
- `WC_Query` stored in `$woocommerce->query`
- `WC_Customer` stored in `$woocommerce->customer`
- `WC_Shipping` stored in `$woocommerce->shipping`
- `WC_Payment_Gateways` stored in `$woocommerce->payment_gateways`
- `WC_Countries` stored in `$woocommerce->countries`
Other classes are auto-loaded on demand.
View the [WooCommerce Class Code Reference](https://woocommerce.github.io/code-reference/classes/WooCommerce.html) for a full list of methods contained in this class.
### [WC_Product](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-4)
WooCommerce has several product classes responsible for loading and outputting product data. This can be loaded through PHP using:
`$product = wc_get_product( $post->ID );`
In the loop this is not always necessary since calling `the_post()` will automatically populate the global `$product` variable if the post is a product.
View the [WC_Product Code Reference](https://woocommerce.github.io/code-reference/classes/WC-Product.html) for a full list of methods contained in this class.
### [WC_Customer](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-5)
The customer class allows you to get data about the current customer, for example:
```php
global $woocommerce;
$customer_country = $woocommerce->customer->get_country();
```
View the [WC_Customer Code Reference](https://woocommerce.github.io/code-reference/classes/WC-Customer.html) for a full list of methods contained in this class.
### [WC_Cart](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-6)
The cart class loads and stores the users cart data in a session. For example, to get the cart subtotal you could use:
```php
global $woocommerce;
$cart_subtotal = $woocommerce->cart->get_cart_subtotal();
```
View the [WC_Cart Code Reference](https://woocommerce.github.io/code-reference/classes/WC-Cart.html) for a full list of methods contained in this class.

View File

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

View File

@ -0,0 +1,110 @@
# Settings API
The WooCommerce Settings API is used by extensions to display, save, and load settings. The best way to make use of the API in your extension is to create a class that extends the `WC_Settings_API` class:
```php
class My_Extension_Settings extends WC_Settings_API {
//
}
```
## Defining form fields
You can define your fields using a method called `init_form_fields` in your class constructor:
```php
$this->init_form_fields();
```
You must have your settings defined before you can load them. Setting definitions go in the `form_fields` array:
```php
/**
* Initialise gateway settings form fields.
*/
function init_form_fields() {
$this->form_fields = array(
'title' => array(
'title' => __( 'Title', 'your-text-domain' ),
'type' => 'text',
'description' => __( 'This controls the title which the user sees during checkout.', 'your-text-domain' ),
'default' => __( 'PayPal', 'your-text-domain' )
),
'description' => array(
'title' => __( 'Description', 'your-text-domain' ),
'type' => 'textarea',
'description' => __( 'This controls the description which the user sees during checkout.', 'your-text-domain' ),
'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account", 'your-text-domain' )
)
);
} // End init_form_fields()
```
(Make sure your class initializes the `form_fields` property so that the "Creation of dynamic property" error is not thrown in PHP 8.2+)
In the above example we define two settings, Title and Description. Title is a text box, whereas Description is a textarea. Notice how you can define a default value and a description for the setting itself.
Setting definitions use the following format:
```php
'setting_name' => array(
'title' => 'Title for your setting shown on the settings page',
'description' => 'Description for your setting shown on the settings page',
'type' => 'text|password|textarea|checkbox|select|multiselect',
'default' => 'Default value for the setting',
'class' => 'Class for the input element',
'css' => 'CSS rules added inline on the input element',
'label' => 'Label', // For checkbox inputs only.
'options' => array( // Array of options for select/multiselect inputs only.
'key' => 'value'
),
)
```
## Displaying your settings
Create a method called `admin_options` containing the following:
```php
function admin_options() {
?>
<h2><?php esc_html_e( 'Your plugin name', 'your-text-domain' ); ?></h2>
<table class="form-table">
<?php $this->generate_settings_html(); ?>
</table>
<?php
}
```
This will output your settings in the correct format.
## Saving your settings
To have your settings save, add your class's `process_admin_options` method to the appropriate `_update_options_` hook. For example, payment gateways should use the payment gateway hook:
```php
add_action( 'woocommerce_update_options_payment_gateways', array( $this, 'process_admin_options' ) );
```
Other types of plugins have similar hooks:
```php
add_action( 'woocommerce_update_options_shipping_methods', array( $this, 'process_admin_options' ) );
```
## Loading your settings
In the constructor you can load the settings you previously defined:
```php
// Load the settings.
$this->init_settings();
```
After that you can load your settings from the settings API. The `init_settings` method above populates the settings variable for you:
```php
// Define user set variables
$this->title = $this->settings['title'];
$this->description = $this->settings['description'];
```

View File

@ -0,0 +1,38 @@
# Using custom attributes in menus and taxonomy archives
Attributes that can be used for the layered nav are a custom taxonomy, which means you can display them in menus, or display products by attributes. This requires some work on your part, and archives must be enabled.
> **Note:** This is a **Developer level** doc. If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
# Register the taxonomy for menus
When registering taxonomies for your custom attributes, WooCommerce calls the following hook:
```php
$show_in_nav_menus = apply_filters('woocommerce_attribute_show_in_nav_menus', false, $name);
```
So, for example, if your attribute slug was `size` you would do the following to register it for menus:
```php
add_filter('woocommerce_attribute_show_in_nav_menus', 'wc_reg_for_menus', 1, 2);
function wc_reg_for_menus( $register, $name = '' ) {
if ( $name == 'pa_size' ) $register = true;
return $register;
}
```
Custom attribute slugs are prefixed with `pa_`, so an attribute called `size` would be `pa_size`
Now use your attribute in **Appearance > Menus**. You will notice, however, that it has default blog styling when you click on a link to your taxonomy term.
# Create a template
You need to theme your attribute to make it display products as you want. To do this:
1. Copy `woocommerce/templates/taxonomy-product_cat.php` into your theme folder
2. Rename the template to reflect your attribute in our example wed use `taxonomy-pa_size.php`
You should now see this template when viewing taxonomy terms for your custom attribute.

View File

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

View File

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

View File

@ -0,0 +1,162 @@
# High Performance Order Storage (HPOS)
WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues.
[High-Performance Order Storage (HPOS)](https://developer.woocommerce.com/2022/09/14/high-performance-order-storage-progress-report/) also previously known as “Custom Order Tables” is a solution that provides an easy-to-understand and solid database structure specifically designed for eCommerce needs. It uses the WooCommerce CRUD design to store order data in custom tables optimized for WooCommerce queries with minimal impact on the stores performance.
In January 2022, we published the [initial plan for the Custom Order Tables feature](https://developer.woocommerce.com/2022/01/17/the-plan-for-the-woocommerce-custom-order-table/) and since then, weve been working hard to bring the High-Performance Order Storage (HPOS) to WooCommerce Core. In May 2022, we invited you to [test the order migration process](https://developer.woocommerce.com/2022/05/16/call-for-early-testing-custom-order-table-migrations/) and provide feedback on how our initial work performs on real stores of varied configurations.
From WooCommerce 8.2, released on October 2023, [High-Performance Order Storage (HPOS)](https://developer.woocommerce.com/2022/09/14/high-performance-order-storage-progress-report/) is officially released under the stable flag and will be enabled by default for new installations.
## [Whats New with High-Performance Order Storage?](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-1)
Bringing High-Performance Order Storage (HPOS) to WooCommerce improves these three essential properties for eCommerce stores.
1/ **Scalability**
The rise in the number of customers and customer orders increases the load on your stores database making it difficult to handle customer order requests and deliver a seamless user experience.
With High-Performance Order Storage, you get dedicated tables for data like orders and order addresses and thus dedicated indexes which results in fewer read/write operations and fewer busy tables. This feature enables eCommerce stores of all shapes and sizes to scale their business to their maximum potential without expert intervention.
2/ **Reliability**
High-Performance Order Storage makes implementing and restoring targeted data backup easier. Youll no longer need to worry about losing orders, inventory numbers, or client information with reliable backup in these custom order tables. Itll also facilitate implementing read/write locks and prevent race conditions.
3/ **Simplicity**
You no longer have to go through a single huge database to locate underlying data and WooCommerce entries.
With High-Performance Order Storage, you can easily browse through the separate tables and easy-to-handle entries, independent of the table `_posts`, to find data or understand the table structure. It also lets you easily develop new plugins, implement designs for shops and products, and modify WooCommerce with more flexibility.
## [Background](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-2)
Before the release of version 8.2, WooCommerce relied on the `_post` and `_postmeta` table structures to store order information, which has served well over the years.
However, High-Performance Order Storage introduces dedicated tables for data like orders and order addresses and thus dedicated indexes which results in fewer read/write operations and fewer busy tables. This feature enables eCommerce stores of all shapes and sizes to scale their business to their maximum potential without expert intervention.
The order data is synced from `_posts` and `_postmeta` table to four custom order tables:
1. `_wc_orders`
2. `_wc_order_addresses`
3. `_wc_order_operational_data`
4. `_wc_orders_meta`
## [Enabling the feature](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-3)
From WooCommerce 8.2, released on October 2023, HPOS is enabled by default for new installations. Existing stores can check [How to enable HPOS](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/enable-hpos.md)
## [Database tables](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-4)
A number of database tables are used to store order data by HPOS. The `get_all_table_names` method in [the OrdersTableDataStore class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) will return the names of all the tables.
## [Authoritative tables](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-5)
At any given time, while the HPOS feature is enabled, there are two roles for the involved database tables: _authoritative_ and _backup_. The authoritative tables are the working tables, where order data will be stored to and retrieved from during normal operation of the store. The _backup_ tables will receive a copy of the authoritative data whenever [synchronization](#synchronization) happens.
If the `woocommerce_custom_orders_table_enabled` options is set to true, HPOS is active and [the new tables](#database-tables) are authoritative, while the posts and post meta tables act as the backup tables. If the option is set to false, it's the other way around. The option can be changed via admin UI (WooCommerce - Settings - Advanced - Custom data stores).
[The CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php) hooks on the `woocommerce_order_data_store` filter so that `WC_Data_Store::load( 'order' );` will return either an instance of [OrdersTableDataStore](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) or an instance of [WC_Order_Data_Store_CPT](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php), depending on which are the authoritative tables.
In order to preserve data integrity, switching the authoritative tables (from the new tables to the posts table or the other way around) isn't allowed while there are orders pending synchronization.
## [Synchronization](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-6)
_Synchronization_ is the process of applying all the pending changes in the authoritative tables to the backup tables. _Orders pending synchronization_ are orders that have been modified in the authoritative tables but the changes haven't been applied to the backup tables yet.
This can happen in a number of ways:
### Immediate synchronization
If the `woocommerce_custom_orders_table_data_sync_enabled` setting is set to true, synchronization happens automatically and immediately as soon as the orders are changed in the authoritative tables.
### Manual synchronization
When immediate synchronization is disabled, it can be triggered manually via command line as follows: `wp wc cot sync`. It can also be triggered programmatically as follows:
```php
$synchronizer = wc_get_container()->get(Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::class);
$order_ids = $synchronizer->get_next_batch_to_process( $batch_size );
if ( count( $order_ids ) ) {
$synchronizer->process_batch( $order_ids );
}
```
where `$batch_size` is the maximum count of orders to process.
### Scheduled synchronization
If immediate synchronization gets activated (`woocommerce_custom_orders_table_data_sync_enabled` is set to true) while there are orders pending synchronization, an instance of [DataSynchronizer](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php) will be enqueued using [BatchProcessingController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php) so that the synchronization of created/modified/deleted orders will happen in batches via scheduled actions. This scheduling happens inside [CustomOrdersTableController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php), by means of hooking into `woocommerce_update_options_advanced_custom_data_stores`.
If for some reason immediate synchronization is already active but synchronization is not scheduled, a trick to restart it is to go to the settings page (WooCommerce - Settings - Advanced - Custom data stores) and hit "Save" even without making any changes. As long as "Keep the posts table and the orders tables synchronized" is checked the synchronization scheduling will happen, even if it was checked before.
If the `woocommerce_auto_flip_authoritative_table_roles` option is set to true (there's a checkbox for it in the settings page), the authoritative tables will be switched automatically once all the orders have been synchronized. This is handled by [the CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php).
### Deletion synchronization
Synchronization of order deletions is tricky: if an order exists in one set of tables (new tables or posts) but not in the other, it's not clear if the missing orders need to be created or if the existing orders need to be deleted. Theoretically, the orders missing from the backup tables imply the former and the orders missing from the authoritative tables imply the latter; but that's dangerous as a bug in the involved code could easily lead to the deletion of legitimate orders.
To achieve a robust order deletion synchronization mechanism the following is done. Whenever an order is deleted and immediate synchronization is disabled, a record is created in the `wp_wc_orders_meta` table that has `deleted_from` as the key and the name of the authoritative table the order was deleted from (`wp_wc_orders` or the posts table). Then at synchronization time these records are processed (the corresponding orders are deleted from the corresponding tables) and deleted afterwards.
An exception to the above are the [placeholder records](#placeholder-records): these are deleted immediately when the corresponding order is deleted from `wp_wc_orders`, even if immediate synchronization is disabled.
When the “**High-Performance Order Storage**” and “**Compatibility mode**” are enabled, WooCommerce populates the HPOS tables with data from posts & postmeta tables. The synchronization between the tables is [explained in detail in this document](https://developer.woocommerce.com/2022/09/29/high-performance-order-storage-backward-compatibility-and-synchronization/#synchronization).
> You can find a deeper explanation about the synchronization between the tables in [this document about high-performance-order-storage-backward-compatibility-and-synchronization](https://developer.woocommerce.com/2022/09/29/high-performance-order-storage-backward-compatibility-and-synchronization/#synchronization).
## [Placeholder records](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-7)
Order IDs must match in both the authoritative tables and the backup tables, otherwise synchronization wouldn't be possible. The order IDs that are compared for order identification and synchronization purposes are the ones from the `id` field in both the `wp_wc_orders` table and the posts table.
If the posts table is authoritative, achieving an order ID match is easy: the record in `wp_wc_orders` is created with the same ID and that's it. However, when the new orders tables are authoritative there's a problem: the posts table is used to store multiple types of data, not only orders; and by the time synchronization needs to happen, a non-order post could already exist having the same ID as the order to synchronize.
To solve this, _placeholder records_ are used. Whenever the new orders tables are authoritative and immediate synchronization is disabled, creating a new order will cause a record with post type `shop_order_placehold` and the same ID as the order to be created in the posts table; this effectively "reserves" the order ID in the posts table. Then, at synchronization time, the record is filled appropriately and its post type is changed to `shop_order`.
## [Order Data Storage](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-8)
You can switch between data stores freely to sync the data between the tables.
- If you select **“WordPress Post Tables”**, the system will save the order data within `_post` and `_postmeta` tables. The order tables are not utilized in this scenario.
![Select WordPress Post Tables](https://woocommerce.com/wp-content/uploads/2023/10/image-18.png?w=650)
- If you select **“High-Performance Order Storage”**, the system will save the order data within the new WooCommerce order tables
![Select High-Performance Order Storage](https://woocommerce.com/wp-content/uploads/2023/10/image-19.png?w=650)
- If you select **“WordPress Post Tables”** and **“Enable compatibility mode”**, the system will sync the order data between the posts/postmeta and the WooCommerce order tables.
![Select WordPress Post Tables and Enable compatibility mode](https://woocommerce.com/wp-content/uploads/2023/10/image-20.png?w=650)
## [Incompatible Plugins](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-9)
If you are using a plugin that is not compatible with High-Performance Order Storage, then the HPOS option will be disabled under **WooCommerce > Settings > Advanced > Features**.
![Incompatible plugin](https://woocommerce.com/wp-content/uploads/2023/10/image-21.png?w=650)
- You can click on “**View and manage**” to review the list of incompatible plugins
- Or you can visit `https://example.com/wp-admin/plugins.php?plugin_status=incompatible_with_feature&feature_id=custom_order_tables` to review the list of incompatible plugins (please replace `example.com` with your site domain)
![Plugins page](https://woocommerce.com/wp-content/uploads/2023/10/image-22.png?w=650)
> **Note:** If you are using a third-party extension that isnt working properly with High-Performance Order Storage then please notify the developers of the extension and ask them to update their extension to add support for HPOS. Its up to the extension developers to add support for HPOS. We have [developer resources and documentation](https://developer.woocommerce.com/2022/09/14/high-performance-order-storage-progress-report/) available to help with their integration efforts.
## [Disabling HPOS](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-10)
If you encounter problems or if you need to continue working with plugins that are not yet compatible with HPOS, then we recommend temporarily switching back to **WordPress posts storage**.
To do this, navigate to **WooCommerce ▸ Settings ▸ Advanced ▸ Features** and start by making sure that **compatibility mode** is enabled. If it was not already enabled, you may find you need to wait for some time while order data is synchronized across data-stores.
![WooCommerce ▸ Settings ▸ Advanced ▸ Features Screen](https://woocommerce.com/wp-content/uploads/2023/10/hpos-feature-settings.png?w=650)
Once synchronization has completed, you can select **WordPress posts storage (legacy)** as your preferred option. You can also disable compatibility mode at this point. Once you are ready to re-enable HPOS, simply follow the instructions posted at the [start of this doc](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-3). Finally, remember to save this page between changes!
As noted earlier, we also strongly recommend reaching out to the support teams of any plugins that are incompatible, so they can take corrective action.

View File

@ -0,0 +1,25 @@
# How to enable HPOS
From WooCommerce 8.2, released on October 2023, HPOS is enabled by default for new installations. Existing stores can switch to the “High-Performance Order Storage” from “WordPress Posts Storage” by following the below steps.
To activate High-Performance Order Storage, existing stores will first need to get both the posts and orders table in sync, which can be done by turning on the setting “**Enable compatibility mode (synchronizes orders to the posts table)**“.
1/ Navigate to **WooCommerce > Settings > Advanced > Features**
2/ Turn on the **“Enable compatibility mode (synchronizes orders to the posts table)”** setting.
![Enable HPOS Screen](https://woocommerce.com/wp-content/uploads/2023/10/New-Project-4.jpg?w=650)
3/ Once this setting is activated, background actions will be scheduled.
- The action `wc_schedule_pending_batch_process` checks whether there are orders that need to be backfilled.
- If there are, it schedules another action `wc_run_batch_process` that actually backfills the orders to post storage.
- You can either wait for these actions to run on their own, which should be quite soon, or you can go to **WooCommerce > Status > Scheduled Actions**, find the actions and click on the run button.
- The action will backfill 25 orders at a time, if there are more orders to be synced, then more actions will be scheduled as soon as the previous actions are completed.
![wc_schedule_pending_batch_process Screen](https://woocommerce.com/wp-content/uploads/2023/10/2.jpg?w=650)
![wc_run_batch_process Screen](https://woocommerce.com/wp-content/uploads/2023/10/New-Project-5.jpg?w=650)
4/ After both tables are successfully synchronized, youll be able to select the option to switch to High-Performance Order Storage (HPOS).
- It is advisable to maintain compatibility mode for some time to ensure a seamless transition. In case of any issues, reverting to the post table can be done instantly.

View File

@ -1,86 +0,0 @@
# High Performance Order Storage (HPOS)
WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues, and that's why HPOS (High-Performance Order Storage) was developed. HPOS is the WooCommerce engine that stores orders in dedicated tables.
HPOS is also referred to as COT (Custom Order Tables) in some parts of the code, that's the early name of the engine.
There are a number of settings that control HPOS operation. Boolean settings are stored using the usual WooCommerce convention: `yes` for enabled ("set to true"), `no` for disabled ("set to false").
Most of the code related to HPOS is in [src/Internal/DataStores/Orders](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/Internal/DataStores/Orders).
## Database tables
A number of database tables are used to store order data by HPOS. The `get_all_table_names` method in [the OrdersTableDataStore class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) will return the names of all the tables.
## Enabling the feature
For HPOS to be usable, the HPOS feature must first be enabled. This should be done programmatically via [the features controller](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/Features/FeaturesController.php), or via admin UI (WooCommerce - Settings - Advanced - Features). The feature enable option name for HPOS is `woocommerce_feature_custom_order_tables_enabled`. The required database tables will be created automatically once the feature is enabled.
## Authoritative tables
At any given time, while the HPOS feature is enabled, there are two roles for the involved database tables: _authoritative_ and _backup_. The authoritative tables are the working tables, where order data will be stored to and retrieved from during normal operation of the store. The _backup_ tables will receive a copy of the authoritative data whenever [synchronization](#synchronization) happens.
If the `woocommerce_custom_orders_table_enabled` options is set to true, HPOS is active and [the new tables](#database-tables) are authoritative, while the posts and post meta tables act as the backup tables. If the option is set to false, it's the other way around. The option can be changed via admin UI (WooCommerce - Settings - Advanced - Custom data stores).
[The CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php) hooks on the `woocommerce_order_data_store` filter so that `WC_Data_Store::load( 'order' );` will return either an instance of [OrdersTableDataStore](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) or an instance of [WC_Order_Data_Store_CPT](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php), depending on which are the authoritative tables.
In order to preserve data integrity, switching the authoritative tables (from the new tables to the posts table or the other way around) isn't allowed while there are orders pending synchronization.
## Synchronization
_Synchronization_ is the process of applying all the pending changes in the authoritative tables to the backup tables. _Orders pending synchronization_ are orders that have been modified in the authoritative tables but the changes haven't been applied to the backup tables yet.
This can happen in a number of ways:
### Immediate synchronization
If the `woocommerce_custom_orders_table_data_sync_enabled` setting is set to true, synchronization happens automatically and immediately as soon as the orders are changed in the authoritative tables.
### Manual synchronization
When immediate synchronization is disabled, it can be triggered manually via command line as follows: `wp wc cot sync`. It can also be triggered programmatically as follows:
```php
$synchronizer = wc_get_container()->get(Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::class);
$order_ids = $synchronizer->get_next_batch_to_process( $batch_size );
if ( count( $order_ids ) ) {
$synchronizer->process_batch( $order_ids );
}
```
where `$batch_size` is the maximum count of orders to process.
### Scheduled synchronization
If immediate synchronization gets activated (`woocommerce_custom_orders_table_data_sync_enabled` is set to true) while there are orders pending synchronization, an instance of [DataSynchronizer](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php) will be enqueued using [BatchProcessingController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php) so that the synchronization of created/modified/deleted orders will happen in batches via scheduled actions. This scheduling happens inside [CustomOrdersTableController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php), by means of hooking into `woocommerce_update_options_advanced_custom_data_stores`.
If for some reason immediate synchronization is already active but synchronization is not scheduled, a trick to restart it is to go to the settings page (WooCommerce - Settings - Advanced - Custom data stores) and hit "Save" even without making any changes. As long as "Keep the posts table and the orders tables synchronized" is checked the synchronization scheduling will happen, even if it was checked before.
If the `woocommerce_auto_flip_authoritative_table_roles` option is set to true (there's a checkbox for it in the settings page), the authoritative tables will be switched automatically once all the orders have been synchronized. This is handled by [the CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php).
### Deletion synchronization
Synchronization of order deletions is tricky: if an order exists in one set of tables (new tables or posts) but not in the other, it's not clear if the missing orders need to be created or if the existing orders need to be deleted. Theoretically, the orders missing from the backup tables imply the former and the orders missing from the authoritative tables imply the latter; but that's dangerous as a bug in the involved code could easily lead to the deletion of legitimate orders.
To achieve a robust order deletion synchronization mechanism the following is done. Whenever an order is deleted and immediate synchronization is disabled, a record is created in the `wp_wc_orders_meta` table that has `deleted_from` as the key and the name of the authoritative table the order was deleted from (`wp_wc_orders` or the posts table). Then at synchronization time these records are processed (the corresponding orders are deleted from the corresponding tables) and deleted afterwards.
An exception to the above are the [placeholder records](#placeholder-records): these are deleted immediately when the corresponding order is deleted from `wp_wc_orders`, even if immediate synchronization is disabled.
## Placeholder records
Order IDs must match in both the authoritative tables and the backup tables, otherwise synchronization wouldn't be possible. The order IDs that are compared for order identification and synchronization purposes are the ones from the `id` field in both the `wp_wc_orders` table and the posts table.
If the posts table is authoritative, achieving an order ID match is easy: the record in `wp_wc_orders` is created with the same ID and that's it. However, when the new orders tables are authoritative there's a problem: the posts table is used to store multiple types of data, not only orders; and by the time synchronization needs to happen, a non-order post could already exist having the same ID as the order to synchronize.
To solve this, _placeholder records_ are used. Whenever the new orders tables are authoritative and immediate synchronization is disabled, creating a new order will cause a record with post type `shop_order_placehold` and the same ID as the order to be created in the posts table; this effectively "reserves" the order ID in the posts table. Then, at synchronization time, the record is filled appropriately and its post type is changed to `shop_order`.

View File

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

View File

@ -0,0 +1,160 @@
# Translating WooCommerce
WooCommerce is already translated into several languages and is translation-ready right out of the box. All thats needed is a translation file for your language.
There are several methods to create a translation, most of which are outlined in the WordPress Codex. In most cases you can contribute to the project on [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/).
To create custom translations you can consider using [Poedit](https://poedit.net/).
## Set up WordPress in your language
To set your WordPress site's language:
1. Go to `WP Admin » Settings » General` and adjust the `Site Language`.
2. Go to `WP Admin » Dashboard » Updates` and click the `Update Translations` button.
Once this has been done, the shop displays in your locale if the language file exists. Otherwise, you need to create the language files (process explained below).
## Contributing your localization to core
We encourage contributions to our translations. If you want to add translated strings or start a new translation, simply register at WordPress.org and submit your translations to [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/) for approval.
## Translating WooCommerce into your language
Both stable and development versions of WooCommerce are available for translation. When you install or update WooCommerce, WordPress will automatically fetch a 100% complete translation for your language. If such a translation isn't available, you can either download it manually or contribute to complete the translation, benefiting all users.
If youre new to translating, check out the [translators handbook](https://make.wordpress.org/polyglots/handbook/tools/glotpress-translate-wordpress-org/) to get started.
### Downloading translations from translate.wordpress.org manually
1. Go to [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/woocommerce) and look for your language in the list.
2. Click the title to be taken to the section for that language.
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/2016-02-17-at-09.57.png)
3. Click the heading under `Set/Sub Project` to view and download a Stable version.
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/2016-02-17-at-09.59.png)
4. Scroll to the bottom for export options. Export a `.mo` file for use on your site.
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/2016-02-17-at-10.00.png)
5. Rename this file to `woocommerce-YOURLANG.mo` (e.g., Great Britain English should be `en_GB`). The corresponding language code can be found by going to [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/) and opening the desired language. The language code is visible in the upper-right corner.
![screenshot](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-17-at-09.44.53.png)
6. Upload to your site under `wp-content/languages/woocommerce/`. Once uploaded, this translation file may be used.
## Creating custom translations
WooCommerce includes a language file (`.pot` file) that contains all of the English text. You can find this language file inside the plugin folder in `woocommerce/i18n/languages/`.
## Creating custom translations with PoEdit
WooCommerce comes with a `.pot` file that can be imported into PoEdit to translate.
To get started:
1. Open PoEdit and select `Create new translation from POT template`.
2. Choose `woocommerce.pot` and PoEdit will show the catalog properties window.
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/Screen-Shot-2013-05-09-at-10.16.46.png)
3. Enter your name and details, so other translators know who you are, and click `OK`.
4. Save your `.po` file. Name it based on what you are translating to, i.e., a GB translation is saved as `woocommerce-en_GB.po`. Now the strings are listed.
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/Screen-Shot-2013-05-09-at-10.20.58.png)
5. Save after translating strings. The `.mo` file is generated automatically.
6. Update your `.po` file by opening it and then go to `Catalog » Update from POT file`.
7. Choose the file and it will be updated accordingly.
## Making your translation upgrade safe
> **Note:** We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/). If you need to further customize a snippet, or extend its functionality, we highly recommend [Codeable](https://codeable.io/?ref=z4Hnp), or a [Certified WooExpert](https://woocommerce.com/experts/).
WooCommerce keeps translations in `wp-content/languages/plugins`, like all other plugins. But if you wish to include a custom translation, you can use the directory `wp-content/languages/woocommerce`, or you can use a snippet to load a custom translation stored elsewhere:
```php
// Code to be placed in functions.php of your theme or a custom plugin file.
add_filter( 'load_textdomain_mofile', 'load_custom_plugin_translation_file', 10, 2 );
/**
* Replace 'textdomain' with your plugin's textdomain. e.g. 'woocommerce'.
* File to be named, for example, yourtranslationfile-en_GB.mo
* File to be placed, for example, wp-content/languages/textdomain/yourtranslationfile-en_GB.mo
*/
function load_custom_plugin_translation_file( $mofile, $domain ) {
if ( 'textdomain' === $domain ) {
$mofile = WP_LANG_DIR . '/textdomain/yourtranslationfile-' . get_locale() . '.mo';
}
return $mofile;
}
```
## Other tools
There are some other third-party tools that can help with translations. The following list shows a few of them.
### Loco Translate
[Loco Translate](https://wordpress.org/plugins/loco-translate/) provides in-browser editing of WordPress translation files and integration with automatic translation services.
### Say what?
[Say what?](https://wordpress.org/plugins/say-what/) allows to effortlessly translate or modify specific words without delving into a WordPress theme's `.po` file.
### String locator
[String Locator](https://wordpress.org/plugins/string-locator/) enables quick searches within themes, plugins, or the WordPress core, displaying a list of files with the matching text and its line number.
## FAQ
### Why some strings on the Checkout page are not being translated?
You may see that some of the strings are not being translated on the Checkout page. For example, in the screenshot below, `Local pickup` shipping method, `Cash on delivery` payment method and a message related to Privacy Policy are not being translated to Russian while the rest of the form is indeed translated:
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/not_translated.jpg)
This usually happens when you first install WooCommerce and select default site language (English) and later change the site language to another one. In WooCommerce, the strings that have not been translated in the screenshot, are stored in the database after the initial WooCommerce installation. Therefore, if the site language is changed to another one, there is no way for WooCommerce to detect a translatable string since these are database entries.
In order to fix it, navigate to WooCommerce settings corresponding to the string you need to change and update the translation there directly. For example, to fix the strings in our case above, you would need to do the following:
**Local pickup**:
1. Go to `WooCommerce » Settings » Shipping » Shipping Zones`.
2. Select the shipping zone where "Local pickup" is listed.
3. Open "Local pickup" settings.
4. Rename the method using your translation.
5. Save the setting.
**Cash on delivery**:
1. Go to `WooCommerce » Settings » Payments`.
2. Select the "Cash on delivery" payment method.
3. Open its settings.
4. Rename the method title, description, and instructions using your translation.
5. Save the setting.
**Privacy policy message**:
1. Go to `WooCommerce » Settings » Accounts & Privacy`.
2. Scroll to the "Privacy policy" section.
3. Edit both the `Registration privacy policy` and `Checkout privacy policy` fields with your translation.
4. Save the settings.
Navigate back to the Checkout page translations should be reflected there.
### I have translated the strings I needed, but some of them dont show up translated on the front end. Why?
If some of your translated strings dont show up as expected on your WooCommerce site, the first thing to check is if these strings have both a Single and Plural form in the Source text section. To do so, open the corresponding translation on [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/), e.g. [the translation for Product and Products](https://translate.wordpress.org/projects/wp-plugins/woocommerce/stable/de/default/?filters%5Bstatus%5D=either&filters%5Boriginal_id%5D=577764&filters%5Btranslation_id%5D=24210880).
This screenshot shows that the Singular translation is available:
![screenshot](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-17-at-10.10.06.png)
While this screenshot shows that the Plural translation is not available:
![screenshot](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-17-at-10.10.21.png)

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Developing extensions for the product editor
# Product Editor Development Handbook
> ⚠️ **Notice:** This documentation is currently a **work in progress**. Please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
@ -8,11 +8,33 @@ The product editor's UI consists of Groups (currently rendered as tabs), Section
![Product editor structure](https://woocommerce.files.wordpress.com/2023/09/groups-sections-fields.jpg)
The form's structure is defined in PHP using a Template. The template can be modified by using the Block Template API to add new Groups, Sections, and Fields as well as remove existing ones.
The form's structure is defined in PHP using a Template, which is a tree structure of blocks. The template can be modified by using the Template API to add new Groups, Sections, and Fields as well as remove existing ones.
Many extensibility implementations can be done using only the PHP-based Block Template API. More complex interactivity can be implemented using JavaScript and React (the same library used to implement the core blocks used in the product editor). [@woocommerce/create-product-editor-block](../../packages/js/create-product-editor-block/README.md) can help scaffold a development environment with JavaScript and React.
For more information about when to perform template modifications, see the [block template lifecycle](./block-template-lifecycle.md).
Many extensibility implementations can be done using only the PHP-based Block Template API alongside our library of [generic blocks](../../packages/js/product-editor/src/blocks/generic/README.md). More complex interactivity can be implemented using JavaScript and React (the same library used to implement the core blocks used in the product editor). [@woocommerce/create-product-editor-block](../../packages/js/create-product-editor-block/README.md) can help scaffold a development environment with JavaScript and React.
## Index
## Declaring compatibility with the product editor
- [Common tasks](common-tasks.md)
To declare compatibility with the product editor, add the following to your plugin's root PHP file:
```php
use Automattic\WooCommerce\Utilities\FeaturesUtil;
add_action(
'before_woocommerce_init',
function() {
if ( class_exists( FeaturesUtil::class ) ) {
FeaturesUtil::declare_compatibility( 'product_block_editor', plugin_basename( __FILE__ ), true );
}
}
);
```
Please note that this check is currently not being enforced: the product editor can still be enabled even without this declaration. However, we might enforce this in the future, so it is recommended to declare compatibility as soon as possible.
## Related documentation
- [Examples on Template API usage](../../plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md)
- [Related hooks and Template API documentation](../../plugins/woocommerce/src/Admin/BlockTemplates/README.md)
- [Generic blocks documentation](../../packages/js/product-editor/src/blocks/generic/README.md)

View File

@ -0,0 +1,63 @@
# Block template lifecycle
A block template is a tree structure of blocks that define the product editor's form structure.
A template can be modified by using the block template API to add new blocks (groups, sections, and fields) as well as remove existing ones.
A template is implemented in PHP and sent to the client. The client then renders the template using React.
The lifecycle of a template is as follows:
- [Creation](#creation)
- [Block addition and removal](#block-addition-and-removal)
- [Actions](#actions)
- [Registration](#registration)
- [Sent to client](#sent-to-client)
- [Rendered on client](#rendered-on-client)
## Creation
A template instance is created by instantiating a class that implements the `Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface` interface.
**A template should be instantiated in or after an `init` action hook, priority 4 or higher.**
## Block addition and removal
After a template instance is created, blocks can be added to or removed from a template using the `add_block()` and `remove_block()` methods, or similar methods that are specific to the type of block being added or removed, such as `add_section()` and `remove_section()`.
Blocks can be added or removed immediately after instantiation.
See the [Automattic\WooCommerce\Admin\BlockTemplates](../../plugins/woocommerce/src/Admin/BlockTemplates/README.md) documentation for more information about these methods.
### Actions
The following actions are fired when blocks are added to or removed from a template, to support extensibility:
- `woocommerce_product_editor_block_template_{template_name}_after_add_block_{block_id}`
- `woocommerce_product_editor_block_template_after_add_block`
- `woocommerce_product_editor_block_template_{template_name}_after_remove_block_{block_id}`
- `woocommerce_product_editor_block_template_after_remove_block`
**In order for your action hooks to be called for all block additions and removals for a template, you should call `add_action()` for each of these hooks before the template is instantiated, in or before an `init` action hook, priority 3 or lower.**
See the [Automattic\WooCommerce\Admin\BlockTemplates](../../plugins/woocommerce/src/Admin/BlockTemplates/README.md) documentation for more information about these hooks.
## Registration
After a template is instantiated, it can be registered with the `Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry`.
Registration is required in order for the template to be sent to the client.
Blocks can be added or removed from a template before or after it is registered, but the template cannot be modified after it is sent to the client.
**In order for the template to be sent to the client, it should be in or before the `admin_enqueue_scripts` action hook, priority 9 or lower.**
## Sent to client
A template is sent to the client in or after the `admin_enqueue_scripts` action hook, priority 10 or higher.
Any template modification after this point will not be sent to the client.
## Rendered on client
When the template is rendered on the client, all blocks in the template have their `hideConditions` evaluated to determine whether they should be rendered or not.

View File

@ -1,34 +0,0 @@
# Common tasks
> ⚠️ **Notice:** This documentation is currently a **work in progress**. Please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
## Adding a group/section/field next to an existing one
Here's a snippet that adds a new block to the catalog section, between the first and second fields (order 15):
```php
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
if ( ! function_exists( 'YOUR_PREFIX_add_block_after_categories' ) ) {
/**
* Add a new block to the template.
*/
function YOUR_PREFIX_add_block_after_categories( BlockInterface $product_categories_field ) {
$product_categories_field->get_parent()->add_block(
[
'id' => 'your-prefix-id',
'blockName' => 'woocommerce/product-checkbox-field',
'order' => $product_categories_field->get_order() + 5,
'attributes' => [
'label' => 'Your Checkbox',
'property' => 'your_checkbox_bool_property',
],
]
);
}
}
add_action( 'woocommerce_block_template_area_product-form_after_add_block_product-categories', 'YOUR_PREFIX_add_block_after_categories' );
```
Result:
![Adding field next to other field](https://woocommerce.files.wordpress.com/2023/09/adding-field-next-to-other-field.png)

View File

@ -0,0 +1,276 @@
# WooCommerce core critical flows
We have identified what we consider to be our most critical user flows within WooCommerce Core. These flows will help us focus and prioritize our testing efforts. They will also help us consider the impact of changes and priority of issues.
These flows will continually evolve as the platform evolves with flows updated, added or re-prioritised.
## Shopper critical flow areas
- 🛒 [Shopper > Shop](#shopper---shop)
- 🛒 [Shopper > Product](#shopper---product)
- 🛒 [Shopper > Cart](#shopper---cart)
- 🛒 [Shopper > Checkout](#shopper---checkout)
- 🛒 [Shopper > Email](#shopper---email)
- 🛒 [Shopper > My Account](#shopper---my-account)
## Merchant critical flow areas
- 💳 [Merchant > Onboarding](#merchant---onboarding)
- 💳 [Merchant > Dashboard](#merchant---dashboard)
- 💳 [Merchant > Settings](#merchant---settings)
- 💳 [Merchant > Coupons](#merchant---coupons)
- 💳 [Merchant > Marketing](#merchant---marketing)
- 💳 [Merchant > Analytics](#merchant---analytics)
- 💳 [Merchant > Products](#merchant---products)
- 💳 [Merchant > Orders](#merchant---orders)
- 💳 [Merchant > Customers](#merchant---customers)
- 💳 [Merchant > Email](#merchant---email)
- 💳 [Merchant > Plugins](#merchant---plugins)
- 💳 [Merchant > My Subscriptions](#merchant---my-subscriptions)
- 💳 [Merchant > Pages](#merchant---pages)
- 💳 [Merchant > Posts](#merchant---posts)
### Shopper - Shop
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------------------------- | --------------------------------------- |
| Shopper | Shop | Search Store | shopper/shop-search-browse-sort.spec.js |
| Shopper | Shop | Browse by categories | shopper/shop-search-browse-sort.spec.js |
| Shopper | Shop | Can sort items | shopper/shop-search-browse-sort.spec.js |
| Shopper | Shop | Add Simple Product to Cart (from shop page) | shopper/cart.spec.js |
| Shopper | Shop | Display shop catalog | |
| Shopper | Shop | Products by tag | |
| Shopper | Shop | Products by attribute | |
| Shopper | Shop | Use product filters | |
| Shopper | Shop | Display product showcase blocks correctly | |
| Shopper | Shop | Navigation menu default links | |
### Shopper - Product
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ---------------------------------------------------- | -------------------------------- |
| Shopper | Product | Add Simple Product to Cart | shopper/product-simple.spec.js |
| Shopper | Product | Add Grouped Product to Cart | shopper/product-grouped.spec.js |
| Shopper | Product | Variable Product info updates depending on variation | shopper/product-variable.spec.js |
| Shopper | Product | Add Variable Product to Cart | shopper/product-variable.spec.js |
| Shopper | Product | Display up-sell product | |
| Shopper | Product | Display releated products | |
| Shopper | Product | Display reviews | |
| Shopper | Product | Add review | |
| Shopper | Product | View product images | |
| Shopper | Product | View product descriptions | |
### Shopper - Cart
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------------------------ | ---------------------------------- |
| Shopper | Cart | Add to cart redirects to cart when enabled | shopper/cart-redirection.spec.js |
| Shopper | Cart | View cart | shopper/cart.spec.js |
| Shopper | Cart | Update product quantity within limits | shopper/cart.spec.js |
| Shopper | Cart | Remove products from cart | shopper/cart.spec.js |
| Shopper | Cart | Apply all coupon types | shopper/cart-coupons.spec.js |
| Shopper | Cart | Display shipping options by address | shopper/calculate-shipping.spec.js |
| Shopper | Cart | View empty cart | |
| Shopper | Cart | Display correct tax | |
| Shopper | Cart | Respect coupon usage contraints | |
| Shopper | Cart | Display cross-sell products | |
| Shopper | Cart | Proceed to checkout | |
### Shopper - Checkout
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ---------------------------------------- | --------------------------------------- |
| Shopper | Checkout | Correct item in Order Review | shopper/checkout.spec.js |
| Shopper | Checkout | Can add shipping address | shopper/checkout.spec.js |
| Shopper | Checkout | Guest can place order | shopper/checkout.spec.js |
| Shopper | Checkout | Create an account | shopper/checkout-create-account.spec.js |
| Shopper | Checkout | Login to existing account | shopper/checkout-login.spec.js |
| Shopper | Checkout | Existing customer can place order | shopper/checkout.spec.js |
| Shopper | Checkout | Use all coupon types | shopper/checkout-coupons.spec.js |
| Shopper | Checkout | View checkout | |
| Shopper | Checkout | Receive warnings when form is incomplete | |
| Shopper | Checkout | Add billing address | |
| Shopper | Checkout | Respect coupon usage contraints | |
| Shopper | Checkout | Display correct tax in checkout | |
| Shopper | Checkout | View order confirmation page | |
### Shopper - Email
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------------------- | ------------------------------------- |
| Shopper | Email | Customer Account Emails Received | shopper/ |
| Shopper | Email | Customer Order Detail Emails Received | shopper/order-email-receiving.spec.js |
### Shopper - My Account
| User Type | Flow Area | Flow Name | Test File |
| --------- | ---------- | ------------------------- | ----------------------------------------- |
| Shopper | My Account | Create an account | shopper/my-account-create-account.spec.js |
| Shopper | My Account | Login to existing account | shopper/my-account.spec.js |
| Shopper | My Account | View Account Details | shopper/my-account.spec.js |
| Shopper | My Account | Update Addresses | shopper/my-account-addresses.spec.js |
| Shopper | My Account | View Orders | shopper/ |
| Shopper | My Account | Pay for Order | shopper/my-account-pay-order.spec.js |
| Shopper | My Account | View Downloads | shopper/my-account-downloads.spec.js |
### Merchant - Onboarding
| User Type | Flow Area | Flow Name | Test File |
| --------- | ------------- | -------------------------------------------------------------- | --------- |
| Merchant | Core Profiler | Introduction & opt-in | |
| Merchant | Core Profiler | User profile information | |
| Merchant | Core Profiler | Business information | |
| Merchant | Core Profiler | Extensions page | |
| Merchant | Core Profiler | WooPayments included in extensions for eligible criteria | |
| Merchant | Core Profiler | WooPayments not included in extensions for ineligible criteria | |
| Merchant | Core Profiler | Install all default extensions | |
| Merchant | Core Profiler | Complete site setup | |
| Merchant | Core Profiler | Skip introduction and confirm business location | |
| Merchant | Core Profiler | Completed profiler doesn't reappear after site upgrade | |
### Merchant - Dashboard
| User Type | Flow Area | Flow Name | Test File |
| --------- | -------------- | ------------------------------------------------------ | --------- |
| Merchant | WC Home | Completing profiler redirects to home | |
| Merchant | WC Home | Complete all steps on task list | |
| Merchant | WC Home | Hide the task list | |
| Merchant | WC Home | Store management displayed after task list finished | |
| Merchant | WC Home | Direct access to analytics reports from stats overview | |
| Merchant | WC Home | Preserve task list completion status after upgrade | |
| Merchant | WC Home | Interact with extended task list | |
| Merchant | Activity Panel | Interact with activity button | |
| Merchant | Inbox | Interact with notes and perform CTAs | |
| Merchant | Inbox | Dismiss single note and all notes | |
### Merchant - Settings
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ----------------------------------------------- | ---------------------------------------- |
| Merchant | Settings | Update General Settings | merchant/settings-general.spec.js |
| Merchant | Settings | Add Tax Rates | merchant/settings-tax.spec.js |
| Merchant | Settings | Add Shipping Zones | merchant/create-shipping-zones.spec.js |
| Merchant | Settings | Add Shipping Classes | merchant/create-shipping-classes.spec.js |
| Merchant | Settings | Enable local pickup for checkout block | |
| Merchant | Settings | Enable HPOS | |
| Merchant | Settings | Update payment settings | |
| Merchant | Settings | Maintain tax and shipping settings post-upgrade | |
### Merchant - Coupons
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | ------------------------------ |
| Merchant | Coupons | Add all coupon types | merchant/create-coupon.spec.js |
| Merchant | Coupons | Add restricted coupon | |
### Merchant - Marketing
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------- | --------- |
| Merchant | Marketing | Display marketing overview | |
### Merchant - Analytics
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------------------------------- | --------------------------------- |
| Merchant | Analytics | View revenue report | admin-analytics/analytics.spec.js |
| Merchant | Analytics | View overview report | |
| Merchant | Analytics | Confirm correct summary numbers on overview report | |
| Merchant | Analytics | Use date filter on overview page | |
| Merchant | Analytics | Customize performance indicators on overview page | |
| Merchant | Analytics | Use date filter on revenue report | |
| Merchant | Analytics | Download revenue report as CSV | |
| Merchant | Analytics | Use advanced filters on orders report | |
| Merchant | Analytics | Analytics settings | |
| Merchant | Analytics | Set custom date range on revenue report | |
### Merchant - Products
| User Type | Flow Area | Flow Name | Test File |
| --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------- |
| Merchant | Products | View all products | |
| Merchant | Products | Search products | merchant/product-search.spec.js |
| Merchant | Products | Add simple product | merchant/create-simple-product.spec.js |
| Merchant | Products | Add variable product | merchant/products/add-variable-product/create-variable-product.spec.js |
| Merchant | Products | Edit product details | merchant/product-edit.spec.js |
| Merchant | Products | Add virtual product | merchant/create-simple-product.spec.js |
| Merchant | Products | Import products CSV | merchant/product-import-csv.spec.js |
| Merchant | Products | Add downloadable product | |
| Merchant | Products | View product reviews list | |
| Merchant | Products | View all products reviews list | |
| Merchant | Products | Edit product review | |
| Merchant | Products | Trash product review | |
| Merchant | Products | Bulk edit products | |
| Merchant | Products | Remove products | |
| Merchant | Products | Manage product images | |
| Merchant | Products | Manage product inventory | |
| Merchant | Products | Manage product attributes | |
| Merchant | Products | Manage global attributes | |
| Merchant | Products | Add up-sell | |
| Merchant | Products | Add cross-sell | |
| Merchant | Products (New) | Disable new product experience | |
| Merchant | Products (New) | Add simple product | |
| Merchant | Products (New) | Edit simple product | |
| Merchant | Products (New) | Manage product images | |
| Merchant | Products (New) | Manage product inventory | |
| Merchant | Products (New) | Manage product attributes | |
### Merchant - Orders
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ---------------------------------------------------------------- | -------------------------------------- |
| Merchant | Orders | View all orders | merchant/ |
| Merchant | Orders | Can add new order basic | merchant/order-edit.spec.js |
| Merchant | Orders | View single order | merchant/order-edit.spec.js |
| Merchant | Orders | Update order status to completed | merchant/order-edit.spec.js |
| Merchant | Orders | Update order status to cancelled | |
| Merchant | Orders | Update order details | merchant/order-edit.spec.js |
| Merchant | Orders | Customer payment page | merchant/customer-payment-page.spec.js |
| Merchant | Orders | Refund order | merchant/order-refund.spec.js |
| Merchant | Orders | Apply coupon | merchant/order-coupon.spec.js |
| Merchant | Orders | Can add new order complex - multiple product types & tax classes | merchant/create-order.spec.js |
| Merchant | Orders | Search orders | merchant/order-search.spec.js |
| Merchant | Orders | Filter orders by order status | merchant/order-status-filter.spec.js |
| Merchant | Orders | Bulk change order status | |
| Merchant | Orders | Add order notes | |
### Merchant - Customers
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | --------- |
| Merchant | Customers | Display customer list | |
### Merchant - Email
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------------------------------- | ----------------------------- |
| Merchant | Email | Receive and check content of new order email | merchant/order-emails.spec.js |
| Merchant | Email | Receive and check content of cancelled order email | |
| Merchant | Email | Receive and check content of failed order email | |
| Merchant | Email | Resent new order email | |
| Merchant | Email | Send invoice/order details to customer via Email | |
### Merchant - Plugins
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------- | -------------------------------------- |
| Merchant | Plugins | Can update WooCommerce | smoke-tests/update-woocommerce.spec.js |
| Merchant | Plugins | Can uninstall WooCommerce | |
### Merchant - My Subscriptions
| User Type | Flow Area | Flow Name | Test File |
| --------- | ---------------- | --------------------------------------- | --------- |
| Merchant | My Subscriptions | Can initiate WooCommerce.com Connection | |
### Merchant - Pages
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | ---------------------------- |
| Merchant | Pages | Can create a new page | merchant/create-page.spec.js |
### Merchant - Posts
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | ---------------------------- |
| Merchant | Posts | Can create a new post | merchant/create-post.spec.js |

View File

@ -0,0 +1,62 @@
# CSS/Sass Naming Conventions
Table of Contents:
- [Introduction](#introduction)
- [Prefixing](#prefixing)
- [Class names](#class-names)
- [Example](#example)
- [TL;DR](#tldr)
## Introduction
Our guidelines are based on those used in [Calypso](https://github.com/Automattic/wp-calypso), which itself follows the [BEM methodology](https://getbem.com/).
Refer to the [Calypso CSS/Sass Coding Guidelines](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md) for full details.
Read more about [BEM key concepts](https://en.bem.info/methodology/key-concepts/).
There are a few differences in WooCommerce which are outlined below.
## Prefixing
As a WordPress plugin WooCommerce has to play nicely with WordPress core and other plugins/themes. To minimize conflict potential, all classes should be prefixed with `.woocommerce-`.
## Class names
When naming classes, remember:
- **Block** - Standalone entity that is meaningful on its own. Such as the name of a component.
- **Element** - Parts of a block and have no standalone meaning. They are semantically tied to its block.
- **Modifier** - Flags on blocks or elements. Use them to change appearance or behavior.
### Example
```css
/* Block */
.woocommerce-loop {}
/* Nested block */
.woocommerce-loop-product {}
/* Modifier */
.woocommerce-loop-product--sale {}
/* Element */
.woocommerce-loop-product__link {}
/* Element */
.woocommerce-loop-product__button-add-to-cart {}
/* Modifier */
.woocommerce-loop-product__button-add-to-cart--added {}
```
**Note:** `.woocommerce-loop-product` is not named as such because the block is nested within `.woocommerce-loop`. It's to be specific so that we can have separate classes for single products, cart products, etc. **Nested blocks do not need to inherit their parents full name.**
## TL;DR
- Follow the [WordPress Coding standards for CSS](https://make.wordpress.org/core/handbook/best-practices/coding-standards/css/) unless it contradicts anything here.
- Follow [Calypso guidelines for CSS](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md).
- Use BEM for [class names](https://en.bem.info/methodology/naming-convention/).
- Prefix all class names.

View File

@ -0,0 +1,88 @@
# Naming Conventions
Table of Contents:
- [PHP](#php)
- [`/src`](#src)
- [`/includes`](#includes)
- [JS](#js)
- [CSS and SASS](#css-and-sass)
## PHP
WooCommerce core generally follows [WordPress PHP naming conventions](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions).
There are some additional conventions that apply, depending on the location of the code.
### `/src`
Classes defined inside `/src` follow the [PSR-4](https://www.php-fig.org/psr/psr-4/) standard. See the [README for `/src`](../../plugins/woocommerce/src/README.md) for more information.
The following conventions apply to this directory:
- No class name prefix is needed, as all classes in this location live within the `Automattic\WooCommerce` namespace.
- Classes are named using `CamelCase` convention.
- Functions are named using `snake_case` convention.
- Class file names should match the class name. They do not need a `class-` prefix.
- The namespace should match the directory structure.
- Hooks are prefixed with `woocommerce_`.
- Hooks are named using `snake_case` convention.
For example, the class defined in `src/Util/StringUtil.php` should be named `StringUtil` and should be in the `Automattic\WooCommerce\Util` namespace.
### `/includes`
The `/includes` directory contains legacy code that does not follow the PSR-4 standard. See the [README for `/includes`](../../plugins/woocommerce/includes/README.md) for more information.
The following conventions apply to this directory:
- Class names are prefixed with `WC_`.
- Classes are named using `Upper_Snake_Case` convention.
- Functions are prefixed with `wc_`.
- Functions are named using `snake_case` convention.
- Hooks are prefixed with `woocommerce_`.
- Hooks are named using `snake_case` convention.
Class name examples:
- `WC_Cache_Helper`
- `WC_Cart`
Function name examples:
- `wc_get_product()`
- `wc_is_active_theme()`
Hook name examples (actions or filters):
- `woocommerce_after_checkout_validation`
- `woocommerce_get_formatted_order_total`
## JS
WooCommerce core follows [WordPress JS naming conventions](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/#naming-conventions).
As with PHP, function, class, and hook names should be prefixed, but the convention for JS is slightly different.
- Global class names are prefixed with `WC`. Class names exported from modules are not prefixed.
- Classes are named using `UpperCamelCase` convention.
- Global function names are prefixed with `wc`. Function names exported from modules are not prefixed.
- Functions are named using `camelCase` convention.
- Hooks names are prefixed with `woocommerce`.
- Hooks are named using `camelCase` convention.
Global class name example:
- `WCOrdersTable`
Global function name example:
- `wcSettings()`
Hook name example (actions or filters):
- `woocommerceTracksEventProperties`
## CSS and SASS
See [CSS/Sass Naming Conventions](./css-sass-naming-conventions.md).

View File

@ -0,0 +1,22 @@
# Removing /product/ , /product-category/ , or /shop/ from the URLs
**Note:** This is a **Developer level** doc. If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
## [In sum](https://github.com/woocommerce/woocommerce/blob/trunk/docs/quality-and-best-practices/removing-product-product-category-or-shop-from-the-urls.md#section-1)
Removing `/product/`, `/product-category/`, or `/shop/` from the URLs is not advisable due to the way WordPress resolves its URLs. It uses the `product-category` (or any other text for that matter) base of a URL to detect that it is a URL leading to a product category. There are SEO plugins that allow you to remove this base, but that can lead to a number of problems with performance and duplicate URLs.
## [Better to avoid](https://github.com/woocommerce/woocommerce/blob/trunk/docs/quality-and-best-practices/removing-product-product-category-or-shop-from-the-urls.md#section-2)
You will make it harder for WordPress to detect what page you are trying to reach when you type in a product category URL. Also, understand that the standard “Page” in WordPress always has no base text in the URL. For example:
- `http://yoursite.com/about-page/` (this is the URL of a standard page)
- `http://yoursite.com/product-category/category-x/` (this is the URL leading to a product category)
What would happen if we remove that product-category part?
- `http://yoursite.com/about-page/`
- `http://yoursite.com/category-x/`
WordPress will have to do much more work to detect what page you are looking for when entering one of the above URLs. That is why we do not recommend using any SEO plugin to achieve this.

View File

@ -0,0 +1,138 @@
# Writing high quality testing instructions
## Introduction
Having clear testing Instructions on pull requests is the first level of quality engineering in WooCommerce, which is key for testing early and minimizing the impact of unexpected effects in the upcoming versions of WooCommerce.
This page contains the following sections:
- [What is a test?](#what-is-a-test)
- [What to cover with the testing instructions](#what-to-cover-with-the-testing-instructions)
- [Flow to write good testing instructions](#flow-to-write-good-testing-instructions)
- [Examples](#examples)
## What is a test?
A test is a method that we can use to check that something meets certain criteria. It is typically defined as a procedure which contains the steps required to put the system under test in a certain state before executing the action to be checked. Therefore, a test consists of the following stages:
- **Preconditions:** All the steps that need to be performed to put the system in the desired state before executing the action we want to check. A test could have many preconditions.
- **Action:** This is the exact step that causes the change we want to check in the system. It should be only one because each test should ideally cover one thing at a time.
- **Validation:** Relates to the steps to be performed in order to validate the result of performing the action in the system. A test could validate more than one thing.
For example, in the process of adding an item to the cart:
- The **preconditions** would be all the steps involved in:
- The product creation process.
- Logging as a shopper.
- Heading to the shop page where the products are listed.
- The **action** would be clicking the _"Add to cart"_ button in the desired product.
- The **validation** stage would include checking that the cart icon (if any) shows 1 more item and the product we selected is now included in the cart.
Specifying the preconditions, actions and validations can be quite beneficial when understanding the scope of a test, because:
- The **preconditions** describe what we have to do so that we can execute the test successfully.
- The **action** lets us know the purpose of the test, in other words, it is the key to understanding what we need to test.
- The **validation** stage lets us know what to expect when executing the test.
In this context, we will refer to testing instructions as the tests we need to execute in order to validate that the changes delivered in a pull request or release work as expected. This means the testing instructions could refer to a test or more, involving the happy path and potential edge cases.
## What to cover with the testing instructions
As stated in the previous section, a test (in our context, a testing instruction) is a method to check that a new change or set of changes meets certain criteria.
Therefore, a PR could have testing instructions for multiple scenarios, in fact, it is recommended to include testing instructions for as many scenarios as needed to cover the changes introduced in the PR. In other words, please **add as many testing instructions as needed to cover the acceptance criteria**, understanding acceptance criteria as _the conditions that a software product must satisfy to be accepted by a user, customer or other stakeholders_ or, in the context of a PR, the conditions that this PR must satisfy to be accepted by users, developers and the WooCommerce community as per requirements.
## Flow to write good testing instructions
1. **Outline the user flows** you want to cover.
2. **Define the environment** where the testing instructions should be executed (server, PHP version, WP version, required plugins, etc), and start writing the testing instructions as if you were starting from a fresh install.
3. Identify the **preconditions**, **action** and **validation** steps.
4. Write **as many preconditions as you need** to explain how to set up the state of WooCommerce so that you can execute the desired action to test every flow.
1. Try to be detailed when explaining the interactions the user needs to perform in WooCommerce.
2. If there are several preconditions for a user flow that is explained in a public guide, feel free to simply link the guide in the testing instructions instead of writing several steps. For example, _"Enable dev mode in WooCommerce Payments by following the steps mentioned [here](https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/dev-mode/)"_.
5. Write **the action step**, which should cover the specific action that we want to test as part of this user flow.
6. Write **as many validation steps** as needed in order to assess that the actual result meets expectations.
1. Bear in mind to check only the steps needed to validate that this change works.
### Considerations for writing high-quality testing instructions
- Define the testing instructions in a way that they can be **understood and followed by everybody**, even for people new to WooCommerce.
- Make sure to describe every detail and **avoid assuming knowledge**, the spectrum of readers might be wide and some people would not know the concepts behind what is being assumed. For example, instead of saying _“Enable the [x] experiment”_, say something like:
```text
- Install the WooCommerce Beta Tester plugin.
- Go to `Tools > WCA Test Helper > Experiments`.
- Toggle the [x] experiment.
```
- Always try to explain in detail **where the user should head to**, for example instead of saying “Go to the Orders page as admin”, say “Go to [url]” or even “Go to WooCommerce > Orders”.
- Try to use real test data. For example, instead of saying _"Enter a name for the product"_, say something like _"Enter 'Blue T-Shirt' as the product name"_. This will make it more self-explanatory and remove potential doubts related to assuming knowledge.
- Make sure you **keep your testing instructions updated** if they become obsolete as part of a new commit.
- If the testing instructions require to add custom code, please **provide the code snippet**.
- If the testing instructions require to install a plugin, please **provide a link to this plugin, or the zip file** to install it.
- If the testing instructions require to hit an API endpoint, please provide the **link to the endpoint documentation**.
- Ideally **provide screenshots and/or videos** that supported what the testing instructions are explaining. If you are using links to collaborative tools then also provide an equivalent screenshot/video for those who do not have access.
## Examples
### Good quality testing instructions
#### Example 1
![Sample of good quality instructions](https://woocommerce.files.wordpress.com/2023/10/213682695-3dc51613-b836-4e7e-93ef-f75078ab48ac.png)
#### Example 2
![Another sample of good quality instructions](https://woocommerce.files.wordpress.com/2023/10/213682778-b552ab07-a518-48a7-9358-16adc5762aca.png)
### Improving real testing instructions
In this section, you will see some real examples of testing instructions that have room for improvement (before) and how we can tweak them (after).
Before:
![Instructions needing improvement](https://woocommerce.files.wordpress.com/2023/10/213682262-25bec5c3-154c-45ec-aa3d-d3e07f52669e.png)
After:
![Improved instructions](https://woocommerce.files.wordpress.com/2023/10/213682303-1b12ab97-f27a-41cb-a8db-da8a78d18840.png)
Improvements:
![Changes made](https://woocommerce.files.wordpress.com/2023/10/213682323-0ecc998d-69ab-4201-8daa-820b948315e8.png)
Before:
![Instructions needing improvement](https://woocommerce.files.wordpress.com/2023/10/213682396-8c52d20e-1fca-4ac1-8345-f381c15a102a.png)
After:
![Improved instructions](https://woocommerce.files.wordpress.com/2023/10/213682480-c01e0e84-5969-4456-8f43-70cbb8509e8d.png)
Improvements:
![Changes made](https://woocommerce.files.wordpress.com/2023/10/213682597-8d06e638-35dd-4ff8-9236-63c6ec5d05b8.jpg)
Before:
![Screenshot 2023-02-02 at 16 07 29](https://woocommerce.files.wordpress.com/2023/10/216365611-b540a814-3b8f-40f3-ae64-81018b9f97fb.png)
After:
![Screenshot 2023-02-02 at 16 22 31](https://woocommerce.files.wordpress.com/2023/10/216366043-967e5daa-6a23-4ab8-adda-5f3082d1ebf7.png)
Improvements:
![Screenshot 2023-02-02 at 16 09 24](https://woocommerce.files.wordpress.com/2023/10/216366152-b331648d-bcef-443b-b126-de2621a20862.png)
Before:
![Screenshot 2023-02-02 at 17 25 07](https://woocommerce.files.wordpress.com/2023/10/216388785-8806ea74-62e6-42da-8887-c8e291e7dfe2-1.png)
After:
![Screenshot 2023-02-02 at 17 49 22](https://woocommerce.files.wordpress.com/2023/10/216388842-e5ab433e-d288-4306-862f-72f6f81ab2cd.png)
Improvements:
![Screenshot 2023-02-02 at 17 39 23](https://woocommerce.files.wordpress.com/2023/10/216388874-c5b21fc3-f693-4a7e-a58a-c5d1b6606682.png)

View File

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

View File

@ -52,6 +52,56 @@ How-to guides are focused and specific, providing instructions on how to accompl
>
> [Divio Framework on How-to-Guide Writing](https://documentation.divio.com/how-to-guides/)
## Custom Linting Rules
At WooCommerce, we're dedicated to maintaining a consistent and high-quality standard for our technical documentation. Our documents primarily adhere to the linting rules provided by `markdownlint`. To assist our contributors, we've detailed our custom configurations and exceptions below.
Note: While we've outlined specific rules above, all other default linting rules from `markdownlint` apply unless otherwise stated. We've only highlighted custom configurations or exceptions here. For a complete list of `markdownlint` rules, you can refer to [this link](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md).
1. **Headings Style**:
- Use the ATX-style (`#`) for headers.
```markdown
# This is an H1
## This is an H2
```
[Reference: MD003](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md#md003---heading-style)
2. **List Indentation**:
- Indent list items with 4 spaces.
```markdown
- Item 1
- Subitem 1.1
```
[Reference: MD007]([https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md)#md007---unordered-list-indentation)
3. **Line Length**:
- No specific restriction on the line length, but keep paragraphs and sentences readable.
[Reference: MD013](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md013---line-length)
4. **Multiple Headings with the Same Content**:
- Multiple headings with the same content are permissible as long as they are not siblings.
[Reference: MD024](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md#md024---no-multiple-headings-with-the-same-content)
5. **Inline HTML**:
- Only the `video` element is allowed when using inline HTML.
```markdown
<video src="path_to_video.mp4" controls></video>
```
[Reference: MD033](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md#md033---inline-html)
6. **Tabs and Whitespace**:
- We're flexible with the use of hard tabs and trailing whitespace. However, for consistency, we recommend using spaces over tabs and avoiding trailing whitespaces.
[Reference: no-hard-tabs & whitespace](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md)
## Formatting
### Visual style

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding feedback snackbar after image background removal

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add useBackgroundRemoval hook for image background removal API requests.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Reworking error handling and return value for useBackgroundRemoval hook.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: This is just a change to developer commands.

View File

@ -78,7 +78,8 @@
"changelog": "composer exec -- changelogger",
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
"build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name",
"test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
"test": "pnpm test:js",
"test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name --",
"lint": "eslint --output-file eslint_report.json --format json src",
"build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json",
"build:css": "webpack",

View File

@ -1 +1,2 @@
export * from './useCompletion';
export * from './useBackgroundRemoval';

View File

@ -0,0 +1,165 @@
/**
* External dependencies
*/
import { renderHook, act } from '@testing-library/react-hooks/dom';
import { waitFor } from '@testing-library/react';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import {
BackgroundRemovalParams,
useBackgroundRemoval,
} from './useBackgroundRemoval';
import { requestJetpackToken } from '../utils/requestJetpackToken';
// Mocking the apiFetch function
jest.mock( '@wordpress/api-fetch', () =>
jest.fn().mockResolvedValue( {
blob: () =>
Promise.resolve(
new Blob( [ new ArrayBuffer( 51200 ) ], {
type: 'image/jpeg',
} )
),
} )
);
jest.mock( '../utils/requestJetpackToken' );
const mockedRequestJetpackToken = requestJetpackToken as jest.MockedFunction<
typeof requestJetpackToken
>;
describe( 'useBackgroundRemoval hook', () => {
let mockRequestParams: BackgroundRemovalParams;
beforeEach( () => {
jest.resetAllMocks();
// Initialize with valid parameters (50kb image file).
const imageFile = new File( [ new ArrayBuffer( 51200 ) ], 'test.png', {
type: 'image/png',
} );
mockRequestParams = {
imageFile,
};
mockedRequestJetpackToken.mockResolvedValue( { token: 'fake_token' } );
} );
it( 'should initialize with correct default values', () => {
const { result } = renderHook( () => useBackgroundRemoval() );
expect( result.current.imageData ).toBeNull();
expect( result.current.loading ).toBeFalsy();
} );
it( 'should return error on empty token', async () => {
mockedRequestJetpackToken.mockResolvedValue( { token: '' } );
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Invalid token' );
} );
it( 'should handle invalid file type', async () => {
mockRequestParams.imageFile = new File(
[ new ArrayBuffer( 51200 ) ],
'test.txt',
{ type: 'text/plain' }
);
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Invalid image file' );
} );
it( 'should return error on image file too small', async () => {
mockRequestParams.imageFile = new File(
[ new ArrayBuffer( 1024 ) ],
'test.png',
{ type: 'image/png' }
); // 1KB
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Image file too small, must be at least 5KB' );
} );
it( 'should return error on image file too large', async () => {
mockRequestParams.imageFile = new File(
[ new ArrayBuffer( 10240 * 1024 * 2 ) ],
'test.png',
{ type: 'image/png' }
); // 10MB
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Image file too large, must be under 10MB' );
} );
it( 'should set loading to true when fetchImage is called', async () => {
(
apiFetch as jest.MockedFunction< typeof apiFetch >
).mockResolvedValue( {
blob: () =>
Promise.resolve(
new Blob( [ new ArrayBuffer( 51200 ) ], {
type: 'image/jpeg',
} )
),
} );
const { result } = renderHook( () => useBackgroundRemoval() );
await act( async () => {
result.current.fetchImage( mockRequestParams );
await waitFor( () =>
expect( result.current.loading ).toBeTruthy()
);
} );
expect( mockedRequestJetpackToken ).toHaveBeenCalled();
} );
it( 'should handle successful API call', async () => {
(
apiFetch as jest.MockedFunction< typeof apiFetch >
).mockResolvedValue( {
blob: () =>
Promise.resolve(
new Blob( [ new ArrayBuffer( 51200 ) ], {
type: 'image/jpeg',
} )
),
} );
const { result } = renderHook( () => useBackgroundRemoval() );
await act( async () => {
await result.current.fetchImage( mockRequestParams );
} );
expect( result.current.loading ).toBe( false );
expect( result.current.imageData ).toBeInstanceOf( Blob );
} );
it( 'should handle API errors', async () => {
(
apiFetch as jest.MockedFunction< typeof apiFetch >
).mockImplementation( () => {
throw new Error( 'API Error' );
} );
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'API Error' );
await waitFor( () => expect( result.current.loading ).toBeFalsy() );
expect( result.current.imageData ).toBe( null );
} );
} );

View File

@ -0,0 +1,85 @@
/**
* External dependencies
*/
import { useState } from 'react';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { createExtendedError } from '../utils/create-extended-error';
import { requestJetpackToken } from '../utils/requestJetpackToken';
export type BackgroundRemovalParams = {
imageFile: File;
};
type BackgroundRemovalResponse = {
loading: boolean;
imageData: Blob | null;
fetchImage: ( params: BackgroundRemovalParams ) => Promise< Blob >;
};
export const useBackgroundRemoval = (): BackgroundRemovalResponse => {
const [ loading, setLoading ] = useState( false );
const [ imageData, setImageData ] = useState< Blob | null >( null );
const fetchImage = async ( params: BackgroundRemovalParams ) => {
setLoading( true );
const { imageFile } = params;
const { token } = await requestJetpackToken();
if ( ! token ) {
throw createExtendedError( 'Invalid token', 'invalid_jwt' );
}
// Validate that the file is an image and has actual content.
if ( ! imageFile.type.startsWith( 'image/' ) ) {
throw createExtendedError(
'Invalid image file',
'invalid_image_file'
);
}
const fileSizeInKB = params.imageFile.size / 1024;
if ( fileSizeInKB < 5 ) {
throw createExtendedError(
'Image file too small, must be at least 5KB',
'image_file_too_small'
);
}
// The WPCOM REST API endpoint has a 10MB image size limit.
if ( fileSizeInKB > 10240 ) {
throw createExtendedError(
'Image file too large, must be under 10MB',
'image_file_too_large'
);
}
const formData = new FormData();
formData.append( 'image_file', imageFile );
formData.append( 'token', token );
try {
const response = await apiFetch( {
url: 'https://public-api.wordpress.com/wpcom/v2/ai-background-removal',
method: 'POST',
body: formData,
parse: false,
credentials: 'omit',
} );
const blob = await (
response as { blob: () => Promise< Blob > }
).blob();
setImageData( blob );
return blob;
} catch ( err ) {
throw err;
} finally {
setLoading( false );
}
};
return { loading, imageData, fetchImage };
};

View File

@ -4,6 +4,7 @@
export {
useCompletion as __experimentalUseCompletion,
UseCompletionError,
useBackgroundRemoval as __experimentalUseBackgroundRemoval,
} from './hooks';
/**

View File

@ -1,2 +1,3 @@
export * from './text-completion';
export * from './create-extended-error';
export * from './requestJetpackToken';

View File

@ -0,0 +1,87 @@
/**
* External dependencies
*/
import debugFactory from 'debug';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { createExtendedError } from './create-extended-error';
export const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
export const JWT_TOKEN_ID = 'jetpack-ai-jwt-token';
export const JWT_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000;
declare global {
interface Window {
JP_CONNECTION_INITIAL_STATE: {
apiNonce: string;
siteSuffix: string;
connectionStatus: { isActive: boolean };
};
}
}
/**
* Request a token from the Jetpack site to use with the API
*
* @return {Promise<{token: string, blogId: string}>} The token and the blogId
*/
export async function requestJetpackToken() {
const token = localStorage.getItem( JWT_TOKEN_ID );
let tokenData;
if ( token ) {
try {
tokenData = JSON.parse( token );
} catch ( err ) {
debugToken( 'Error parsing token', err );
throw createExtendedError(
'Error parsing cached token',
'token_parse_error'
);
}
}
if ( tokenData && tokenData?.expire > Date.now() ) {
debugToken( 'Using cached token' );
return tokenData;
}
const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
try {
const data: { token: string; blog_id: string } = await apiFetch( {
path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
credentials: 'same-origin',
headers: {
'X-WP-Nonce': apiNonce,
},
method: 'POST',
} );
const newTokenData = {
token: data.token,
blogId: siteSuffix,
/**
* Let's expire the token in 2 minutes
*/
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
};
debugToken( 'Storing new token' );
localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
return newTokenData;
} catch ( e ) {
throw createExtendedError(
'Error fetching new token',
'token_fetch_error'
);
}
}

View File

@ -1,89 +1,7 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import debugFactory from 'debug';
/**
* Internal dependencies
*/
import { createExtendedError } from './create-extended-error';
const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
const JWT_TOKEN_ID = 'jetpack-ai-jwt-token';
const JWT_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000;
declare global {
interface Window {
JP_CONNECTION_INITIAL_STATE: {
apiNonce: string;
siteSuffix: string;
connectionStatus: { isActive: boolean };
};
}
}
/**
* Request a token from the Jetpack site to use with the API
*
* @return {Promise<{token: string, blogId: string}>} The token and the blogId
*/
export async function requestJetpackToken() {
const token = localStorage.getItem( JWT_TOKEN_ID );
let tokenData;
if ( token ) {
try {
tokenData = JSON.parse( token );
} catch ( err ) {
debugToken( 'Error parsing token', err );
throw createExtendedError(
'Error parsing cached token',
'token_parse_error'
);
}
}
if ( tokenData && tokenData?.expire > Date.now() ) {
debugToken( 'Using cached token' );
return tokenData;
}
const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
try {
const data: { token: string; blog_id: string } = await apiFetch( {
path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
credentials: 'same-origin',
headers: {
'X-WP-Nonce': apiNonce,
},
method: 'POST',
} );
const newTokenData = {
token: data.token,
blogId: siteSuffix,
/**
* Let's expire the token in 2 minutes
*/
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
};
debugToken( 'Storing new token' );
localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
return newTokenData;
} catch ( e ) {
throw createExtendedError(
'Error fetching new token',
'token_fetch_error'
);
}
}
import { requestJetpackToken } from './requestJetpackToken';
/**
* Leaving this here to make it easier to debug the streaming API calls for now

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: This is just a change to developer commands.

View File

@ -32,7 +32,8 @@
"scripts": {
"turbo:build": "pnpm run clean && npm run compile",
"turbo:test": "jest",
"test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
"test": "pnpm test:js",
"test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
"prepare": "composer install",
"changelog": "composer exec -- changelogger",
"clean": "rm -rf ./dist ./tsconfig.tsbuildinfo",

View File

@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: Just updating dependencies in prep for a future PR (to figure out why these are breaking the build).

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add conditional visibility support to registered blocks.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: This is just a change to developer commands.

View File

@ -28,8 +28,11 @@
"access": "public"
},
"dependencies": {
"@woocommerce/expression-evaluation": "workspace:*",
"@wordpress/block-editor": "^9.8.0",
"@wordpress/blocks": "^12.3.0"
"@wordpress/blocks": "^12.3.0",
"@wordpress/data": "wp-6.0",
"@wordpress/element": "wp-6.0"
},
"devDependencies": {
"@babel/core": "^7.21.3",
@ -37,6 +40,7 @@
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^27.4.1",
"@types/react": "^17.0.2",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/wordpress__block-editor": "^7.0.0",
"@types/wordpress__blocks": "^11.0.7",
@ -51,6 +55,8 @@
"jest-cli": "^27.5.1",
"postcss": "^8.4.7",
"postcss-loader": "^4.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
@ -65,12 +71,18 @@
"changelog": "composer exec -- changelogger",
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
"build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name",
"test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
"test": "pnpm test:js",
"test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
"lint": "eslint --output-file eslint_report.json --format json src",
"build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json",
"build:css": "webpack",
"start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"",
"prepack": "pnpm run clean && pnpm run build",
"lint:fix": "eslint src --fix"
},
"peerDependencies": {
"@types/react": "^17.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

View File

@ -4,8 +4,18 @@
import {
Block,
BlockConfiguration,
BlockEditProps,
registerBlockType,
} from '@wordpress/blocks';
import { createElement } from '@wordpress/element';
import { evaluate } from '@woocommerce/expression-evaluation';
import { ComponentType } from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet, natively (not until 7.0.0).
// Including `@types/wordpress__data` as a devDependency causes build issues,
// so just going type-free for now.
// eslint-disable-next-line @woocommerce/dependency-group
import { useSelect, select as WPSelect } from '@wordpress/data';
interface BlockRepresentation< T extends Record< string, object > > {
name?: string;
@ -13,6 +23,82 @@ interface BlockRepresentation< T extends Record< string, object > > {
settings: Partial< BlockConfiguration< T > >;
}
type UseEvaluationContext = ( context: Record< string, unknown > ) => {
getEvaluationContext: (
select: typeof WPSelect
) => Record< string, unknown >;
};
function defaultUseEvaluationContext( context: Record< string, unknown > ) {
return {
getEvaluationContext: () => context,
};
}
function getEdit<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record< string, object > = Record< string, object >
>(
edit: ComponentType< BlockEditProps< T > >,
useEvaluationContext: UseEvaluationContext
): ComponentType< BlockEditProps< T > > {
return ( props ) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore context is added to the block props by the block editor.
const { context } = props;
const { _templateBlockHideConditions: hideConditions } =
props.attributes;
const { getEvaluationContext } = useEvaluationContext( context );
const shouldHide = useSelect(
( select: typeof WPSelect ) => {
if ( ! hideConditions || ! Array.isArray( hideConditions ) ) {
return false;
}
const evaluationContext = getEvaluationContext( select );
return hideConditions.some( ( condition ) =>
evaluate( condition.expression, evaluationContext )
);
},
[ getEvaluationContext, hideConditions ]
);
if ( ! edit || shouldHide ) {
return null;
}
return createElement( edit, props );
};
}
function augmentAttributes<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record< string, any > = Record< string, any >
>( attributes: T ) {
// Note: If you modify this function, also update the server-side
// Automattic\WooCommerce\Admin\Features\ProductBlockEditor\BlockRegistry::augment_attributes() function.
return {
...attributes,
...{
_templateBlockId: {
type: 'string',
__experimentalRole: 'content',
},
_templateBlockOrder: {
type: 'integer',
__experimentalRole: 'content',
},
_templateBlockHideConditions: {
type: 'array',
__experimentalRole: 'content',
},
},
};
}
/**
* Function to register an individual block.
*
@ -22,30 +108,33 @@ interface BlockRepresentation< T extends Record< string, object > > {
export function registerWooBlockType<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record< string, any > = Record< string, any >
>( block: BlockRepresentation< T > ): Block< T > | undefined {
>(
block: BlockRepresentation< T >,
useEvaluationContext?: UseEvaluationContext
): Block< T > | undefined {
if ( ! block ) {
return;
}
const { metadata, settings, name } = block;
const { edit } = settings;
const templateBlockAttributes = {
_templateBlockId: {
type: 'string',
__experimentalRole: 'content',
},
_templateBlockOrder: {
type: 'integer',
__experimentalRole: 'content',
},
};
if ( ! edit ) {
return;
}
const augmentedMetadata = {
...metadata,
attributes: {
...metadata.attributes,
...templateBlockAttributes,
},
attributes: augmentAttributes( metadata.attributes ),
};
return registerBlockType< T >( { name, ...augmentedMetadata }, settings );
return registerBlockType< T >(
{ name, ...augmentedMetadata },
{
...settings,
edit: getEdit< T >(
edit,
useEvaluationContext ?? defaultUseEvaluationContext
),
}
);
}

View File

@ -26,6 +26,7 @@ describe( 'registerWooBlockType', () => {
},
settings: {
foo: 'bar',
edit: jest.fn(),
},
};
@ -49,10 +50,15 @@ describe( 'registerWooBlockType', () => {
type: 'integer',
__experimentalRole: 'content',
},
_templateBlockHideConditions: {
type: 'array',
__experimentalRole: 'content',
},
},
},
{
foo: 'bar',
edit: expect.any( Function ),
}
);
} );

View File

@ -2,6 +2,35 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [12.2.0](https://www.npmjs.com/package/@woocommerce/components/v/12.2.0) - 2023-10-17
- Patch - Add class back in for increase specificity of css for dropdown button. [#40494]
- Minor - Categories dropdown display error #39810 [#39811]
- Patch - Fixed empty component logo color, used generic rather than old pink [#39182]
- Patch - Fix invalid focus state of the experimental select control [#40519]
- Minor - Fix new category name field [#39857]
- Patch - Fix select control dropdown menu double scroll and width [#39989]
- Minor - Select attribute after pressing their names #39456 [#39574]
- Patch - TreeSelectControl Component - Make sure individuallySelectParent prop is respected [#40301]
- Minor - Add AI wizard business info step for Customize Your Store task [#39979]
- Minor - Add customize store assembler hub onboarding tour [#39981]
- Minor - Add ProgressBar component [#39979]
- Minor - Add tags (or general taxonomy ) block [#39966]
- Minor - Add Tooltip to each list item when need it [#39770]
- Minor - An international phone number input with country selection, and mobile phone numbers validation. [#40335]
- Minor - Image gallery and media uploader now support initial selected images. [#40633]
- Minor - Refactor Pagination component and split out into multiple re-usable components. Also added a `usePagination` hook. [#39967]
- Minor - Set button optional in MediaUploader component [#40526]
- Minor - Update ImageGallery block toolbar, moving some options to an ellipsis dropdown menu. [#39753]
- Minor - Allow users to select multiple items from the media library while adding images #39741 [#39741]
- Patch - Make eslint emit JSON report for annotating PRs. [#39704]
- Minor - Update pnpm to 8.6.7 [#39245]
- Patch - Upgraded Storybook to 6.5.17-alpha.0 for TypeScript 5 compatibility [#39745]
- Minor - Upgrade TypeScript to 5.1.6 [#39531]
- Patch - Add z-index=1 to tour-kit close btn to ensure it's clickable [#40456]
- Minor - Remove unnecessary use of woocommerce-page selector for DropdownButton styling. [#40218]
- Patch - Small condition change in the date time picker to avoid edge case where inputControl is null. [#40642]
## [12.1.0](https://www.npmjs.com/package/@woocommerce/components/v/12.1.0) - 2023-07-13
- Patch - Altering styles to correctly target fields within slot fills on product editor. [#36500]
@ -157,7 +186,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Minor - Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). [#35140]
- Minor - Improve experimental SelectControl accessibility [#35140]
- Minor - Improve Sortable component acessibility [#35140]
- - Create new experimental SelectControl component [#35140]
- Major - Create new experimental SelectControl component [#35140]
## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add tags (or general taxonomy ) block

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add Tooltip to each list item when need it

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add AI wizard business info step for Customize Your Store task

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