From 3a6c3cde9f15803684c127cef7b430c0ab6c7e22 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Wed, 9 Aug 2023 09:05:00 +0800 Subject: [PATCH] Code analyzer: find multiple hooks in single patch, add tests (#39566) --- pnpm-lock.yaml | 705 +- tools/code-analyzer/README.md | 4 +- tools/code-analyzer/jest.config.js | 5 + tools/code-analyzer/package.json | 8 +- tools/code-analyzer/src/lib/__mocks__/uuid.js | 3 + .../src/lib/__tests__/fixtures/diff.txt | 39086 ++++++++++++++++ .../src/lib/__tests__/hook-changes-test.ts | 94 + tools/code-analyzer/src/lib/hook-changes.ts | 122 +- tools/code-analyzer/src/lib/scan-changes.ts | 6 +- 9 files changed, 39975 insertions(+), 58 deletions(-) create mode 100644 tools/code-analyzer/jest.config.js create mode 100644 tools/code-analyzer/src/lib/__mocks__/uuid.js create mode 100644 tools/code-analyzer/src/lib/__tests__/fixtures/diff.txt create mode 100644 tools/code-analyzer/src/lib/__tests__/hook-changes-test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c2247c8d2f..2f7cff3c4d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3486,6 +3486,9 @@ importers: specifier: ^8.3.2 version: 8.3.2 devDependencies: + '@types/jest': + specifier: ^27.4.1 + version: 27.4.1 '@types/node': specifier: ^16.18.18 version: 16.18.21 @@ -3495,6 +3498,12 @@ importers: eslint: specifier: ^8.32.0 version: 8.32.0 + jest: + specifier: ^29.6.2 + version: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + ts-jest: + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.21.3)(jest@29.6.2)(typescript@5.1.6) ts-node: specifier: ^10.2.1 version: 10.9.1(@types/node@16.18.21)(typescript@5.1.6) @@ -4182,7 +4191,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.16 commander: 4.1.1 convert-source-map: 1.8.0 fs-readdir-recursive: 1.1.0 @@ -10847,6 +10856,18 @@ packages: jest-util: 29.5.0 slash: 3.0.0 + /@jest/console@29.6.2: + resolution: {integrity: sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + chalk: 4.1.2 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + slash: 3.0.0 + dev: true + /@jest/core@24.9.0: resolution: {integrity: sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==} engines: {node: '>= 6'} @@ -11050,6 +11071,49 @@ packages: - ts-node dev: true + /@jest/core@29.6.2(ts-node@10.9.1): + resolution: {integrity: sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.6.2 + '@jest/reporters': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.2.0 + exit: 0.1.2 + graceful-fs: 4.2.9 + jest-changed-files: 29.5.0 + jest-config: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + jest-haste-map: 29.6.2 + jest-message-util: 29.6.2 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-resolve-dependencies: 29.6.2 + jest-runner: 29.6.2 + jest-runtime: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + jest-watcher: 29.6.2 + micromatch: 4.0.5 + pretty-format: 29.6.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/create-cache-key-function@27.5.1: resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11105,12 +11169,29 @@ packages: '@types/node': 16.18.21 jest-mock: 29.5.0 + /@jest/environment@29.6.2: + resolution: {integrity: sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + jest-mock: 29.6.2 + dev: true + /@jest/expect-utils@29.5.0: resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 + /@jest/expect-utils@29.6.2: + resolution: {integrity: sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.3 + dev: true + /@jest/expect@29.5.0: resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11120,6 +11201,16 @@ packages: transitivePeerDependencies: - supports-color + /@jest/expect@29.6.2: + resolution: {integrity: sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.6.2 + jest-snapshot: 29.6.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/fake-timers@24.9.0: resolution: {integrity: sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==} engines: {node: '>= 6'} @@ -11176,6 +11267,18 @@ packages: jest-mock: 29.5.0 jest-util: 29.5.0 + /@jest/fake-timers@29.6.2: + resolution: {integrity: sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@sinonjs/fake-timers': 10.0.2 + '@types/node': 16.18.21 + jest-message-util: 29.6.2 + jest-mock: 29.6.2 + jest-util: 29.6.2 + dev: true + /@jest/globals@25.5.2: resolution: {integrity: sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==} engines: {node: '>= 8.3'} @@ -11213,6 +11316,18 @@ packages: transitivePeerDependencies: - supports-color + /@jest/globals@29.6.2: + resolution: {integrity: sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.2 + '@jest/expect': 29.6.2 + '@jest/types': 29.6.1 + jest-mock: 29.6.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/reporters@24.9.0: resolution: {integrity: sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==} engines: {node: '>= 6'} @@ -11386,12 +11501,56 @@ packages: - supports-color dev: true + /@jest/reporters@29.6.2: + resolution: {integrity: sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 16.18.21 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.9 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.1.0 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.4 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + jest-worker: 29.6.2 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.1.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@29.4.3: resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.25.24 + /@jest/schemas@29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jest/source-map@24.9.0: resolution: {integrity: sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==} engines: {node: '>= 6'} @@ -11435,6 +11594,15 @@ packages: callsites: 3.1.0 graceful-fs: 4.2.9 + /@jest/source-map@29.6.0: + resolution: {integrity: sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.9 + dev: true + /@jest/test-result@24.9.0: resolution: {integrity: sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==} engines: {node: '>= 6'} @@ -11482,6 +11650,16 @@ packages: '@types/istanbul-lib-coverage': 2.0.3 collect-v8-coverage: 1.0.1 + /@jest/test-result@29.6.2: + resolution: {integrity: sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.6.2 + '@jest/types': 29.6.1 + '@types/istanbul-lib-coverage': 2.0.3 + collect-v8-coverage: 1.0.1 + dev: true + /@jest/test-sequencer@24.9.0: resolution: {integrity: sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==} engines: {node: '>= 6'} @@ -11550,6 +11728,16 @@ packages: slash: 3.0.0 dev: true + /@jest/test-sequencer@29.6.2: + resolution: {integrity: sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.2 + graceful-fs: 4.2.9 + jest-haste-map: 29.6.2 + slash: 3.0.0 + dev: true + /@jest/transform@24.9.0: resolution: {integrity: sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==} engines: {node: '>= 6'} @@ -11664,6 +11852,29 @@ packages: transitivePeerDependencies: - supports-color + /@jest/transform@29.6.2: + resolution: {integrity: sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.21.3 + '@jest/types': 29.6.1 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.9 + jest-haste-map: 29.6.2 + jest-regex-util: 29.4.3 + jest-util: 29.6.2 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/types@24.9.0: resolution: {integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==} engines: {node: '>= 6'} @@ -11714,6 +11925,18 @@ packages: '@types/yargs': 17.0.24 chalk: 4.1.2 + /@jest/types@29.6.1: + resolution: {integrity: sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + '@types/istanbul-lib-coverage': 2.0.3 + '@types/istanbul-reports': 3.0.1 + '@types/node': 16.18.21 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping@0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} @@ -11745,7 +11968,6 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 - dev: true /@jridgewell/trace-mapping@0.3.17: resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} @@ -11753,6 +11975,13 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -13043,6 +13272,10 @@ packages: /@sinclair/typebox@0.25.24: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sindresorhus/is@4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -21999,6 +22232,24 @@ packages: - supports-color dev: true + /babel-jest@29.6.2(@babel/core@7.21.3): + resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.21.3 + '@jest/transform': 29.6.2 + '@types/babel__core': 7.1.16 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.5.0(@babel/core@7.21.3) + chalk: 4.1.2 + graceful-fs: 4.2.9 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-loader@8.2.3(@babel/core@7.17.8)(webpack@5.70.0): resolution: {integrity: sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==} engines: {node: '>= 8.9'} @@ -25455,6 +25706,15 @@ packages: /dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-eql@3.0.1: resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} engines: {node: '>=0.12'} @@ -27534,6 +27794,18 @@ packages: jest-message-util: 29.5.0 jest-util: 29.5.0 + /expect@29.6.2: + resolution: {integrity: sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.6.2 + '@types/node': 16.18.21 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.2 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + dev: true + /expose-loader@3.1.0(webpack@5.70.0): resolution: {integrity: sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==} engines: {node: '>= 12.13.0'} @@ -31337,6 +31609,35 @@ packages: - supports-color dev: true + /jest-circus@29.6.2: + resolution: {integrity: sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.2 + '@jest/expect': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.6.2 + jest-matcher-utils: 29.6.2 + jest-message-util: 29.6.2 + jest-runtime: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 + p-limit: 3.1.0 + pretty-format: 29.6.2 + pure-rand: 6.0.1 + slash: 3.0.0 + stack-utils: 2.0.5 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-cli@24.9.0: resolution: {integrity: sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==} engines: {node: '>= 6'} @@ -31470,6 +31771,35 @@ packages: - ts-node dev: true + /jest-cli@29.6.2(@types/node@16.18.21)(ts-node@10.9.1): + resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.2(ts-node@10.9.1) + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.9 + import-local: 3.0.3 + jest-config: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + jest-util: 29.6.2 + jest-validate: 29.6.2 + prompts: 2.4.2 + yargs: 17.5.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@24.9.0: resolution: {integrity: sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==} engines: {node: '>= 6'} @@ -31640,6 +31970,47 @@ packages: - supports-color dev: true + /jest-config@29.6.2(@types/node@16.18.21)(ts-node@10.9.1): + resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@jest/test-sequencer': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + babel-jest: 29.6.2(@babel/core@7.21.3) + chalk: 4.1.2 + ci-info: 3.2.0 + deepmerge: 4.3.0 + glob: 7.2.3 + graceful-fs: 4.2.9 + jest-circus: 29.6.2 + jest-environment-node: 29.6.2 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-runner: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.6.2 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1(@types/node@16.18.21)(typescript@5.1.6) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-dev-server@4.4.0: resolution: {integrity: sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A==} dependencies: @@ -31730,6 +32101,16 @@ packages: jest-get-type: 29.4.3 pretty-format: 29.5.0 + /jest-diff@29.6.2: + resolution: {integrity: sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.6.2 + dev: true + /jest-docblock@24.9.0: resolution: {integrity: sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==} engines: {node: '>= 6'} @@ -31819,6 +32200,17 @@ packages: pretty-format: 29.5.0 dev: true + /jest-each@29.6.2: + resolution: {integrity: sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + chalk: 4.1.2 + jest-get-type: 29.4.3 + jest-util: 29.6.2 + pretty-format: 29.6.2 + dev: true + /jest-environment-jsdom@24.9.0: resolution: {integrity: sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==} engines: {node: '>= 6'} @@ -31968,6 +32360,18 @@ packages: jest-mock: 29.5.0 jest-util: 29.5.0 + /jest-environment-node@29.6.2: + resolution: {integrity: sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.2 + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + jest-mock: 29.6.2 + jest-util: 29.6.2 + dev: true + /jest-environment-puppeteer@4.4.0: resolution: {integrity: sha512-iV8S8+6qkdTM6OBR/M9gKywEk8GDSOe05hspCs5D8qKSwtmlUfdtHfB4cakdc68lC6YfK3AUsLirpfgodCHjzQ==} dependencies: @@ -32116,6 +32520,25 @@ packages: optionalDependencies: fsevents: 2.3.2 + /jest-haste-map@29.6.2: + resolution: {integrity: sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/graceful-fs': 4.1.5 + '@types/node': 16.18.21 + anymatch: 3.1.2 + fb-watchman: 2.0.1 + graceful-fs: 4.2.9 + jest-regex-util: 29.4.3 + jest-util: 29.6.2 + jest-worker: 29.6.2 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /jest-jasmine2@24.9.0: resolution: {integrity: sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==} engines: {node: '>= 6'} @@ -32260,6 +32683,14 @@ packages: jest-get-type: 29.4.3 pretty-format: 29.5.0 + /jest-leak-detector@29.6.2: + resolution: {integrity: sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.3 + pretty-format: 29.6.2 + dev: true + /jest-matcher-utils@24.9.0: resolution: {integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==} engines: {node: '>= 6'} @@ -32307,6 +32738,16 @@ packages: jest-get-type: 29.4.3 pretty-format: 29.5.0 + /jest-matcher-utils@29.6.2: + resolution: {integrity: sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.6.2 + jest-get-type: 29.4.3 + pretty-format: 29.6.2 + dev: true + /jest-message-util@24.9.0: resolution: {integrity: sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==} engines: {node: '>= 6'} @@ -32380,6 +32821,21 @@ packages: slash: 3.0.0 stack-utils: 2.0.5 + /jest-message-util@29.6.2: + resolution: {integrity: sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.6.1 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.9 + micromatch: 4.0.5 + pretty-format: 29.6.2 + slash: 3.0.0 + stack-utils: 2.0.5 + dev: true + /jest-mock-extended@1.0.18(jest@27.5.1)(typescript@5.1.6): resolution: {integrity: sha512-qf1n7lIa2dTxxPIBr+FlXrbj3hnV1sG9DPZsrr2H/8W+Jw0wt6OmeOQsPcjRuW8EXIECC9pDXsSIfEdn+HP7JQ==} peerDependencies: @@ -32428,6 +32884,15 @@ packages: '@types/node': 16.18.21 jest-util: 29.5.0 + /jest-mock@29.6.2: + resolution: {integrity: sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + jest-util: 29.6.2 + dev: true + /jest-pnp-resolver@1.2.2(jest-resolve@24.9.0): resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} @@ -32486,6 +32951,18 @@ packages: dependencies: jest-resolve: 29.5.0 + /jest-pnp-resolver@1.2.2(jest-resolve@29.6.2): + resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.6.2 + dev: true + /jest-puppeteer@4.4.0(puppeteer-core@3.0.0): resolution: {integrity: sha512-ZaiCTlPZ07B9HW0erAWNX6cyzBqbXMM7d2ugai4epBDKpKvRDpItlRQC6XjERoJELKZsPziFGS0OhhUvTvQAXA==} peerDependencies: @@ -32584,6 +33061,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies@29.6.2: + resolution: {integrity: sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.4.3 + jest-snapshot: 29.6.2 + transitivePeerDependencies: + - supports-color + dev: true + /jest-resolve@24.9.0: resolution: {integrity: sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==} engines: {node: '>= 6'} @@ -32653,6 +33140,21 @@ packages: resolve.exports: 2.0.2 slash: 3.0.0 + /jest-resolve@29.6.2: + resolution: {integrity: sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.9 + jest-haste-map: 29.6.2 + jest-pnp-resolver: 1.2.2(jest-resolve@29.6.2) + jest-util: 29.6.2 + jest-validate: 29.6.2 + resolve: 1.22.1 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + /jest-runner-groups@2.2.0(jest-docblock@29.4.3)(jest-runner@29.5.0): resolution: {integrity: sha512-Sp/B9ZX0CDAKa9dIkgH0sGyl2eDuScV4SVvOxqhBMxqWpsNAkmol/C58aTFmPWZj+C0ZTW1r1BSu66MTCN+voA==} engines: {node: '>= 10.14.2'} @@ -32814,6 +33316,35 @@ packages: transitivePeerDependencies: - supports-color + /jest-runner@29.6.2: + resolution: {integrity: sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.6.2 + '@jest/environment': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.9 + jest-docblock: 29.4.3 + jest-environment-node: 29.6.2 + jest-haste-map: 29.6.2 + jest-leak-detector: 29.6.2 + jest-message-util: 29.6.2 + jest-resolve: 29.6.2 + jest-runtime: 29.6.2 + jest-util: 29.6.2 + jest-watcher: 29.6.2 + jest-worker: 29.6.2 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + /jest-runtime@24.9.0: resolution: {integrity: sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==} engines: {node: '>= 6'} @@ -32984,6 +33515,36 @@ packages: transitivePeerDependencies: - supports-color + /jest-runtime@29.6.2: + resolution: {integrity: sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.2 + '@jest/fake-timers': 29.6.2 + '@jest/globals': 29.6.2 + '@jest/source-map': 29.6.0 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.9 + jest-haste-map: 29.6.2 + jest-message-util: 29.6.2 + jest-mock: 29.6.2 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-serializer@24.9.0: resolution: {integrity: sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==} engines: {node: '>= 6'} @@ -33135,6 +33696,34 @@ packages: transitivePeerDependencies: - supports-color + /jest-snapshot@29.6.2: + resolution: {integrity: sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.21.3 + '@babel/generator': 7.21.3 + '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.21.3) + '@babel/plugin-syntax-typescript': 7.18.6(@babel/core@7.21.3) + '@babel/types': 7.22.4 + '@jest/expect-utils': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.3) + chalk: 4.1.2 + expect: 29.6.2 + graceful-fs: 4.2.9 + jest-diff: 29.6.2 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.6.2 + jest-message-util: 29.6.2 + jest-util: 29.6.2 + natural-compare: 1.4.0 + pretty-format: 29.6.2 + semver: 7.5.3 + transitivePeerDependencies: + - supports-color + dev: true + /jest-util@24.9.0: resolution: {integrity: sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==} engines: {node: '>= 6'} @@ -33199,6 +33788,18 @@ packages: graceful-fs: 4.2.9 picomatch: 2.3.1 + /jest-util@29.6.2: + resolution: {integrity: sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + chalk: 4.1.2 + ci-info: 3.2.0 + graceful-fs: 4.2.9 + picomatch: 2.3.1 + dev: true + /jest-validate@24.9.0: resolution: {integrity: sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==} engines: {node: '>= 6'} @@ -33256,6 +33857,18 @@ packages: leven: 3.1.0 pretty-format: 29.5.0 + /jest-validate@29.6.2: + resolution: {integrity: sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + camelcase: 6.2.1 + chalk: 4.1.2 + jest-get-type: 29.4.3 + leven: 3.1.0 + pretty-format: 29.6.2 + dev: true + /jest-watcher@24.9.0: resolution: {integrity: sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==} engines: {node: '>= 6'} @@ -33321,6 +33934,20 @@ packages: jest-util: 29.5.0 string-length: 4.0.2 + /jest-watcher@29.6.2: + resolution: {integrity: sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.6.2 + string-length: 4.0.2 + dev: true + /jest-worker@24.9.0: resolution: {integrity: sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==} engines: {node: '>= 6'} @@ -33362,6 +33989,16 @@ packages: merge-stream: 2.0.0 supports-color: 8.1.1 + /jest-worker@29.6.2: + resolution: {integrity: sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 16.18.21 + jest-util: 29.6.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + /jest@24.9.0: resolution: {integrity: sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==} engines: {node: '>= 6'} @@ -33446,6 +34083,27 @@ packages: - ts-node dev: true + /jest@29.6.2(@types/node@16.18.21)(ts-node@10.9.1): + resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.2(ts-node@10.9.1) + '@jest/types': 29.6.1 + import-local: 3.0.3 + jest-cli: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jmespath@0.16.0: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} @@ -38485,6 +39143,15 @@ packages: ansi-styles: 5.2.0 react-is: 18.2.0 + /pretty-format@29.6.2: + resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /pretty-hrtime@1.0.3: resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=} engines: {node: '>= 0.8'} @@ -43448,6 +44115,40 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.1.1(@babel/core@7.21.3)(jest@29.6.2)(typescript@5.1.6): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.21.3 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.3 + typescript: 5.1.6 + yargs-parser: 21.1.1 + dev: true + /ts-loader@9.4.1(typescript@5.1.6)(webpack@5.76.3): resolution: {integrity: sha512-384TYAqGs70rn9F0VBnh6BPTfhga7yFNdC5gXbQpDrBj9/KsT4iRkGqKXhziofHOlE2j6YEaiTYVGKKvPhGWvw==} engines: {node: '>=12.0.0'} diff --git a/tools/code-analyzer/README.md b/tools/code-analyzer/README.md index 6d81997288e..cf5c98dae08 100644 --- a/tools/code-analyzer/README.md +++ b/tools/code-analyzer/README.md @@ -12,7 +12,7 @@ Currently there are 3 commands: Here is an example `analyzer` command, run from this directory: -`pnpm run analyzer -- lint "release/6.8" "6.8.0" -b release/6.7` +`pnpm analyzer lint "release/6.8" "6.8.0" -b release/6.7` In this command we compare the `release/6.7` and `release/6.8` branches to find differences, and we're looking for changes introduced since `6.8.0` (using the `@since` tag). @@ -22,7 +22,7 @@ To find out more about the other arguments to the command you can run `pnpm run Here is an example `major-minor` command, run from this directory: -`pnpm run analyzer major-minor -- "release/6.8" "plugins/woocommerce/woocommerce.php"` +`pnpm analyzer major-minor "release/6.8" "plugins/woocommerce/woocommerce.php"` In this command we checkout the branch `release/6.8` and check the version of the woocommerce.php mainfile located at the path passed. Note that at the time of writing the main file in this particular branch reports `6.8.1` so the output of this command is `6.8.0`. diff --git a/tools/code-analyzer/jest.config.js b/tools/code-analyzer/jest.config.js new file mode 100644 index 00000000000..b413e106db6 --- /dev/null +++ b/tools/code-analyzer/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/tools/code-analyzer/package.json b/tools/code-analyzer/package.json index a6b6d97fc66..8509dd26826 100644 --- a/tools/code-analyzer/package.json +++ b/tools/code-analyzer/package.json @@ -9,26 +9,30 @@ "dependencies": { "@actions/core": "^1.10.0", "@commander-js/extra-typings": "^10.0.3", - "@woocommerce/monorepo-utils": "workspace:*", "@oclif/core": "^2.4.0", "@tsconfig/node16": "^1.0.3", "@types/uuid": "^8.3.4", + "@woocommerce/monorepo-utils": "workspace:*", "commander": "^9.4.0", "dotenv": "^10.0.0", "simple-git": "^3.10.0", "uuid": "^8.3.2" }, "devDependencies": { + "@types/jest": "^27.4.1", "@types/node": "^16.18.18", "@woocommerce/eslint-plugin": "workspace:*", "eslint": "^8.32.0", + "jest": "^29.6.2", + "ts-jest": "^29.1.1", "ts-node": "^10.2.1", "tslib": "^2.3.1", "typescript": "^5.1.6" }, "scripts": { "lint": "eslint . --ext .ts", - "posttest": "pnpm lint", + "turbo:test": "jest", + "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "analyzer": "node -r ts-node/register ./src/commands/analyzer/index.ts" }, "engines": { diff --git a/tools/code-analyzer/src/lib/__mocks__/uuid.js b/tools/code-analyzer/src/lib/__mocks__/uuid.js new file mode 100644 index 00000000000..49f15593a13 --- /dev/null +++ b/tools/code-analyzer/src/lib/__mocks__/uuid.js @@ -0,0 +1,3 @@ +// See https://stackoverflow.com/a/75924883/472987 +const uuid = require( '../../../node_modules/uuid/dist' ); +module.exports = uuid; diff --git a/tools/code-analyzer/src/lib/__tests__/fixtures/diff.txt b/tools/code-analyzer/src/lib/__tests__/fixtures/diff.txt new file mode 100644 index 00000000000..d7e183271ed --- /dev/null +++ b/tools/code-analyzer/src/lib/__tests__/fixtures/diff.txt @@ -0,0 +1,39086 @@ +diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md +index 8176590cf5..1a6c107656 100644 +--- a/.github/PULL_REQUEST_TEMPLATE.md ++++ b/.github/PULL_REQUEST_TEMPLATE.md +@@ -2,7 +2,7 @@ + + - [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md)? + - [ ] Does your code follow the [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)? +-- [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change? ++- [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change? + + + +diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml +index 08fd2e67ea..49d96a9ae6 100644 +--- a/.github/workflows/build-release.yml ++++ b/.github/workflows/build-release.yml +@@ -61,3 +61,18 @@ jobs: + token: ${{ secrets.E2E_WORKFLOW_GH_TOKEN }} + ref: refs/heads/trunk + inputs: '{ "release_id": "${{ github.event.release.id }}" }' ++ send-release-notification: ++ if: github.event.release.prerelease == true && github.event.release.draft == false && github.event.release.tag_name != 'nightly' ++ name: Send Release Notification ++ needs: build ++ runs-on: ubuntu-20.04 ++ steps: ++ - name: Invoke Pre-Release Event ++ uses: aurelien-baudet/workflow-dispatch@v2 ++ with: ++ workflow: ${{ secrets.CD_RELEASE_NOTIFICATION_WORKFLOW }} ++ repo: ${{ secrets.CD_NOTIFICATION_REPO }} ++ token: ${{ secrets.CD_GH_TOKEN }} ++ ref: refs/heads/trunk ++ inputs: '{ "tag_name": "${{ github.event.release.tag_name }}" }' ++ +diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml +new file mode 100644 +index 0000000000..f54c5cf4b2 +--- /dev/null ++++ b/.github/workflows/package-release.yml +@@ -0,0 +1,15 @@ ++name: Package release ++on: ++ workflow_dispatch: ++ inputs: ++ packages: ++ description: 'Enter a specific package to release, or releases separated by commas, ie @woocommerce/components,@woocommerce/number. Leaving this input blank will release all eligible packages.' ++ required: false ++ default: '-a' ++jobs: ++ prepare: ++ name: Run the prepare script ++ runs-on: ubuntu-20.04 ++ steps: ++ - name: Run ++ run: echo "hello world" +diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml +index 911808ba55..381e8d14db 100644 +--- a/.github/workflows/pr-build-and-e2e-tests.yml ++++ b/.github/workflows/pr-build-and-e2e-tests.yml +@@ -1,14 +1,14 @@ + name: Run tests against PR + on: +- pull_request: +- workflow_dispatch: ++ pull_request: ++ workflow_dispatch: + + concurrency: +- group: ${{ github.workflow }}-${{ github.ref }} +- cancel-in-progress: true ++ group: ${{ github.workflow }}-${{ github.ref }} ++ cancel-in-progress: true + + env: +- E2E_PLAYWRIGHT: ${{ false }} ++ E2E_PLAYWRIGHT: ${{ true }} + + jobs: + e2e-tests-run: +@@ -18,13 +18,13 @@ jobs: + - uses: actions/checkout@v3 + - uses: ./.github/actions/cache-deps + with: +- workflow_name: pr-build-and-e2e-tests +- workflow_cache: ${{ secrets.WORKFLOW_CACHE }} ++ workflow_name: pr-build-and-e2e-tests ++ workflow_cache: ${{ secrets.WORKFLOW_CACHE }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: +- php-version: '7.4' ++ php-version: '7.4' + + - name: Install and Build + uses: ./.github/actions/install-build +@@ -35,6 +35,7 @@ jobs: + + - name: Run tests command. + if: env.E2E_PLAYWRIGHT != 'true' ++ id: run-puppeteer-e2e-tests + working-directory: plugins/woocommerce + env: + WC_E2E_SCREENSHOTS: 1 +@@ -42,6 +43,21 @@ jobs: + E2E_SLACK_CHANNEL: ${{ secrets.E2E_SLACK_CHANNEL }} + run: pnpm exec wc-e2e test:e2e + ++ - name: Archive Puppeteer E2E test results ++ if: | ++ always() && ++ env.E2E_PLAYWRIGHT != 'true' && ++ ( ++ steps.run-puppeteer-e2e-tests.conclusion != 'cancelled' || ++ steps.run-puppeteer-e2e-tests.conclusion != 'skipped' ++ ) ++ uses: actions/upload-artifact@v3 ++ with: ++ name: pptr-e2e-test-results ++ path: plugins/woocommerce/tests/e2e/test-results.json ++ if-no-files-found: ignore ++ retention-days: 5 ++ + - name: Download and install Chromium browser. + if: env.E2E_PLAYWRIGHT == 'true' + working-directory: plugins/woocommerce +@@ -97,8 +113,8 @@ jobs: + - uses: actions/checkout@v3 + - uses: ./.github/actions/cache-deps + with: +- workflow_name: pr-build-and-e2e-tests +- workflow_cache: ${{ secrets.WORKFLOW_CACHE }} ++ workflow_name: pr-build-and-e2e-tests ++ workflow_cache: ${{ secrets.WORKFLOW_CACHE }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 +@@ -137,8 +153,8 @@ jobs: + - uses: actions/checkout@v3 + - uses: ./.github/actions/cache-deps + with: +- workflow_name: pr-build-and-e2e-tests +- workflow_cache: ${{ secrets.WORKFLOW_CACHE }} ++ workflow_name: pr-build-and-e2e-tests ++ workflow_cache: ${{ secrets.WORKFLOW_CACHE }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 +@@ -165,6 +181,80 @@ jobs: + run: | + ./k6 run plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js + ++ test-summary: ++ name: Post test results ++ if: | ++ always() && ++ ( ++ contains( needs.*.result, 'success' ) || ++ contains( needs.*.result, 'failure' ) ++ ) ++ runs-on: ubuntu-20.04 ++ needs: [api-tests-run, e2e-tests-run] ++ steps: ++ - name: Create dirs ++ run: | ++ mkdir -p repo ++ mkdir -p artifacts/api ++ mkdir -p artifacts/e2e ++ mkdir -p output ++ ++ - name: Checkout code ++ uses: actions/checkout@v3 ++ with: ++ path: repo ++ ++ - name: Download API test report artifact ++ uses: actions/download-artifact@v3 ++ with: ++ name: api-test-report---pr-${{ github.event.number }} ++ path: artifacts/api ++ ++ - name: Download Playwright E2E test report artifact ++ if: env.E2E_PLAYWRIGHT == 'true' ++ uses: actions/download-artifact@v3 ++ with: ++ name: e2e-test-report---pr-${{ github.event.number }} ++ path: artifacts/e2e ++ ++ - name: Download Puppeteer E2E test report artifact ++ if: env.E2E_PLAYWRIGHT != 'true' ++ uses: actions/download-artifact@v3 ++ with: ++ name: pptr-e2e-test-results ++ path: artifacts/e2e ++ ++ - name: Prepare test summary ++ id: prepare-test-summary ++ uses: actions/github-script@v6 ++ env: ++ API_SUMMARY_PATH: ${{ github.workspace }}/artifacts/api/allure-report/widgets/summary.json ++ E2E_PW_SUMMARY_PATH: ${{ github.workspace }}/artifacts/e2e/allure-report/widgets/summary.json ++ E2E_PPTR_SUMMARY_PATH: ${{ github.workspace }}/artifacts/e2e/test-results.json ++ PR_NUMBER: ${{ github.event.number }} ++ SHA: ${{ github.event.pull_request.head.sha }} ++ with: ++ result-encoding: string ++ script: | ++ const script = require( './repo/.github/workflows/scripts/prepare-test-summary.js' ) ++ return await script( { core } ) ++ ++ - name: Find PR comment by github-actions[bot] ++ uses: peter-evans/find-comment@v2 ++ id: find-comment ++ with: ++ issue-number: ${{ github.event.pull_request.number }} ++ comment-author: 'github-actions[bot]' ++ body-includes: Test Results Summary ++ ++ - name: Create or update PR comment ++ uses: peter-evans/create-or-update-comment@v2 ++ with: ++ comment-id: ${{ steps.find-comment.outputs.comment-id }} ++ issue-number: ${{ github.event.pull_request.number }} ++ body: ${{ steps.prepare-test-summary.outputs.result }} ++ edit-mode: replace ++ + publish-test-reports: + name: Publish test reports + if: | +diff --git a/.github/workflows/pr-code-sniff.yml b/.github/workflows/pr-code-sniff.yml +index 4443cc348b..0d3dc8ab3d 100644 +--- a/.github/workflows/pr-code-sniff.yml ++++ b/.github/workflows/pr-code-sniff.yml +@@ -15,13 +15,13 @@ jobs: + steps: + - uses: actions/checkout@v3 + with: +- fetch-depth: 100 ++ fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 +- tools: composer, cs2pr ++ tools: composer + + - name: Tool versions + run: | +@@ -36,11 +36,9 @@ jobs: + - name: Install and Build + uses: ./.github/actions/install-build + +- - name: Run code sniff +- continue-on-error: true +- working-directory: plugins/woocommerce +- run: ./tests/bin/phpcs.sh "${{ github.event.pull_request.base.sha }}" "${{ github.event.after }}" +- +- - name: Show PHPCS results in PR +- working-directory: plugins/woocommerce +- run: cs2pr ./phpcs-report.xml ++ - name: Run code sniffer ++ uses: thenabeel/action-phpcs@v8 ++ with: ++ files: "**.php" ++ phpcs_path: plugins/woocommerce/vendor/bin/phpcs ++ standard: phpcs.xml +diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml +index 2beaac201c..22255a4a9a 100644 +--- a/.github/workflows/pr-highlight-changes.yml ++++ b/.github/workflows/pr-highlight-changes.yml +@@ -17,12 +17,12 @@ jobs: + pnpm build:feature-config --filter=woocommerce + - name: Run analyzer + id: run +- run: ./tools/code-analyzer/bin/dev analyzer "$GITHUB_HEAD_REF" -o github ++ run: ./tools/code-analyzer/bin/dev analyzer "$GITHUB_HEAD_REF" + - name: Print results + id: results + run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}${{ steps.run.outputs.schema }}${{ steps.run.outputs.database }}" + comment: +- name: Add comment to hightlight changes ++ name: Add comment to highlight changes + needs: analyze + runs-on: ubuntu-20.04 + steps: +diff --git a/.github/workflows/pr-lint-monorepo.yml b/.github/workflows/pr-lint-monorepo.yml +index a21f14ef2b..476c012a2f 100644 +--- a/.github/workflows/pr-lint-monorepo.yml ++++ b/.github/workflows/pr-lint-monorepo.yml +@@ -21,9 +21,19 @@ jobs: + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' ++ tools: composer + + - name: Check change files are touched for touched projects + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: php tools/monorepo/check-changelogger-use.php --debug "$BASE" "$HEAD" ++ ++ - name: Install PNPM ++ run: npm install -g pnpm@^6.24.2 ++ ++ - name: Install dependencies ++ run: pnpm install ++ ++ - name: Run changelog validation ++ run: pnpm changelog --filter=* -- validate +diff --git a/.github/workflows/pr-lint-test-js.yml b/.github/workflows/pr-lint-test-js.yml +index 04633f4863..2db5a6b6b5 100644 +--- a/.github/workflows/pr-lint-test-js.yml ++++ b/.github/workflows/pr-lint-test-js.yml +@@ -35,7 +35,7 @@ jobs: + uses: ./.github/actions/install-build + + - name: Lint +- run: pnpm -- turbo run lint --filter='@woocommerce/admin-library...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' ++ run: pnpm -- turbo run lint --filter='woocommerce/client/admin...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' + + - name: Test +- run: pnpm -- turbo run test --filter='@woocommerce/admin-library...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' ++ run: pnpm -- turbo run test --filter='woocommerce/client/admin...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' +diff --git a/.github/workflows/prepare-package-release.yml b/.github/workflows/prepare-package-release.yml +new file mode 100644 +index 0000000000..29036fdf45 +--- /dev/null ++++ b/.github/workflows/prepare-package-release.yml +@@ -0,0 +1,62 @@ ++name: Prepare package release ++on: ++ workflow_dispatch: ++ inputs: ++ packages: ++ description: 'Enter a specific package to release, or packages separated by commas, ie @woocommerce/components,@woocommerce/number. Leaving this input to the default "-a" will prepare to release all eligible packages.' ++ required: false ++ default: '-a' ++jobs: ++ prepare: ++ name: Run prepare script ++ runs-on: ubuntu-20.04 ++ steps: ++ - uses: actions/checkout@v3 ++ - uses: ./.github/actions/cache-deps ++ with: ++ workflow_name: prepare-package-release ++ workflow_cache: ${{ secrets.WORKFLOW_CACHE }} ++ ++ - name: Setup PHP ++ uses: shivammathur/setup-php@v2 ++ with: ++ php-version: '7.4' ++ ++ - name: Install PNPM ++ run: npm install -g pnpm@^6.24.2 ++ ++ - name: Install dependencies ++ run: pnpm install ++ ++ - name: Execute script ++ run: ./tools/package-release/bin/dev prepare ${{ github.event.inputs.packages }} ++ ++ - name: Print git status ++ run: git status ++ ++ - name: Get current date ++ id: date ++ run: echo "::set-output name=date::$(date +'%Y-%m-%d')" ++ ++ - name: Set all package string ++ id: all_description ++ if: ${{ github.event.inputs.packages == '-a'}} ++ run: echo "::set-output name=str::all packages" ++ ++ - name: Set Specific packages string ++ id: specific_description ++ if: ${{ github.event.inputs.packages != '-a'}} ++ run: echo "::set-output name=str::${{ github.event.inputs.packages }}" ++ ++ - name: Create Pull Request ++ uses: peter-evans/create-pull-request@v4 ++ with: ++ token: ${{ secrets.GITHUB_TOKEN }} ++ commit-message: 'Automated change: Prep ${{ steps.all_description.outputs.str || steps.specific_description.outputs.str }} for release.' ++ branch: release/packages-${{ steps.date.outputs.date }} ++ delete-branch: true ++ title: Prepare Packages for Release ++ reviewers: ${{ github.actor }} ++ body: | ++ # Prepare ${{ steps.all_description.outputs.str || steps.specific_description.outputs.str }} for release. ++ This PR has been autogenerated by [Prepare Package Release workflow](https://github.com/woocommerce/woocommerce/actions/workflows/prepare-package-release.yml) in run [${{ github.run_id }}](https://github.com/woocommerce/woocommerce/actions/runs/${{ github.run_id }}) +diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml +index 71dd29b060..33e289bf86 100644 +--- a/.github/workflows/release-code-freeze.yml ++++ b/.github/workflows/release-code-freeze.yml +@@ -2,76 +2,159 @@ name: "Enforce release code freeze" + on: + schedule: + - cron: '0 16 * * 4' # Run at 1600 UTC on Thursdays. ++ workflow_dispatch: ++ inputs: ++ timeOverride: ++ description: "Time Override: The time to use in checking whether the action should run (default: 'now')." ++ default: 'now' ++ skipSlackPing: ++ description: "Skip Slack Ping: If true, the Slack ping will be skipped (useful for testing)" ++ type: boolean ++ slackChannelOverride: ++ description: "Slack Channel Override: The channel ID to send the Slack ping about the freeze" ++ ++env: ++ TIME_OVERRIDE: ${{ inputs.timeOverride }} ++ GIT_COMMITTER_NAME: 'WooCommerce Bot' ++ GIT_COMMITTER_EMAIL: 'no-reply@woocommerce.com' ++ GIT_AUTHOR_NAME: 'WooCommerce Bot' ++ GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com' + + jobs: + maybe-create-next-milestone-and-release-branch: + name: "Maybe create next milestone and release branch" + runs-on: ubuntu-20.04 ++ outputs: ++ branch: ${{ steps.freeze.outputs.branch }} ++ release_version: ${{ steps.freeze.outputs.release_version }} ++ next_version: ${{ steps.freeze.outputs.next_version }} + steps: +- - name: "Get the action script" +- run: | +- scripts="post-request-shared.php release-code-freeze.php" +- for script in $scripts +- do +- curl \ +- --silent \ +- --fail \ +- --header 'Authorization: bearer ${{ secrets.GITHUB_TOKEN }}' \ +- --header 'User-Agent: GitHub action to enforce release code freeze' \ +- --header 'Accept: application/vnd.github.v3.raw' \ +- --output $script \ +- --location "$GITHUB_API_URL/repos/${{ github.repository }}/contents/.github/workflows/scripts/$script?ref=$GITHUB_REF" +- done +- env: +- GITHUB_API_URL: ${{ env.GITHUB_API_URL }} +- GITHUB_REF: ${{ env.GITHUB_REF }} ++ - name: Checkout code ++ uses: actions/checkout@v3 ++ with: ++ fetch-depth: 100 ++ ++ - uses: ./.github/actions/cache-deps ++ with: ++ workflow_name: release-code-freeze ++ workflow_cache: ${{ secrets.WORKFLOW_CACHE }} ++ ++ - name: Install PNPM ++ run: npm install -g pnpm@^6.24.2 ++ + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' ++ tools: composer ++ ++ - name: Install dependencies ++ run: pnpm install ++ + - name: "Run the script to enforce the code freeze" +- run: php release-code-freeze.php ++ id: freeze ++ run: php .github/workflows/scripts/release-code-freeze.php + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ++ GITHUB_OUTPUTS: 1 + +- notify-slack: +- name: "Sends code freeze notification to Slack" +- runs-on: ubuntu-20.04 +- needs: maybe-create-next-milestone-and-release-branch +- steps: +- - name: Get outgoing release version ++ - name: "Git fetch the newly created release branch" ++ run: git fetch origin ${{ steps.freeze.outputs.branch }} ++ ++ - name: "Checkout the release branch" ++ run: git checkout ${{ steps.freeze.outputs.branch }} ++ ++ - name: "Create a new branch for the changelog update PR" ++ run: git checkout -b ${{ format( 'update/{0}-changelog', steps.freeze.outputs.release_version ) }} ++ ++ - name: "Generate the changelog file" ++ run: pnpm changelog --filter=woocommerce -- write --add-pr-num -n -vvv --use-version ${{ steps.freeze.outputs.release_version }} ++ ++ - name: "git rm deleted files" ++ run: git rm $(git ls-files --deleted) ++ ++ - name: "Commit deletion" ++ run: git commit -m "Delete changelog files from ${{ steps.freeze.outputs.release_version }} release" ++ ++ - name: "Remember the deletion commit hash" ++ id: rev-parse ++ run: echo "::set-output name=hash::$(git rev-parse HEAD)" ++ ++ - name: "Insert NEXT_CHANGELOG contents into changelog and readme" ++ run: php .github/workflows/scripts/release-changelog.php ++ ++ - name: "git add changelog and readme files" ++ run: git add changelog.txt plugins/woocommerce/readme.txt ++ ++ - name: "Commit changelog and readme files" ++ run: git commit -m "Update the changelog and readme files for the ${{ steps.freeeze.outputs.release_version }} release" ++ ++ - name: "Push update branch to origin" ++ run: git push origin ${{ format( 'update/{0}-changelog', steps.freeze.outputs.release_version ) }} ++ ++ - name: "Stash any other undesired changes" ++ run: git stash ++ ++ - name: "Checkout trunk" ++ run: git checkout trunk ++ ++ - name: "Create a branch for the changelog files deletion" ++ run: git checkout -b ${{ format( 'delete/{0}-changelog', steps.freeze.outputs.release_version ) }} ++ ++ - name: "Cherry-pick the deletion commit" ++ run: git cherry-pick ${{ steps.rev-parse.outputs.hash }} ++ ++ - name: "Push deletion branch to origin" ++ run: git push origin ${{ format( 'delete/{0}-changelog', steps.freeze.outputs.release_version ) }} ++ ++ - name: "Create release branch PR" ++ id: release-pr + uses: actions/github-script@v6 +- id: outgoing + with: + script: | +- const latest = await github.rest.repos.getLatestRelease({ +- owner: 'woocommerce', +- repo: 'woocommerce' +- }); +- +- let version = parseFloat( latest.data.tag_name ) + 0.1; +- version = parseFloat( version ).toPrecision( 2 ); ++ const result = await github.rest.pulls.create( { ++ owner: "${{ github.repository_owner }}", ++ repo: "${{ github.event.repository.name }}", ++ head: "${{ format( 'update/{0}-changelog', steps.freeze.outputs.release_version ) }}", ++ base: "${{ steps.freeze.outputs.branch }}", ++ title: "${{ format( 'Release: Prepare the changelog for {0}', steps.freeze.outputs.release_version ) }}", ++ body: "${{ format( 'This pull request was automatically generated during the code freeze to prepare the changelog for {0}', steps.freeze.outputs.release_version ) }}" ++ } ); ++ ++ return result.data.number; + +- return version; +- - name: Get next release version ++ - run: echo '${{ steps.release-pr.outputs.result }}' ++ ++ - name: "Create trunk PR" ++ id: trunk-pr + uses: actions/github-script@v6 +- id: next + with: + script: | +- const latest = await github.rest.repos.getLatestRelease({ +- owner: 'woocommerce', +- repo: 'woocommerce' +- }); +- +- let version = parseFloat( latest.data.tag_name ) + 0.2; +- version = parseFloat( version ).toPrecision( 2 ); ++ const result = await github.rest.pulls.create( { ++ owner: "${{ github.repository_owner }}", ++ repo: "${{ github.event.repository.name }}", ++ head: "${{ format( 'delete/{0}-changelog', steps.freeze.outputs.release_version ) }}", ++ base: "trunk", ++ title: "${{ format( 'Release: Remove {0} change files', steps.freeze.outputs.release_version ) }}", ++ body: "${{ format( 'This pull request was automatically generated during the code freeze to remove the changefiles from {0} that are compiled into the `{1}` branch via #{2}', steps.freeze.outputs.release_version, steps.freeze.outputs.branch, steps.release-pr.outputs.result ) }}" ++ } ); ++ ++ return result.data.number; ++ + +- return version; ++ notify-slack: ++ name: "Sends code freeze notification to Slack" ++ if: ${{ inputs.skipSlackPing != true }} ++ runs-on: ubuntu-20.04 ++ needs: maybe-create-next-milestone-and-release-branch ++ steps: + - name: Slack + uses: archive/github-actions-slack@v2.0.0 + id: notify + with: +- slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }} +- slack-channel: ${{ secrets.WOO_RELEASE_SLACK_CHANNEL }} +- slack-text: ":warning-8c: ${{ steps.outgoing.outputs.result }} Code Freeze :ice_cube: +-The automation to cut the release branch for ${{ steps.outgoing.outputs.result }} has run. Any PRs that were not already merged will be a part of ${{ steps.next.outputs.result }} by default. If you have something that needs to make ${{ steps.outgoing.outputs.result }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>." ++ slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }} ++ slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }} ++ slack-text: | ++ :warning-8c: ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }} Code Freeze :ice_cube: ++ ++ The automation to cut the release branch for ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }} has run. Any PRs that were not already merged will be a part of ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }} by default. If you have something that needs to make ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>. +diff --git a/.github/workflows/scripts/prepare-test-summary.js b/.github/workflows/scripts/prepare-test-summary.js +new file mode 100644 +index 0000000000..73d7c45760 +--- /dev/null ++++ b/.github/workflows/scripts/prepare-test-summary.js +@@ -0,0 +1,149 @@ ++/** ++ * Script to generate the test results summary to be posted as a GitHub Job Summary and as a PR comment. ++ */ ++const { ++ E2E_PLAYWRIGHT, ++ API_SUMMARY_PATH, ++ E2E_PW_SUMMARY_PATH, ++ E2E_PPTR_SUMMARY_PATH, ++ SHA, ++ PR_NUMBER, ++} = process.env; ++ ++/** ++ * Convert the given `duration` from milliseconds to a more user-friendly string. ++ * For example, if `duration = 323000`, this function would return `5m 23s`. ++ * ++ * @param {Number} duration Duration in millisecods, as read from either the `summary.json` file in the Allure report, or from the `test-results.json` file from the Jest-Puppeteer report. ++ * @returns String in "5m 23s" format. ++ */ ++const getFormattedDuration = ( duration ) => { ++ const durationMinutes = Math.floor( duration / 1000 / 60 ); ++ const durationSeconds = Math.floor( ( duration / 1000 ) % 60 ); ++ return `${ durationMinutes }m ${ durationSeconds }s`; ++}; ++ ++/** ++ * Extract the test report statistics (the number of tests that passed, failed, skipped, etc.) from Allure report's `summary.json` file. ++ * ++ * @param {string} summaryJSONPath Path to the Allure report's `summary.json` file. ++ * @param {string} testHeader The kind of test that generated the Allure report. For example, "E2E Tests". ++ * @returns Array containing stringified values of test stats. ++ */ ++const getAllureSummaryStats = ( summaryJSONPath, testHeader ) => { ++ const summary = require( summaryJSONPath ); ++ const { passed, failed, skipped, broken, unknown, total } = ++ summary.statistic; ++ const { duration } = summary.time; ++ const durationFormatted = getFormattedDuration( duration ); ++ ++ return [ ++ testHeader, ++ passed.toString(), ++ failed.toString(), ++ broken.toString(), ++ skipped.toString(), ++ unknown.toString(), ++ total.toString(), ++ durationFormatted, ++ ]; ++}; ++ ++/** ++ * Get API test result stats. ++ * ++ * @returns Array of API test result stats. ++ */ ++const getAPIStatsArr = () => { ++ return getAllureSummaryStats( API_SUMMARY_PATH, 'API Tests' ); ++}; ++ ++/** ++ * Get E2E test result stats. ++ * ++ * @returns Array of E2E test result stats. ++ */ ++const getE2EStatsArr = () => { ++ if ( E2E_PLAYWRIGHT === 'true' ) { ++ return getAllureSummaryStats( E2E_PW_SUMMARY_PATH, 'E2E Tests' ); ++ } else { ++ const summary = require( E2E_PPTR_SUMMARY_PATH ); ++ const { ++ numPassedTests: passed, ++ numFailedTests: failed, ++ numTotalTests: total, ++ numPendingTests: skipped, ++ numRuntimeErrorTestSuites: broken, ++ numTodoTests: unknown, ++ startTime, ++ testResults, ++ } = summary; ++ const endTime = testResults[ testResults.length - 1 ].endTime; ++ const duration = endTime - startTime; ++ const durationFormatted = getFormattedDuration( duration ); ++ ++ return [ ++ 'E2E Tests', ++ passed.toString(), ++ failed.toString(), ++ broken.toString(), ++ skipped.toString(), ++ unknown.toString(), ++ total.toString(), ++ durationFormatted, ++ ]; ++ } ++}; ++ ++/** ++ * Generate the contents of the test results summary and post it on the workflow run. ++ * ++ * @param {*} params Objects passed from the calling GitHub Action workflow. ++ * @returns Stringified content of the test results summary. ++ */ ++module.exports = async ( { core } ) => { ++ const apiStats = getAPIStatsArr(); ++ const e2eStats = getE2EStatsArr(); ++ ++ const contents = core.summary ++ .addHeading( 'Test Results Summary' ) ++ .addRaw( `Commit SHA: ${ SHA }` ) ++ .addBreak() ++ .addBreak() ++ .addTable( [ ++ [ ++ { data: 'Test :test_tube:', header: true }, ++ { data: 'Passed :white_check_mark:', header: true }, ++ { data: 'Failed :rotating_light:', header: true }, ++ { data: 'Broken :construction:', header: true }, ++ { data: 'Skipped :next_track_button:', header: true }, ++ { data: 'Unknown :grey_question:', header: true }, ++ { data: 'Total :bar_chart:', header: true }, ++ { data: 'Duration :stopwatch:', header: true }, ++ ], ++ apiStats, ++ e2eStats, ++ ] ) ++ .addRaw( 'To view the full API test report, click ' ) ++ .addLink( ++ 'here.', ++ `https://woocommerce.github.io/woocommerce-test-reports/pr/${ PR_NUMBER }/api/` ++ ) ++ .addBreak() ++ .addRaw( 'To view the full E2E test report, click ' ) ++ .addLink( ++ 'here.', ++ 'https://woocommerce.github.io/woocommerce-test-reports' ++ ) ++ .addBreak() ++ .addRaw( 'To view all test reports, visit the ' ) ++ .addLink( ++ 'WooCommerce Test Reports Dashboard', ++ 'https://woocommerce.github.io/woocommerce-test-reports/' ++ ) ++ .stringify(); ++ ++ await core.summary.write(); ++ ++ return contents; ++}; +diff --git a/.github/workflows/scripts/release-changelog.php b/.github/workflows/scripts/release-changelog.php +new file mode 100644 +index 0000000000..7071f2b553 +--- /dev/null ++++ b/.github/workflows/scripts/release-changelog.php +@@ -0,0 +1,34 @@ ++prepare. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - Correct generation of variation name when attribute is 0. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - couple of follow up fixes for Playwright tests [#33766](https://github.com/woocommerce/woocommerce/pull/33766) ++* Fix - Fix "Save changes" does not work on "Free features" step [#33844](https://github.com/woocommerce/woocommerce/pull/33844) ++* Fix - Fix AdditionalPayments task name [#33727](https://github.com/woocommerce/woocommerce/pull/33727) ++* Fix - Fix broken design of Single Product template in Twenty Twenty-Two when product had no reviews or additional info. Fix on sale badge being cut off on the Single Product template. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - Fixed 33810 - Columns of revenue table and when exporting that table, then generating CSV file, I have matched the sequence of columns of that CSV file. And I have also changed the name of the column of Net revenue in the CSV file to Net sales. [#33818](https://github.com/woocommerce/woocommerce/pull/33818) ++* Fix - Fixed incorrect tax calculation in edit order for "Local Pickup" shipping method. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - Fixed missing wcpay setup task, task fill page was missing for the woocommerce-payments task [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - Fixed some incorrect spellings in the shipping settings areas. [#33836](https://github.com/woocommerce/woocommerce/pull/33836) ++* Fix - Fix for broken table layout when reordering products on the sorting screen. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - Fix missing manage button for TikTok [#33731](https://github.com/woocommerce/woocommerce/pull/33731) ++* Fix - Fix missing text domain strings [#33780](https://github.com/woocommerce/woocommerce/pull/33780) ++* Fix - Fix print shipping banner close modal styles [#33587](https://github.com/woocommerce/woocommerce/pull/33587) ++* Fix - Fix shipping display logic country code [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - Fix tooltip not showing in variations meta box, and update tooltip copy. #33741 [#33741](https://github.com/woocommerce/woocommerce/pull/33741) ++* Fix - Fix tracks not including required properties when triggered by API requests. #33872 [#33872](https://github.com/woocommerce/woocommerce/pull/33872) ++* Fix - Fix Uncaught DOMException on WooCommerce -> Extensions page [#33711](https://github.com/woocommerce/woocommerce/pull/33711) ++* Fix - Fix untranslated texts on payment setting page [#33718](https://github.com/woocommerce/woocommerce/pull/33718) ++* Fix - Fix WC Status widget errors when analytics feature is disabled [#33816](https://github.com/woocommerce/woocommerce/pull/33816) ++* Fix - Fix wrong copy in payment recommendations [#33665](https://github.com/woocommerce/woocommerce/pull/33665) ++* Fix - Fix wrong copy in the accordion link for payment task [#33662](https://github.com/woocommerce/woocommerce/pull/33662) ++* Fix - Fix wrong copy in the payments task on the Home Task list [#33663](https://github.com/woocommerce/woocommerce/pull/33663) ++* Fix - Fix wrong copy in the payment task [#33749](https://github.com/woocommerce/woocommerce/pull/33749) ++* Fix - Fix wrong link and copy in payment setting page [#33716](https://github.com/woocommerce/woocommerce/pull/33716) ++* Fix - Fix wrong link in Set up payments screen [#33715](https://github.com/woocommerce/woocommerce/pull/33715) ++* Fix - Import correct controls for export function [#33709](https://github.com/woocommerce/woocommerce/pull/33709) ++* Fix - Revert marketing task completion logic to only complete after actioned by user [#33676](https://github.com/woocommerce/woocommerce/pull/33676) ++* Fix - Update StoreDetails task action url to navigate to the setting page [#33671](https://github.com/woocommerce/woocommerce/pull/33671) ++* Fix - WooCommerce Admin plugin deactivation message was causing much unnecessary alarm and has been improved #33592 [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Fix - `WP_Background_Process` updated to work nicely with memory_limits expressed in units other than 'M'. [#30908](https://github.com/woocommerce/woocommerce/pull/30908) ++* Add - Added a mechanism to customize the data being sent via AJAX by the order meta box [#33563](https://github.com/woocommerce/woocommerce/pull/33563) ++* Add - Added partial spotlight floater to shipping smart defaults tour. [#33772](https://github.com/woocommerce/woocommerce/pull/33772) ++* Add - Added tests for react admin translations [#33510](https://github.com/woocommerce/woocommerce/pull/33510) ++* Add - Add JS tests for shipping recommendation task [#33633](https://github.com/woocommerce/woocommerce/pull/33633) ++* Add - Add new experimental shipping task flow [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Add - Add shipping-smart-defaults intro tooltips. [#33772](https://github.com/woocommerce/woocommerce/pull/33772) ++* Add - Add the command `wc com extension list` to retrieve and display the extension list for the connected site. [#33783](https://github.com/woocommerce/woocommerce/pull/33783) ++* Add - Implement `create()` in COT datastore. [#33341](https://github.com/woocommerce/woocommerce/pull/33341) ++* Add - Implement `delete()` in the COT datastore [#33708](https://github.com/woocommerce/woocommerce/pull/33708) ++* Add - Include Tracks indicator property for block editor on product update. #33321 [#33321](https://github.com/woocommerce/woocommerce/pull/33321) ++* Add - Product creation experience: improve inventory copy #33754 [#33754](https://github.com/woocommerce/woocommerce/pull/33754) ++* Add - Tracks for shipping smart defaults [#33748](https://github.com/woocommerce/woocommerce/pull/33748) ++* Update - Deploy header task variant from task list experiment [#33750](https://github.com/woocommerce/woocommerce/pull/33750) ++* Update - Implement bulk actions in the new orders admin list table. [#33687](https://github.com/woocommerce/woocommerce/pull/33687) ++* Update - Making default state of product image meta boxes more prominent. [#33707](https://github.com/woocommerce/woocommerce/pull/33707) ++* Update - Randomize the order of sections in Recommended Marketing Extensions [#33851](https://github.com/woocommerce/woocommerce/pull/33851) ++* Update - Removed two-col task list expierments code [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Update - Remove legacy image sizes [#33772](https://github.com/woocommerce/woocommerce/pull/33772) ++* Update - Set ddefault shipping methods when store country is the US and Jetpack is intalled [#33788](https://github.com/woocommerce/woocommerce/pull/33788) ++* Update - Set smart shipping feature flags to true [#33819](https://github.com/woocommerce/woocommerce/pull/33819) ++* Update - Update display shipping task logic and add ReviewShippingOptions task [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Update - Update review shipping options task complete logic [#33650](https://github.com/woocommerce/woocommerce/pull/33650) ++* Update - Update shipping recommendations display logic [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Update - Update shipping task fields when shipping smart default feature is enabled. [#33630](https://github.com/woocommerce/woocommerce/pull/33630) ++* Update - Update wcpay suggestion UI in payment task [#33717](https://github.com/woocommerce/woocommerce/pull/33717) ++* Update - Update wcpay to include a mention of in-person payments for Canada [#33669](https://github.com/woocommerce/woocommerce/pull/33669) ++* Update - Update WooCommerce Blocks to 8.0.0 [#33736](https://github.com/woocommerce/woocommerce/pull/33736) ++* Update - Uses WC_Data_Store directly to count the shipping zones to avoid any unncessary query to the D.B [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Dev - Add playwright e2e README.md [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Dev - Add `$transaction_id` as arg to various `payment_complete` hooks. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Dev - Add `wc com extension install` CLI command [#33775](https://github.com/woocommerce/woocommerce/pull/33775) ++* Dev - Proper comment of get_formatted_shipping_address function. [#33823](https://github.com/woocommerce/woocommerce/pull/33823) ++* Tweak - Add a helper function to simplify ExPlat call [#33871](https://github.com/woocommerce/woocommerce/pull/33871) ++* Tweak - Add payment icons with Discover other payment providers link [#33744](https://github.com/woocommerce/woocommerce/pull/33744) ++* Tweak - Add the `WC_Product_CSV_Exporter` object as a third parameter of the `woocommerce_product_export_row_data` filter. [#33390](https://github.com/woocommerce/woocommerce/pull/33390) ++* Tweak - Fix code that throws deprecation notices in tests in PHP 8.1 [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Tweak - MakeThis PR makes Set your store location and Review your shipping options steps clickable on the shipping task when shipping-smart-defaults feature is enabled. [#33661](https://github.com/woocommerce/woocommerce/pull/33661) ++* Tweak - Remove blog URL from requests to external IP lookup services. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Tweak - Set default shipping methods based on the OBW selections [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Tweak - Show a generic error when trying to process a checkout while offline. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Tweak - The file writing mode used during CSV exports is now filterable via new hook `woocommerce_csv_exporter_fopen_mode`. [#33652](https://github.com/woocommerce/woocommerce/pull/33652) ++* Tweak - Update Danish currency symbol to match CLDR R41 spec. [#33643](https://github.com/woocommerce/woocommerce/pull/33643) ++* Tweak - Use transient to cache Shipping:has_shipping_zones() method [#33813](https://github.com/woocommerce/woocommerce/pull/33813) ++* Performance - Improve performance of Shipping Task. [#33886](https://github.com/woocommerce/woocommerce/pull/33886) ++* Enhancement - Add capability for set location step to be manually navigated in shipping recommendation task [#33667](https://github.com/woocommerce/woocommerce/pull/33667) ++ + = 6.7.0 2022-07-12 = + + * Add - Add support to remote inbox notification actions to link to other wp-admin pages. [#33237](https://github.com/woocommerce/woocommerce/pull/33237) +diff --git a/package.json b/package.json +index 99b4cbaff3..b8304993ff 100644 +--- a/package.json ++++ b/package.json +@@ -20,7 +20,8 @@ + "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install", + "storybook": "./tools/storybook/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true start-storybook -c ./tools/storybook/.storybook -p 6007 --ci", + "storybook-rtl": "USE_RTL_STYLE=true pnpm run storybook", +- "create-extension": "node ./tools/create-extension/index.js" ++ "create-extension": "node ./tools/create-extension/index.js", ++ "cherry-pick": "node ./tools/cherry-pick/bin/run" + }, + "devDependencies": { + "@babel/preset-env": "^7.16.11", +diff --git a/packages/js/admin-e2e-tests/changelog/add-require-turbo b/packages/js/admin-e2e-tests/changelog/add-require-turbo +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/admin-e2e-tests/changelog/add-require-turbo +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/admin-e2e-tests/changelog/fix-changelogger-phpcs b/packages/js/admin-e2e-tests/changelog/fix-changelogger-phpcs +new file mode 100644 +index 0000000000..10fdefc7d2 +--- /dev/null ++++ b/packages/js/admin-e2e-tests/changelog/fix-changelogger-phpcs +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: PHPCS violation fixes ++ ++ +diff --git a/packages/js/admin-e2e-tests/changelog/fix-packages-postinall b/packages/js/admin-e2e-tests/changelog/fix-packages-postinall +new file mode 100644 +index 0000000000..fda60a5df8 +--- /dev/null ++++ b/packages/js/admin-e2e-tests/changelog/fix-packages-postinall +@@ -0,0 +1,4 @@ ++Significance: minor ++Type: dev ++ ++Remove PHP and Composer dependencies for packaged JS packages +diff --git a/packages/js/admin-e2e-tests/changelog/update-add-tests-for-translations b/packages/js/admin-e2e-tests/changelog/update-add-tests-for-translations +new file mode 100644 +index 0000000000..4428a9c325 +--- /dev/null ++++ b/packages/js/admin-e2e-tests/changelog/update-add-tests-for-translations +@@ -0,0 +1,4 @@ ++Significance: patch ++Type: dev ++ ++Add tests for react-admin translations. +\ No newline at end of file +diff --git a/packages/js/admin-e2e-tests/composer.json b/packages/js/admin-e2e-tests/composer.json +index 735ef994bd..90649ce14a 100644 +--- a/packages/js/admin-e2e-tests/composer.json ++++ b/packages/js/admin-e2e-tests/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/admin-e2e-tests/composer.lock b/packages/js/admin-e2e-tests/composer.lock +index ed3dc9842f..602ff6fabd 100644 +--- a/packages/js/admin-e2e-tests/composer.lock ++++ b/packages/js/admin-e2e-tests/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "c407559c42a561d597c47dd3ab221487", ++ "content-hash": "cae17ca18e2a2a6cefe200df88081346", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json +index 8ac33e230f..924001b401 100644 +--- a/packages/js/admin-e2e-tests/package.json ++++ b/packages/js/admin-e2e-tests/package.json +@@ -42,6 +42,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "jest-mock-extended": "^1.0.18", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +@@ -50,12 +51,12 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", +- "build": "tsc --build", ++ "build": "node ./node_modules/require-turbo && tsc --build", + "start": "tsc --build --watch", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", + "prepack": "pnpm run clean && pnpm run build" + }, +diff --git a/packages/js/admin-e2e-tests/src/fixtures/reset.ts b/packages/js/admin-e2e-tests/src/fixtures/reset.ts +index c7cc5d07c2..594bba9447 100644 +--- a/packages/js/admin-e2e-tests/src/fixtures/reset.ts ++++ b/packages/js/admin-e2e-tests/src/fixtures/reset.ts +@@ -10,6 +10,7 @@ const { utils } = require( '@woocommerce/e2e-utils' ); + const { PLUGIN_NAME } = process.env; + + const resetEndpoint = '/woocommerce-reset/v1/state'; ++const switchLanguageEndpoint = '/woocommerce-reset/v1/switch-language'; + + const pluginName = PLUGIN_NAME ? PLUGIN_NAME : 'WooCommerce'; + const pluginNameSlug = utils.getSlug( pluginName ); +@@ -31,3 +32,9 @@ export async function resetWooCommerceState() { + expect( response.statusCode ).toEqual( 200 ); + await deactivateAndDeleteAllPlugins( skippedPlugins ); + } ++ ++export async function switchLanguage( lang: string ) { ++ await httpClient.post( switchLanguageEndpoint, { ++ lang, ++ } ); ++} +diff --git a/packages/js/admin-e2e-tests/src/pages/BasePage.ts b/packages/js/admin-e2e-tests/src/pages/BasePage.ts +index 7b4ff46390..8a4c66057e 100644 +--- a/packages/js/admin-e2e-tests/src/pages/BasePage.ts ++++ b/packages/js/admin-e2e-tests/src/pages/BasePage.ts +@@ -47,9 +47,8 @@ export abstract class BasePage { + + getDropdownTypeahead( selector: string ): DropdownTypeaheadField { + if ( ! this.dropDownTypeAheadElements[ selector ] ) { +- this.dropDownTypeAheadElements[ +- selector +- ] = new DropdownTypeaheadField( page, selector ); ++ this.dropDownTypeAheadElements[ selector ] = ++ new DropdownTypeaheadField( page, selector ); + } + + return this.dropDownTypeAheadElements[ selector ]; +diff --git a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts +index 717cacf418..4739b2e629 100644 +--- a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts ++++ b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts +@@ -392,8 +392,7 @@ const testSubscriptionsInclusion = () => { + await profileWizard.productTypes.isDisplayed( 7 ); + await profileWizard.productTypes.selectProduct( 'Subscriptions' ); + await expect( page ).not.toMatchElement( 'p', { +- text: +- 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.', ++ text: 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.', + } ); + + await profileWizard.continue(); +@@ -472,8 +471,7 @@ const testSubscriptionsInclusion = () => { + await profileWizard.productTypes.isDisplayed( 7 ); + await profileWizard.productTypes.selectProduct( 'Subscriptions' ); + await expect( page ).toMatchElement( 'p', { +- text: +- 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.', ++ text: 'The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.', + } ); + + await profileWizard.continue(); +diff --git a/packages/js/admin-e2e-tests/src/specs/index.ts b/packages/js/admin-e2e-tests/src/specs/index.ts +index dcb88e5985..a160a813e3 100644 +--- a/packages/js/admin-e2e-tests/src/specs/index.ts ++++ b/packages/js/admin-e2e-tests/src/specs/index.ts +@@ -7,3 +7,4 @@ export * from './tasks/payment'; + export * from './tasks/purchase'; + export * from './homescreen/task-list'; + export * from './homescreen/activity-panel'; ++export * from './translations'; +diff --git a/packages/js/admin-e2e-tests/src/specs/translations.ts b/packages/js/admin-e2e-tests/src/specs/translations.ts +new file mode 100644 +index 0000000000..ff68bb7e71 +--- /dev/null ++++ b/packages/js/admin-e2e-tests/src/specs/translations.ts +@@ -0,0 +1,85 @@ ++/** ++ * Internal dependencies ++ */ ++import { Login } from '../pages/Login'; ++import { OnboardingWizard } from '../pages/OnboardingWizard'; ++import { WcHomescreen } from '../pages/WcHomescreen'; ++import { Analytics } from '../pages/Analytics'; ++import { switchLanguage } from '../fixtures'; ++ ++/* eslint-disable @typescript-eslint/no-var-requires */ ++const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); ++/* eslint-enable @typescript-eslint/no-var-requires */ ++ ++const testAdminTranslations = () => { ++ describe( 'Test client, package, and PHP class translations,', () => { ++ const profileWizard = new OnboardingWizard( page ); ++ const homeScreen = new WcHomescreen( page ); ++ const analyticsPage = new Analytics( page ); ++ const login = new Login( page ); ++ ++ beforeAll( async () => { ++ await login.login(); ++ await switchLanguage( 'en_US' ); ++ await profileWizard.navigate(); ++ await profileWizard.skipStoreSetup(); ++ } ); ++ afterAll( async () => {} ); ++ ++ it( 'tests translations in PHP class, client, and component', async () => { ++ await homeScreen.isDisplayed(); ++ await homeScreen.possiblyDismissWelcomeModal(); ++ await homeScreen.navigate(); ++ await homeScreen.isDisplayed(); ++ const matchMenu = async ( expected: string ) => { ++ await expect( page ).toMatchElement( ++ '.toplevel_page_woocommerce ul li.wp-first-item a', ++ { ++ text: expected, ++ } ++ ); ++ }; ++ const matchH1 = async ( expected: string ) => { ++ await expect( page ).toMatchElement( 'h1', { ++ text: expected, ++ } ); ++ }; ++ ++ const matchDatePickerContentButton = async ( expected: string ) => { ++ await expect( page ).toMatchElement( ++ '.woocommerce-filters-date__button-group button', ++ { ++ text: expected, ++ } ++ ); ++ }; ++ ++ matchMenu( 'Home' ); ++ matchH1( 'Home' ); ++ ++ await switchLanguage( 'es_ES' ); ++ await page.reload(); ++ matchMenu( 'Inicio' ); ++ matchH1( 'Inicio' ); ++ ++ await switchLanguage( 'en_US' ); ++ ++ // Navigate to the Analytics page and test the component translation ++ await analyticsPage.navigate(); ++ await analyticsPage.isDisplayed(); ++ await analyticsPage.click( '.woocommerce-filters-filter button' ); ++ await matchDatePickerContentButton( 'Update' ); ++ ++ await switchLanguage( 'es_ES' ); ++ await page.reload(); ++ await analyticsPage.isDisplayed(); ++ await analyticsPage.click( '.woocommerce-filters-filter button' ); ++ await matchDatePickerContentButton( 'Actualizar' ); ++ ++ // Rendimiento ++ await switchLanguage( 'en_US' ); ++ } ); ++ } ); ++}; ++ ++module.exports = { testAdminTranslations }; +diff --git a/packages/js/admin-e2e-tests/src/utils/actions.ts b/packages/js/admin-e2e-tests/src/utils/actions.ts +index 54218cbd22..7a37cefd6d 100644 +--- a/packages/js/admin-e2e-tests/src/utils/actions.ts ++++ b/packages/js/admin-e2e-tests/src/utils/actions.ts +@@ -61,8 +61,7 @@ const verifyPublishAndTrash = async ( + await expect( page ).toMatchElement( + '#woocommerce-order-notes .note_content', + { +- text: +- 'Order status changed from Pending payment to Processing.', ++ text: 'Order status changed from Pending payment to Processing.', + } + ); + } +diff --git a/packages/js/api-core-tests/README.md b/packages/js/api-core-tests/README.md +index edce06bbb4..5f63b2f3fd 100644 +--- a/packages/js/api-core-tests/README.md ++++ b/packages/js/api-core-tests/README.md +@@ -22,7 +22,7 @@ For local setup, create a `.env` file in this folder with the three required val + Alternatively, these values can be passed in via the command line. For example: + + ```shell +-BASE_URL=http://localhost:8084 USER_KEY=admin USER_SECRET=password npm run e2e:api ++BASE_URL=http://localhost:8086 USER_KEY=admin USER_SECRET=password npm run e2e:api + ``` + + When using a username and password combination instead of a consumer secret and consumer key, make sure to have the [JSON Basic Authentication plugin](https://github.com/WP-API/Basic-Auth) installed and activated on the test site. +diff --git a/packages/js/api-core-tests/changelog/add-require-turbo b/packages/js/api-core-tests/changelog/add-require-turbo +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/api-core-tests/changelog/add-require-turbo +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/api-core-tests/composer.lock b/packages/js/api-core-tests/composer.lock +deleted file mode 100644 +index a4ca13764f..0000000000 +--- a/packages/js/api-core-tests/composer.lock ++++ /dev/null +@@ -1,1021 +0,0 @@ +-{ +- "_readme": [ +- "This file locks the dependencies of your project to a known state", +- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", +- "This file is @generated automatically" +- ], +- "content-hash": "dfa2036cb7c8dbe5fac357bce6f30a0f", +- "packages": [], +- "packages-dev": [ +- { +- "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", +- "source": { +- "type": "git", +- "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "shasum": "" +- }, +- "require": { +- "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" +- }, +- "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" +- }, +- "bin": [ +- "bin/changelogger" +- ], +- "type": "project", +- "extra": { +- "autotagger": true, +- "branch-alias": { +- "dev-master": "3.0.x-dev" +- }, +- "mirror-repo": "Automattic/jetpack-changelogger", +- "version-constants": { +- "::VERSION": "src/Application.php" +- }, +- "changelogger": { +- "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" +- } +- }, +- "autoload": { +- "psr-4": { +- "Automattic\\Jetpack\\Changelogger\\": "src", +- "Automattic\\Jetpack\\Changelog\\": "lib" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", +- "time": "2021-11-02T14:06:49+00:00" +- }, +- { +- "name": "psr/container", +- "version": "1.1.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/php-fig/container.git", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Psr\\Container\\": "src/" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "PHP-FIG", +- "homepage": "https://www.php-fig.org/" +- } +- ], +- "description": "Common Container Interface (PHP FIG PSR-11)", +- "homepage": "https://github.com/php-fig/container", +- "keywords": [ +- "PSR-11", +- "container", +- "container-interface", +- "container-interop", +- "psr" +- ], +- "time": "2021-03-05T17:36:06+00:00" +- }, +- { +- "name": "symfony/console", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/console.git", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/console/zipball/bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/deprecation-contracts": "^2.1", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php73": "^1.8", +- "symfony/polyfill-php80": "^1.16", +- "symfony/service-contracts": "^1.1|^2", +- "symfony/string": "^5.1|^6.0" +- }, +- "conflict": { +- "psr/log": ">=3", +- "symfony/dependency-injection": "<4.4", +- "symfony/dotenv": "<5.1", +- "symfony/event-dispatcher": "<4.4", +- "symfony/lock": "<4.4", +- "symfony/process": "<4.4" +- }, +- "provide": { +- "psr/log-implementation": "1.0|2.0" +- }, +- "require-dev": { +- "psr/log": "^1|^2", +- "symfony/config": "^4.4|^5.0|^6.0", +- "symfony/dependency-injection": "^4.4|^5.0|^6.0", +- "symfony/event-dispatcher": "^4.4|^5.0|^6.0", +- "symfony/lock": "^4.4|^5.0|^6.0", +- "symfony/process": "^4.4|^5.0|^6.0", +- "symfony/var-dumper": "^4.4|^5.0|^6.0" +- }, +- "suggest": { +- "psr/log": "For using the console logger", +- "symfony/event-dispatcher": "", +- "symfony/lock": "", +- "symfony/process": "" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Console\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Eases the creation of beautiful and testable command line interfaces", +- "homepage": "https://symfony.com", +- "keywords": [ +- "cli", +- "command line", +- "console", +- "terminal" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/deprecation-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/deprecation-contracts.git", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "files": [ +- "function.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "A generic function and convention to trigger deprecation notices", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-12T14:48:14+00:00" +- }, +- { +- "name": "symfony/polyfill-ctype", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-ctype.git", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-ctype": "*" +- }, +- "suggest": { +- "ext-ctype": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Ctype\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Gert de Pagter", +- "email": "BackEndTea@gmail.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for ctype functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "ctype", +- "polyfill", +- "portable" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-grapheme", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-grapheme.git", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Grapheme\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's grapheme_* functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "grapheme", +- "intl", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-26T17:12:59+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-normalizer", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-normalizer.git", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Normalizer\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's Normalizer class and related functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "intl", +- "normalizer", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-02-19T12:13:01+00:00" +- }, +- { +- "name": "symfony/polyfill-mbstring", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-mbstring.git", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/11b9acb5e8619aef6455735debf77dde8825795c", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-mbstring": "*" +- }, +- "suggest": { +- "ext-mbstring": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Mbstring\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for the Mbstring extension", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "mbstring", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-php73", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php73.git", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php73\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-06-05T21:20:04+00:00" +- }, +- { +- "name": "symfony/polyfill-php80", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php80.git", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php80\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Ion Bazan", +- "email": "ion.bazan@gmail.com" +- }, +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-09-13T13:58:33+00:00" +- }, +- { +- "name": "symfony/process", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/process.git", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/process/zipball/cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-php80": "^1.16" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Process\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Executes commands in sub-processes", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/service-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/service-contracts.git", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "psr/container": "^1.1" +- }, +- "conflict": { +- "ext-psr": "<1.1|>=2" +- }, +- "suggest": { +- "symfony/service-implementation": "" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Contracts\\Service\\": "" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Generic abstractions related to writing services", +- "homepage": "https://symfony.com", +- "keywords": [ +- "abstractions", +- "contracts", +- "decoupling", +- "interfaces", +- "interoperability", +- "standards" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-13T09:35:11+00:00" +- }, +- { +- "name": "symfony/string", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/string.git", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/string/zipball/dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-ctype": "~1.8", +- "symfony/polyfill-intl-grapheme": "~1.0", +- "symfony/polyfill-intl-normalizer": "~1.0", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php80": "~1.15" +- }, +- "require-dev": { +- "symfony/error-handler": "^4.4|^5.0|^6.0", +- "symfony/http-client": "^4.4|^5.0|^6.0", +- "symfony/translation-contracts": "^1.1|^2", +- "symfony/var-exporter": "^4.4|^5.0|^6.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\String\\": "" +- }, +- "files": [ +- "Resources/functions.php" +- ], +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", +- "homepage": "https://symfony.com", +- "keywords": [ +- "grapheme", +- "i18n", +- "string", +- "unicode", +- "utf-8", +- "utf8" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-28T19:23:26+00:00" +- }, +- { +- "name": "wikimedia/at-ease", +- "version": "v2.1.0", +- "source": { +- "type": "git", +- "url": "https://github.com/wikimedia/at-ease.git", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.9" +- }, +- "require-dev": { +- "mediawiki/mediawiki-codesniffer": "35.0.0", +- "mediawiki/minus-x": "1.1.1", +- "ockcyp/covers-validator": "1.3.3", +- "php-parallel-lint/php-console-highlighter": "0.5.0", +- "php-parallel-lint/php-parallel-lint": "1.2.0", +- "phpunit/phpunit": "^8.5" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" +- }, +- "files": [ +- "src/Wikimedia/Functions.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "authors": [ +- { +- "name": "Tim Starling", +- "email": "tstarling@wikimedia.org" +- }, +- { +- "name": "MediaWiki developers", +- "email": "wikitech-l@lists.wikimedia.org" +- } +- ], +- "description": "Safe replacement to @ for suppressing warnings.", +- "homepage": "https://www.mediawiki.org/wiki/at-ease", +- "time": "2021-02-27T15:53:37+00:00" +- } +- ], +- "aliases": [], +- "minimum-stability": "dev", +- "stability-flags": [], +- "prefer-stable": false, +- "prefer-lowest": false, +- "platform": [], +- "platform-dev": [], +- "plugin-api-version": "1.1.0" +-} +diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json +index 9db8eeaaee..f920cfda77 100644 +--- a/packages/js/api-core-tests/package.json ++++ b/packages/js/api-core-tests/package.json +@@ -9,7 +9,7 @@ + "e2e:hello": "jest --group=hello", + "make:collection": "node utils/api-collection/build-collection.js", + "report": "allure generate --clean && allure serve", +- "lint": "eslint data endpoints tests utils --ext=js,ts,tsx", ++ "lint": "node ./node_modules/require-turbo && eslint data endpoints tests utils --ext=js,ts,tsx", + "lint:fix": "eslint data endpoints tests utils --ext=js,ts,tsx --fix" + }, + "repository": { +@@ -32,7 +32,8 @@ + }, + "devDependencies": { + "@woocommerce/eslint-plugin": "workspace:*", +- "eslint": "^8.12.0" ++ "eslint": "^8.12.0", ++ "require-turbo": "workspace:*" + }, + "publishConfig": { + "access": "public" +diff --git a/packages/js/api/changelog/add-require-turbo b/packages/js/api/changelog/add-require-turbo +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/api/changelog/add-require-turbo +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/api/changelog/fix-changelogger-phpcs b/packages/js/api/changelog/fix-changelogger-phpcs +new file mode 100644 +index 0000000000..10fdefc7d2 +--- /dev/null ++++ b/packages/js/api/changelog/fix-changelogger-phpcs +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: PHPCS violation fixes ++ ++ +diff --git a/packages/js/api/changelog/fix-packages-postinall b/packages/js/api/changelog/fix-packages-postinall +new file mode 100644 +index 0000000000..fda60a5df8 +--- /dev/null ++++ b/packages/js/api/changelog/fix-packages-postinall +@@ -0,0 +1,4 @@ ++Significance: minor ++Type: dev ++ ++Remove PHP and Composer dependencies for packaged JS packages +diff --git a/packages/js/api/composer.json b/packages/js/api/composer.json +index 7643259118..384ed03b22 100644 +--- a/packages/js/api/composer.json ++++ b/packages/js/api/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/api/composer.lock b/packages/js/api/composer.lock +index e6b1839f4b..e2ee0a36ab 100644 +--- a/packages/js/api/composer.lock ++++ b/packages/js/api/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "e2aff2624b649c34dfbe4a964dcf573b", ++ "content-hash": "2ac4a9ea3ab4687cb26b0075128628de", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/api/package.json b/packages/js/api/package.json +index 198eaf1da4..d085d34e04 100644 +--- a/packages/js/api/package.json ++++ b/packages/js/api/package.json +@@ -26,15 +26,15 @@ + ], + "sideEffects": false, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "rm -rf ./dist ./tsconfig.tsbuildinfo", + "compile": "tsc -b", +- "build": "pnpm run clean && npm run compile", ++ "build": "node ./node_modules/require-turbo && pnpm run clean && npm run compile", + "prepack": "pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest" ++ "test": "node ./node_modules/require-turbo && jest" + }, + "dependencies": { + "axios": "^0.24.0", +@@ -50,6 +50,7 @@ + "axios-mock-adapter": "^1.20.0", + "eslint": "^8.2.0", + "jest": "^25", ++ "require-turbo": "workspace:*", + "ts-jest": "^25", + "typescript": "^4.4.4" + }, +diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md +index 25ae64b976..3e45922428 100644 +--- a/packages/js/components/CHANGELOG.md ++++ b/packages/js/components/CHANGELOG.md +@@ -1,4 +1,17 @@ +-## [10.1.0](https://www.npmjs.com/package/@woocommerce/components/v/10.1.0) - 2022-06-09 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [10.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/components/v/10.2.0) - 2022-07-08 ++ ++- Minor - Add step name to tour kit step type and export CloseHandler type to be reused elsewhere ++- Minor - Tree Select Control Component ++- Minor - Updated @automattic/tour-kit to 1.1.1 which has live resize functionality ++- Minor - Plugins component skip button is now optional ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++- Patch - Tweak tour kit gap between content and controls ++ ++## [10.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/components/v/10.1.0) - 2022-06-09 + + - Minor - Add tour kit component + - Minor - Update dependency `memoize-one` to ^6.0.0. #32936 +@@ -12,6 +25,4 @@ + - Patch - Fix documentation for `TableCard` component + - Patch - Update `StepperProps` prop types. #32712 + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/components/CHANGELOG.md). +diff --git a/packages/js/components/changelog/add-tour-kit-live-resize b/packages/js/components/changelog/add-tour-kit-live-resize +deleted file mode 100644 +index 54eaebb879..0000000000 +--- a/packages/js/components/changelog/add-tour-kit-live-resize ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: add +- +-Updated @automattic/tour-kit to 1.1.1 which has live resize functionality +\ No newline at end of file +diff --git a/packages/js/components/changelog/add-tour-kit-step-name b/packages/js/components/changelog/add-tour-kit-step-name +deleted file mode 100644 +index b03649effc..0000000000 +--- a/packages/js/components/changelog/add-tour-kit-step-name ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: add +- +-Add step name to tour kit step type and export CloseHandler type to be reused elsewhere +diff --git a/packages/js/components/changelog/dev-update-eslint-config b/packages/js/components/changelog/dev-update-eslint-config +new file mode 100644 +index 0000000000..8dbabc989d +--- /dev/null ++++ b/packages/js/components/changelog/dev-update-eslint-config +@@ -0,0 +1,4 @@ ++Significance: patch ++Type: fix ++ ++Fix missing text domain +diff --git a/packages/js/components/changelog/tweak-tour-kit-gap-between-content-and-controls b/packages/js/components/changelog/tweak-tour-kit-gap-between-content-and-controls +deleted file mode 100644 +index 08bd1d0c03..0000000000 +--- a/packages/js/components/changelog/tweak-tour-kit-gap-between-content-and-controls ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: patch +-Type: tweak +- +-Tweak tour kit gap between content and controls +diff --git a/packages/js/components/composer.json b/packages/js/components/composer.json +index 44254f79de..9bb6df4245 100644 +--- a/packages/js/components/composer.json ++++ b/packages/js/components/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/components/composer.lock b/packages/js/components/composer.lock +index bf7e37495a..87b9fcf1a8 100644 +--- a/packages/js/components/composer.lock ++++ b/packages/js/components/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "60aa0827b607ee34ee91ad1429694c57", ++ "content-hash": "75af54f4e83b1e2c7c96371c3288e355", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/components/package.json b/packages/js/components/package.json +index ea6075e85a..2fd271b525 100644 +--- a/packages/js/components/package.json ++++ b/packages/js/components/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/components", +- "version": "10.1.0", ++ "version": "10.2.0", + "description": "UI components for WooCommerce.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -106,6 +106,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "postcss-loader": "^3.0.0", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "sass-loader": "^10.2.1", + "ts-jest": "^27.1.3", +@@ -114,17 +115,17 @@ + "webpack-cli": "^3.3.12" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", +- "build": "pnpm run build:js && pnpm run build:css", ++ "build": "node ./node_modules/require-turbo && pnpm run build:js && pnpm run build:css", + "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:css": "webpack", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "lint": "eslint src --ext=js,ts,tsx", ++ "lint": "node ./node_modules/require-turbo && eslint src --ext=js,ts,tsx", + "lint:fix": "eslint src --ext=js,ts,tsx --fix", + "prepack": "pnpm run clean && pnpm run build", + "start": "concurrently \"tsc --build ./tsconfig.json --watch\" \"webpack --watch\"", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test:update-snapshots": "pnpm run test -- --updateSnapshot", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, +diff --git a/packages/js/components/src/advanced-filters/number-filter.js b/packages/js/components/src/advanced-filters/number-filter.js +index ff35fb8640..b03594d17e 100644 +--- a/packages/js/components/src/advanced-filters/number-filter.js ++++ b/packages/js/components/src/advanced-filters/number-filter.js +@@ -233,13 +233,8 @@ class NumberFilter extends Component { + } + + render() { +- const { +- className, +- config, +- filter, +- onFilterChange, +- isEnglish, +- } = this.props; ++ const { className, config, filter, onFilterChange, isEnglish } = ++ this.props; + const { rule } = filter; + const { labels, rules } = config; + +diff --git a/packages/js/components/src/advanced-filters/search-filter.js b/packages/js/components/src/advanced-filters/search-filter.js +index e230598ea6..9a5a175d9c 100644 +--- a/packages/js/components/src/advanced-filters/search-filter.js ++++ b/packages/js/components/src/advanced-filters/search-filter.js +@@ -98,13 +98,8 @@ class SearchFilter extends Component { + } + + render() { +- const { +- className, +- config, +- filter, +- onFilterChange, +- isEnglish, +- } = this.props; ++ const { className, config, filter, onFilterChange, isEnglish } = ++ this.props; + const { selected } = this.state; + const { rule } = filter; + const { input, labels, rules } = config; +diff --git a/packages/js/components/src/advanced-filters/select-filter.js b/packages/js/components/src/advanced-filters/select-filter.js +index f2726647c5..b80c08e841 100644 +--- a/packages/js/components/src/advanced-filters/select-filter.js ++++ b/packages/js/components/src/advanced-filters/select-filter.js +@@ -66,13 +66,8 @@ class SelectFilter extends Component { + } + + render() { +- const { +- className, +- config, +- filter, +- onFilterChange, +- isEnglish, +- } = this.props; ++ const { className, config, filter, onFilterChange, isEnglish } = ++ this.props; + const { options } = this.state; + const { rule, value } = filter; + const { labels, rules } = config; +diff --git a/packages/js/components/src/advanced-filters/stories/index.js b/packages/js/components/src/advanced-filters/stories/index.js +index 40ac520ed0..7b6d63bab1 100644 +--- a/packages/js/components/src/advanced-filters/stories/index.js ++++ b/packages/js/components/src/advanced-filters/stories/index.js +@@ -36,8 +36,7 @@ const advancedFilters = { + add: 'Order Status', + remove: 'Remove order status filter', + rule: 'Select an order status filter match', +- title: +- '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}', ++ title: '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}', + filter: 'Select an order status', + }, + rules: [ +@@ -105,8 +104,7 @@ const advancedFilters = { + add: 'Item Quantity', + remove: 'Remove item quantity filter', + rule: 'Select an item quantity filter match', +- title: +- '{{title}}Item Quantity is{{/title}} {{rule /}} {{filter /}}', ++ title: '{{title}}Item Quantity is{{/title}} {{rule /}} {{filter /}}', + }, + rules: [ + { +diff --git a/packages/js/components/src/advanced-filters/test/advanced-filters.test.js b/packages/js/components/src/advanced-filters/test/advanced-filters.test.js +index fd6095a5ec..cfa098fcd4 100644 +--- a/packages/js/components/src/advanced-filters/test/advanced-filters.test.js ++++ b/packages/js/components/src/advanced-filters/test/advanced-filters.test.js +@@ -41,8 +41,7 @@ const advancedFiltersConfig = { + add: 'Order Status', + remove: 'Remove order status filter', + rule: 'Select an order status filter match', +- title: +- '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}', ++ title: '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}', + filter: 'Select an order status', + }, + rules: [ +@@ -110,8 +109,7 @@ const advancedFiltersConfig = { + add: 'Item Quantity', + remove: 'Remove item quantity filter', + rule: 'Select an item quantity filter match', +- title: +- '{{title}}Item Quantity is{{/title}} {{rule /}} {{filter /}}', ++ title: '{{title}}Item Quantity is{{/title}} {{rule /}} {{filter /}}', + }, + rules: [ + { +diff --git a/packages/js/components/src/calendar/date-picker.js b/packages/js/components/src/calendar/date-picker.js +index d605dd495b..7fe681e7fa 100644 +--- a/packages/js/components/src/calendar/date-picker.js ++++ b/packages/js/components/src/calendar/date-picker.js +@@ -76,14 +76,8 @@ class DatePicker extends Component { + } + + render() { +- const { +- date, +- disabled, +- text, +- dateFormat, +- error, +- isInvalidDate, +- } = this.props; ++ const { date, disabled, text, dateFormat, error, isInvalidDate } = ++ this.props; + + return ( + key.visible ); + const colorKeys = +diff --git a/packages/js/components/src/chart/d3chart/utils/test/axis-y.js b/packages/js/components/src/chart/d3chart/utils/test/axis-y.js +index c8a663cb5a..fc42860d53 100644 +--- a/packages/js/components/src/chart/d3chart/utils/test/axis-y.js ++++ b/packages/js/components/src/chart/d3chart/utils/test/axis-y.js +@@ -15,19 +15,13 @@ describe( 'getYGrids', () => { + describe( 'positive charts', () => { + it( 'returns decimal values when yMax is <= 1 and yMin is 0', () => { + expect( getYGrids( 0, 1, 0.3333333333333333 ) ).toEqual( [ +- 0, +- 0.3333333333333333, +- 0.6666666666666666, +- 1, ++ 0, 0.3333333333333333, 0.6666666666666666, 1, + ] ); + } ); + + it( 'returns decimal values when yMax and yMin are <= 1', () => { + expect( getYGrids( 1, 1, 0.3333333333333333 ) ).toEqual( [ +- 0, +- 0.3333333333333333, +- 0.6666666666666666, +- 1, ++ 0, 0.3333333333333333, 0.6666666666666666, 1, + ] ); + } ); + +@@ -37,10 +31,7 @@ describe( 'getYGrids', () => { + + it( 'returns up to four values when yMax is a big number', () => { + expect( getYGrids( 0, 12000, 4000 ) ).toEqual( [ +- 0, +- 4000, +- 8000, +- 12000, ++ 0, 4000, 8000, 12000, + ] ); + } ); + } ); +@@ -48,19 +39,13 @@ describe( 'getYGrids', () => { + describe( 'negative charts', () => { + it( 'returns decimal values when yMin is >= -1 and yMax is 0', () => { + expect( getYGrids( -1, 0, 0.3333333333333333 ) ).toEqual( [ +- 0, +- -0.3333333333333333, +- -0.6666666666666666, +- -1, ++ 0, -0.3333333333333333, -0.6666666666666666, -1, + ] ); + } ); + + it( 'returns decimal values when yMax and yMin are >= -1', () => { + expect( getYGrids( -1, -1, 0.3333333333333333 ) ).toEqual( [ +- 0, +- -0.3333333333333333, +- -0.6666666666666666, +- -1, ++ 0, -0.3333333333333333, -0.6666666666666666, -1, + ] ); + } ); + +@@ -70,10 +55,7 @@ describe( 'getYGrids', () => { + + it( 'returns up to four values when yMin is a big negative number', () => { + expect( getYGrids( -12000, 0, 4000 ) ).toEqual( [ +- 0, +- -4000, +- -8000, +- -12000, ++ 0, -4000, -8000, -12000, + ] ); + } ); + } ); +@@ -81,11 +63,7 @@ describe( 'getYGrids', () => { + describe( 'positive & negative charts', () => { + it( 'returns decimal values when yMax is <= 1 and yMin is 0', () => { + expect( getYGrids( -1, 1, 0.5 ) ).toEqual( [ +- 0, +- -0.5, +- -1, +- 0.5, +- 1, ++ 0, -0.5, -1, 0.5, 1, + ] ); + } ); + +@@ -95,11 +73,7 @@ describe( 'getYGrids', () => { + + it( 'returns up to six values when yMax is a big number', () => { + expect( getYGrids( -12000, 12000, 6000 ) ).toEqual( [ +- 0, +- -6000, +- -12000, +- 6000, +- 12000, ++ 0, -6000, -12000, 6000, 12000, + ] ); + } ); + } ); +diff --git a/packages/js/components/src/chart/d3chart/utils/test/scales.js b/packages/js/components/src/chart/d3chart/utils/test/scales.js +index cc2e9e9161..ead4a76241 100644 +--- a/packages/js/components/src/chart/d3chart/utils/test/scales.js ++++ b/packages/js/components/src/chart/d3chart/utils/test/scales.js +@@ -96,8 +96,7 @@ describe( 'X scales', () => { + new Date( '2018-06-04T00:00:00' ), + ] ); + expect( scaleTime().rangeRound ).toHaveBeenLastCalledWith( [ +- 0, +- 100, ++ 0, 100, + ] ); + } ); + } ); +@@ -149,12 +148,10 @@ describe( 'Y scales', () => { + getYScale( 100, 0, 15000000 ); + + expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ +- 0, +- 15000000, ++ 0, 15000000, + ] ); + expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ +- 100, +- 0, ++ 100, 0, + ] ); + } ); + +@@ -162,12 +159,10 @@ describe( 'Y scales', () => { + getYScale( 100, -15000000, 0 ); + + expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ +- -15000000, +- 0, ++ -15000000, 0, + ] ); + expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ +- 100, +- 0, ++ 100, 0, + ] ); + } ); + +diff --git a/packages/js/components/src/date-range-filter-picker/stories/index.js b/packages/js/components/src/date-range-filter-picker/stories/index.js +index 8035c15bb8..c8d77a9cba 100644 +--- a/packages/js/components/src/date-range-filter-picker/stories/index.js ++++ b/packages/js/components/src/date-range-filter-picker/stories/index.js +@@ -22,9 +22,8 @@ const storeGetDateParamsFromQuery = partialRight( + ); + const storeGetCurrentDates = partialRight( getCurrentDates, defaultDateRange ); + const { period, compare, before, after } = storeGetDateParamsFromQuery( query ); +-const { primary: primaryDate, secondary: secondaryDate } = storeGetCurrentDates( +- query +-); ++const { primary: primaryDate, secondary: secondaryDate } = ++ storeGetCurrentDates( query ); + const dateQuery = { + period, + compare, +diff --git a/packages/js/components/src/dynamic-form/dynamic-form.tsx b/packages/js/components/src/dynamic-form/dynamic-form.tsx +index e0dc14391d..ab1c0279f2 100644 +--- a/packages/js/components/src/dynamic-form/dynamic-form.tsx ++++ b/packages/js/components/src/dynamic-form/dynamic-form.tsx +@@ -61,9 +61,10 @@ export const DynamicForm: React.FC< DynamicFormProps > = ( { + const fields = + baseFields instanceof Array ? baseFields : Object.values( baseFields ); + +- const initialValues = useMemo( () => getInitialConfigValues( fields ), [ +- fields, +- ] ); ++ const initialValues = useMemo( ++ () => getInitialConfigValues( fields ), ++ [ fields ] ++ ); + + return ( +
{ + it( 'should render an image uploader prepopulated with an upload', () => { + const image = { + id: 1234, +- url: +- 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg', ++ url: 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg', + }; + const { getByRole } = render( ); + +diff --git a/packages/js/components/src/list/index.js b/packages/js/components/src/list/index.js +index f407327e53..ec458d3eb2 100644 +--- a/packages/js/components/src/list/index.js ++++ b/packages/js/components/src/list/index.js +@@ -23,8 +23,7 @@ function List( props ) { + + deprecated( 'List with items prop is deprecated', { + version: '9.0.0', +- hint: +- 'See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.', ++ hint: 'See ExperimentalList / ExperimentalListItem for the new API that will replace this component in future versions.', + } ); + + return ( +diff --git a/packages/js/components/src/order-status/stories/index.js b/packages/js/components/src/order-status/stories/index.js +index 2bfaf7b1c6..1f07975fd4 100644 +--- a/packages/js/components/src/order-status/stories/index.js ++++ b/packages/js/components/src/order-status/stories/index.js +@@ -5,9 +5,9 @@ import { __ } from '@wordpress/i18n'; + import { OrderStatus } from '@woocommerce/components'; + + const orderStatusMap = { +- processing: __( 'Processing Order' ), +- pending: __( 'Pending Order' ), +- completed: __( 'Completed Order' ), ++ processing: __( 'Processing Order', 'woocommerce' ), ++ pending: __( 'Pending Order', 'woocommerce' ), ++ completed: __( 'Completed Order', 'woocommerce' ), + }; + + export const Basic = () => ( +diff --git a/packages/js/components/src/pagination/index.js b/packages/js/components/src/pagination/index.js +index c9575623d0..24369c6b91 100644 +--- a/packages/js/components/src/pagination/index.js ++++ b/packages/js/components/src/pagination/index.js +@@ -192,13 +192,8 @@ class Pagination extends Component { + } + + render() { +- const { +- total, +- perPage, +- className, +- showPagePicker, +- showPerPagePicker, +- } = this.props; ++ const { total, perPage, className, showPagePicker, showPerPagePicker } = ++ this.props; + this.pageCount = Math.ceil( total / perPage ); + + const classes = classNames( 'woocommerce-pagination', className ); +diff --git a/packages/js/components/src/plugins/index.js b/packages/js/components/src/plugins/index.js +index b721d5739f..0aa93ba70e 100644 +--- a/packages/js/components/src/plugins/index.js ++++ b/packages/js/components/src/plugins/index.js +@@ -36,11 +36,8 @@ export class Plugins extends Component { + event.preventDefault(); + } + +- const { +- installAndActivatePlugins, +- isRequesting, +- pluginSlugs, +- } = this.props; ++ const { installAndActivatePlugins, isRequesting, pluginSlugs } = ++ this.props; + + // Avoid double activating. + if ( isRequesting ) { +@@ -69,7 +66,9 @@ export class Plugins extends Component { + } + + skipInstaller() { +- this.props.onSkip(); ++ if ( this.props.onSkip ) { ++ this.props.onSkip(); ++ } + } + + render() { +@@ -78,6 +77,7 @@ export class Plugins extends Component { + skipText, + autoInstall, + pluginSlugs, ++ onSkip, + onAbort, + abortText, + } = this.props; +@@ -93,9 +93,14 @@ export class Plugins extends Component { + > + { __( 'Retry', 'woocommerce' ) } + +- ++ { onSkip && ( ++ ++ ) } + + ); + } +@@ -127,9 +132,11 @@ export class Plugins extends Component { + > + { __( 'Install & enable', 'woocommerce' ) } + +- ++ { onSkip && ( ++ ++ ) } + { onAbort && ( + ++ ) } ++ ++ { ++ onChange( e.target.checked, option ); ++ } } ++ onKeyDown={ ( e ) => { ++ handleKeyDown( e, option ); ++ } } ++ /> ++ ++ ++ { hasChildren && expanded && ( ++
++ ++
++ ) } ++ ++ ); ++ } ); ++}; ++ ++export default Options; +diff --git a/packages/js/components/src/tree-select-control/stories/index.js b/packages/js/components/src/tree-select-control/stories/index.js +new file mode 100644 +index 0000000000..da400a324b +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/stories/index.js +@@ -0,0 +1,96 @@ ++/** ++ * External dependencies ++ */ ++import { useState } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import TreeSelectControl from '../index'; ++ ++const treeSelectControlOptions = [ ++ { ++ value: 'EU', ++ label: 'Europe', ++ children: [ ++ { value: 'ES', label: 'Spain' }, ++ { value: 'FR', label: 'France' }, ++ { key: 'FR-Colonies', value: 'FR', label: 'France (Colonies)' }, ++ ], ++ }, ++ { ++ value: 'AS', ++ label: 'Asia', ++ children: [ ++ { ++ value: 'JP', ++ label: 'Japan', ++ children: [ ++ { ++ value: 'TO', ++ label: 'Tokio', ++ children: [ ++ { value: 'SI', label: 'Shibuya' }, ++ { value: 'GI', label: 'Ginza' }, ++ ], ++ }, ++ { value: 'OK', label: 'Okinawa' }, ++ ], ++ }, ++ { value: 'CH', label: 'China' }, ++ { ++ value: 'MY', ++ label: 'Malaysia', ++ children: [ { value: 'KU', label: 'Kuala Lumpur' } ], ++ }, ++ ], ++ }, ++ { ++ value: 'NA', ++ label: 'North America', ++ children: [ ++ { ++ value: 'US', ++ label: 'United States', ++ children: [ ++ { value: 'NY', label: 'New York' }, ++ { value: 'TX', label: 'Texas' }, ++ { value: 'GE', label: 'Georgia' }, ++ ], ++ }, ++ { ++ value: 'CA', ++ label: 'Canada', ++ }, ++ ], ++ }, ++]; ++ ++const Template = ( args ) => { ++ const [ selected, setSelected ] = useState( [ 'ES' ] ); ++ ++ return ( ++ ++ ); ++}; ++ ++export const Base = Template.bind( {} ); ++ ++Base.args = { ++ id: 'my-id', ++ label: 'Select Countries', ++ placeholder: 'Search countries', ++ disabled: false, ++ options: treeSelectControlOptions, ++ maxVisibleTags: 3, ++ selectAllLabel: 'All countries', ++}; ++ ++export default { ++ title: 'WooCommerce Admin/components/TreeSelectControl', ++ component: TreeSelectControl, ++}; +diff --git a/packages/js/components/src/tree-select-control/tags.js b/packages/js/components/src/tree-select-control/tags.js +new file mode 100644 +index 0000000000..ea7019fe71 +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/tags.js +@@ -0,0 +1,99 @@ ++/** ++ * External dependencies ++ */ ++import { __, sprintf } from '@wordpress/i18n'; ++import { useState, createElement } from '@wordpress/element'; ++import { Button } from '@wordpress/components'; ++ ++/** ++ * Internal dependencies ++ */ ++import Tag from '../tag'; ++ ++/** ++ * A list of tags to display selected items. ++ * ++ * @param {Object} props The component props ++ * @param {Object[]} [props.tags=[]] The tags ++ * @param {Function} props.onChange The method called when a tag is removed ++ * @param {boolean} props.disabled True if the plugin is disabled ++ * @param {number} [props.maxVisibleTags=0] The maximum number of tags to show. 0 or less than 0 evaluates to "Show All". ++ */ ++const Tags = ( { ++ tags = [], ++ disabled, ++ maxVisibleTags = 0, ++ onChange = () => {}, ++} ) => { ++ const [ showAll, setShowAll ] = useState( false ); ++ const maxTags = Math.max( 0, maxVisibleTags ); ++ const shouldShowAll = showAll || ! maxTags; ++ const visibleTags = shouldShowAll ? tags : tags.slice( 0, maxTags ); ++ ++ if ( ! tags.length ) { ++ return null; ++ } ++ ++ /** ++ * Callback to remove a Tag. ++ * The function is defined this way because in the WooCommerce Tag Component the remove logic ++ * is defined as `onClick={ remove(key) }` hence we need to do this to avoid calling remove function ++ * on each render. ++ * ++ * @param {string} key The key for the Tag to be deleted ++ */ ++ const remove = ( key ) => { ++ return () => { ++ if ( disabled ) { ++ return; ++ } ++ onChange( tags.filter( ( tag ) => tag.id !== key ) ); ++ }; ++ }; ++ ++ return ( ++
++ { visibleTags.map( ( item, i ) => { ++ if ( ! item.label ) { ++ return null; ++ } ++ const screenReaderLabel = sprintf( ++ // translators: 1: Tag Label, 2: Current Tag index, 3: Total amount of tags. ++ __( '%1$s (%2$d of %3$d)', 'woocommerce' ), ++ item.label, ++ i + 1, ++ tags.length ++ ); ++ return ( ++ ++ ); ++ } ) } ++ ++ { maxTags > 0 && tags.length > maxTags && ( ++ ++ ) } ++
++ ); ++}; ++ ++export default Tags; +diff --git a/packages/js/components/src/tree-select-control/test/control.test.js b/packages/js/components/src/tree-select-control/test/control.test.js +new file mode 100644 +index 0000000000..a2ecc86f91 +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/test/control.test.js +@@ -0,0 +1,113 @@ ++/** ++ * External dependencies ++ */ ++import { screen, render, fireEvent } from '@testing-library/react'; ++import userEvent from '@testing-library/user-event'; ++import { createElement } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import Control from '../control'; ++ ++describe( 'TreeSelectControl - Control Component', () => { ++ const onTagsChange = jest.fn().mockName( 'onTagsChange' ); ++ const ref = { ++ current: { ++ focus: jest.fn(), ++ }, ++ }; ++ ++ it( 'Renders the tags and calls onTagsChange when they change', () => { ++ const { queryByText, queryByLabelText, rerender } = render( ++ ++ ); ++ ++ expect( queryByText( 'Spain (1 of 1)' ) ).toBeTruthy(); ++ userEvent.click( queryByLabelText( 'Remove Spain' ) ); ++ expect( onTagsChange ).toHaveBeenCalledTimes( 1 ); ++ expect( onTagsChange ).toHaveBeenCalledWith( [] ); ++ ++ rerender( ++ ++ ); ++ ++ expect( screen.queryByText( 'Spain (1 of 1)' ) ).toBeTruthy(); ++ userEvent.click( screen.queryByLabelText( 'Remove Spain' ) ); ++ expect( onTagsChange ).toHaveBeenCalledTimes( 1 ); ++ } ); ++ ++ it( 'Calls onInputChange when typing', () => { ++ const onInputChange = jest ++ .fn() ++ .mockName( 'onInputChange' ) ++ .mockImplementation( ( e ) => e.target.value ); ++ const { queryByRole } = render( ++ ++ ); ++ ++ const input = queryByRole( 'combobox' ); ++ expect( input ).toBeTruthy(); ++ expect( input.hasAttribute( 'disabled' ) ).toBeFalsy(); ++ userEvent.type( input, 'a' ); ++ expect( onInputChange ).toHaveBeenCalledTimes( 1 ); ++ expect( onInputChange ).toHaveNthReturnedWith( 1, 'a' ); ++ fireEvent.change( input, { target: { value: 'test' } } ); ++ expect( onInputChange ).toHaveBeenCalledTimes( 2 ); ++ expect( onInputChange ).toHaveNthReturnedWith( 2, 'test' ); ++ } ); ++ ++ it( 'Allows disabled input', () => { ++ const onInputChange = jest.fn().mockName( 'onInputChange' ); ++ const { queryByRole } = render( ++ ++ ); ++ ++ const input = queryByRole( 'combobox' ); ++ expect( input ).toBeTruthy(); ++ expect( input.hasAttribute( 'disabled' ) ).toBeTruthy(); ++ userEvent.type( input, 'a' ); ++ expect( onInputChange ).not.toHaveBeenCalled(); ++ } ); ++ ++ it( 'Calls onFocus callback when it is focused', () => { ++ const onFocus = jest.fn().mockName( 'onFocus' ); ++ const { queryByRole } = render( ++ ++ ); ++ userEvent.click( queryByRole( 'combobox' ) ); ++ expect( onFocus ).toHaveBeenCalled(); ++ } ); ++ ++ it( 'Renders placeholder when there are no tags and is not expanded', () => { ++ const { rerender } = render( ); ++ let input = screen.queryByRole( 'combobox' ); ++ let placeholder = input.getAttribute( 'placeholder' ); ++ expect( placeholder ).toBe( 'Select' ); ++ ++ rerender( ++ ++ ); ++ ++ input = screen.queryByRole( 'combobox' ); ++ placeholder = input.getAttribute( 'placeholder' ); ++ expect( placeholder ).toBeFalsy(); ++ ++ rerender( ); ++ input = screen.queryByRole( 'combobox' ); ++ placeholder = input.getAttribute( 'placeholder' ); ++ expect( placeholder ).toBeFalsy(); ++ } ); ++} ); +diff --git a/packages/js/components/src/tree-select-control/test/index.test.js b/packages/js/components/src/tree-select-control/test/index.test.js +new file mode 100644 +index 0000000000..a78eed711d +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/test/index.test.js +@@ -0,0 +1,151 @@ ++/** ++ * External dependencies ++ */ ++import { fireEvent, render } from '@testing-library/react'; ++import { createElement } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import TreeSelectControl from '../index'; ++ ++const options = [ ++ { ++ value: 'EU', ++ label: 'Europe', ++ children: [ ++ { value: 'ES', label: 'Spain' }, ++ { value: 'FR', label: 'France' }, ++ { value: 'IT', label: 'Italy' }, ++ ], ++ }, ++ { ++ value: 'AS', ++ label: 'Asia', ++ }, ++]; ++ ++describe( 'TreeSelectControl Component', () => { ++ it( 'Expands and collapse the Tree', () => { ++ const { queryByRole } = render( ++ ++ ); ++ ++ const control = queryByRole( 'combobox' ); ++ expect( queryByRole( 'tree' ) ).toBeFalsy(); ++ fireEvent.click( control ); ++ expect( queryByRole( 'tree' ) ).toBeTruthy(); ++ } ); ++ ++ it( 'Calls onChange property with the selected values', () => { ++ const onChange = jest.fn().mockName( 'onChange' ); ++ ++ const { queryByLabelText, queryByRole, rerender } = render( ++ ++ ); ++ ++ const control = queryByRole( 'combobox' ); ++ fireEvent.click( control ); ++ let checkbox = queryByLabelText( 'Europe' ); ++ fireEvent.click( checkbox ); ++ expect( onChange ).toHaveBeenCalledWith( [ 'ES', 'FR', 'IT' ] ); ++ ++ checkbox = queryByLabelText( 'Asia' ); ++ fireEvent.click( checkbox ); ++ expect( onChange ).toHaveBeenCalledWith( [ 'AS' ] ); ++ ++ rerender( ++ ++ ); ++ ++ checkbox = queryByLabelText( 'Asia' ); ++ fireEvent.click( checkbox ); ++ expect( onChange ).toHaveBeenCalledWith( [ 'ES', 'AS' ] ); ++ } ); ++ ++ it( 'Renders the label', () => { ++ const { queryByLabelText } = render( ++ ++ ); ++ ++ expect( queryByLabelText( 'Select' ) ).toBeTruthy(); ++ } ); ++ ++ it( 'Renders the All Options', () => { ++ const onChange = jest.fn().mockName( 'onChange' ); ++ const { queryByLabelText, queryByRole, rerender } = render( ++ ++ ); ++ ++ const control = queryByRole( 'combobox' ); ++ fireEvent.click( control ); ++ const allCheckbox = queryByLabelText( 'All' ); ++ ++ expect( allCheckbox ).toBeTruthy(); ++ ++ fireEvent.click( allCheckbox ); ++ expect( onChange ).toHaveBeenCalledWith( [ 'ES', 'FR', 'IT', 'AS' ] ); ++ ++ rerender( ++ ++ ); ++ fireEvent.click( allCheckbox ); ++ expect( onChange ).toHaveBeenCalledWith( [] ); ++ } ); ++ ++ it( 'Renders the All Options custom Label', () => { ++ const { queryByLabelText, queryByRole } = render( ++ ++ ); ++ ++ const control = queryByRole( 'combobox' ); ++ fireEvent.click( control ); ++ const allCheckbox = queryByLabelText( 'All countries' ); ++ ++ expect( allCheckbox ).toBeTruthy(); ++ } ); ++ ++ it( 'Filters Options on Search', () => { ++ const { queryByLabelText, queryByRole } = render( ++ ++ ); ++ ++ const control = queryByRole( 'combobox' ); ++ fireEvent.click( control ); ++ expect( queryByLabelText( 'Europe' ) ).toBeTruthy(); ++ expect( queryByLabelText( 'Asia' ) ).toBeTruthy(); ++ ++ fireEvent.change( control, { target: { value: 'Asi' } } ); ++ ++ expect( queryByLabelText( 'Europe' ) ).toBeFalsy(); // none of its children match Asi ++ expect( queryByLabelText( 'Asia' ) ).toBeTruthy(); // match Asi ++ ++ fireEvent.change( control, { target: { value: 'As' } } ); // doesnt trigger if length < 3 ++ ++ expect( queryByLabelText( 'Europe' ) ).toBeTruthy(); ++ expect( queryByLabelText( 'Asia' ) ).toBeTruthy(); ++ expect( queryByLabelText( 'Spain' ) ).toBeFalsy(); // not expanded ++ ++ fireEvent.change( control, { target: { value: 'pain' } } ); ++ ++ expect( queryByLabelText( 'Europe' ) ).toBeTruthy(); // contains Spain ++ expect( queryByLabelText( 'Spain' ) ).toBeTruthy(); // match pain ++ expect( queryByLabelText( 'France' ) ).toBeFalsy(); // doesn't match pain ++ expect( queryByLabelText( 'Asia' ) ).toBeFalsy(); // doesn't match pain ++ } ); ++} ); +diff --git a/packages/js/components/src/tree-select-control/test/options.test.js b/packages/js/components/src/tree-select-control/test/options.test.js +new file mode 100644 +index 0000000000..1f92879843 +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/test/options.test.js +@@ -0,0 +1,142 @@ ++/** ++ * External dependencies ++ */ ++import { fireEvent, render } from '@testing-library/react'; ++import { createElement } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import TreeSelectControl from '../index'; ++ ++/** ++ * In jsdom, the width and height of all elements are zero, ++ * so setting `offsetWidth` to avoid them to be filtered out ++ * by `isVisible` in focusable. ++ * Ref: https://github.com/WordPress/gutenberg/blob/%40wordpress/dom%403.1.1/packages/dom/src/focusable.js#L42-L48 ++ */ ++jest.mock( '@wordpress/dom', () => { ++ const { focus } = jest.requireActual( '@wordpress/dom' ); ++ const descriptor = { configurable: true, get: () => 1 }; ++ function find( context ) { ++ context.querySelectorAll( '*' ).forEach( ( element ) => { ++ Object.defineProperty( element, 'offsetWidth', descriptor ); ++ } ); ++ return focus.focusable.find( ...arguments ); ++ } ++ return { ++ focus: { ++ ...focus, ++ focusable: { ...focus.focusable, find }, ++ }, ++ }; ++} ); ++ ++const options = [ ++ { ++ value: 'EU', ++ label: 'Europe', ++ children: [ ++ { value: 'ES', label: 'Spain' }, ++ { value: 'FR', label: 'France' }, ++ { value: 'IT', label: 'Italy' }, ++ ], ++ }, ++ { ++ value: 'NA', ++ label: 'North America', ++ children: [ { value: 'US', label: 'United States' } ], ++ }, ++]; ++ ++describe( 'TreeSelectControl - Options Component', () => { ++ it( 'Expands and collapses groups', () => { ++ const { queryAllByRole, queryByText, queryByRole } = render( ++ ++ ); ++ ++ const control = queryByRole( 'combobox' ); ++ fireEvent.click( control ); ++ ++ const optionItem = queryByText( 'Europe' ); ++ const option = options[ 0 ]; ++ expect( optionItem ).toBeTruthy(); ++ ++ option.children.forEach( ( child ) => { ++ const childItem = queryByText( child.label ); ++ expect( childItem ).toBeFalsy(); ++ } ); ++ ++ const button = queryAllByRole( 'button' ); ++ fireEvent.click( button[ 0 ] ); ++ ++ option.children.forEach( ( child ) => { ++ const childItem = queryByText( child.label ); ++ expect( childItem ).toBeTruthy(); ++ } ); ++ ++ fireEvent.click( button[ 0 ] ); ++ ++ option.children.forEach( ( child ) => { ++ const childItem = queryByText( child.label ); ++ expect( childItem ).toBeFalsy(); ++ } ); ++ ++ fireEvent.click( optionItem ); ++ ++ option.children.forEach( ( child ) => { ++ const childItem = queryByText( child.label ); ++ expect( childItem ).toBeTruthy(); ++ } ); ++ } ); ++ ++ it( 'Partially selects groups', () => { ++ const { queryByRole, queryByText } = render( ++ ++ ); ++ ++ fireEvent.click( queryByRole( 'combobox' ) ); ++ ++ const partiallyCheckedOption = queryByText( 'Europe' ); ++ const unCheckedOption = queryByText( 'North America' ); ++ ++ expect( partiallyCheckedOption ).toBeTruthy(); ++ expect( unCheckedOption ).toBeTruthy(); ++ ++ const partiallyCheckedOptionWrapper = partiallyCheckedOption.closest( ++ '.woocommerce-tree-select-control__option' ++ ); ++ const unCheckedOptionWrapper = unCheckedOption.closest( ++ '.woocommerce-tree-select-control__option' ++ ); ++ ++ expect( partiallyCheckedOptionWrapper ).toBeTruthy(); ++ expect( unCheckedOptionWrapper ).toBeTruthy(); ++ ++ expect( ++ partiallyCheckedOptionWrapper.classList.contains( ++ 'is-partially-checked' ++ ) ++ ).toBeTruthy(); ++ ++ expect( ++ unCheckedOptionWrapper.classList.contains( 'is-partially-checked' ) ++ ).toBeFalsy(); ++ } ); ++ ++ it( 'Clears search input when option changes', () => { ++ const { queryAllByRole, queryByRole } = render( ++ ++ ); ++ ++ const input = queryByRole( 'combobox' ); ++ fireEvent.click( input ); ++ fireEvent.change( input, { target: { value: 'Fra' } } ); ++ expect( input.value ).toBe( 'Fra' ); ++ ++ const checkbox = queryAllByRole( 'checkbox' ); ++ fireEvent.click( checkbox[ 0 ] ); ++ ++ expect( input.value ).toBe( '' ); ++ } ); ++} ); +diff --git a/packages/js/components/src/tree-select-control/test/tags.test.js b/packages/js/components/src/tree-select-control/test/tags.test.js +new file mode 100644 +index 0000000000..a0c1724a80 +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/test/tags.test.js +@@ -0,0 +1,49 @@ ++/** ++ * External dependencies ++ */ ++import { fireEvent, render } from '@testing-library/react'; ++import { createElement } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import Tags from '../tags'; ++ ++const tags = [ ++ { id: 'ES', label: 'Spain' }, ++ { id: 'FR', label: 'France' }, ++]; ++ ++describe( 'TreeSelectControl - Tags Component', () => { ++ it( 'Shows all tags by default', () => { ++ const { queryAllByRole } = render( ); ++ ++ expect( queryAllByRole( 'button' ).length ).toBe( 2 ); ++ } ); ++ ++ it( 'Limit Tags visibility', () => { ++ const { queryByText } = render( ++ ++ ); ++ ++ expect( queryByText( 'Spain' ) ).toBeTruthy(); ++ expect( queryByText( 'France' ) ).toBeFalsy(); ++ ++ const showMore = queryByText( '+ 1 more' ); ++ expect( queryByText( 'Show less' ) ).toBeFalsy(); ++ expect( showMore ).toBeTruthy(); ++ fireEvent.click( showMore ); ++ ++ expect( queryByText( 'Spain' ) ).toBeTruthy(); ++ expect( queryByText( 'France' ) ).toBeTruthy(); ++ ++ expect( queryByText( 'Show less' ) ).toBeTruthy(); ++ fireEvent.click( showMore ); ++ ++ expect( queryByText( 'Spain' ) ).toBeTruthy(); ++ expect( queryByText( 'France' ) ).toBeFalsy(); ++ ++ expect( queryByText( 'Show less' ) ).toBeFalsy(); ++ expect( queryByText( '+ 1 more' ) ).toBeTruthy(); ++ } ); ++} ); +diff --git a/packages/js/components/src/tree-select-control/useIsEqualRefValue.js b/packages/js/components/src/tree-select-control/useIsEqualRefValue.js +new file mode 100644 +index 0000000000..c43175ab42 +--- /dev/null ++++ b/packages/js/components/src/tree-select-control/useIsEqualRefValue.js +@@ -0,0 +1,26 @@ ++/** ++ * External dependencies ++ */ ++import { isEqual } from 'lodash'; ++import { useRef } from '@wordpress/element'; ++ ++/** ++ * Stores value in a ref. In subsequent render, value will be compared with ref.current using `isEqual` comparison. ++ * If it is equal, returns ref.current; else, set ref.current to be value. ++ * ++ * This is useful for objects used in hook dependencies. ++ * ++ * @param {*} value Value to be stored in ref. ++ * @return {*} Value stored in ref. ++ */ ++const useIsEqualRefValue = ( value ) => { ++ const optionsRef = useRef( value ); ++ ++ if ( ! isEqual( optionsRef.current, value ) ) { ++ optionsRef.current = value; ++ } ++ ++ return optionsRef.current; ++}; ++ ++export default useIsEqualRefValue; +diff --git a/packages/js/csv-export/CHANGELOG.md b/packages/js/csv-export/CHANGELOG.md +index 56a82f18a6..e66002d38c 100644 +--- a/packages/js/csv-export/CHANGELOG.md ++++ b/packages/js/csv-export/CHANGELOG.md +@@ -1,13 +1,15 @@ +-# Changelog ++# Changelog + + This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +-## [1.6.0](https://www.npmjs.com/package/@woocommerce/csv-export/v/1.6.0) - 2022-06-14 ++## [1.7.0](https://www.npmjs.com/package/@woocommerce/packages/js/csv-export/v/1.7.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [1.6.0](https://www.npmjs.com/package/@woocommerce/packages/js/csv-export/v/1.6.0) - 2022-06-14 + + - Minor - Add Jetpack Changelogger + - Patch - Migrate @woocommerce/csv-export to TS + - Patch - Standardize lint scripts: add lint:fix + +---- +- +-[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/94367ca55947673b50ee75b212403d3671c386c6/packages/js/csv-export/CHANGELOG.md). ++[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/csv-export/CHANGELOG.md). +diff --git a/packages/js/csv-export/composer.json b/packages/js/csv-export/composer.json +index a660ef5999..2ac2f972b1 100644 +--- a/packages/js/csv-export/composer.json ++++ b/packages/js/csv-export/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/csv-export/composer.lock b/packages/js/csv-export/composer.lock +index fc1a5df3c2..b6ccc8b354 100644 +--- a/packages/js/csv-export/composer.lock ++++ b/packages/js/csv-export/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "067dbaeae565b00ae9f0a7034df6a7a1", ++ "content-hash": "9d346e4d6a7b7aae6eba7dc306295ea8", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/csv-export/package.json b/packages/js/csv-export/package.json +index 95495e5704..0be79e99d1 100644 +--- a/packages/js/csv-export/package.json ++++ b/packages/js/csv-export/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/csv-export", +- "version": "1.6.0", ++ "version": "1.7.0", + "description": "WooCommerce utility library to convert data to CSV files.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -29,15 +29,15 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "devDependencies": { +@@ -46,6 +46,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/currency/CHANGELOG.md b/packages/js/currency/CHANGELOG.md +index 5bf30d7cdb..0fa26123f1 100644 +--- a/packages/js/currency/CHANGELOG.md ++++ b/packages/js/currency/CHANGELOG.md +@@ -1,9 +1,15 @@ +-## [4.1.0](https://www.npmjs.com/package/@woocommerce/currency/v/4.1.0) - 2022-06-14 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [4.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/currency/v/4.2.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [4.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/currency/v/4.1.0) - 2022-06-14 + + - Minor - Add Jetpack Changelogger + - Patch - Migrate @woocommerce/currency to TS + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/currency/CHANGELOG.md). +diff --git a/packages/js/currency/composer.json b/packages/js/currency/composer.json +index 773818ed72..f4975d986e 100644 +--- a/packages/js/currency/composer.json ++++ b/packages/js/currency/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/currency/composer.lock b/packages/js/currency/composer.lock +index 699e0161e7..2b5a46517c 100644 +--- a/packages/js/currency/composer.lock ++++ b/packages/js/currency/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "849ae385b414545641c24bd1c0d7e076", ++ "content-hash": "f4d8c783fbd5ae4f782df2d5f1a6fdc2", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/currency/package.json b/packages/js/currency/package.json +index fa7a005989..d345c7c650 100644 +--- a/packages/js/currency/package.json ++++ b/packages/js/currency/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/currency", +- "version": "4.1.0", ++ "version": "4.2.0", + "description": "WooCommerce currency utilities.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -32,15 +32,15 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "devDependencies": { +@@ -49,6 +49,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/currency/src/index.tsx b/packages/js/currency/src/index.tsx +index 88eca6d755..50d8b233e1 100644 +--- a/packages/js/currency/src/index.tsx ++++ b/packages/js/currency/src/index.tsx +@@ -287,8 +287,7 @@ export function getCurrencyData() { + version: '3.1.0', + alternative: 'CurrencyFactory.getDataForCountry', + plugin: 'WooCommerce Admin', +- hint: +- 'Pass in the country, locale data, and symbol info to use getDataForCountry', ++ hint: 'Pass in the country, locale data, and symbol info to use getDataForCountry', + } ); + + // See https://github.com/woocommerce/woocommerce-admin/issues/3101. +diff --git a/packages/js/customer-effort-score/CHANGELOG.md b/packages/js/customer-effort-score/CHANGELOG.md +index d81e827fe0..b248653c9e 100644 +--- a/packages/js/customer-effort-score/CHANGELOG.md ++++ b/packages/js/customer-effort-score/CHANGELOG.md +@@ -1,4 +1,12 @@ +-## [2.1.0](https://www.npmjs.com/package/@woocommerce/customer-effort-score/v/2.1.0) - 2022-06-14 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [2.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/customer-effort-score/v/2.2.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [2.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/customer-effort-score/v/2.1.0) - 2022-06-14 + + - Minor - Add new simple customer feedback component for inline CES feedback. #32538 + - Minor - Add Jetpack Changelogger +@@ -6,6 +14,4 @@ + - Patch - Migrate @woocommerce/customer-effort-score to TS + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/customer-effort-score/CHANGELOG.md). +diff --git a/packages/js/customer-effort-score/composer.json b/packages/js/customer-effort-score/composer.json +index f5d1338e57..bc3c8c5826 100644 +--- a/packages/js/customer-effort-score/composer.json ++++ b/packages/js/customer-effort-score/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/customer-effort-score/composer.lock b/packages/js/customer-effort-score/composer.lock +index f686aad9de..f56c2fc116 100644 +--- a/packages/js/customer-effort-score/composer.lock ++++ b/packages/js/customer-effort-score/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "d56de16e3502c3fc45e9a661d742e1c6", ++ "content-hash": "2d332eda546c5e999a9db224719b3d40", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/customer-effort-score/package.json b/packages/js/customer-effort-score/package.json +index 6fca5bcfad..fa98da90ca 100644 +--- a/packages/js/customer-effort-score/package.json ++++ b/packages/js/customer-effort-score/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/customer-effort-score", +- "version": "2.1.0", ++ "version": "2.2.0", + "description": "WooCommerce utility to measure user effort.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -49,6 +49,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "postcss-loader": "^3.0.0", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "sass-loader": "^10.2.1", + "ts-jest": "^27.1.3", +@@ -61,17 +62,17 @@ + "react-dom": "^17.0.0" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "pnpm run build:js && pnpm run build:css", ++ "build": "node ./node_modules/require-turbo && pnpm run build:js && pnpm run build:css", + "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:css": "webpack", + "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { +diff --git a/packages/js/data/CHANGELOG.md b/packages/js/data/CHANGELOG.md +index d4b2582bc2..2fecf6633b 100644 +--- a/packages/js/data/CHANGELOG.md ++++ b/packages/js/data/CHANGELOG.md +@@ -1,6 +1,22 @@ +-## [4.0.0](https://www.npmjs.com/package/@woocommerce/data/v/4.0.0) - 2022-06-14 ++# Changelog + +-- Major - Remove `PaymentMethodsState` type. Use `Plugin` instead. #32683 ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [4.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/data/v/4.1.0) - 2022-07-08 ++ ++- Minor - Fix 'Cannot read properties of undefined' error when clicking Export button on Analytic pages. ++- Minor - Add CRUD data store utilities ++- Minor - Add product deletion via datastore API #33285 ++- Minor - Add product shipping class data store. #33765 ++- Patch - Fix product type ++- Patch - Migrate @woocommerce/data user and use-select-with-refresh to TS ++- Patch - Migrate item store to TS ++- Minor - Migrate onboarding data store to TS ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [4.0.0](https://www.npmjs.com/package/@woocommerce/packages/js/data/v/4.0.0) - 2022-06-14 ++ ++- Major [ **BREAKING CHANGE** ] - Remove `PaymentMethodsState` type. Use `Plugin` instead. #32683 + - Minor - Add create product actions in products data store #33278 + - Minor - Add new orders data store, for retrieving orders data. #33063 + - Minor - Add update product actions to product data store #33282 +@@ -26,6 +42,4 @@ + - Patch - Standardize lint scripts: add lint:fix + - Patch - Update @woocomerce/data client api error types. #32939 + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/data/CHANGELOG.md). +diff --git a/packages/js/data/changelog/add-33127 b/packages/js/data/changelog/add-33127 +deleted file mode 100644 +index bb456af5fd..0000000000 +--- a/packages/js/data/changelog/add-33127 ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: add +- +-Add product deletion via datastore API #33285 +diff --git a/packages/js/data/changelog/add-33443_shipping_zones_data_store b/packages/js/data/changelog/add-33443_shipping_zones_data_store +new file mode 100644 +index 0000000000..9c33d89ea0 +--- /dev/null ++++ b/packages/js/data/changelog/add-33443_shipping_zones_data_store +@@ -0,0 +1,4 @@ ++Significance: minor ++Type: add ++ ++Add new data store for shipping zones. #33830 +diff --git a/packages/js/data/changelog/dev-migrate-onboarding-store-to-ts b/packages/js/data/changelog/dev-migrate-onboarding-store-to-ts +deleted file mode 100644 +index 7e955f6b2b..0000000000 +--- a/packages/js/data/changelog/dev-migrate-onboarding-store-to-ts ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: dev +- +-Migrate onboarding data store to TS +diff --git a/packages/js/data/changelog/fix-34048-historical-import-unusable b/packages/js/data/changelog/fix-34048-historical-import-unusable +new file mode 100644 +index 0000000000..e479a9dddb +--- /dev/null ++++ b/packages/js/data/changelog/fix-34048-historical-import-unusable +@@ -0,0 +1,4 @@ ++Significance: patch ++Type: fix ++ ++Fix missing resolver import in data/import +diff --git a/packages/js/data/changelog/fix-export-button-error b/packages/js/data/changelog/fix-export-button-error +deleted file mode 100644 +index 94c6897331..0000000000 +--- a/packages/js/data/changelog/fix-export-button-error ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: fix +-Type: minor +- +-Fix 'Cannot read properties of undefined' error when clicking Export button on Analytic pages. +\ No newline at end of file +diff --git a/packages/js/data/composer.json b/packages/js/data/composer.json +index cbe983f490..62170ffbdf 100644 +--- a/packages/js/data/composer.json ++++ b/packages/js/data/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/data/composer.lock b/packages/js/data/composer.lock +index fb304621c9..55d17fcef6 100644 +--- a/packages/js/data/composer.lock ++++ b/packages/js/data/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "7f867d5a9325169cfbdcd419d00ab1b1", ++ "content-hash": "e8aae6511dda74f8220d58b7ae1e9e74", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/data/package.json b/packages/js/data/package.json +index f48317de96..a54c8d20dc 100644 +--- a/packages/js/data/package.json ++++ b/packages/js/data/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/data", +- "version": "4.0.0", ++ "version": "4.1.0", + "description": "WooCommerce Admin data store and utilities", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -58,6 +58,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "redux": "^4.1.0", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +@@ -69,15 +70,15 @@ + "react-dom": "^17.0.0" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { +diff --git a/packages/js/data/src/crud/README.md b/packages/js/data/src/crud/README.md +new file mode 100644 +index 0000000000..48e190951c +--- /dev/null ++++ b/packages/js/data/src/crud/README.md +@@ -0,0 +1,98 @@ ++# CRUD Data Stores ++ ++The CRUD data store is a set of utilities to allow faster and less error prone creation of data stores that have create, read, update, and delete capabilities. ++ ++## Usage ++ ++The CRUD data store methods can be used in one of a couple ways. ++ ++### Default data store ++ ++If the default CRUD actions work well for your use case, you can use the quicker, more opinionated setup. ++ ++```js ++import { createCrudDataStore } from '../crud'; ++ ++createCrudDataStore( { ++ storeName: 'my/custom/store', ++ resourceName: 'MyThing', ++ pluralResourceName: 'MyThings', ++ namespace: '/my/rest/namespace', ++} ); ++``` ++ ++This will register a data store named `my/custom/store` with the following default selectors: ++ ++| Selector | Description | ++| --- | --- | ++| `getMyThing( id )` | Get an item by ID | ++| `getMyThingError( id )` | Get the error for an item. | ++| `getMyThings( query = {} )` | Get all items, optionally by a specific query. | ++| `getMyThingsError( query = {} )` | Get the error for a set of items by query. | ++ ++Example usage: `wp.data.select('my/custom/store').getMyThing( 3 );` ++ ++The following resolvers will be added: ++ ++| Resolver | Method | Endpoint | ++| --- | --- | --- | ++| `getMyThing( id )` | GET | `/` | ++| `getMyThings( query = {} )` | GET | `` | ++ ++The following actions are available for dispatch on the created data store: ++ ++| Resolver | Method | Endpoint | ++| --- | --- | --- | ++| `createMyThing( query )` | POST | `` | ++| `deleteMyThing( id, force = true )` | DELETE | `/` | ++| `updatetMyThing( id, query )` | PUT | `/` | ++ ++Example usage: `wp.data.dispatch('my/custom/store').updateMyThing( 3, { name: 'New name' } );` ++ ++### Customized data store ++ ++If the default settings are not adequate for your needs, you can always create your own data store and supplement the default CRUD actions with your own. ++ ++```js ++import { createSelectors } from '../crud/selectors'; ++import { createResolvers } from '../crud/selectors'; ++import { createActions } from '../crud/actions'; ++import { registerStore, combineReducers } from '@wordpress/data'; ++ ++const dataStoreArgs = { ++ resourceName: 'MyThing', ++ pluralResourceName: 'MyThings', ++} ++ ++const crudActions = createActions( dataStoreArgs ) ++const crudSelectors = createSelectors( dataStoreArgs ) ++const crudResolvers = createResolvers( { ...dataStoreArgs, namespace: 'my/rest/namespace' } ) ++ ++registerStore( 'my/custom/store', { ++ reducer: combineReducers( { reducer, myReducer } ), ++ actions: { ...crudActions, myActions }, ++ controls, ++ selectors: { ...crudSelectors, mySelectors }, ++ resolvers: { ...crudResolvers, myResolvers }, ++} ); ++``` ++ ++## Structure ++ ++The data store schema is set in such a way that allows queries to be cached and previously downloaded resources to be more readily available. ++ ++```js ++{ ++ items: { ++ 21: { ... }, ++ }, ++ errors: { ++ 'GET_ITEMS:page=3': 'There was an error trying to fetch page 3', ++ }, ++ data: { ++ 'GET_ITEMS:page=2' : [ 21 ], ++ } ++} ++``` ++ ++By default, the CRUD data store expects a property of `id` to be present on all resources. +\ No newline at end of file +diff --git a/packages/js/data/src/crud/action-types.ts b/packages/js/data/src/crud/action-types.ts +new file mode 100644 +index 0000000000..29bcd5ea9b +--- /dev/null ++++ b/packages/js/data/src/crud/action-types.ts +@@ -0,0 +1,14 @@ ++export enum TYPES { ++ CREATE_ITEM_ERROR = 'CREATE_ITEM_ERROR', ++ CREATE_ITEM_SUCCESS = 'CREATE_ITEM_SUCCESS', ++ DELETE_ITEM_ERROR = 'DELETE_ITEM_ERROR', ++ DELETE_ITEM_SUCCESS = 'DELETE_ITEM_SUCCESS', ++ GET_ITEM_ERROR = 'GET_ITEM_ERROR', ++ GET_ITEM_SUCCESS = 'GET_ITEM_SUCCESS', ++ GET_ITEMS_ERROR = 'GET_ITEMS_ERROR', ++ GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS', ++ UPDATE_ITEM_ERROR = 'UPDATE_ITEM_ERROR', ++ UPDATE_ITEM_SUCCESS = 'UPDATE_ITEM_SUCCESS', ++} ++ ++export default TYPES; +diff --git a/packages/js/data/src/crud/actions.ts b/packages/js/data/src/crud/actions.ts +new file mode 100644 +index 0000000000..42960bde82 +--- /dev/null ++++ b/packages/js/data/src/crud/actions.ts +@@ -0,0 +1,172 @@ ++/** ++ * External dependencies ++ */ ++import { addQueryArgs } from '@wordpress/url'; ++import { apiFetch } from '@wordpress/data-controls'; ++ ++/** ++ * Internal dependencies ++ */ ++import CRUD_ACTIONS from './crud-actions'; ++import TYPES from './action-types'; ++import { IdType, Item, ItemQuery } from './types'; ++ ++type ResolverOptions = { ++ resourceName: string; ++ namespace: string; ++}; ++ ++export function createItemError( query: Partial< ItemQuery >, error: unknown ) { ++ return { ++ type: TYPES.CREATE_ITEM_ERROR as const, ++ query, ++ error, ++ errorType: CRUD_ACTIONS.CREATE_ITEM, ++ }; ++} ++ ++export function createItemSuccess( id: IdType, item: Item ) { ++ return { ++ type: TYPES.CREATE_ITEM_SUCCESS as const, ++ id, ++ item, ++ }; ++} ++ ++export function deleteItemError( id: IdType, error: unknown ) { ++ return { ++ type: TYPES.DELETE_ITEM_ERROR as const, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.DELETE_ITEM, ++ }; ++} ++ ++export function deleteItemSuccess( id: IdType, force: boolean, item: Item ) { ++ return { ++ type: TYPES.DELETE_ITEM_SUCCESS as const, ++ id, ++ force, ++ item, ++ }; ++} ++ ++export function getItemError( id: unknown, error: unknown ) { ++ return { ++ type: TYPES.GET_ITEM_ERROR as const, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.GET_ITEM, ++ }; ++} ++ ++export function getItemSuccess( id: IdType, item: Item ) { ++ return { ++ type: TYPES.GET_ITEM_SUCCESS as const, ++ id, ++ item, ++ }; ++} ++ ++export function getItemsError( query: unknown, error: unknown ) { ++ return { ++ type: TYPES.GET_ITEMS_ERROR as const, ++ query, ++ error, ++ errorType: CRUD_ACTIONS.GET_ITEMS, ++ }; ++} ++ ++export function getItemsSuccess( query: unknown, items: Item[] ) { ++ return { ++ type: TYPES.GET_ITEMS_SUCCESS as const, ++ items, ++ query, ++ }; ++} ++ ++export function updateItemError( id: unknown, error: unknown ) { ++ return { ++ type: TYPES.UPDATE_ITEM_ERROR as const, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.UPDATE_ITEM, ++ }; ++} ++ ++export function updateItemSuccess( id: IdType, item: Item ) { ++ return { ++ type: TYPES.UPDATE_ITEM_SUCCESS as const, ++ id, ++ item, ++ }; ++} ++ ++export const createDispatchActions = ( { ++ namespace, ++ resourceName, ++}: ResolverOptions ) => { ++ const createItem = function* ( query: Partial< ItemQuery > ) { ++ try { ++ const item: Item = yield apiFetch( { ++ path: addQueryArgs( namespace, query ), ++ method: 'POST', ++ } ); ++ ++ yield createItemSuccess( item.id, item ); ++ return item; ++ } catch ( error ) { ++ yield createItemError( query, error ); ++ throw error; ++ } ++ }; ++ ++ const deleteItem = function* ( id: IdType, force = true ) { ++ try { ++ const item: Item = yield apiFetch( { ++ path: addQueryArgs( `${ namespace }/${ id }`, { force } ), ++ method: 'DELETE', ++ } ); ++ ++ yield deleteItemSuccess( id, force, item ); ++ return item; ++ } catch ( error ) { ++ yield deleteItemError( id, error ); ++ throw error; ++ } ++ }; ++ ++ const updateItem = function* ( id: IdType, query: Partial< ItemQuery > ) { ++ try { ++ const item: Item = yield apiFetch( { ++ path: addQueryArgs( `${ namespace }/${ id }`, query ), ++ method: 'PUT', ++ } ); ++ ++ yield updateItemSuccess( item.id, item ); ++ return item; ++ } catch ( error ) { ++ yield updateItemError( query, error ); ++ throw error; ++ } ++ }; ++ ++ return { ++ [ `create${ resourceName }` ]: createItem, ++ [ `delete${ resourceName }` ]: deleteItem, ++ [ `update${ resourceName }` ]: updateItem, ++ }; ++}; ++ ++export type Actions = ReturnType< ++ | typeof createItemError ++ | typeof createItemSuccess ++ | typeof deleteItemError ++ | typeof deleteItemSuccess ++ | typeof getItemError ++ | typeof getItemSuccess ++ | typeof getItemsError ++ | typeof getItemsSuccess ++ | typeof updateItemError ++ | typeof updateItemSuccess ++>; +diff --git a/packages/js/data/src/crud/crud-actions.ts b/packages/js/data/src/crud/crud-actions.ts +new file mode 100644 +index 0000000000..c70af5aa0a +--- /dev/null ++++ b/packages/js/data/src/crud/crud-actions.ts +@@ -0,0 +1,9 @@ ++export enum CRUD_ACTIONS { ++ CREATE_ITEM = 'CREATE_ITEM', ++ DELETE_ITEM = 'DELETE_ITEM', ++ GET_ITEM = 'GET_ITEM', ++ GET_ITEMS = 'GET_ITEMS', ++ UPDATE_ITEM = 'UPDATE_ITEM', ++} ++ ++export default CRUD_ACTIONS; +diff --git a/packages/js/data/src/crud/index.ts b/packages/js/data/src/crud/index.ts +new file mode 100644 +index 0000000000..a59fe38ea3 +--- /dev/null ++++ b/packages/js/data/src/crud/index.ts +@@ -0,0 +1,48 @@ ++/** ++ * External dependencies ++ */ ++import { registerStore } from '@wordpress/data'; ++import { Reducer } from 'redux'; ++ ++/** ++ * Internal dependencies ++ */ ++import { createSelectors } from './selectors'; ++import { createDispatchActions } from './actions'; ++import controls from '../controls'; ++import { createResolvers } from './resolvers'; ++import { createReducer, ResourceState } from './reducer'; ++ ++type CrudDataStore = { ++ storeName: string; ++ resourceName: string; ++ pluralResourceName: string; ++ namespace: string; ++}; ++ ++export const createCrudDataStore = ( { ++ storeName, ++ resourceName, ++ namespace, ++ pluralResourceName, ++}: CrudDataStore ) => { ++ const reducer = createReducer(); ++ const actions = createDispatchActions( { ++ resourceName, ++ namespace, ++ } ); ++ const resolvers = createResolvers( { ++ resourceName, ++ pluralResourceName, ++ namespace, ++ } ); ++ const selectors = createSelectors( { resourceName, pluralResourceName } ); ++ ++ registerStore( storeName, { ++ reducer: reducer as Reducer< ResourceState >, ++ actions, ++ selectors, ++ resolvers, ++ controls, ++ } ); ++}; +diff --git a/packages/js/data/src/crud/reducer.ts b/packages/js/data/src/crud/reducer.ts +new file mode 100644 +index 0000000000..767975da27 +--- /dev/null ++++ b/packages/js/data/src/crud/reducer.ts +@@ -0,0 +1,140 @@ ++/** ++ * External dependencies ++ */ ++import { Reducer } from 'redux'; ++ ++/** ++ * Internal dependencies ++ */ ++import { Actions } from './actions'; ++import CRUD_ACTIONS from './crud-actions'; ++import { getResourceName } from '../utils'; ++import { IdType, Item, ItemQuery } from './types'; ++import { TYPES } from './action-types'; ++ ++export type Data = Record< IdType, Item >; ++export type ResourceState = { ++ items: Record< ++ string, ++ { ++ data: IdType[]; ++ } ++ >; ++ data: Data; ++ errors: Record< string, unknown >; ++}; ++ ++export const createReducer = () => { ++ const reducer: Reducer< ResourceState, Actions > = ( ++ state = { ++ items: {}, ++ data: {}, ++ errors: {}, ++ }, ++ payload ++ ) => { ++ if ( payload && 'type' in payload ) { ++ switch ( payload.type ) { ++ case TYPES.CREATE_ITEM_ERROR: ++ case TYPES.GET_ITEMS_ERROR: ++ return { ++ ...state, ++ errors: { ++ ...state.errors, ++ [ getResourceName( ++ payload.errorType, ++ ( payload.query || {} ) as ItemQuery ++ ) ]: payload.error, ++ }, ++ }; ++ ++ case TYPES.CREATE_ITEM_SUCCESS: ++ case TYPES.GET_ITEM_SUCCESS: ++ case TYPES.UPDATE_ITEM_SUCCESS: ++ const itemData = state.data || {}; ++ return { ++ ...state, ++ data: { ++ ...itemData, ++ [ payload.id ]: { ++ ...( itemData[ payload.id ] || {} ), ++ ...payload.item, ++ }, ++ }, ++ }; ++ ++ case TYPES.DELETE_ITEM_SUCCESS: ++ const itemIds = Object.keys( state.data ); ++ const nextData = itemIds.reduce< Data >( ++ ( items: Data, id: string ) => { ++ if ( id !== payload.id.toString() ) { ++ items[ id ] = state.data[ id ]; ++ return items; ++ } ++ if ( payload.force ) { ++ return items; ++ } ++ items[ id ] = payload.item; ++ return items; ++ }, ++ {} as Data ++ ); ++ ++ return { ++ ...state, ++ data: nextData, ++ }; ++ ++ case TYPES.DELETE_ITEM_ERROR: ++ case TYPES.GET_ITEM_ERROR: ++ case TYPES.UPDATE_ITEM_ERROR: ++ return { ++ ...state, ++ errors: { ++ ...state.errors, ++ [ getResourceName( payload.errorType, { ++ id: payload.id, ++ } ) ]: payload.error, ++ }, ++ }; ++ ++ case TYPES.GET_ITEMS_SUCCESS: ++ const ids: IdType[] = []; ++ ++ const nextResources = payload.items.reduce< ++ Record< string, Item > ++ >( ( result, item ) => { ++ ids.push( item.id ); ++ result[ item.id ] = { ++ ...( state.data[ item.id ] || {} ), ++ ...item, ++ }; ++ return result; ++ }, {} ); ++ ++ const itemQuery = getResourceName( ++ CRUD_ACTIONS.GET_ITEMS, ++ ( payload.query || {} ) as ItemQuery ++ ); ++ ++ return { ++ ...state, ++ items: { ++ ...state.items, ++ [ itemQuery ]: { data: ids }, ++ }, ++ data: { ++ ...state.data, ++ ...nextResources, ++ }, ++ }; ++ ++ default: ++ return state; ++ } ++ } ++ return state; ++ }; ++ ++ return reducer; ++}; +diff --git a/packages/js/data/src/crud/resolvers.ts b/packages/js/data/src/crud/resolvers.ts +new file mode 100644 +index 0000000000..e24f524eb3 +--- /dev/null ++++ b/packages/js/data/src/crud/resolvers.ts +@@ -0,0 +1,74 @@ ++/** ++ * External dependencies ++ */ ++import { apiFetch } from '@wordpress/data-controls'; ++ ++/** ++ * Internal dependencies ++ */ ++import { ++ getItemError, ++ getItemSuccess, ++ getItemsError, ++ getItemsSuccess, ++} from './actions'; ++import { request } from '../utils'; ++import { Item, ItemQuery } from './types'; ++ ++type ResolverOptions = { ++ resourceName: string; ++ pluralResourceName: string; ++ namespace: string; ++}; ++ ++export const createResolvers = ( { ++ resourceName, ++ pluralResourceName, ++ namespace, ++}: ResolverOptions ) => { ++ const getItem = function* ( id: number ) { ++ try { ++ const item: Item = yield apiFetch( { ++ path: `${ namespace }/${ id }`, ++ method: 'GET', ++ } ); ++ ++ yield getItemSuccess( item.id, item ); ++ return item; ++ } catch ( error ) { ++ yield getItemError( id, error ); ++ throw error; ++ } ++ }; ++ ++ const getItems = function* ( query?: Partial< ItemQuery > ) { ++ // Require ID when requesting specific fields to later update the resource data. ++ const resourceQuery = query ? { ...query } : {}; ++ ++ if ( ++ resourceQuery && ++ resourceQuery._fields && ++ ! resourceQuery._fields.includes( 'id' ) ++ ) { ++ resourceQuery._fields = [ 'id', ...resourceQuery._fields ]; ++ } ++ ++ try { ++ const { items }: { items: Item[] } = yield request< ++ ItemQuery, ++ Item ++ >( namespace, resourceQuery ); ++ ++ yield getItemsSuccess( query, items ); ++ return items; ++ } catch ( error ) { ++ yield getItemsError( query, error ); ++ throw error; ++ } ++ }; ++ ++ return { ++ [ `get${ resourceName }` ]: getItem, ++ [ `get${ pluralResourceName }` ]: getItems, ++ }; ++}; +diff --git a/packages/js/data/src/crud/selectors.ts b/packages/js/data/src/crud/selectors.ts +new file mode 100644 +index 0000000000..61f687cd42 +--- /dev/null ++++ b/packages/js/data/src/crud/selectors.ts +@@ -0,0 +1,116 @@ ++/** ++ * External dependencies ++ */ ++import createSelector from 'rememo'; ++ ++/** ++ * Internal dependencies ++ */ ++import { getResourceName } from '../utils'; ++import { IdType, Item, ItemQuery } from './types'; ++import { ResourceState } from './reducer'; ++import CRUD_ACTIONS from './crud-actions'; ++ ++type SelectorOptions = { ++ resourceName: string; ++ pluralResourceName: string; ++}; ++ ++export const getItemCreateError = ( ++ state: ResourceState, ++ query: ItemQuery ++) => { ++ const itemQuery = getResourceName( CRUD_ACTIONS.CREATE_ITEM, query ); ++ return state.errors[ itemQuery ]; ++}; ++ ++export const getItemDeleteError = ( state: ResourceState, id: IdType ) => { ++ const itemQuery = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { id } ); ++ return state.errors[ itemQuery ]; ++}; ++ ++export const getItem = ( state: ResourceState, id: IdType ) => { ++ return state.data[ id ]; ++}; ++ ++export const getItemError = ( state: ResourceState, id: IdType ) => { ++ const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } ); ++ return state.errors[ itemQuery ]; ++}; ++ ++export const getItems = createSelector( ++ ( state: ResourceState, query?: ItemQuery ) => { ++ const itemQuery = getResourceName( ++ CRUD_ACTIONS.GET_ITEMS, ++ query || {} ++ ); ++ ++ const ids = state.items[ itemQuery ] ++ ? state.items[ itemQuery ].data ++ : undefined; ++ ++ if ( ! ids ) { ++ return null; ++ } ++ ++ if ( query && query._fields ) { ++ return ids.map( ( id: IdType ) => { ++ return query._fields.reduce( ++ ( item: Partial< Item >, field: string ) => { ++ return { ++ ...item, ++ [ field ]: state.data[ id ][ field ], ++ }; ++ }, ++ {} as Partial< Item > ++ ); ++ } ); ++ } ++ ++ return ids ++ .map( ( id: IdType ) => { ++ return state.data[ id ]; ++ } ) ++ .filter( ( item ) => item !== undefined ); ++ }, ++ ( state, query ) => { ++ const itemQuery = getResourceName( ++ CRUD_ACTIONS.GET_ITEMS, ++ query || {} ++ ); ++ const ids = state.items[ itemQuery ] ++ ? state.items[ itemQuery ].data ++ : undefined; ++ return [ ++ state.items[ itemQuery ], ++ ...( ids || [] ).map( ( id: string ) => { ++ return state.data[ id ]; ++ } ), ++ ]; ++ } ++); ++ ++export const getItemsError = ( state: ResourceState, query?: ItemQuery ) => { ++ const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query || {} ); ++ return state.errors[ itemQuery ]; ++}; ++ ++export const getItemUpdateError = ( state: ResourceState, id: IdType ) => { ++ const itemQuery = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, { id } ); ++ return state.errors[ itemQuery ]; ++}; ++ ++export const createSelectors = ( { ++ resourceName, ++ pluralResourceName, ++}: SelectorOptions ) => { ++ return { ++ [ `get${ resourceName }` ]: getItem, ++ [ `get${ resourceName }Error` ]: getItemError, ++ [ `get${ pluralResourceName }` ]: getItems, ++ [ `get${ pluralResourceName }Error` ]: getItemsError, ++ [ `get${ resourceName }CreateError` ]: getItemCreateError, ++ [ `get${ resourceName }DeleteError` ]: getItemDeleteError, ++ [ `get${ resourceName }UpdateError` ]: getItemUpdateError, ++ }; ++}; +diff --git a/packages/js/data/src/crud/test/actions.ts b/packages/js/data/src/crud/test/actions.ts +new file mode 100644 +index 0000000000..31170a0a9c +--- /dev/null ++++ b/packages/js/data/src/crud/test/actions.ts +@@ -0,0 +1,18 @@ ++/** ++ * Internal dependencies ++ */ ++import { createDispatchActions } from '../actions'; ++ ++const selectors = createDispatchActions( { ++ resourceName: 'Product', ++ namespace: '/products', ++} ); ++ ++describe( 'crud selectors', () => { ++ it( 'should return methods for the default actions', () => { ++ expect( Object.keys( selectors ).length ).toEqual( 3 ); ++ expect( selectors ).toHaveProperty( 'createProduct' ); ++ expect( selectors ).toHaveProperty( 'deleteProduct' ); ++ expect( selectors ).toHaveProperty( 'updateProduct' ); ++ } ); ++} ); +diff --git a/packages/js/data/src/crud/test/reducer.ts b/packages/js/data/src/crud/test/reducer.ts +new file mode 100644 +index 0000000000..c45f8e0435 +--- /dev/null ++++ b/packages/js/data/src/crud/test/reducer.ts +@@ -0,0 +1,301 @@ ++/** ++ * Internal dependencies ++ */ ++import { Actions } from '../actions'; ++import { createReducer, ResourceState } from '../reducer'; ++import { CRUD_ACTIONS } from '../crud-actions'; ++import { getResourceName } from '../../utils'; ++import { Item, ItemQuery } from '../types'; ++import TYPES from '../action-types'; ++ ++const defaultState: ResourceState = { ++ items: {}, ++ errors: {}, ++ data: {}, ++}; ++ ++const reducer = createReducer(); ++ ++describe( 'crud reducer', () => { ++ it( 'should return a default state', () => { ++ const state = reducer( undefined, {} as Actions ); ++ expect( state ).toEqual( defaultState ); ++ expect( state ).not.toBe( defaultState ); ++ } ); ++ ++ it( 'should handle GET_ITEM_SUCCESS', () => { ++ const itemType = 'guyisms'; ++ const initialState: ResourceState = { ++ items: { ++ [ itemType ]: { ++ data: [ 1, 2 ], ++ }, ++ }, ++ errors: {}, ++ data: { ++ 1: { id: 1, name: 'Donkey', status: 'draft' }, ++ 2: { id: 2, name: 'Sauce', status: 'publish' }, ++ }, ++ }; ++ const update: Item = { ++ id: 2, ++ status: 'draft', ++ }; ++ ++ const state = reducer( initialState, { ++ type: TYPES.GET_ITEM_SUCCESS, ++ id: update.id, ++ item: update, ++ } ); ++ ++ expect( state.items ).toEqual( initialState.items ); ++ expect( state.errors ).toEqual( initialState.errors ); ++ ++ expect( state.data[ 1 ] ).toEqual( initialState.data[ 1 ] ); ++ expect( state.data[ 2 ].id ).toEqual( initialState.data[ 2 ].id ); ++ expect( state.data[ 2 ].title ).toEqual( initialState.data[ 2 ].title ); ++ expect( state.data[ 2 ].status ).toEqual( update.status ); ++ } ); ++ ++ it( 'should handle GET_ITEMS_SUCCESS', () => { ++ const items: Item[] = [ ++ { id: 1, name: 'Yum!' }, ++ { id: 2, name: 'Dynamite!' }, ++ ]; ++ const query: Partial< ItemQuery > = { status: 'draft' }; ++ const state = reducer( defaultState, { ++ type: TYPES.GET_ITEMS_SUCCESS, ++ items, ++ query, ++ } ); ++ ++ const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); ++ ++ expect( state.items[ resourceName ].data ).toHaveLength( 2 ); ++ expect( state.items[ resourceName ].data.includes( 1 ) ).toBeTruthy(); ++ expect( state.items[ resourceName ].data.includes( 2 ) ).toBeTruthy(); ++ ++ expect( state.data[ 1 ] ).toEqual( items[ 0 ] ); ++ expect( state.data[ 2 ] ).toEqual( items[ 1 ] ); ++ } ); ++ ++ it( 'GET_ITEMS_SUCCESS should not remove previously added fields, only update new ones', () => { ++ const initialState: ResourceState = { ++ ...defaultState, ++ data: { ++ 1: { id: 1, name: 'Yum!', price: '10.00', description: 'test' }, ++ 2: { ++ id: 2, ++ name: 'Dynamite!', ++ price: '10.00', ++ description: 'dynamite', ++ }, ++ }, ++ }; ++ ++ const items: Item[] = [ ++ { id: 1, name: 'Yum! Updated' }, ++ { id: 2, name: 'Dynamite!' }, ++ ]; ++ const query: Partial< ItemQuery > = { status: 'draft' }; ++ const state = reducer( initialState, { ++ type: TYPES.GET_ITEMS_SUCCESS, ++ items, ++ query, ++ } ); ++ ++ const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); ++ ++ expect( state.items[ resourceName ].data ).toHaveLength( 2 ); ++ expect( state.items[ resourceName ].data.includes( 1 ) ).toBeTruthy(); ++ expect( state.items[ resourceName ].data.includes( 2 ) ).toBeTruthy(); ++ ++ expect( state.data[ 1 ].name ).toEqual( items[ 0 ].name ); ++ expect( state.data[ 1 ].price ).toEqual( initialState.data[ 1 ].price ); ++ expect( state.data[ 1 ].description ).toEqual( ++ initialState.data[ 1 ].description ++ ); ++ expect( state.data[ 2 ] ).toEqual( initialState.data[ 2 ] ); ++ } ); ++ ++ it( 'should handle GET_ITEMS_ERROR', () => { ++ const query: Partial< ItemQuery > = { status: 'draft' }; ++ const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); ++ const error = 'Baaam!'; ++ const state = reducer( defaultState, { ++ type: TYPES.GET_ITEMS_ERROR, ++ query, ++ error, ++ errorType: CRUD_ACTIONS.GET_ITEMS, ++ } ); ++ ++ expect( state.errors[ resourceName ] ).toBe( error ); ++ } ); ++ ++ it( 'should handle GET_ITEM_ERROR', () => { ++ const id = 3; ++ const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } ); ++ const error = 'Baaam!'; ++ const state = reducer( defaultState, { ++ type: TYPES.GET_ITEM_ERROR, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.GET_ITEM, ++ } ); ++ ++ expect( state.errors[ resourceName ] ).toBe( error ); ++ } ); ++ ++ it( 'should handle GET_ITEM_ERROR', () => { ++ const id = 3; ++ const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } ); ++ const error = 'Baaam!'; ++ const state = reducer( defaultState, { ++ type: TYPES.GET_ITEM_ERROR, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.GET_ITEM, ++ } ); ++ ++ expect( state.errors[ resourceName ] ).toBe( error ); ++ } ); ++ ++ it( 'should handle CREATE_ITEM_ERROR', () => { ++ const query = { name: 'Invalid product' }; ++ const resourceName = getResourceName( CRUD_ACTIONS.CREATE_ITEM, query ); ++ const error = 'Baaam!'; ++ const state = reducer( defaultState, { ++ type: TYPES.CREATE_ITEM_ERROR, ++ query, ++ error, ++ errorType: CRUD_ACTIONS.CREATE_ITEM, ++ } ); ++ ++ expect( state.errors[ resourceName ] ).toBe( error ); ++ } ); ++ ++ it( 'should handle UPDATE_ITEM_ERROR', () => { ++ const id = 2; ++ const resourceName = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, { ++ id, ++ } ); ++ const error = 'Baaam!'; ++ const state = reducer( defaultState, { ++ type: TYPES.UPDATE_ITEM_ERROR, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.UPDATE_ITEM, ++ } ); ++ ++ expect( state.errors[ resourceName ] ).toBe( error ); ++ } ); ++ ++ it( 'should handle UPDATE_ITEM_SUCCESS', () => { ++ const itemType = 'guyisms'; ++ const initialState: ResourceState = { ++ items: { ++ [ itemType ]: { ++ data: [ 1, 2 ], ++ }, ++ }, ++ errors: {}, ++ data: { ++ 1: { id: 1, name: 'Donkey', status: 'draft' }, ++ 2: { id: 2, name: 'Sauce', status: 'publish' }, ++ }, ++ }; ++ const item: Item = { ++ id: 2, ++ name: 'Holy smokes!', ++ status: 'draft', ++ }; ++ ++ const state = reducer( initialState, { ++ type: TYPES.UPDATE_ITEM_SUCCESS, ++ id: item.id, ++ item, ++ } ); ++ ++ expect( state.items ).toEqual( initialState.items ); ++ expect( state.errors ).toEqual( initialState.errors ); ++ ++ expect( state.data[ 1 ] ).toEqual( initialState.data[ 1 ] ); ++ expect( state.data[ 2 ].id ).toEqual( initialState.data[ 2 ].id ); ++ expect( state.data[ 2 ].title ).toEqual( initialState.data[ 2 ].title ); ++ expect( state.data[ 2 ].name ).toEqual( item.name ); ++ } ); ++ ++ it( 'should handle CREATE_ITEM_SUCCESS', () => { ++ const item: Item = { ++ id: 2, ++ name: 'Off the hook!', ++ status: 'draft', ++ }; ++ ++ const state = reducer( defaultState, { ++ type: TYPES.CREATE_ITEM_SUCCESS, ++ id: item.id, ++ item, ++ } ); ++ ++ expect( state.data[ 2 ].name ).toEqual( item.name ); ++ expect( state.data[ 2 ].status ).toEqual( item.status ); ++ } ); ++ ++ it( 'should handle DELETE_ITEM_SUCCESS', () => { ++ const itemType = 'guyisms'; ++ const initialState: ResourceState = { ++ items: { ++ [ itemType ]: { ++ data: [ 1, 2 ], ++ }, ++ }, ++ errors: {}, ++ data: { ++ 1: { id: 1, name: 'Donkey', status: 'draft' }, ++ 2: { id: 2, name: 'Sauce', status: 'publish' }, ++ }, ++ }; ++ const item1Updated: Item = { ++ id: 1, ++ status: 'pending', ++ }; ++ const item2Updated: Item = { ++ id: 2, ++ status: 'trash', ++ }; ++ ++ let state = reducer( initialState, { ++ type: TYPES.DELETE_ITEM_SUCCESS, ++ id: item1Updated.id, ++ item: item1Updated, ++ force: true, ++ } ); ++ state = reducer( state, { ++ type: TYPES.DELETE_ITEM_SUCCESS, ++ id: item2Updated.id, ++ item: item2Updated, ++ force: false, ++ } ); ++ ++ expect( state.errors ).toEqual( initialState.errors ); ++ expect( state.data[ 1 ] ).toEqual( undefined ); ++ expect( state.data[ 2 ].status ).toEqual( 'trash' ); ++ } ); ++ ++ it( 'should handle DELETE_ITEM_ERROR', () => { ++ const id = 2; ++ const resourceName = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { ++ id, ++ } ); ++ const error = 'Baaam!'; ++ const state = reducer( defaultState, { ++ type: TYPES.DELETE_ITEM_ERROR, ++ id, ++ error, ++ errorType: CRUD_ACTIONS.DELETE_ITEM, ++ } ); ++ ++ expect( state.errors[ resourceName ] ).toBe( error ); ++ } ); ++} ); +diff --git a/packages/js/data/src/crud/test/resolvers.ts b/packages/js/data/src/crud/test/resolvers.ts +new file mode 100644 +index 0000000000..96aee036a3 +--- /dev/null ++++ b/packages/js/data/src/crud/test/resolvers.ts +@@ -0,0 +1,18 @@ ++/** ++ * Internal dependencies ++ */ ++import { createResolvers } from '../resolvers'; ++ ++const resolvers = createResolvers( { ++ resourceName: 'Product', ++ pluralResourceName: 'Products', ++ namespace: '/products', ++} ); ++ ++describe( 'crud resolvers', () => { ++ it( 'should return methods for the default resolvers', () => { ++ expect( Object.keys( resolvers ).length ).toEqual( 2 ); ++ expect( resolvers ).toHaveProperty( 'getProduct' ); ++ expect( resolvers ).toHaveProperty( 'getProducts' ); ++ } ); ++} ); +diff --git a/packages/js/data/src/crud/test/selectors.ts b/packages/js/data/src/crud/test/selectors.ts +new file mode 100644 +index 0000000000..99116eb2a9 +--- /dev/null ++++ b/packages/js/data/src/crud/test/selectors.ts +@@ -0,0 +1,22 @@ ++/** ++ * Internal dependencies ++ */ ++import { createSelectors } from '../selectors'; ++ ++const selectors = createSelectors( { ++ resourceName: 'Product', ++ pluralResourceName: 'Products', ++} ); ++ ++describe( 'crud selectors', () => { ++ it( 'should return methods for the default selectors', () => { ++ expect( Object.keys( selectors ).length ).toEqual( 7 ); ++ expect( selectors ).toHaveProperty( 'getProduct' ); ++ expect( selectors ).toHaveProperty( 'getProducts' ); ++ expect( selectors ).toHaveProperty( 'getProductError' ); ++ expect( selectors ).toHaveProperty( 'getProductsError' ); ++ expect( selectors ).toHaveProperty( 'getProductCreateError' ); ++ expect( selectors ).toHaveProperty( 'getProductDeleteError' ); ++ expect( selectors ).toHaveProperty( 'getProductUpdateError' ); ++ } ); ++} ); +diff --git a/packages/js/data/src/crud/types.ts b/packages/js/data/src/crud/types.ts +new file mode 100644 +index 0000000000..5143746f89 +--- /dev/null ++++ b/packages/js/data/src/crud/types.ts +@@ -0,0 +1,117 @@ ++/** ++ * Internal dependencies ++ */ ++import { BaseQueryParams, WPDataSelector, WPDataSelectors } from '../types'; ++import { ++ getItem, ++ getItemError, ++ getItems, ++ getItemsError, ++ getItemCreateError, ++ getItemDeleteError, ++ getItemUpdateError, ++} from './selectors'; ++ ++export type IdType = number | string; ++ ++export type Item = { ++ id: IdType; ++ [ key: string ]: unknown; ++}; ++ ++export type ItemQuery = BaseQueryParams & { ++ [ key: string ]: unknown; ++}; ++ ++type WithRequiredProperty< Type, Key extends keyof Type > = Type & { ++ [ Property in Key ]-?: Type[ Property ]; ++}; ++ ++export type CrudActions< ++ ResourceName, ++ ItemType, ++ MutableProperties, ++ RequiredFields extends keyof MutableProperties | undefined = undefined ++> = MapActions< ++ { ++ create: ( query: Partial< ItemType > ) => Item; ++ update: ( query: Partial< ItemType > ) => Item; ++ }, ++ ResourceName, ++ RequiredFields extends keyof MutableProperties ++ ? WithRequiredProperty< Partial< MutableProperties >, RequiredFields > ++ : Partial< MutableProperties >, ++ Generator< unknown, ItemType > ++> & ++ MapActions< ++ { ++ delete: ( id: IdType ) => Item; ++ }, ++ ResourceName, ++ IdType, ++ Generator< unknown, ItemType > ++ >; ++ ++export type CrudSelectors< ++ ResourceName, ++ PluralResourceName, ++ ItemType, ++ ItemQueryType, ++ MutableProperties ++> = MapSelectors< ++ { ++ '': WPDataSelector< typeof getItem >; ++ }, ++ ResourceName, ++ IdType, ++ ItemType ++> & ++ MapSelectors< ++ { ++ Error: WPDataSelector< typeof getItemError >; ++ DeleteError: WPDataSelector< typeof getItemDeleteError >; ++ UpdateError: WPDataSelector< typeof getItemUpdateError >; ++ }, ++ ResourceName, ++ IdType, ++ unknown ++ > & ++ MapSelectors< ++ { ++ '': WPDataSelector< typeof getItems >; ++ }, ++ PluralResourceName, ++ ItemQueryType, ++ ItemType[] ++ > & ++ MapSelectors< ++ { ++ Error: WPDataSelector< typeof getItemsError >; ++ }, ++ PluralResourceName, ++ ItemQueryType, ++ unknown ++ > & ++ MapSelectors< ++ { ++ CreateError: WPDataSelector< typeof getItemCreateError >; ++ }, ++ PluralResourceName, ++ MutableProperties, ++ unknown ++ > & ++ WPDataSelectors; ++ ++export type MapSelectors< Type, ResourceName, ParamType, ReturnType > = { ++ [ Property in keyof Type as `get${ Capitalize< ++ string & ResourceName ++ > }${ Capitalize< string & Property > }` ]: ( x?: ParamType ) => ReturnType; ++}; ++ ++export type MapActions< Type, ResourceName, ParamType, ReturnType > = { ++ [ Property in keyof Type as `${ Lowercase< ++ string & Property ++ > }${ Capitalize< string & ResourceName > }` ]: ( ++ x: ParamType ++ ) => ReturnType; ++}; +diff --git a/packages/js/data/src/export/reducer.ts b/packages/js/data/src/export/reducer.ts +index 919534e7a9..9e2f110210 100644 +--- a/packages/js/data/src/export/reducer.ts ++++ b/packages/js/data/src/export/reducer.ts +@@ -29,9 +29,8 @@ const reducer: Reducer< ExportState, Action > = ( + ...state.requesting, + [ action.selector ]: { + ...state.requesting[ action.selector ], +- [ hashExportArgs( +- action.selectorArgs +- ) ]: action.isRequesting, ++ [ hashExportArgs( action.selectorArgs ) ]: ++ action.isRequesting, + }, + }, + }; +diff --git a/packages/js/data/src/import/index.ts b/packages/js/data/src/import/index.ts +index cc0e8bfa4f..327b16e91a 100644 +--- a/packages/js/data/src/import/index.ts ++++ b/packages/js/data/src/import/index.ts +@@ -11,6 +11,7 @@ import { Reducer, AnyAction } from 'redux'; + import { STORE_NAME } from './constants'; + import * as selectors from './selectors'; + import * as actions from './actions'; ++import * as resolvers from './resolvers'; + import reducer, { State } from './reducer'; + import { WPDataSelectors } from '../types'; + export * from './types'; +@@ -21,6 +22,7 @@ registerStore< State >( STORE_NAME, { + actions, + controls, + selectors, ++ resolvers, + } ); + + export const IMPORT_STORE_NAME = STORE_NAME; +diff --git a/packages/js/data/src/import/test/reducer.ts b/packages/js/data/src/import/test/reducer.ts +index 42676c9c45..815dad2646 100644 +--- a/packages/js/data/src/import/test/reducer.ts ++++ b/packages/js/data/src/import/test/reducer.ts +@@ -115,9 +115,11 @@ describe( 'import reducer', () => { + } ); + const stringifiedQuery = JSON.stringify( query ); + expect( +- ( state.errors[ stringifiedQuery ] as { +- code: string; +- } ).code ++ ( ++ state.errors[ stringifiedQuery ] as { ++ code: string; ++ } ++ ).code + ).toBe( 'error' ); + } ); + } ); +diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts +index 1aae57e0f3..2bdf1f64c8 100644 +--- a/packages/js/data/src/index.ts ++++ b/packages/js/data/src/index.ts +@@ -18,6 +18,9 @@ export { ITEMS_STORE_NAME } from './items'; + export { PAYMENT_GATEWAYS_STORE_NAME } from './payment-gateways'; + export { PRODUCTS_STORE_NAME } from './products'; + export { ORDERS_STORE_NAME } from './orders'; ++export { PRODUCT_ATTRIBUTES_STORE_NAME } from './product-attributes'; ++export { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME } from './product-shipping-classes'; ++export { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones'; + export { PaymentGateway } from './payment-gateways/types'; + + // Export hooks +@@ -89,6 +92,9 @@ import type { COUNTRIES_STORE_NAME } from './countries'; + import type { PAYMENT_GATEWAYS_STORE_NAME } from './payment-gateways'; + import type { PRODUCTS_STORE_NAME } from './products'; + import type { ORDERS_STORE_NAME } from './orders'; ++import type { PRODUCT_ATTRIBUTES_STORE_NAME } from './product-attributes'; ++import type { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME } from './product-shipping-classes'; ++import type { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones'; + + export type WCDataStoreName = + | typeof REVIEWS_STORE_NAME +@@ -104,7 +110,10 @@ export type WCDataStoreName = + | typeof COUNTRIES_STORE_NAME + | typeof PAYMENT_GATEWAYS_STORE_NAME + | typeof PRODUCTS_STORE_NAME +- | typeof ORDERS_STORE_NAME; ++ | typeof ORDERS_STORE_NAME ++ | typeof PRODUCT_ATTRIBUTES_STORE_NAME ++ | typeof EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ++ | typeof EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME; + + /** + * Internal dependencies +@@ -116,6 +125,9 @@ import { OnboardingSelectors } from './onboarding/selectors'; + import { OptionsSelectors } from './options/types'; + import { ProductsSelectors } from './products/selectors'; + import { OrdersSelectors } from './orders/selectors'; ++import { ProductAttributeSelectors } from './product-attributes/types'; ++import { ProductShippingClassSelectors } from './product-shipping-classes/types'; ++import { ShippingZonesSelectors } from './shipping-zones/types'; + + // As we add types to all the package selectors we can fill out these unknown types with real ones. See one + // of the already typed selectors for an example of how you can do this. +@@ -145,8 +157,14 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME + ? WPDataSelectors + : T extends typeof PRODUCTS_STORE_NAME + ? ProductsSelectors ++ : T extends typeof PRODUCT_ATTRIBUTES_STORE_NAME ++ ? ProductAttributeSelectors ++ : T extends typeof EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ++ ? ProductShippingClassSelectors + : T extends typeof ORDERS_STORE_NAME + ? OrdersSelectors ++ : T extends typeof EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME ++ ? ShippingZonesSelectors + : never; + + export interface WCDataSelector { +@@ -155,4 +173,7 @@ export interface WCDataSelector { + + // Other exports + export { ActionDispatchers as PluginsStoreActions } from './plugins/actions'; ++export { ActionDispatchers as ProductAttributesActions } from './product-attributes/types'; + export { ActionDispatchers as ProductsStoreActions } from './products/actions'; ++export { ActionDispatchers as ProductShippingClassesActions } from './product-shipping-classes/types'; ++export { ActionDispatchers as ShippingZonesActions } from './shipping-zones/types'; +diff --git a/packages/js/data/src/items/action-types.js b/packages/js/data/src/items/action-types.ts +similarity index 93% +rename from packages/js/data/src/items/action-types.js +rename to packages/js/data/src/items/action-types.ts +index 4bea03c6d1..6fc29e8792 100644 +--- a/packages/js/data/src/items/action-types.js ++++ b/packages/js/data/src/items/action-types.ts +@@ -3,6 +3,6 @@ const TYPES = { + SET_ITEMS: 'SET_ITEMS', + SET_ITEMS_TOTAL_COUNT: 'SET_ITEMS_TOTAL_COUNT', + SET_ERROR: 'SET_ERROR', +-}; ++} as const; + + export default TYPES; +diff --git a/packages/js/data/src/items/actions.js b/packages/js/data/src/items/actions.ts +similarity index 63% +rename from packages/js/data/src/items/actions.js +rename to packages/js/data/src/items/actions.ts +index 71662a367f..e4e54bc91c 100644 +--- a/packages/js/data/src/items/actions.js ++++ b/packages/js/data/src/items/actions.ts +@@ -9,8 +9,9 @@ import { addQueryArgs } from '@wordpress/url'; + */ + import TYPES from './action-types'; + import { NAMESPACE, WC_ADMIN_NAMESPACE } from '../constants'; ++import { ItemType, Item, ProductItem, Query, ItemID } from './types'; + +-export function setItem( itemType, id, item ) { ++export function setItem( itemType: ItemType, id: ItemID, item: Item ) { + return { + type: TYPES.SET_ITEM, + id, +@@ -19,7 +20,12 @@ export function setItem( itemType, id, item ) { + }; + } + +-export function setItems( itemType, query, items, totalCount ) { ++export function setItems( ++ itemType: ItemType, ++ query: Query, ++ items: Item[], ++ totalCount?: number ++) { + return { + type: TYPES.SET_ITEMS, + items, +@@ -29,7 +35,11 @@ export function setItems( itemType, query, items, totalCount ) { + }; + } + +-export function setItemsTotalCount( itemType, query, totalCount ) { ++export function setItemsTotalCount( ++ itemType: ItemType, ++ query: Query, ++ totalCount: number ++) { + return { + type: TYPES.SET_ITEMS_TOTAL_COUNT, + itemType, +@@ -38,7 +48,11 @@ export function setItemsTotalCount( itemType, query, totalCount ) { + }; + } + +-export function setError( itemType, query, error ) { ++export function setError( ++ itemType: ItemType | 'createProductFromTemplate', ++ query: Record< string, unknown >, ++ error: unknown ++) { + return { + type: TYPES.SET_ERROR, + itemType, +@@ -47,7 +61,10 @@ export function setError( itemType, query, error ) { + }; + } + +-export function* updateProductStock( product, quantity ) { ++export function* updateProductStock( ++ product: Partial< ProductItem > & { id: ProductItem[ 'id' ] }, ++ quantity: number ++) { + const updatedProduct = { ...product, stock_quantity: quantity }; + const { id, parent_id: parentId, type } = updatedProduct; + +@@ -75,18 +92,24 @@ export function* updateProductStock( product, quantity ) { + } catch ( error ) { + // Update failed, return product back to original state. + yield setItem( 'products', id, product ); +- yield setError( 'products', id, error ); ++ yield setError( 'products', { id }, error ); + return false; + } + } + +-export function* createProductFromTemplate( itemFields, query ) { ++export function* createProductFromTemplate( ++ itemFields: { ++ template_name: string; ++ status: string; ++ }, ++ query: Query ++) { + try { + const url = addQueryArgs( + `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/create_product_from_template`, + query || {} + ); +- const newItem = yield apiFetch( { ++ const newItem: { id: ProductItem[ 'id' ] } = yield apiFetch( { + path: url, + method: 'POST', + data: itemFields, +@@ -98,3 +121,10 @@ export function* createProductFromTemplate( itemFields, query ) { + throw error; + } + } ++ ++export type Action = ReturnType< ++ | typeof setItem ++ | typeof setItems ++ | typeof setItemsTotalCount ++ | typeof setError ++>; +diff --git a/packages/js/data/src/items/constants.ts b/packages/js/data/src/items/constants.ts +index dbe32f31d2..baf7809bf8 100644 +--- a/packages/js/data/src/items/constants.ts ++++ b/packages/js/data/src/items/constants.ts +@@ -1 +1 @@ +-export const STORE_NAME = 'wc/admin/items'; ++export const STORE_NAME = 'wc/admin/items' as const; +diff --git a/packages/js/data/src/items/index.js b/packages/js/data/src/items/index.js +deleted file mode 100644 +index 7b3ca7e73f..0000000000 +--- a/packages/js/data/src/items/index.js ++++ /dev/null +@@ -1,25 +0,0 @@ +-/** +- * External dependencies +- */ +- +-import { registerStore } from '@wordpress/data'; +- +-/** +- * Internal dependencies +- */ +-import { STORE_NAME } from './constants'; +-import * as selectors from './selectors'; +-import * as actions from './actions'; +-import * as resolvers from './resolvers'; +-import controls from '../controls'; +-import reducer from './reducer'; +- +-registerStore( STORE_NAME, { +- reducer, +- actions, +- controls, +- selectors, +- resolvers, +-} ); +- +-export const ITEMS_STORE_NAME = STORE_NAME; +diff --git a/packages/js/data/src/items/index.ts b/packages/js/data/src/items/index.ts +new file mode 100644 +index 0000000000..bd6cb4938a +--- /dev/null ++++ b/packages/js/data/src/items/index.ts +@@ -0,0 +1,43 @@ ++/** ++ * External dependencies ++ */ ++import { registerStore } from '@wordpress/data'; ++import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores'; ++import { Reducer, AnyAction } from 'redux'; ++/** ++ * Internal dependencies ++ */ ++import { STORE_NAME } from './constants'; ++import * as selectors from './selectors'; ++import * as actions from './actions'; ++import * as resolvers from './resolvers'; ++import reducer, { State } from './reducer'; ++import controls from '../controls'; ++import { WPDataActions, WPDataSelectors } from '../types'; ++import { getItemsType } from './selectors'; ++export * from './types'; ++export type { State }; ++ ++registerStore< State >( STORE_NAME, { ++ reducer: reducer as Reducer< State, AnyAction >, ++ actions, ++ controls, ++ selectors, ++ resolvers, ++} ); ++ ++export const ITEMS_STORE_NAME = STORE_NAME; ++ ++export type ItemsSelector = Omit< ++ SelectFromMap< typeof selectors >, ++ 'getItems' ++> & { ++ getItems: getItemsType; ++} & WPDataSelectors; ++ ++declare module '@wordpress/data' { ++ function dispatch( ++ key: typeof STORE_NAME ++ ): DispatchFromMap< typeof actions & WPDataActions >; ++ function select( key: typeof STORE_NAME ): ItemsSelector; ++} +diff --git a/packages/js/data/src/items/reducer.js b/packages/js/data/src/items/reducer.js +deleted file mode 100644 +index 6070799425..0000000000 +--- a/packages/js/data/src/items/reducer.js ++++ /dev/null +@@ -1,79 +0,0 @@ +-/** +- * Internal dependencies +- */ +-import TYPES from './action-types'; +-import { getResourceName } from '../utils'; +-import { getTotalCountResourceName } from './utils'; +- +-const reducer = ( +- state = { +- items: {}, +- errors: {}, +- data: {}, +- }, +- { type, id, itemType, query, item, items, totalCount, error } +-) => { +- switch ( type ) { +- case TYPES.SET_ITEM: +- const itemData = state.data[ itemType ] || {}; +- return { +- ...state, +- data: { +- ...state.data, +- [ itemType ]: { +- ...itemData, +- [ id ]: { +- ...( itemData[ id ] || {} ), +- ...item, +- }, +- }, +- }, +- }; +- case TYPES.SET_ITEMS: +- const ids = []; +- const nextItems = items.reduce( ( result, theItem ) => { +- ids.push( theItem.id ); +- result[ theItem.id ] = theItem; +- return result; +- }, {} ); +- const resourceName = getResourceName( itemType, query ); +- return { +- ...state, +- items: { +- ...state.items, +- [ resourceName ]: { data: ids }, +- }, +- data: { +- ...state.data, +- [ itemType ]: { +- ...state.data[ itemType ], +- ...nextItems, +- }, +- }, +- }; +- case TYPES.SET_ITEMS_TOTAL_COUNT: +- const totalResourceName = getTotalCountResourceName( +- itemType, +- query +- ); +- return { +- ...state, +- items: { +- ...state.items, +- [ totalResourceName ]: totalCount, +- }, +- }; +- case TYPES.SET_ERROR: +- return { +- ...state, +- errors: { +- ...state.errors, +- [ getResourceName( itemType, query ) ]: error, +- }, +- }; +- default: +- return state; +- } +-}; +- +-export default reducer; +diff --git a/packages/js/data/src/items/reducer.ts b/packages/js/data/src/items/reducer.ts +new file mode 100644 +index 0000000000..b50e9b3f2d +--- /dev/null ++++ b/packages/js/data/src/items/reducer.ts +@@ -0,0 +1,97 @@ ++/** ++ * External dependencies ++ */ ++ ++import type { Reducer } from 'redux'; ++ ++/** ++ * Internal dependencies ++ */ ++import TYPES from './action-types'; ++import { getResourceName } from '../utils'; ++import { getTotalCountResourceName } from './utils'; ++import { Action } from './actions'; ++import { ItemsState, Item, ItemID } from './types'; ++ ++const initialState: ItemsState = { ++ items: {}, ++ errors: {}, ++ data: {}, ++}; ++ ++const reducer: Reducer< ItemsState, Action > = ( ++ state = initialState, ++ action ++) => { ++ switch ( action.type ) { ++ case TYPES.SET_ITEM: ++ const itemData = state.data[ action.itemType ] || {}; ++ return { ++ ...state, ++ data: { ++ ...state.data, ++ [ action.itemType ]: { ++ ...itemData, ++ [ action.id ]: { ++ ...( itemData[ action.id ] || {} ), ++ ...action.item, ++ }, ++ }, ++ }, ++ }; ++ case TYPES.SET_ITEMS: ++ const ids: Array< ItemID > = []; ++ const nextItems = action.items.reduce< Record< ItemID, Item > >( ++ ( result, theItem ) => { ++ ids.push( theItem.id ); ++ result[ theItem.id ] = theItem; ++ return result; ++ }, ++ {} ++ ); ++ const resourceName = getResourceName( ++ action.itemType, ++ action.query ++ ); ++ return { ++ ...state, ++ items: { ++ ...state.items, ++ [ resourceName ]: { data: ids }, ++ }, ++ data: { ++ ...state.data, ++ [ action.itemType ]: { ++ ...state.data[ action.itemType ], ++ ...nextItems, ++ }, ++ }, ++ }; ++ case TYPES.SET_ITEMS_TOTAL_COUNT: ++ const totalResourceName = getTotalCountResourceName( ++ action.itemType, ++ action.query ++ ); ++ return { ++ ...state, ++ items: { ++ ...state.items, ++ [ totalResourceName ]: action.totalCount, ++ }, ++ }; ++ case TYPES.SET_ERROR: ++ return { ++ ...state, ++ errors: { ++ ...state.errors, ++ [ getResourceName( action.itemType, action.query ) ]: ++ action.error, ++ }, ++ }; ++ default: ++ return state; ++ } ++}; ++ ++export type State = ReturnType< typeof reducer >; ++export default reducer; +diff --git a/packages/js/data/src/items/resolvers.js b/packages/js/data/src/items/resolvers.ts +similarity index 79% +rename from packages/js/data/src/items/resolvers.js +rename to packages/js/data/src/items/resolvers.ts +index 9f5fbc9908..390995b9b0 100644 +--- a/packages/js/data/src/items/resolvers.js ++++ b/packages/js/data/src/items/resolvers.ts +@@ -4,8 +4,9 @@ + import { NAMESPACE } from '../constants'; + import { setError, setItems, setItemsTotalCount } from './actions'; + import { request } from '../utils'; ++import { ItemType, Query } from './types'; + +-export function* getItems( itemType, query ) { ++export function* getItems( itemType: ItemType, query: Query ) { + try { + const endpoint = + itemType === 'categories' ? 'products/categories' : itemType; +@@ -13,6 +14,7 @@ export function* getItems( itemType, query ) { + `${ NAMESPACE }/${ endpoint }`, + query + ); ++ + yield setItemsTotalCount( itemType, query, totalCount ); + yield setItems( itemType, query, items ); + } catch ( error ) { +@@ -20,11 +22,7 @@ export function* getItems( itemType, query ) { + } + } + +-export function* getReviewsTotalCount( itemType, query ) { +- yield getItemsTotalCount( itemType, query ); +-} +- +-export function* getItemsTotalCount( itemType, query ) { ++export function* getItemsTotalCount( itemType: ItemType, query: Query ) { + try { + const totalsQuery = { + ...query, +@@ -42,3 +40,7 @@ export function* getItemsTotalCount( itemType, query ) { + yield setError( itemType, query, error ); + } + } ++ ++export function* getReviewsTotalCount( itemType: ItemType, query: Query ) { ++ yield getItemsTotalCount( itemType, query ); ++} +diff --git a/packages/js/data/src/items/selectors.js b/packages/js/data/src/items/selectors.js +deleted file mode 100644 +index 5e019fe433..0000000000 +--- a/packages/js/data/src/items/selectors.js ++++ /dev/null +@@ -1,47 +0,0 @@ +-/** +- * External dependencies +- */ +-import createSelector from 'rememo'; +- +-/** +- * Internal dependencies +- */ +-import { getResourceName } from '../utils'; +-import { getTotalCountResourceName } from './utils'; +- +-export const getItems = createSelector( +- ( state, itemType, query, defaultValue = new Map() ) => { +- const resourceName = getResourceName( itemType, query ); +- const ids = +- state.items[ resourceName ] && state.items[ resourceName ].data; +- if ( ! ids ) { +- return defaultValue; +- } +- return ids.reduce( ( map, id ) => { +- map.set( id, state.data[ itemType ][ id ] ); +- return map; +- }, new Map() ); +- }, +- ( state, itemType, query ) => { +- const resourceName = getResourceName( itemType, query ); +- return [ state.items[ resourceName ] ]; +- } +-); +- +-export const getItemsTotalCount = ( +- state, +- itemType, +- query, +- defaultValue = 0 +-) => { +- const resourceName = getTotalCountResourceName( itemType, query ); +- const totalCount = state.items.hasOwnProperty( resourceName ) +- ? state.items[ resourceName ] +- : defaultValue; +- return totalCount; +-}; +- +-export const getItemsError = ( state, itemType, query ) => { +- const resourceName = getResourceName( itemType, query ); +- return state.errors[ resourceName ]; +-}; +diff --git a/packages/js/data/src/items/selectors.ts b/packages/js/data/src/items/selectors.ts +new file mode 100644 +index 0000000000..9285827129 +--- /dev/null ++++ b/packages/js/data/src/items/selectors.ts +@@ -0,0 +1,74 @@ ++/** ++ * External dependencies ++ */ ++import createSelector from 'rememo'; ++ ++/** ++ * Internal dependencies ++ */ ++import { getResourceName } from '../utils'; ++import { getTotalCountResourceName } from './utils'; ++ ++import { ItemType, ItemsState, Query, ItemInfer } from './types'; ++ ++export type getItemsType = < T extends ItemType >( ++ itemType: T, ++ query: Query, ++ defaultValue?: Map< number, ItemInfer< T > | undefined > ++) => Map< number, ItemInfer< T > | undefined >; ++ ++type getItemsSelectorType = < T extends ItemType >( ++ state: ItemsState, ++ itemType: T, ++ query: Query, ++ defaultValue?: Map< number, ItemInfer< T > | undefined > ++) => Map< number, Map< number, ItemInfer< T > | undefined > >; ++ ++export const getItems = createSelector< getItemsSelectorType >( ++ ( state, itemType, query, defaultValue = new Map() ) => { ++ const resourceName = getResourceName( itemType, query ); ++ ++ let ids; ++ if ( ++ state.items[ resourceName ] && ++ typeof state.items[ resourceName ] === 'object' ++ ) { ++ ids = ( state.items[ resourceName ] as Record< string, number[] > ) ++ .data; ++ } ++ ++ if ( ! ids ) { ++ return defaultValue; ++ } ++ return ids.reduce( ( map, id: number ) => { ++ map.set( id, state.data[ itemType ]?.[ id ] ); ++ return map; ++ }, new Map() ); ++ }, ++ ( state, itemType, query ) => { ++ const resourceName = getResourceName( itemType, query ); ++ return [ state.items[ resourceName ] ]; ++ } ++); ++ ++export const getItemsTotalCount = ( ++ state: ItemsState, ++ itemType: ItemType, ++ query: Query, ++ defaultValue = 0 ++) => { ++ const resourceName = getTotalCountResourceName( itemType, query ); ++ const totalCount = state.items.hasOwnProperty( resourceName ) ++ ? state.items[ resourceName ] ++ : defaultValue; ++ return totalCount; ++}; ++ ++export const getItemsError = ( ++ state: ItemsState, ++ itemType: ItemType, ++ query: Query ++) => { ++ const resourceName = getResourceName( itemType, query ); ++ return state.errors[ resourceName ]; ++}; +diff --git a/packages/js/data/src/items/test/reducer.js b/packages/js/data/src/items/test/reducer.ts +similarity index 62% +rename from packages/js/data/src/items/test/reducer.js +rename to packages/js/data/src/items/test/reducer.ts +index e7c73e4067..5b299c6989 100644 +--- a/packages/js/data/src/items/test/reducer.js ++++ b/packages/js/data/src/items/test/reducer.ts +@@ -5,6 +5,7 @@ import reducer from '../reducer'; + import TYPES from '../action-types'; + import { getResourceName } from '../../utils'; + import { getTotalCountResourceName } from '../utils'; ++import { ProductItem } from '../types'; + + const defaultState = { + items: {}, +@@ -14,13 +15,14 @@ const defaultState = { + + describe( 'items reducer', () => { + it( 'should return a default state', () => { ++ // @ts-expect-error - we're testing the default state + const state = reducer( undefined, {} ); + expect( state ).toEqual( defaultState ); + expect( state ).not.toBe( defaultState ); + } ); + + it( 'should handle SET_ITEM', () => { +- const itemType = 'guyisms'; ++ const itemType = 'products'; + const initialState = { + items: { + [ itemType ]: { +@@ -31,14 +33,14 @@ describe( 'items reducer', () => { + errors: {}, + data: { + [ itemType ]: { +- 1: { id: 1, title: 'Donkey', status: 'flavortown' }, +- 2: { id: 2, title: 'Sauce', status: 'flavortown' }, ++ 1: { id: 1, name: 'Donkey', tax_status: 'flavortown' }, ++ 2: { id: 2, name: 'Sauce', tax_status: 'flavortown' }, + }, + }, + }; + const update = { + id: 2, +- status: 'bomb dot com', ++ tax_status: 'bomb dot com', + }; + + const state = reducer( initialState, { +@@ -51,26 +53,28 @@ describe( 'items reducer', () => { + expect( state.items ).toEqual( initialState.items ); + expect( state.errors ).toEqual( initialState.errors ); + +- expect( state.data[ itemType ][ '1' ] ).toEqual( +- initialState.data[ itemType ][ '1' ] +- ); +- expect( state.data[ itemType ][ '2' ].id ).toEqual( ++ const item = state.data[ itemType ] || {}; ++ ++ expect( item[ '1' ] ).toEqual( initialState.data[ itemType ][ '1' ] ); ++ expect( item[ '2' ].id ).toEqual( + initialState.data[ itemType ][ '2' ].id + ); +- expect( state.data[ itemType ][ '2' ].title ).toEqual( +- initialState.data[ itemType ][ '2' ].title ++ expect( ( item[ '2' ] as ProductItem ).name ).toEqual( ++ initialState.data[ itemType ][ '2' ].name ++ ); ++ expect( ( item[ '2' ] as Partial< ProductItem > ).tax_status ).toEqual( ++ update.tax_status + ); +- expect( state.data[ itemType ][ '2' ].status ).toEqual( update.status ); + } ); + + it( 'should handle SET_ITEMS', () => { + const items = [ +- { id: 1, title: 'Yum!' }, +- { id: 2, title: 'Dynamite!' }, ++ { id: 1, name: 'Yum!' }, ++ { id: 2, name: 'Dynamite!' }, + ]; + const totalCount = 45; +- const query = { status: 'flavortown' }; +- const itemType = 'BBQ'; ++ const query = { page: 1 }; ++ const itemType = 'products'; + const state = reducer( defaultState, { + type: TYPES.SET_ITEMS, + items, +@@ -81,16 +85,31 @@ describe( 'items reducer', () => { + + const resourceName = getResourceName( itemType, query ); + +- expect( state.items[ resourceName ].data ).toHaveLength( 2 ); +- expect( state.items[ resourceName ].data.includes( 1 ) ).toBeTruthy(); +- expect( state.items[ resourceName ].data.includes( 2 ) ).toBeTruthy(); +- +- expect( state.data[ itemType ][ '1' ] ).toBe( items[ 0 ] ); +- expect( state.data[ itemType ][ '2' ] ).toBe( items[ 1 ] ); ++ expect( ++ ( state.items[ resourceName ] as { [ key: string ]: number[] } ) ++ .data ++ ).toHaveLength( 2 ); ++ expect( ++ ( ++ state.items[ resourceName ] as { ++ [ key: string ]: number[]; ++ } ++ ).data.includes( 1 ) ++ ).toBeTruthy(); ++ expect( ++ ( ++ state.items[ resourceName ] as { ++ [ key: string ]: number[]; ++ } ++ ).data.includes( 2 ) ++ ).toBeTruthy(); ++ ++ expect( ( state.data[ itemType ] || {} )[ '1' ] ).toBe( items[ 0 ] ); ++ expect( ( state.data[ itemType ] || {} )[ '2' ] ).toBe( items[ 1 ] ); + } ); + + it( 'should handle SET_ITEMS_TOTAL_COUNT', () => { +- const itemType = 'BBQ'; ++ const itemType = 'products'; + const initialQuery = { + status: 'flavortown', + page: 1, +@@ -105,6 +124,8 @@ describe( 'items reducer', () => { + items: { + [ resourceName ]: 1, + }, ++ data: {}, ++ errors: {}, + }; + + // Additional coverage for getTotalCountResourceName(). +@@ -112,7 +133,7 @@ describe( 'items reducer', () => { + status: 'flavortown', + page: 2, + per_page: 10, +- _fields: [ 'id', 'title', 'status' ], ++ _fields: [ 'id', 'name', 'status' ], + }; + + const state = reducer( initialState, { +@@ -123,6 +144,8 @@ describe( 'items reducer', () => { + } ); + + expect( state ).toEqual( { ++ data: {}, ++ errors: {}, + items: { + [ resourceName ]: 2, + }, +@@ -131,7 +154,7 @@ describe( 'items reducer', () => { + + it( 'should handle SET_ERROR', () => { + const query = { status: 'flavortown' }; +- const itemType = 'BBQ'; ++ const itemType = 'products'; + const resourceName = getResourceName( itemType, query ); + const error = 'Baaam!'; + const state = reducer( defaultState, { +diff --git a/packages/js/data/src/items/test/utils.js b/packages/js/data/src/items/test/utils.ts +similarity index 100% +rename from packages/js/data/src/items/test/utils.js +rename to packages/js/data/src/items/test/utils.ts +diff --git a/packages/js/data/src/items/types.ts b/packages/js/data/src/items/types.ts +new file mode 100644 +index 0000000000..1e542cd3ef +--- /dev/null ++++ b/packages/js/data/src/items/types.ts +@@ -0,0 +1,229 @@ ++/** ++ * Internal dependencies ++ */ ++import { BaseQueryParams } from '../types/query-params'; ++ ++type Link = { ++ href: string; ++}; ++ ++// Category, Product, Customer item id is a number, and leaderboards item is a string. ++export type ItemID = number | string; ++ ++export type ItemType = 'categories' | 'products' | 'customers' | 'leaderboards'; ++ ++export type ItemImage = { ++ id: number; ++ date_created: string; ++ date_created_gmt: string; ++ date_modified: string; ++ date_modified_gmt: string; ++ src: string; ++ name: string; ++ alt: string; ++}; ++ ++// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php#L97-L208 ++export type CategoryItem = { ++ id: number; ++ name: string; ++ slug: string; ++ parent: number; ++ description: string; ++ display: string; ++ image: null | ItemImage; ++ menu_order: number; ++ count: number; ++ _links: { ++ collection: Array< Link >; ++ self: Array< Link >; ++ }; ++}; ++ ++// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Products.php#L72-L83 ++// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php#L809-L1423 ++export type ProductItem = { ++ id: number; ++ name: string; ++ slug: string; ++ permalink: string; ++ attributes: Array< { ++ id: number; ++ name: string; ++ position: number; ++ visible: boolean; ++ variation: boolean; ++ options: string[]; ++ } >; ++ average_rating: string; ++ backordered: boolean; ++ backorders: string; ++ backorders_allowed: boolean; ++ button_text: string; ++ catalog_visibility: string; ++ categories: Array< { ++ id: number; ++ name: string; ++ slug: string; ++ } >; ++ cross_sell_ids: number[]; ++ date_created: string; ++ date_created_gmt: string; ++ date_modified: string; ++ date_modified_gmt: string; ++ date_on_sale_from: null | string; ++ date_on_sale_from_gmt: null | string; ++ date_on_sale_to: null | string; ++ date_on_sale_to_gmt: null | string; ++ default_attributes: Array< { ++ id: number; ++ name: string; ++ option: string; ++ } >; ++ description: string; ++ dimensions: { length: string; width: string; height: string }; ++ download_expiry: number; ++ download_limit: number; ++ downloadable: boolean; ++ downloads: Array< { ++ id: number; ++ name: string; ++ file: string; ++ } >; ++ external_url: string; ++ featured: boolean; ++ grouped_products: Array< number >; ++ has_options: boolean; ++ images: Array< ItemImage >; ++ low_stock_amount: null | number; ++ manage_stock: boolean; ++ menu_order: number; ++ meta_data: Array< { ++ id: number; ++ key: string; ++ value: string; ++ } >; ++ on_sale: boolean; ++ parent_id: number; ++ price: string; ++ price_html: string; ++ purchasable: boolean; ++ purchase_note: string; ++ rating_count: number; ++ regular_price: string; ++ related_ids: number[]; ++ reviews_allowed: boolean; ++ sale_price: string; ++ shipping_class: string; ++ shipping_class_id: number; ++ shipping_required: boolean; ++ shipping_taxable: boolean; ++ short_description: string; ++ sku: string; ++ sold_individually: boolean; ++ status: string; ++ stock_quantity: number; ++ stock_status: string; ++ tags: Array< { ++ id: number; ++ name: string; ++ slug: string; ++ } >; ++ tax_class: string; ++ tax_status: string; ++ total_sales: number; ++ type: string; ++ upsell_ids: number[]; ++ variations: Array< { ++ id: number; ++ date_created: string; ++ date_created_gmt: string; ++ date_modified: string; ++ date_modified_gmt: string; ++ attributes: Array< { ++ id: number; ++ name: string; ++ option: string; ++ } >; ++ image: string; ++ price: string; ++ regular_price: string; ++ sale_price: string; ++ sku: string; ++ stock_quantity: number; ++ tax_class: string; ++ tax_status: string; ++ total_sales: number; ++ weight: string; ++ } >; ++ virtual: boolean; ++ weight: string; ++ last_order_date: string; ++}; ++ ++// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php#L221-L318 ++export type CustomerItem = { ++ id: number; ++ user_id: number; ++ name: string; ++ username: string; ++ country: string; ++ city: string; ++ state: string; ++ postcode: string; ++ date_registered: string; ++ date_registered_gmt: string; ++ date_last_active: string; ++ date_last_active_gmt: string; ++ orders_count: number; ++ total_spent: number; ++ avg_order_value: number; ++ _links: { ++ self: Array< Link >; ++ }; ++}; ++ ++// https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Leaderboards.php#L527-L585 ++export type LeaderboardItem = { ++ id: string; ++ label: string; ++ headers: { ++ label: string; ++ }; ++ rows: { ++ display: string; ++ value: string; ++ }; ++}; ++ ++export type Item = Partial< ++ CategoryItem | ProductItem | CustomerItem | LeaderboardItem ++> & { ++ id: ItemID; ++}; ++ ++export type ItemInfer< T > = Partial< ++ T extends 'categories' ++ ? CategoryItem ++ : T extends 'products' ++ ? ProductItem ++ : T extends 'customers' ++ ? CustomerItem ++ : T extends 'leaderboards' ++ ? LeaderboardItem ++ : never ++> & { ++ id: ItemID; ++}; ++ ++export type ItemsState = { ++ items: ++ | Record< string, { data: ItemID[] } | number > ++ | Record< string, never >; ++ data: Partial< Record< ItemType, Record< ItemID, Item > > >; ++ errors: { ++ [ key: string ]: unknown; ++ }; ++}; ++ ++export type Query = Partial< BaseQueryParams >; +diff --git a/packages/js/data/src/items/utils.js b/packages/js/data/src/items/utils.ts +similarity index 77% +rename from packages/js/data/src/items/utils.js +rename to packages/js/data/src/items/utils.ts +index 085270c062..9ae50b9cce 100644 +--- a/packages/js/data/src/items/utils.js ++++ b/packages/js/data/src/items/utils.ts +@@ -2,12 +2,24 @@ + * External dependencies + */ + import { appendTimestamp, getCurrentDates } from '@woocommerce/date'; +- ++import { select as wpSelect } from '@wordpress/data'; + /** + * Internal dependencies + */ + import { STORE_NAME } from './constants'; + import { getResourceName } from '../utils'; ++import { ItemInfer, ItemType, Query } from './types'; ++import { ItemsSelector } from './'; ++ ++type Options = { ++ id: number; ++ per_page: number; ++ persisted_query: Query; ++ filterQuery: Query; ++ query: { [ key: string ]: string | undefined }; ++ select: typeof wpSelect; ++ defaultDateRange: string; ++}; + + /** + * Returns leaderboard data to render a leaderboard table. +@@ -17,11 +29,12 @@ import { getResourceName } from '../utils'; + * @param {number} options.per_page Per page limit + * @param {Object} options.persisted_query Persisted query passed to endpoint + * @param {Object} options.query Query parameters in the url ++ * @param {Object} options.filterQuery Query parameters to filter the leaderboard + * @param {Object} options.select Instance of @wordpress/select + * @param {string} options.defaultDateRange User specified default date range. + * @return {Object} Object containing leaderboard responses. + */ +-export function getLeaderboard( options ) { ++export function getLeaderboard( options: Options ) { + const endpoint = 'leaderboards'; + const { + per_page: perPage, +@@ -30,6 +43,7 @@ export function getLeaderboard( options ) { + select, + filterQuery, + } = options; ++ + const { getItems, getItemsError, isResolving } = select( STORE_NAME ); + const response = { + isRequesting: false, +@@ -49,7 +63,10 @@ export function getLeaderboard( options ) { + // Disable eslint rule requiring `getItems` to be defined below because the next two statements + // depend on `getItems` to have been called. + // eslint-disable-next-line @wordpress/no-unused-vars-before-return +- const leaderboards = getItems( endpoint, leaderboardQuery ); ++ const leaderboards = getItems< 'leaderboards' >( ++ endpoint, ++ leaderboardQuery ++ ); + + if ( isResolving( 'getItems', [ endpoint, leaderboardQuery ] ) ) { + return { ...response, isRequesting: true }; +@@ -69,15 +86,15 @@ export function getLeaderboard( options ) { + * @param {Object} options Query options. + * @return {Object} Object containing API request information and the matching items. + */ +-export function searchItemsByString( +- selector, +- endpoint, +- search, +- options = {} ++export function searchItemsByString< T extends ItemType >( ++ selector: ItemsSelector, ++ endpoint: T, ++ search: string[], ++ options: Query = {} + ) { + const { getItems, getItemsError, isResolving } = selector; + +- const items = {}; ++ const items: Record< number, ItemInfer< T > | undefined > = {}; + let isRequesting = false; + let isError = false; + search.forEach( ( searchWord ) => { +@@ -86,7 +103,7 @@ export function searchItemsByString( + per_page: 10, + ...options, + }; +- const newItems = getItems( endpoint, query ); ++ const newItems = getItems< T >( endpoint, query ); + newItems.forEach( ( item, id ) => { + items[ id ] = item; + } ); +@@ -111,11 +128,8 @@ export function searchItemsByString( + * @param {Object} query Query for item totals count. + * @return {string} Resource name for item totals. + */ +-export function getTotalCountResourceName( itemType, query ) { +- // Disable eslint rule because we're using this spread to omit properties +- // that don't affect item totals count results. +- // eslint-disable-next-line no-unused-vars, camelcase ++export function getTotalCountResourceName( itemType: string, query: Query ) { + const { _fields, page, per_page, ...totalsQuery } = query; + +- return getResourceName( 'total-' + itemType, totalsQuery ); ++ return getResourceName( 'total-' + itemType, { ...totalsQuery } ); + } +diff --git a/packages/js/data/src/navigation/with-navigation-hydration.js b/packages/js/data/src/navigation/with-navigation-hydration.js +index 60d5341614..4bc56a1080 100644 +--- a/packages/js/data/src/navigation/with-navigation-hydration.js ++++ b/packages/js/data/src/navigation/with-navigation-hydration.js +@@ -25,14 +25,10 @@ export const withNavigationHydration = ( data ) => + return; + } + +- const { isResolving, hasFinishedResolution } = select( +- STORE_NAME +- ); +- const { +- startResolution, +- finishResolution, +- setMenuItems, +- } = registry.dispatch( STORE_NAME ); ++ const { isResolving, hasFinishedResolution } = ++ select( STORE_NAME ); ++ const { startResolution, finishResolution, setMenuItems } = ++ registry.dispatch( STORE_NAME ); + + if ( + ! isResolving( 'getMenuItems' ) && +diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts +index a41a295ba1..975f36f17e 100644 +--- a/packages/js/data/src/onboarding/types.ts ++++ b/packages/js/data/src/onboarding/types.ts +@@ -100,7 +100,7 @@ export type ProductCount = '0' | '1-10' | '11-100' | '101 - 1000' | '1000+'; + export type ProductTypeSlug = + | 'physical' + | 'bookings' +- | 'download' ++ | 'downloads' + | 'memberships' + | 'product-add-ons' + | 'product-bundles' +@@ -126,7 +126,7 @@ export type RevenueTypeSlug = + | 'more-than-250000'; + + export type ProfileItems = { +- business_extensions: [ ] | null; ++ business_extensions: [] | null; + completed: boolean | null; + industry: Industry[] | null; + number_employees: string | null; +diff --git a/packages/js/data/src/onboarding/with-onboarding-hydration.tsx b/packages/js/data/src/onboarding/with-onboarding-hydration.tsx +index 914d01f9fd..91732717c7 100644 +--- a/packages/js/data/src/onboarding/with-onboarding-hydration.tsx ++++ b/packages/js/data/src/onboarding/with-onboarding-hydration.tsx +@@ -28,9 +28,8 @@ export const withOnboardingHydration = ( data: { + return; + } + +- const { isResolving, hasFinishedResolution } = select( +- STORE_NAME +- ); ++ const { isResolving, hasFinishedResolution } = ++ select( STORE_NAME ); + const { + startResolution, + finishResolution, +diff --git a/packages/js/data/src/options/with-options-hydration.tsx b/packages/js/data/src/options/with-options-hydration.tsx +index 8a4d5bc7ef..fca323a3eb 100644 +--- a/packages/js/data/src/options/with-options-hydration.tsx ++++ b/packages/js/data/src/options/with-options-hydration.tsx +@@ -21,11 +21,8 @@ export const useOptionsHydration = ( data: Options ) => { + } + + const { isResolving, hasFinishedResolution } = select( STORE_NAME ); +- const { +- startResolution, +- finishResolution, +- receiveOptions, +- } = registry.dispatch( STORE_NAME ); ++ const { startResolution, finishResolution, receiveOptions } = ++ registry.dispatch( STORE_NAME ); + const names = Object.keys( dataRef.current ); + + names.forEach( ( name ) => { +diff --git a/packages/js/data/src/orders/reducer.ts b/packages/js/data/src/orders/reducer.ts +index accd0b789c..e788e43979 100644 +--- a/packages/js/data/src/orders/reducer.ts ++++ b/packages/js/data/src/orders/reducer.ts +@@ -88,9 +88,8 @@ const reducer: Reducer< OrdersState, Actions > = ( + ...state, + errors: { + ...state.errors, +- [ getOrderResourceName( +- payload.query +- ) ]: payload.error, ++ [ getOrderResourceName( payload.query ) ]: ++ payload.error, + }, + }; + default: +diff --git a/packages/js/data/src/orders/resolvers.ts b/packages/js/data/src/orders/resolvers.ts +index efbf3bb592..748318dbca 100644 +--- a/packages/js/data/src/orders/resolvers.ts ++++ b/packages/js/data/src/orders/resolvers.ts +@@ -24,13 +24,11 @@ export function* getOrders( query: Partial< OrdersQuery > ) { + ordersQuery._fields = [ 'id', ...ordersQuery._fields ]; + } + try { +- const { +- items, +- totalCount, +- }: { items: Order[]; totalCount: number } = yield request< +- OrdersQuery, +- Order +- >( WC_ORDERS_NAMESPACE, ordersQuery ); ++ const { items, totalCount }: { items: Order[]; totalCount: number } = ++ yield request< OrdersQuery, Order >( ++ WC_ORDERS_NAMESPACE, ++ ordersQuery ++ ); + yield getOrdersTotalCountSuccess( query, totalCount ); + yield getOrdersSuccess( query, items, totalCount ); + return items; +diff --git a/packages/js/data/src/orders/test/reducer.ts b/packages/js/data/src/orders/test/reducer.ts +index 0506c33d98..3dd6652e53 100644 +--- a/packages/js/data/src/orders/test/reducer.ts ++++ b/packages/js/data/src/orders/test/reducer.ts +@@ -100,8 +100,6 @@ describe( 'orders reducer', () => { + totalCount, + } ); + +- const resourceName = getOrderResourceName( query ); +- + expect( state.data[ 1 ].total ).toEqual( initialState.data[ 1 ].total ); + expect( state.data[ 2 ] ).toEqual( orders[ 1 ] ); + } ); +diff --git a/packages/js/data/src/orders/utils.ts b/packages/js/data/src/orders/utils.ts +index b4a36c336a..d03ca76c76 100644 +--- a/packages/js/data/src/orders/utils.ts ++++ b/packages/js/data/src/orders/utils.ts +@@ -28,9 +28,6 @@ export function getOrderResourceName( query: Partial< OrdersQuery > ) { + export function getTotalOrderCountResourceName( + query: Partial< OrdersQuery > + ) { +- // Disable eslint rule because we're using this spread to omit properties +- // that don't affect item totals count results. +- // eslint-disable-next-line no-unused-vars, camelcase + const { _fields, page, per_page, ...totalsQuery } = query; + + return getOrderResourceName( totalsQuery ); +diff --git a/packages/js/data/src/payment-gateways/actions.ts b/packages/js/data/src/payment-gateways/actions.ts +index afed637d2d..95fecda37c 100644 +--- a/packages/js/data/src/payment-gateways/actions.ts ++++ b/packages/js/data/src/payment-gateways/actions.ts +@@ -30,9 +30,7 @@ export function getPaymentGatewaysSuccess( + }; + } + +-export function getPaymentGatewaysError( +- error: unknown +-): { ++export function getPaymentGatewaysError( error: unknown ): { + type: ACTION_TYPES.GET_PAYMENT_GATEWAYS_ERROR; + error: unknown; + } { +@@ -50,9 +48,7 @@ export function getPaymentGatewayRequest(): { + }; + } + +-export function getPaymentGatewayError( +- error: unknown +-): { ++export function getPaymentGatewayError( error: unknown ): { + type: ACTION_TYPES.GET_PAYMENT_GATEWAY_ERROR; + error: unknown; + } { +@@ -62,9 +58,7 @@ export function getPaymentGatewayError( + }; + } + +-export function getPaymentGatewaySuccess( +- paymentGateway: PaymentGateway +-): { ++export function getPaymentGatewaySuccess( paymentGateway: PaymentGateway ): { + type: ACTION_TYPES.GET_PAYMENT_GATEWAY_SUCCESS; + paymentGateway: PaymentGateway; + } { +@@ -74,9 +68,7 @@ export function getPaymentGatewaySuccess( + }; + } + +-export function updatePaymentGatewaySuccess( +- paymentGateway: PaymentGateway +-): { ++export function updatePaymentGatewaySuccess( paymentGateway: PaymentGateway ): { + type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_SUCCESS; + paymentGateway: PaymentGateway; + } { +@@ -93,9 +85,7 @@ export function updatePaymentGatewayRequest(): { + }; + } + +-export function updatePaymentGatewayError( +- error: unknown +-): { ++export function updatePaymentGatewayError( error: unknown ): { + type: ACTION_TYPES.UPDATE_PAYMENT_GATEWAY_ERROR; + error: unknown; + } { +diff --git a/packages/js/data/src/plugins/test/reducer.ts b/packages/js/data/src/plugins/test/reducer.ts +index 627831f51b..2a7d09ed60 100644 +--- a/packages/js/data/src/plugins/test/reducer.ts ++++ b/packages/js/data/src/plugins/test/reducer.ts +@@ -145,10 +145,12 @@ describe( 'plugins reducer', () => { + /* eslint-disable dot-notation */ + + expect( +- ( state.errors[ 'getInstalledPlugins' ] as Record< +- string, +- string[] +- > ).jetpack[ 0 ] ++ ( ++ state.errors[ 'getInstalledPlugins' ] as Record< ++ string, ++ string[] ++ > ++ ).jetpack[ 0 ] + ).toBe( 'error' ); + expect( state.requesting[ 'getInstalledPlugins' ] ).toBe( false ); + /* eslint-enable dot-notation */ +diff --git a/packages/js/data/src/plugins/with-plugins-hydration.tsx b/packages/js/data/src/plugins/with-plugins-hydration.tsx +index 04dafd8cdd..193dd3f9c9 100644 +--- a/packages/js/data/src/plugins/with-plugins-hydration.tsx ++++ b/packages/js/data/src/plugins/with-plugins-hydration.tsx +@@ -42,9 +42,8 @@ export const withPluginsHydration = ( data: PluginHydrationData ) => + return; + } + +- const { isResolving, hasFinishedResolution } = select( +- STORE_NAME +- ); ++ const { isResolving, hasFinishedResolution } = ++ select( STORE_NAME ); + const { + startResolution, + finishResolution, +diff --git a/packages/js/data/src/product-attributes/constants.ts b/packages/js/data/src/product-attributes/constants.ts +new file mode 100644 +index 0000000000..47f09db06d +--- /dev/null ++++ b/packages/js/data/src/product-attributes/constants.ts +@@ -0,0 +1,3 @@ ++export const STORE_NAME = 'wc/admin/products/attributes'; ++ ++export const WC_PRODUCT_ATTRIBUTES_NAMESPACE = '/wc/v3/products/attributes'; +diff --git a/packages/js/data/src/product-attributes/index.ts b/packages/js/data/src/product-attributes/index.ts +new file mode 100644 +index 0000000000..69852d815f +--- /dev/null ++++ b/packages/js/data/src/product-attributes/index.ts +@@ -0,0 +1,14 @@ ++/** ++ * Internal dependencies ++ */ ++import { STORE_NAME, WC_PRODUCT_ATTRIBUTES_NAMESPACE } from './constants'; ++import { createCrudDataStore } from '../crud'; ++ ++createCrudDataStore( { ++ storeName: STORE_NAME, ++ resourceName: 'ProductAttribute', ++ pluralResourceName: 'ProductAttributes', ++ namespace: WC_PRODUCT_ATTRIBUTES_NAMESPACE, ++} ); ++ ++export const PRODUCT_ATTRIBUTES_STORE_NAME = STORE_NAME; +diff --git a/packages/js/data/src/product-attributes/types.ts b/packages/js/data/src/product-attributes/types.ts +new file mode 100644 +index 0000000000..11f38ddad1 +--- /dev/null ++++ b/packages/js/data/src/product-attributes/types.ts +@@ -0,0 +1,44 @@ ++/** ++ * External dependencies ++ */ ++import { DispatchFromMap } from '@automattic/data-stores'; ++ ++/** ++ * Internal dependencies ++ */ ++import { CrudActions, CrudSelectors } from '../crud/types'; ++ ++type ProductAttribute = { ++ id: number; ++ slug: string; ++ name: string; ++ type: string; ++ order_by: string; ++ has_archives: boolean; ++}; ++ ++type Query = { ++ context?: string; ++}; ++ ++type ReadOnlyProperties = 'id'; ++ ++type MutableProperties = Partial< ++ Omit< ProductAttribute, ReadOnlyProperties > ++>; ++ ++type ProductAttributeActions = CrudActions< ++ 'ProductAttribute', ++ ProductAttribute, ++ MutableProperties ++>; ++ ++export type ProductAttributeSelectors = CrudSelectors< ++ 'ProductAttribute', ++ 'ProductAttributes', ++ ProductAttribute, ++ Query, ++ MutableProperties ++>; ++ ++export type ActionDispatchers = DispatchFromMap< ProductAttributeActions >; +diff --git a/packages/js/data/src/product-shipping-classes/README.md b/packages/js/data/src/product-shipping-classes/README.md +new file mode 100644 +index 0000000000..bf9f6e5006 +--- /dev/null ++++ b/packages/js/data/src/product-shipping-classes/README.md +@@ -0,0 +1,43 @@ ++# Product Shipping Classes Data Store ++ ++This data store provides functions to interact with the [Product Shipping Class REST endpoints](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-shipping-classes). ++Under the hood this data store makes use of the [CRUD data store](../crud/README.md). ++ ++**Note: This data store is listed as experimental still as it is still in active development.** ++ ++## Usage ++ ++This data store can be accessed under the `experimental/wc/admin/products/shipping-classes` name. It is recommended you make use of the export constant `EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME`. ++ ++Example: ++ ++```js ++import { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME } from '@woocommerce/data'; ++import { useDispatch } from '@wordpress/data'; ++ ++function Component() { ++ const actions = useDispatch( ++ EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ++ ); ++ actions.createProductShippingClass( { name: 'test' } ); ++} ++``` ++ ++## Selections and actions: ++ ++| Selector | Description | ++| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ++| `getProductShippingClass( id: number )` | Gets a Product Shipping Class by ID | ++| `getProductShippingClassError( id )` | Get the error for a failing GET shipping class request. | ++| `getProductShippingClasses( query = {} )` | Get all product shipping classes, optionally by a specific query, see `Query` type [here](./types.ts). | ++| `getProductShippingClassesError( query = {} )` | Get the error for a GET request for all shipping classes. | ++ ++Example usage: `wp.data.select( EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ).getProductShippingClass( 3 );` ++ ++| Actions | Method | Description | ++| -------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------- | ++| `createProductShippingClass( shippingClassObject )` | POST | Creates shipping class, see `ProductShippingClass` [here](./types.ts) for values | ++| `deleteProductShippingClass( id )` | DELETE | Deletes a shipping class by ID | ++| `updatetProductShippingClass( id, shippingClassObject )` | PUT | Updates a shipping class, see `ProductShippingClass` [here](./types.ts) for values | ++ ++Example usage: `wp.data.dispatch( EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME ).updateProductShippingClass( 3, { name: 'New name' } );` +diff --git a/packages/js/data/src/product-shipping-classes/constants.ts b/packages/js/data/src/product-shipping-classes/constants.ts +new file mode 100644 +index 0000000000..6ddcbe8a40 +--- /dev/null ++++ b/packages/js/data/src/product-shipping-classes/constants.ts +@@ -0,0 +1,4 @@ ++export const STORE_NAME = 'experimental/wc/admin/products/shipping-classes'; ++ ++export const WC_PRODUCT_SHIPPING_CLASSES_NAMESPACE = ++ '/wc/v3/products/shipping_classes'; +diff --git a/packages/js/data/src/product-shipping-classes/index.ts b/packages/js/data/src/product-shipping-classes/index.ts +new file mode 100644 +index 0000000000..9b8e1f1f70 +--- /dev/null ++++ b/packages/js/data/src/product-shipping-classes/index.ts +@@ -0,0 +1,14 @@ ++/** ++ * Internal dependencies ++ */ ++import { STORE_NAME, WC_PRODUCT_SHIPPING_CLASSES_NAMESPACE } from './constants'; ++import { createCrudDataStore } from '../crud'; ++ ++createCrudDataStore( { ++ storeName: STORE_NAME, ++ resourceName: 'ProductShippingClass', ++ pluralResourceName: 'ProductShippingClasses', ++ namespace: WC_PRODUCT_SHIPPING_CLASSES_NAMESPACE, ++} ); ++ ++export const EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME = STORE_NAME; +diff --git a/packages/js/data/src/product-shipping-classes/types.ts b/packages/js/data/src/product-shipping-classes/types.ts +new file mode 100644 +index 0000000000..79d72e85f2 +--- /dev/null ++++ b/packages/js/data/src/product-shipping-classes/types.ts +@@ -0,0 +1,46 @@ ++/** ++ * External dependencies ++ */ ++import { DispatchFromMap } from '@automattic/data-stores'; ++ ++/** ++ * Internal dependencies ++ */ ++import { CrudActions, CrudSelectors } from '../crud/types'; ++import { BaseQueryParams } from '../types'; ++ ++type ProductShippingClass = { ++ id: number; ++ slug: string; ++ name: string; ++ description: string; ++ count: number; ++}; ++ ++type Query = BaseQueryParams< keyof ProductShippingClass > & { ++ context?: string; ++ hide_empty?: boolean; ++ slug?: string; ++ product?: number; ++}; ++ ++type ReadOnlyProperties = 'id'; ++ ++type MutableProperties = Omit< ProductShippingClass, ReadOnlyProperties >; ++ ++type ProductShippingClassActions = CrudActions< ++ 'ProductShippingClass', ++ ProductShippingClass, ++ MutableProperties, ++ 'name' ++>; ++ ++export type ProductShippingClassSelectors = CrudSelectors< ++ 'ProductShippingClass', ++ 'ProductShippingClasses', ++ ProductShippingClass, ++ Query, ++ MutableProperties ++>; ++ ++export type ActionDispatchers = DispatchFromMap< ProductShippingClassActions >; +diff --git a/packages/js/data/src/products/reducer.ts b/packages/js/data/src/products/reducer.ts +index d44b7e5b3a..a581998ac5 100644 +--- a/packages/js/data/src/products/reducer.ts ++++ b/packages/js/data/src/products/reducer.ts +@@ -95,9 +95,8 @@ const reducer: Reducer< ProductState, Actions > = ( + ...state, + errors: { + ...state.errors, +- [ getProductResourceName( +- payload.query +- ) ]: payload.error, ++ [ getProductResourceName( payload.query ) ]: ++ payload.error, + }, + }; + case TYPES.UPDATE_PRODUCT_ERROR: +diff --git a/packages/js/data/src/products/resolvers.ts b/packages/js/data/src/products/resolvers.ts +index a38616098d..ff81694140 100644 +--- a/packages/js/data/src/products/resolvers.ts ++++ b/packages/js/data/src/products/resolvers.ts +@@ -24,13 +24,11 @@ export function* getProducts( query: Partial< ProductQuery > ) { + productsQuery._fields = [ 'id', ...productsQuery._fields ]; + } + try { +- const { +- items, +- totalCount, +- }: { items: Product[]; totalCount: number } = yield request< +- ProductQuery, +- Product +- >( WC_PRODUCT_NAMESPACE, productsQuery ); ++ const { items, totalCount }: { items: Product[]; totalCount: number } = ++ yield request< ProductQuery, Product >( ++ WC_PRODUCT_NAMESPACE, ++ productsQuery ++ ); + yield getProductsTotalCountSuccess( query, totalCount ); + yield getProductsSuccess( query, items, totalCount ); + return items; +diff --git a/packages/js/data/src/products/utils.ts b/packages/js/data/src/products/utils.ts +index 7ac799d1a7..3e4582f6ae 100644 +--- a/packages/js/data/src/products/utils.ts ++++ b/packages/js/data/src/products/utils.ts +@@ -28,10 +28,6 @@ export function getProductResourceName( query: Partial< ProductQuery > ) { + export function getTotalProductCountResourceName( + query: Partial< ProductQuery > + ) { +- // Disable eslint rule because we're using this spread to omit properties +- // that don't affect item totals count results. +- // eslint-disable-next-line no-unused-vars, camelcase + const { _fields, page, per_page, ...totalsQuery } = query; +- + return getProductResourceName( totalsQuery ); + } +diff --git a/packages/js/data/src/reports/utils.js b/packages/js/data/src/reports/utils.js +index 367391c482..fbe58240fb 100644 +--- a/packages/js/data/src/reports/utils.js ++++ b/packages/js/data/src/reports/utils.js +@@ -232,9 +232,8 @@ function getRequestQuery( options ) { + */ + export function getSummaryNumbers( options ) { + const { endpoint, select } = options; +- const { getReportStats, getReportStatsError, isResolving } = select( +- STORE_NAME +- ); ++ const { getReportStats, getReportStatsError, isResolving } = ++ select( STORE_NAME ); + const response = { + isRequesting: false, + isError: false, +@@ -355,16 +354,12 @@ export function getReportChartData( options ) { + if ( options.select && ! options.selector ) { + deprecated( 'option.select', { + version: '1.7.0', +- hint: +- 'You can pass the report selectors through option.selector now.', ++ hint: 'You can pass the report selectors through option.selector now.', + } ); + reportSelectors = options.select( STORE_NAME ); + } +- const { +- getReportStats, +- getReportStatsError, +- isResolving, +- } = reportSelectors; ++ const { getReportStats, getReportStatsError, isResolving } = ++ reportSelectors; + + const requestQuery = getRequestQuery( options ); + // Disable eslint rule requiring `stats` to be defined below because the next two if statements +@@ -514,16 +509,12 @@ export function getReportTableData( options ) { + if ( options.select && ! options.selector ) { + deprecated( 'option.select', { + version: '1.7.0', +- hint: +- 'You can pass the report selectors through option.selector now.', ++ hint: 'You can pass the report selectors through option.selector now.', + } ); + reportSelectors = options.select( STORE_NAME ); + } +- const { +- getReportItems, +- getReportItemsError, +- hasFinishedResolution, +- } = reportSelectors; ++ const { getReportItems, getReportItemsError, hasFinishedResolution } = ++ reportSelectors; + + const tableQuery = reportsUtils.getReportTableQuery( options ); + const response = { +diff --git a/packages/js/data/src/settings/use-settings.js b/packages/js/data/src/settings/use-settings.js +index 11fabfd874..2a40c11310 100644 +--- a/packages/js/data/src/settings/use-settings.js ++++ b/packages/js/data/src/settings/use-settings.js +@@ -10,28 +10,29 @@ import { useCallback } from '@wordpress/element'; + import { STORE_NAME } from './constants'; + + export const useSettings = ( group, settingsKeys = [] ) => { +- const { +- requestedSettings, +- settingsError, +- isRequesting, +- isDirty, +- } = useSelect( +- ( select ) => { +- const { +- getLastSettingsErrorForGroup, +- getSettingsForGroup, +- getIsDirty, +- isUpdateSettingsRequesting, +- } = select( STORE_NAME ); +- return { +- requestedSettings: getSettingsForGroup( group, settingsKeys ), +- settingsError: Boolean( getLastSettingsErrorForGroup( group ) ), +- isRequesting: isUpdateSettingsRequesting( group ), +- isDirty: getIsDirty( group, settingsKeys ), +- }; +- }, +- [ group, ...settingsKeys.sort() ] +- ); ++ const { requestedSettings, settingsError, isRequesting, isDirty } = ++ useSelect( ++ ( select ) => { ++ const { ++ getLastSettingsErrorForGroup, ++ getSettingsForGroup, ++ getIsDirty, ++ isUpdateSettingsRequesting, ++ } = select( STORE_NAME ); ++ return { ++ requestedSettings: getSettingsForGroup( ++ group, ++ settingsKeys ++ ), ++ settingsError: Boolean( ++ getLastSettingsErrorForGroup( group ) ++ ), ++ isRequesting: isUpdateSettingsRequesting( group ), ++ isDirty: getIsDirty( group, settingsKeys ), ++ }; ++ }, ++ [ group, ...settingsKeys.sort() ] ++ ); + const { + persistSettingsForGroup, + updateAndPersistSettingsForGroup, +diff --git a/packages/js/data/src/settings/with-settings-hydration.js b/packages/js/data/src/settings/with-settings-hydration.js +index 91d5b20e04..017f530e75 100644 +--- a/packages/js/data/src/settings/with-settings-hydration.js ++++ b/packages/js/data/src/settings/with-settings-hydration.js +@@ -20,9 +20,8 @@ export const withSettingsHydration = ( group, settings ) => + return; + } + +- const { isResolving, hasFinishedResolution } = select( +- STORE_NAME +- ); ++ const { isResolving, hasFinishedResolution } = ++ select( STORE_NAME ); + const { + startResolution, + finishResolution, +diff --git a/packages/js/data/src/shipping-zones/README.md b/packages/js/data/src/shipping-zones/README.md +new file mode 100644 +index 0000000000..5b2540de53 +--- /dev/null ++++ b/packages/js/data/src/shipping-zones/README.md +@@ -0,0 +1,46 @@ ++# Shipping Zones Data Store ++ ++This data store provides functions to interact with the [Shipping Zones REST endpoints](https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-zones). ++Under the hood this data store makes use of the [CRUD data store](../crud/README.md). ++ ++**Note: This data store is listed as experimental still as it is still in active development.** ++ ++## Usage ++ ++This data store can be accessed under the `experimental/wc/admin/shipping/zones` name. It is recommended you make use of the export constant `EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME`. ++ ++Example: ++ ++```ts ++import { ++ EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME, ++ ShippingZonesActions, ++} from '@woocommerce/data'; ++import { useDispatch } from '@wordpress/data'; ++ ++function Component() { ++ const actions = useDispatch( ++ EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME ++ ) as ShippingZonesActions; ++ actions.createShippingZone( { name: 'test' } ); ++} ++``` ++ ++## Selections and actions: ++ ++| Selector | Description | ++| -------------------------------------- | ------------------------------------------------------- | ++| `getShippingZone( id: number )` | Gets a Shipping Zone by ID | ++| `getShippingZoneError( id )` | Get the error for a failing GET shipping zone request. | ++| `getShippingZones( query = {} )` | Get all shipping zones, query object is empty. | ++| `getShippingZoneesError( query = {} )` | Get the error for a GET request for all shipping zones. | ++ ++Example usage: `wp.data.select( EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME ).getShippingZone( 3 );` ++ ++| Actions | Method | Description | ++| ----------------------------------------------- | ------ | ------------------------------------------------------------------------- | ++| `createShippingZone( shippingZoneObject )` | POST | Creates shipping zone, see `ShippingZone` [here](./types.ts) for values | ++| `deleteShippingZone( id )` | DELETE | Deletes a shipping class by ID | ++| `updatetShippingZone( id, shippingZoneObject )` | PUT | Updates a shipping zone, see `ShippingZone` [here](./types.ts) for values | ++ ++Example usage: `wp.data.dispatch( EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME ).updateShippingZone( 3, { name: 'New name' } );` +diff --git a/packages/js/data/src/shipping-zones/constants.ts b/packages/js/data/src/shipping-zones/constants.ts +new file mode 100644 +index 0000000000..f9c847e51a +--- /dev/null ++++ b/packages/js/data/src/shipping-zones/constants.ts +@@ -0,0 +1,3 @@ ++export const STORE_NAME = 'experimental/wc/admin/shipping/zones'; ++ ++export const WC_SHIPPING_ZONES_NAMESPACE = '/wc/v3/shipping/zones'; +diff --git a/packages/js/data/src/shipping-zones/index.ts b/packages/js/data/src/shipping-zones/index.ts +new file mode 100644 +index 0000000000..e7b45c8a1c +--- /dev/null ++++ b/packages/js/data/src/shipping-zones/index.ts +@@ -0,0 +1,14 @@ ++/** ++ * Internal dependencies ++ */ ++import { STORE_NAME, WC_SHIPPING_ZONES_NAMESPACE } from './constants'; ++import { createCrudDataStore } from '../crud'; ++ ++createCrudDataStore( { ++ storeName: STORE_NAME, ++ resourceName: 'ShippingZone', ++ pluralResourceName: 'ShippingZones', ++ namespace: WC_SHIPPING_ZONES_NAMESPACE, ++} ); ++ ++export const EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME = STORE_NAME; +diff --git a/packages/js/data/src/shipping-zones/types.ts b/packages/js/data/src/shipping-zones/types.ts +new file mode 100644 +index 0000000000..4008831f47 +--- /dev/null ++++ b/packages/js/data/src/shipping-zones/types.ts +@@ -0,0 +1,36 @@ ++/** ++ * External dependencies ++ */ ++import { DispatchFromMap } from '@automattic/data-stores'; ++ ++/** ++ * Internal dependencies ++ */ ++import { CrudActions, CrudSelectors } from '../crud/types'; ++ ++type ShippingZone = { ++ id: number; ++ name: string; ++ order: number; ++}; ++ ++type ReadOnlyProperties = 'id'; ++ ++type MutableProperties = Omit< ShippingZone, ReadOnlyProperties >; ++ ++type ShippingZonesActions = CrudActions< ++ 'ShippingZone', ++ ShippingZone, ++ MutableProperties, ++ 'name' ++>; ++ ++export type ShippingZonesSelectors = CrudSelectors< ++ 'ShippingZone', ++ 'ShippingZones', ++ ShippingZone, ++ undefined, ++ MutableProperties ++>; ++ ++export type ActionDispatchers = DispatchFromMap< ShippingZonesActions >; +diff --git a/packages/js/data/src/types/wp-data.ts b/packages/js/data/src/types/wp-data.ts +index 46b5287fc6..f6deb9b4f3 100644 +--- a/packages/js/data/src/types/wp-data.ts ++++ b/packages/js/data/src/types/wp-data.ts +@@ -4,18 +4,18 @@ + // [wp.data.getSelectors](https://github.com/WordPress/gutenberg/blob/319deee5f4d4838d6bc280e9e2be89c7f43f2509/packages/data/src/store/index.js#L16-L20) + // [selector.js](https://github.com/WordPress/gutenberg/blob/trunk/packages/data/src/redux-store/metadata/selectors.js#L48-L52) + export type WPDataSelectors = { +- getIsResolving: ( selector: string, args?: string[] ) => boolean; +- hasStartedResolution: ( selector: string, args?: string[] ) => boolean; +- hasFinishedResolution: ( selector: string, args?: string[] ) => boolean; +- isResolving: ( selector: string, args?: string[] ) => boolean; ++ getIsResolving: ( selector: string, args?: unknown[] ) => boolean; ++ hasStartedResolution: ( selector: string, args?: unknown[] ) => boolean; ++ hasFinishedResolution: ( selector: string, args?: unknown[] ) => boolean; ++ isResolving: ( selector: string, args?: unknown[] ) => boolean; + getCachedResolvers: () => unknown; + }; + + // [wp.data.getActions](https://github.com/WordPress/gutenberg/blob/319deee5f4d4838d6bc280e9e2be89c7f43f2509/packages/data/src/store/index.js#L31-L35) + // [actions.js](https://github.com/WordPress/gutenberg/blob/aa2bed9010aa50467cb43063e370b70a91591e9b/packages/data/src/redux-store/metadata/actions.js) + export type WPDataActions = { +- startResolution: ( selector: string, args?: string[] ) => void; +- finishResolution: ( selector: string, args?: string[] ) => void; ++ startResolution: ( selector: string, args?: unknown[] ) => void; ++ finishResolution: ( selector: string, args?: unknown[] ) => void; + invalidateResolution: ( selector: string ) => void; + invalidateResolutionForStore: ( selector: string ) => void; + invalidateResolutionForStoreSelector: ( selector: string ) => void; +diff --git a/packages/js/data/src/use-select-with-refresh.js b/packages/js/data/src/use-select-with-refresh.ts +similarity index 50% +rename from packages/js/data/src/use-select-with-refresh.js +rename to packages/js/data/src/use-select-with-refresh.ts +index 99d5e5fe8f..81319989b4 100644 +--- a/packages/js/data/src/use-select-with-refresh.js ++++ b/packages/js/data/src/use-select-with-refresh.ts +@@ -4,13 +4,20 @@ + import { useEffect, useRef } from '@wordpress/element'; + import { useSelect } from '@wordpress/data'; + +-const useInterval = ( callback, interval ) => { +- const savedCallback = useRef(); ++const useInterval = ( ++ callback: ( ...args: unknown[] ) => void, ++ interval: Parameters< typeof setInterval >[ 1 ] ++) => { ++ const savedCallback = useRef< ( ...args: unknown[] ) => void >(); + useEffect( () => { + savedCallback.current = callback; + }, [ callback ] ); + useEffect( () => { +- const handler = ( ...args ) => savedCallback.current( ...args ); ++ const handler = ( ...args: unknown[] ) => { ++ if ( savedCallback.current ) { ++ savedCallback.current( ...args ); ++ } ++ }; + if ( interval !== null ) { + const id = setInterval( handler, interval ); + return () => clearInterval( id ); +@@ -19,10 +26,10 @@ const useInterval = ( callback, interval ) => { + }; + + export const useSelectWithRefresh = ( +- mapSelectToProps, +- invalidationCallback, +- interval, +- dependencies ++ mapSelectToProps: Parameters< typeof useSelect >[ 0 ], ++ invalidationCallback: Parameters< typeof useInterval >[ 0 ], ++ interval: Parameters< typeof useInterval >[ 1 ], ++ dependencies: Parameters< typeof useSelect >[ 1 ] + ) => { + const result = useSelect( mapSelectToProps, dependencies ); + useInterval( invalidationCallback, interval ); +diff --git a/packages/js/data/src/user/index.js b/packages/js/data/src/user/index.ts +similarity index 100% +rename from packages/js/data/src/user/index.js +rename to packages/js/data/src/user/index.ts +diff --git a/packages/js/data/src/user/test/use-user-preferences.js b/packages/js/data/src/user/test/use-user-preferences.ts +similarity index 98% +rename from packages/js/data/src/user/test/use-user-preferences.js +rename to packages/js/data/src/user/test/use-user-preferences.ts +index ccfb1aa139..3954cc31ab 100644 +--- a/packages/js/data/src/user/test/use-user-preferences.js ++++ b/packages/js/data/src/user/test/use-user-preferences.ts +@@ -140,8 +140,8 @@ describe( 'useUserPreferences() hook', () => { + 'function' + ); + +- // Passing an array, not an object. +- const updateResult = await result.current.updateUserPreferences( [] ); ++ // Passing an empty object. ++ const updateResult = await result.current.updateUserPreferences( {} ); + + expect( updateResult ).toMatchObject( { + error: new Error( 'Invalid woocommerce_meta data for update.' ), +@@ -292,7 +292,9 @@ describe( 'useUserPreferences() hook', () => { + }, + } ); + +- await result.current.updateUserPreferences( { ++ await result.current.updateUserPreferences< { ++ revenue_report_columns: string[]; ++ } >( { + revenue_report_columns: [ 'shipping', 'taxes' ], + } ); + +diff --git a/packages/js/data/src/user/types.ts b/packages/js/data/src/user/types.ts +new file mode 100644 +index 0000000000..122da493e9 +--- /dev/null ++++ b/packages/js/data/src/user/types.ts +@@ -0,0 +1,44 @@ ++/** ++ * External dependencies ++ */ ++ ++import schema from '@wordpress/core-data'; ++ ++export type UserPreferences = { ++ activity_panel_inbox_last_read?: string; ++ activity_panel_reviews_last_read?: string; ++ android_app_banner_dismissed?: string; ++ categories_report_columns?: string; ++ coupons_report_columns?: string; ++ customers_report_columns?: string; ++ dashboard_chart_interval?: string; ++ dashboard_chart_type?: string; ++ dashboard_leaderboard_rows?: string; ++ dashboard_sections?: string; ++ help_panel_highlight_shown?: string; ++ homepage_layout?: string; ++ homepage_stats?: string; ++ orders_report_columns?: string; ++ products_report_columns?: string; ++ revenue_report_columns?: string; ++ task_list_tracked_started_tasks?: { ++ [ key: string ]: number; ++ }; ++ taxes_report_columns?: string; ++ variations_report_columns?: string; ++}; ++ ++export type WoocommerceMeta = UserPreferences & { ++ task_list_tracked_started_tasks?: string; ++}; ++ ++export type WCUser< ++ T extends keyof schema.Schema.BaseUser< 'view' > = schema.Schema.ViewKeys.User ++> = Pick< ++ schema.Schema.BaseUser< 'view' >, ++ schema.Schema.ViewKeys.User | T ++> & { ++ // https://github.com/woocommerce/woocommerce/blob/3eb1938f4a0d0a93c9bcaf2a904f96bd501177fc/plugins/woocommerce/src/Internal/Admin/WCAdminUser.php#L40-L58 ++ woocommerce_meta: WoocommerceMeta; ++ is_super_admin: boolean; ++}; +diff --git a/packages/js/data/src/user/use-user-preferences.ts b/packages/js/data/src/user/use-user-preferences.ts +index f675e3380e..5a49d1fe85 100644 +--- a/packages/js/data/src/user/use-user-preferences.ts ++++ b/packages/js/data/src/user/use-user-preferences.ts +@@ -3,47 +3,12 @@ + */ + import { mapValues } from 'lodash'; + import { useDispatch, useSelect } from '@wordpress/data'; +-import schema from '@wordpress/core-data'; + + /** + * Internal dependencies + */ + import { STORE_NAME } from './constants'; +- +-type UserPreferences = { +- activity_panel_inbox_last_read?: string; +- activity_panel_reviews_last_read?: string; +- android_app_banner_dismissed?: string; +- categories_report_columns?: string; +- coupons_report_columns?: string; +- customers_report_columns?: string; +- dashboard_chart_interval?: string; +- dashboard_chart_type?: string; +- dashboard_leaderboard_rows?: string; +- dashboard_sections?: string; +- help_panel_highlight_shown?: string; +- homepage_layout?: string; +- homepage_stats?: string; +- orders_report_columns?: string; +- products_report_columns?: string; +- revenue_report_columns?: string; +- task_list_tracked_started_tasks?: { +- [ key: string ]: number; +- }; +- taxes_report_columns?: string; +- variations_report_columns?: string; +-}; +- +-type WoocommerceMeta = UserPreferences & { +- task_list_tracked_started_tasks?: string; +-}; +- +-type WCUser = Pick< +- schema.Schema.BaseUser< 'view' >, +- schema.Schema.ViewKeys.User +-> & { +- woocommerce_meta: WoocommerceMeta; +-}; ++import { WCUser, UserPreferences } from './types'; + + /** + * Retrieve and decode the user's WooCommerce meta values. +@@ -181,7 +146,11 @@ export const useUserPreferences = () => { + }; + } ); + +- const updateUserPreferences = ( userPrefs: UserPreferences ) => { ++ const updateUserPreferences = < ++ T extends Record< string, unknown > = UserPreferences ++ >( ++ userPrefs: UserPreferences | T ++ ) => { + // WP 5.3.x doesn't have the User entity defined. + if ( typeof saveUser !== 'function' ) { + // Polyfill saveUser() - wrapper of saveEntityRecord. +diff --git a/packages/js/data/src/user/use-user.js b/packages/js/data/src/user/use-user.ts +similarity index 57% +rename from packages/js/data/src/user/use-user.js +rename to packages/js/data/src/user/use-user.ts +index 060aff1a94..40bdaed104 100644 +--- a/packages/js/data/src/user/use-user.js ++++ b/packages/js/data/src/user/use-user.ts +@@ -7,6 +7,7 @@ import { useSelect } from '@wordpress/data'; + * Internal dependencies + */ + import { STORE_NAME } from './constants'; ++import { WCUser } from './types'; + + /** + * Custom react hook for shortcut methods around user. +@@ -15,22 +16,22 @@ import { STORE_NAME } from './constants'; + */ + export const useUser = () => { + const userData = useSelect( ( select ) => { +- const { +- getCurrentUser, +- hasStartedResolution, +- hasFinishedResolution, +- } = select( STORE_NAME ); ++ // TODO: Update @types/wordpress__core-data to include the 'hasStartedResolution', 'hasFinishedResolution' method. ++ // @ts-expect-errors Property 'hasStartedResolution', 'hasFinishedResolution' does not exist on type @types/wordpress__core-data ++ const { getCurrentUser, hasStartedResolution, hasFinishedResolution } = ++ select( STORE_NAME ); + + return { + isRequesting: + hasStartedResolution( 'getCurrentUser' ) && + ! hasFinishedResolution( 'getCurrentUser' ), +- user: getCurrentUser(), ++ // We register additional user data in backend so we need to use a type assertion here for WC user. ++ user: getCurrentUser() as WCUser< 'capabilities' >, + getCurrentUser, + }; + } ); + +- const currentUserCan = ( capability ) => { ++ const currentUserCan = ( capability: string ) => { + if ( userData.user && userData.user.is_super_admin ) { + return true; + } +diff --git a/packages/js/data/src/user/with-current-user-hydration.js b/packages/js/data/src/user/with-current-user-hydration.tsx +similarity index 78% +rename from packages/js/data/src/user/with-current-user-hydration.js +rename to packages/js/data/src/user/with-current-user-hydration.tsx +index bfdb070891..a073199963 100644 +--- a/packages/js/data/src/user/with-current-user-hydration.js ++++ b/packages/js/data/src/user/with-current-user-hydration.tsx +@@ -9,26 +9,27 @@ import { createElement, useRef } from '@wordpress/element'; + * Internal dependencies + */ + import { STORE_NAME } from './constants'; ++import { WCUser } from './types'; + + /** + * Higher-order component used to hydrate current user data. + * + * @param {Object} currentUser Current user object in the same format as the WP REST API returns. + */ +-export const withCurrentUserHydration = ( currentUser ) => +- createHigherOrderComponent( ++export const withCurrentUserHydration = ( currentUser: WCUser ) => ++ createHigherOrderComponent< Record< string, unknown > >( + ( OriginalComponent ) => ( props ) => { + const userRef = useRef( currentUser ); + + // Use currentUser to hydrate calls to @wordpress/core-data's getCurrentUser(). ++ // @ts-expect-error // @ts-expect-error registry is not defined in the wp.data typings + useSelect( ( select, registry ) => { + if ( ! userRef.current ) { + return; + } + +- const { isResolving, hasFinishedResolution } = select( +- STORE_NAME +- ); ++ const { isResolving, hasFinishedResolution } = ++ select( STORE_NAME ); + const { + startResolution, + finishResolution, +diff --git a/packages/js/data/src/utils.ts b/packages/js/data/src/utils.ts +index 8fbc6cf56f..cd345c5109 100644 +--- a/packages/js/data/src/utils.ts ++++ b/packages/js/data/src/utils.ts +@@ -47,12 +47,11 @@ export function* request< Query extends BaseQueryParams, DataType >( + const url: string = addQueryArgs( namespace, query ); + const isUnboundedRequest = query.per_page === -1; + const fetch = isUnboundedRequest ? apiFetch : fetchWithHeaders; +- const response: +- | DataType[] +- | ( { data: DataType[] } & Response ) = yield fetch( { +- path: url, +- method: 'GET', +- } ); ++ const response: DataType[] | ( { data: DataType[] } & Response ) = ++ yield fetch( { ++ path: url, ++ method: 'GET', ++ } ); + + if ( isUnboundedRequest && ! ( 'data' in response ) ) { + return { items: response, totalCount: response.length }; +diff --git a/packages/js/data/typings/index.d.ts b/packages/js/data/typings/index.d.ts +index afa620c3bb..2afe19f015 100644 +--- a/packages/js/data/typings/index.d.ts ++++ b/packages/js/data/typings/index.d.ts +@@ -1,13 +1,16 @@ +-import { select, resolveSelect, dispatch } from '@wordpress/data'; ++/** ++ * External dependencies ++ */ + import { Entity } from '@wordpress/core-data'; ++import * as controls from '@wordpress/data-controls'; + + + declare module '@wordpress/data' { + // TODO: update @wordpress/data types to include this. + const controls: { +- select: select; +- resolveSelect: resolveSelect; +- dispatch: dispatch; ++ select: typeof controls.select; ++ resolveSelect: typeof controls.resolveSelect; ++ dispatch: typeof controls.dispatch; + }; + } + +diff --git a/packages/js/date/CHANGELOG.md b/packages/js/date/CHANGELOG.md +index 377c9b47da..0a1d29f1a2 100644 +--- a/packages/js/date/CHANGELOG.md ++++ b/packages/js/date/CHANGELOG.md +@@ -1,9 +1,15 @@ +-## [4.1.0](https://www.npmjs.com/package/@woocommerce/date/v/4.1.0) - 2022-06-14 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [4.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/date/v/4.2.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [4.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/date/v/4.1.0) - 2022-06-14 + + - Minor - Add Jetpack Changelogger + - Patch - Migrate @woocommerce/date to TS + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/date/CHANGELOG.md). +diff --git a/packages/js/date/composer.json b/packages/js/date/composer.json +index 45b94f39dc..ba72efa1dc 100644 +--- a/packages/js/date/composer.json ++++ b/packages/js/date/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/date/composer.lock b/packages/js/date/composer.lock +index f73f24296f..762fa0eaa3 100644 +--- a/packages/js/date/composer.lock ++++ b/packages/js/date/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "e785fad0bf057c42f0470efed08095de", ++ "content-hash": "c2e5c404a4fee4f6a5892f989459c502", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/date/package.json b/packages/js/date/package.json +index 95332d773e..04cae8d409 100644 +--- a/packages/js/date/package.json ++++ b/packages/js/date/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/date", +- "version": "4.1.0", ++ "version": "4.2.0", + "description": "WooCommerce date utilities.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -36,6 +36,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +@@ -47,15 +48,15 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { +diff --git a/packages/js/date/src/index.ts b/packages/js/date/src/index.ts +index f7d8b25a26..c0e0ac8c06 100644 +--- a/packages/js/date/src/index.ts ++++ b/packages/js/date/src/index.ts +@@ -557,12 +557,8 @@ export const getCurrentDates = ( + throw Error( 'Invalid date range' ); + } + +- const { +- primaryStart, +- primaryEnd, +- secondaryStart, +- secondaryEnd, +- } = dateValue; ++ const { primaryStart, primaryEnd, secondaryStart, secondaryEnd } = ++ dateValue; + + return getCurrentDatesMemoized( + period, +diff --git a/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md +index 500fae1798..f8f591dd58 100644 +--- a/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md ++++ b/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md +@@ -1,9 +1,15 @@ +-## [2.1.0](https://www.npmjs.com/package/@woocommerce/dependency-extraction-webpack-plugin/v/2.1.0) - 2022-06-14 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [2.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/dependency-extraction-webpack-plugin/v/2.2.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [2.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/dependency-extraction-webpack-plugin/v/2.1.0) - 2022-06-14 + + - Patch - Add '@woocommerce/extend-cart-checkout-block' to list of packages +-- Minor - Add Jetpack Changelogger + - Patch - Standardize lint scripts: add lint:fix +- +---- ++- Minor - Add Jetpack Changelogger + + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md). +diff --git a/packages/js/dependency-extraction-webpack-plugin/composer.json b/packages/js/dependency-extraction-webpack-plugin/composer.json +index 822e7d6977..e093fbc6bc 100644 +--- a/packages/js/dependency-extraction-webpack-plugin/composer.json ++++ b/packages/js/dependency-extraction-webpack-plugin/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/dependency-extraction-webpack-plugin/composer.lock b/packages/js/dependency-extraction-webpack-plugin/composer.lock +index 22f31bed40..c03a208a91 100644 +--- a/packages/js/dependency-extraction-webpack-plugin/composer.lock ++++ b/packages/js/dependency-extraction-webpack-plugin/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "8046ae27794628dc50a7b04eb1fda2fc", ++ "content-hash": "024163a2e226f019b11933129ddfd115", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/dependency-extraction-webpack-plugin/package.json b/packages/js/dependency-extraction-webpack-plugin/package.json +index 2a8c83e47e..e526b85fd3 100644 +--- a/packages/js/dependency-extraction-webpack-plugin/package.json ++++ b/packages/js/dependency-extraction-webpack-plugin/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/dependency-extraction-webpack-plugin", +- "version": "2.1.0", ++ "version": "2.2.0", + "description": "WooCommerce Dependency Extraction Webpack Plugin", + "author": "Automattic", + "license": "GPL-2.0-or-later", +@@ -29,6 +29,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2", +@@ -36,9 +37,9 @@ + "webpack-cli": "^3.3.12" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix" + }, + "lint-staged": { +diff --git a/packages/js/e2e-core-tests/composer.lock b/packages/js/e2e-core-tests/composer.lock +deleted file mode 100644 +index 7e7499aca3..0000000000 +--- a/packages/js/e2e-core-tests/composer.lock ++++ /dev/null +@@ -1,1021 +0,0 @@ +-{ +- "_readme": [ +- "This file locks the dependencies of your project to a known state", +- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", +- "This file is @generated automatically" +- ], +- "content-hash": "25ff60cdb096034e20fe82f606dfd33b", +- "packages": [], +- "packages-dev": [ +- { +- "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", +- "source": { +- "type": "git", +- "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "shasum": "" +- }, +- "require": { +- "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" +- }, +- "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" +- }, +- "bin": [ +- "bin/changelogger" +- ], +- "type": "project", +- "extra": { +- "autotagger": true, +- "branch-alias": { +- "dev-master": "3.0.x-dev" +- }, +- "mirror-repo": "Automattic/jetpack-changelogger", +- "version-constants": { +- "::VERSION": "src/Application.php" +- }, +- "changelogger": { +- "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" +- } +- }, +- "autoload": { +- "psr-4": { +- "Automattic\\Jetpack\\Changelogger\\": "src", +- "Automattic\\Jetpack\\Changelog\\": "lib" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", +- "time": "2021-11-02T14:06:49+00:00" +- }, +- { +- "name": "psr/container", +- "version": "1.1.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/php-fig/container.git", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Psr\\Container\\": "src/" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "PHP-FIG", +- "homepage": "https://www.php-fig.org/" +- } +- ], +- "description": "Common Container Interface (PHP FIG PSR-11)", +- "homepage": "https://github.com/php-fig/container", +- "keywords": [ +- "PSR-11", +- "container", +- "container-interface", +- "container-interop", +- "psr" +- ], +- "time": "2021-03-05T17:36:06+00:00" +- }, +- { +- "name": "symfony/console", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/console.git", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/console/zipball/bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/deprecation-contracts": "^2.1", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php73": "^1.8", +- "symfony/polyfill-php80": "^1.16", +- "symfony/service-contracts": "^1.1|^2", +- "symfony/string": "^5.1|^6.0" +- }, +- "conflict": { +- "psr/log": ">=3", +- "symfony/dependency-injection": "<4.4", +- "symfony/dotenv": "<5.1", +- "symfony/event-dispatcher": "<4.4", +- "symfony/lock": "<4.4", +- "symfony/process": "<4.4" +- }, +- "provide": { +- "psr/log-implementation": "1.0|2.0" +- }, +- "require-dev": { +- "psr/log": "^1|^2", +- "symfony/config": "^4.4|^5.0|^6.0", +- "symfony/dependency-injection": "^4.4|^5.0|^6.0", +- "symfony/event-dispatcher": "^4.4|^5.0|^6.0", +- "symfony/lock": "^4.4|^5.0|^6.0", +- "symfony/process": "^4.4|^5.0|^6.0", +- "symfony/var-dumper": "^4.4|^5.0|^6.0" +- }, +- "suggest": { +- "psr/log": "For using the console logger", +- "symfony/event-dispatcher": "", +- "symfony/lock": "", +- "symfony/process": "" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Console\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Eases the creation of beautiful and testable command line interfaces", +- "homepage": "https://symfony.com", +- "keywords": [ +- "cli", +- "command line", +- "console", +- "terminal" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/deprecation-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/deprecation-contracts.git", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "files": [ +- "function.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "A generic function and convention to trigger deprecation notices", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-12T14:48:14+00:00" +- }, +- { +- "name": "symfony/polyfill-ctype", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-ctype.git", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-ctype": "*" +- }, +- "suggest": { +- "ext-ctype": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Ctype\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Gert de Pagter", +- "email": "BackEndTea@gmail.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for ctype functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "ctype", +- "polyfill", +- "portable" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-grapheme", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-grapheme.git", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Grapheme\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's grapheme_* functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "grapheme", +- "intl", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-26T17:12:59+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-normalizer", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-normalizer.git", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Normalizer\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's Normalizer class and related functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "intl", +- "normalizer", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-02-19T12:13:01+00:00" +- }, +- { +- "name": "symfony/polyfill-mbstring", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-mbstring.git", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/11b9acb5e8619aef6455735debf77dde8825795c", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-mbstring": "*" +- }, +- "suggest": { +- "ext-mbstring": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Mbstring\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for the Mbstring extension", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "mbstring", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-php73", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php73.git", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php73\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-06-05T21:20:04+00:00" +- }, +- { +- "name": "symfony/polyfill-php80", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php80.git", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php80\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Ion Bazan", +- "email": "ion.bazan@gmail.com" +- }, +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-09-13T13:58:33+00:00" +- }, +- { +- "name": "symfony/process", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/process.git", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/process/zipball/cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-php80": "^1.16" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Process\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Executes commands in sub-processes", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/service-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/service-contracts.git", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "psr/container": "^1.1" +- }, +- "conflict": { +- "ext-psr": "<1.1|>=2" +- }, +- "suggest": { +- "symfony/service-implementation": "" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Contracts\\Service\\": "" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Generic abstractions related to writing services", +- "homepage": "https://symfony.com", +- "keywords": [ +- "abstractions", +- "contracts", +- "decoupling", +- "interfaces", +- "interoperability", +- "standards" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-13T09:35:11+00:00" +- }, +- { +- "name": "symfony/string", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/string.git", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/string/zipball/dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-ctype": "~1.8", +- "symfony/polyfill-intl-grapheme": "~1.0", +- "symfony/polyfill-intl-normalizer": "~1.0", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php80": "~1.15" +- }, +- "require-dev": { +- "symfony/error-handler": "^4.4|^5.0|^6.0", +- "symfony/http-client": "^4.4|^5.0|^6.0", +- "symfony/translation-contracts": "^1.1|^2", +- "symfony/var-exporter": "^4.4|^5.0|^6.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\String\\": "" +- }, +- "files": [ +- "Resources/functions.php" +- ], +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", +- "homepage": "https://symfony.com", +- "keywords": [ +- "grapheme", +- "i18n", +- "string", +- "unicode", +- "utf-8", +- "utf8" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-28T19:23:26+00:00" +- }, +- { +- "name": "wikimedia/at-ease", +- "version": "v2.1.0", +- "source": { +- "type": "git", +- "url": "https://github.com/wikimedia/at-ease.git", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.9" +- }, +- "require-dev": { +- "mediawiki/mediawiki-codesniffer": "35.0.0", +- "mediawiki/minus-x": "1.1.1", +- "ockcyp/covers-validator": "1.3.3", +- "php-parallel-lint/php-console-highlighter": "0.5.0", +- "php-parallel-lint/php-parallel-lint": "1.2.0", +- "phpunit/phpunit": "^8.5" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" +- }, +- "files": [ +- "src/Wikimedia/Functions.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "authors": [ +- { +- "name": "Tim Starling", +- "email": "tstarling@wikimedia.org" +- }, +- { +- "name": "MediaWiki developers", +- "email": "wikitech-l@lists.wikimedia.org" +- } +- ], +- "description": "Safe replacement to @ for suppressing warnings.", +- "homepage": "https://www.mediawiki.org/wiki/at-ease", +- "time": "2021-02-27T15:53:37+00:00" +- } +- ], +- "aliases": [], +- "minimum-stability": "dev", +- "stability-flags": [], +- "prefer-stable": false, +- "prefer-lowest": false, +- "platform": [], +- "platform-dev": [], +- "plugin-api-version": "1.1.0" +-} +diff --git a/packages/js/e2e-environment/composer.lock b/packages/js/e2e-environment/composer.lock +deleted file mode 100644 +index 0907ad54d2..0000000000 +--- a/packages/js/e2e-environment/composer.lock ++++ /dev/null +@@ -1,1021 +0,0 @@ +-{ +- "_readme": [ +- "This file locks the dependencies of your project to a known state", +- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", +- "This file is @generated automatically" +- ], +- "content-hash": "b8b1db72d3888837553c6d782593ee3a", +- "packages": [], +- "packages-dev": [ +- { +- "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", +- "source": { +- "type": "git", +- "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "shasum": "" +- }, +- "require": { +- "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" +- }, +- "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" +- }, +- "bin": [ +- "bin/changelogger" +- ], +- "type": "project", +- "extra": { +- "autotagger": true, +- "branch-alias": { +- "dev-master": "3.0.x-dev" +- }, +- "mirror-repo": "Automattic/jetpack-changelogger", +- "version-constants": { +- "::VERSION": "src/Application.php" +- }, +- "changelogger": { +- "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" +- } +- }, +- "autoload": { +- "psr-4": { +- "Automattic\\Jetpack\\Changelogger\\": "src", +- "Automattic\\Jetpack\\Changelog\\": "lib" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", +- "time": "2021-11-02T14:06:49+00:00" +- }, +- { +- "name": "psr/container", +- "version": "1.1.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/php-fig/container.git", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Psr\\Container\\": "src/" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "PHP-FIG", +- "homepage": "https://www.php-fig.org/" +- } +- ], +- "description": "Common Container Interface (PHP FIG PSR-11)", +- "homepage": "https://github.com/php-fig/container", +- "keywords": [ +- "PSR-11", +- "container", +- "container-interface", +- "container-interop", +- "psr" +- ], +- "time": "2021-03-05T17:36:06+00:00" +- }, +- { +- "name": "symfony/console", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/console.git", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/console/zipball/bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/deprecation-contracts": "^2.1", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php73": "^1.8", +- "symfony/polyfill-php80": "^1.16", +- "symfony/service-contracts": "^1.1|^2", +- "symfony/string": "^5.1|^6.0" +- }, +- "conflict": { +- "psr/log": ">=3", +- "symfony/dependency-injection": "<4.4", +- "symfony/dotenv": "<5.1", +- "symfony/event-dispatcher": "<4.4", +- "symfony/lock": "<4.4", +- "symfony/process": "<4.4" +- }, +- "provide": { +- "psr/log-implementation": "1.0|2.0" +- }, +- "require-dev": { +- "psr/log": "^1|^2", +- "symfony/config": "^4.4|^5.0|^6.0", +- "symfony/dependency-injection": "^4.4|^5.0|^6.0", +- "symfony/event-dispatcher": "^4.4|^5.0|^6.0", +- "symfony/lock": "^4.4|^5.0|^6.0", +- "symfony/process": "^4.4|^5.0|^6.0", +- "symfony/var-dumper": "^4.4|^5.0|^6.0" +- }, +- "suggest": { +- "psr/log": "For using the console logger", +- "symfony/event-dispatcher": "", +- "symfony/lock": "", +- "symfony/process": "" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Console\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Eases the creation of beautiful and testable command line interfaces", +- "homepage": "https://symfony.com", +- "keywords": [ +- "cli", +- "command line", +- "console", +- "terminal" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/deprecation-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/deprecation-contracts.git", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "files": [ +- "function.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "A generic function and convention to trigger deprecation notices", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-12T14:48:14+00:00" +- }, +- { +- "name": "symfony/polyfill-ctype", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-ctype.git", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-ctype": "*" +- }, +- "suggest": { +- "ext-ctype": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Ctype\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Gert de Pagter", +- "email": "BackEndTea@gmail.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for ctype functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "ctype", +- "polyfill", +- "portable" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-grapheme", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-grapheme.git", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Grapheme\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's grapheme_* functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "grapheme", +- "intl", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-26T17:12:59+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-normalizer", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-normalizer.git", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Normalizer\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's Normalizer class and related functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "intl", +- "normalizer", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-02-19T12:13:01+00:00" +- }, +- { +- "name": "symfony/polyfill-mbstring", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-mbstring.git", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/11b9acb5e8619aef6455735debf77dde8825795c", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-mbstring": "*" +- }, +- "suggest": { +- "ext-mbstring": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Mbstring\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for the Mbstring extension", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "mbstring", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-php73", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php73.git", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php73\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-06-05T21:20:04+00:00" +- }, +- { +- "name": "symfony/polyfill-php80", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php80.git", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php80\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Ion Bazan", +- "email": "ion.bazan@gmail.com" +- }, +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-09-13T13:58:33+00:00" +- }, +- { +- "name": "symfony/process", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/process.git", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/process/zipball/cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-php80": "^1.16" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Process\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Executes commands in sub-processes", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/service-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/service-contracts.git", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "psr/container": "^1.1" +- }, +- "conflict": { +- "ext-psr": "<1.1|>=2" +- }, +- "suggest": { +- "symfony/service-implementation": "" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Contracts\\Service\\": "" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Generic abstractions related to writing services", +- "homepage": "https://symfony.com", +- "keywords": [ +- "abstractions", +- "contracts", +- "decoupling", +- "interfaces", +- "interoperability", +- "standards" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-13T09:35:11+00:00" +- }, +- { +- "name": "symfony/string", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/string.git", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/string/zipball/dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-ctype": "~1.8", +- "symfony/polyfill-intl-grapheme": "~1.0", +- "symfony/polyfill-intl-normalizer": "~1.0", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php80": "~1.15" +- }, +- "require-dev": { +- "symfony/error-handler": "^4.4|^5.0|^6.0", +- "symfony/http-client": "^4.4|^5.0|^6.0", +- "symfony/translation-contracts": "^1.1|^2", +- "symfony/var-exporter": "^4.4|^5.0|^6.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\String\\": "" +- }, +- "files": [ +- "Resources/functions.php" +- ], +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", +- "homepage": "https://symfony.com", +- "keywords": [ +- "grapheme", +- "i18n", +- "string", +- "unicode", +- "utf-8", +- "utf8" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-28T19:23:26+00:00" +- }, +- { +- "name": "wikimedia/at-ease", +- "version": "v2.1.0", +- "source": { +- "type": "git", +- "url": "https://github.com/wikimedia/at-ease.git", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.9" +- }, +- "require-dev": { +- "mediawiki/mediawiki-codesniffer": "35.0.0", +- "mediawiki/minus-x": "1.1.1", +- "ockcyp/covers-validator": "1.3.3", +- "php-parallel-lint/php-console-highlighter": "0.5.0", +- "php-parallel-lint/php-parallel-lint": "1.2.0", +- "phpunit/phpunit": "^8.5" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" +- }, +- "files": [ +- "src/Wikimedia/Functions.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "authors": [ +- { +- "name": "Tim Starling", +- "email": "tstarling@wikimedia.org" +- }, +- { +- "name": "MediaWiki developers", +- "email": "wikitech-l@lists.wikimedia.org" +- } +- ], +- "description": "Safe replacement to @ for suppressing warnings.", +- "homepage": "https://www.mediawiki.org/wiki/at-ease", +- "time": "2021-02-27T15:53:37+00:00" +- } +- ], +- "aliases": [], +- "minimum-stability": "dev", +- "stability-flags": [], +- "prefer-stable": false, +- "prefer-lowest": false, +- "platform": [], +- "platform-dev": [], +- "plugin-api-version": "1.1.0" +-} +diff --git a/packages/js/e2e-utils/changelog/bump-wordpress-e2e-test-utils b/packages/js/e2e-utils/changelog/bump-wordpress-e2e-test-utils +new file mode 100644 +index 0000000000..2a51ec593e +--- /dev/null ++++ b/packages/js/e2e-utils/changelog/bump-wordpress-e2e-test-utils +@@ -0,0 +1,4 @@ ++Significance: patch ++Type: dev ++ ++Update `@wordpress/e2e-test-utils` to `wp-5.8: 5.3.2` +diff --git a/packages/js/e2e-utils/composer.lock b/packages/js/e2e-utils/composer.lock +deleted file mode 100644 +index a5d2be9ba8..0000000000 +--- a/packages/js/e2e-utils/composer.lock ++++ /dev/null +@@ -1,1021 +0,0 @@ +-{ +- "_readme": [ +- "This file locks the dependencies of your project to a known state", +- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", +- "This file is @generated automatically" +- ], +- "content-hash": "0ca2417cb40cd24e2e3ea7a86be839f8", +- "packages": [], +- "packages-dev": [ +- { +- "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", +- "source": { +- "type": "git", +- "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "shasum": "" +- }, +- "require": { +- "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" +- }, +- "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" +- }, +- "bin": [ +- "bin/changelogger" +- ], +- "type": "project", +- "extra": { +- "autotagger": true, +- "branch-alias": { +- "dev-master": "3.0.x-dev" +- }, +- "mirror-repo": "Automattic/jetpack-changelogger", +- "version-constants": { +- "::VERSION": "src/Application.php" +- }, +- "changelogger": { +- "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" +- } +- }, +- "autoload": { +- "psr-4": { +- "Automattic\\Jetpack\\Changelogger\\": "src", +- "Automattic\\Jetpack\\Changelog\\": "lib" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", +- "time": "2021-11-02T14:06:49+00:00" +- }, +- { +- "name": "psr/container", +- "version": "1.1.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/php-fig/container.git", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", +- "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Psr\\Container\\": "src/" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "PHP-FIG", +- "homepage": "https://www.php-fig.org/" +- } +- ], +- "description": "Common Container Interface (PHP FIG PSR-11)", +- "homepage": "https://github.com/php-fig/container", +- "keywords": [ +- "PSR-11", +- "container", +- "container-interface", +- "container-interop", +- "psr" +- ], +- "time": "2021-03-05T17:36:06+00:00" +- }, +- { +- "name": "symfony/console", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/console.git", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/console/zipball/bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "reference": "bea7632e3b1d12decedba0a7fe7a7e0ebf7ee2f4", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/deprecation-contracts": "^2.1", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php73": "^1.8", +- "symfony/polyfill-php80": "^1.16", +- "symfony/service-contracts": "^1.1|^2", +- "symfony/string": "^5.1|^6.0" +- }, +- "conflict": { +- "psr/log": ">=3", +- "symfony/dependency-injection": "<4.4", +- "symfony/dotenv": "<5.1", +- "symfony/event-dispatcher": "<4.4", +- "symfony/lock": "<4.4", +- "symfony/process": "<4.4" +- }, +- "provide": { +- "psr/log-implementation": "1.0|2.0" +- }, +- "require-dev": { +- "psr/log": "^1|^2", +- "symfony/config": "^4.4|^5.0|^6.0", +- "symfony/dependency-injection": "^4.4|^5.0|^6.0", +- "symfony/event-dispatcher": "^4.4|^5.0|^6.0", +- "symfony/lock": "^4.4|^5.0|^6.0", +- "symfony/process": "^4.4|^5.0|^6.0", +- "symfony/var-dumper": "^4.4|^5.0|^6.0" +- }, +- "suggest": { +- "psr/log": "For using the console logger", +- "symfony/event-dispatcher": "", +- "symfony/lock": "", +- "symfony/process": "" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Console\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Eases the creation of beautiful and testable command line interfaces", +- "homepage": "https://symfony.com", +- "keywords": [ +- "cli", +- "command line", +- "console", +- "terminal" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/deprecation-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/deprecation-contracts.git", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "files": [ +- "function.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "A generic function and convention to trigger deprecation notices", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-12T14:48:14+00:00" +- }, +- { +- "name": "symfony/polyfill-ctype", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-ctype.git", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-ctype": "*" +- }, +- "suggest": { +- "ext-ctype": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Ctype\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Gert de Pagter", +- "email": "BackEndTea@gmail.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for ctype functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "ctype", +- "polyfill", +- "portable" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-grapheme", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-grapheme.git", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "reference": "5911fe42c266a5917aef12e45fbd3a640a9e3b18", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Grapheme\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's grapheme_* functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "grapheme", +- "intl", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-26T17:12:59+00:00" +- }, +- { +- "name": "symfony/polyfill-intl-normalizer", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-intl-normalizer.git", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "suggest": { +- "ext-intl": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Intl\\Normalizer\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for intl's Normalizer class and related functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "intl", +- "normalizer", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-02-19T12:13:01+00:00" +- }, +- { +- "name": "symfony/polyfill-mbstring", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-mbstring.git", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/11b9acb5e8619aef6455735debf77dde8825795c", +- "reference": "11b9acb5e8619aef6455735debf77dde8825795c", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-mbstring": "*" +- }, +- "suggest": { +- "ext-mbstring": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Mbstring\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for the Mbstring extension", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "mbstring", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, +- { +- "name": "symfony/polyfill-php73", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php73.git", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php73\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-06-05T21:20:04+00:00" +- }, +- { +- "name": "symfony/polyfill-php80", +- "version": "dev-main", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-php80.git", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", +- "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Polyfill\\Php80\\": "" +- }, +- "files": [ +- "bootstrap.php" +- ], +- "classmap": [ +- "Resources/stubs" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Ion Bazan", +- "email": "ion.bazan@gmail.com" +- }, +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "polyfill", +- "portable", +- "shim" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-09-13T13:58:33+00:00" +- }, +- { +- "name": "symfony/process", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/process.git", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/process/zipball/cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "reference": "cbdd4cdf3fc834638c13f3ba26c2ce657a3987ec", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-php80": "^1.16" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\Process\\": "" +- }, +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Fabien Potencier", +- "email": "fabien@symfony.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Executes commands in sub-processes", +- "homepage": "https://symfony.com", +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-11-04T16:48:04+00:00" +- }, +- { +- "name": "symfony/service-contracts", +- "version": "2.5.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/service-contracts.git", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "reference": "56b990c18120c91eaf0d38a93fabfa2a1f7fa413", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "psr/container": "^1.1" +- }, +- "conflict": { +- "ext-psr": "<1.1|>=2" +- }, +- "suggest": { +- "symfony/service-implementation": "" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "2.5-dev" +- }, +- "thanks": { +- "name": "symfony/contracts", +- "url": "https://github.com/symfony/contracts" +- } +- }, +- "autoload": { +- "psr-4": { +- "Symfony\\Contracts\\Service\\": "" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Generic abstractions related to writing services", +- "homepage": "https://symfony.com", +- "keywords": [ +- "abstractions", +- "contracts", +- "decoupling", +- "interfaces", +- "interoperability", +- "standards" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-07-13T09:35:11+00:00" +- }, +- { +- "name": "symfony/string", +- "version": "5.4.x-dev", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/string.git", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/string/zipball/dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "reference": "dad92b16d84cb661f39c85a5dbb6e4792b92e90f", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.5", +- "symfony/polyfill-ctype": "~1.8", +- "symfony/polyfill-intl-grapheme": "~1.0", +- "symfony/polyfill-intl-normalizer": "~1.0", +- "symfony/polyfill-mbstring": "~1.0", +- "symfony/polyfill-php80": "~1.15" +- }, +- "require-dev": { +- "symfony/error-handler": "^4.4|^5.0|^6.0", +- "symfony/http-client": "^4.4|^5.0|^6.0", +- "symfony/translation-contracts": "^1.1|^2", +- "symfony/var-exporter": "^4.4|^5.0|^6.0" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Symfony\\Component\\String\\": "" +- }, +- "files": [ +- "Resources/functions.php" +- ], +- "exclude-from-classmap": [ +- "/Tests/" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Nicolas Grekas", +- "email": "p@tchwork.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", +- "homepage": "https://symfony.com", +- "keywords": [ +- "grapheme", +- "i18n", +- "string", +- "unicode", +- "utf-8", +- "utf8" +- ], +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-28T19:23:26+00:00" +- }, +- { +- "name": "wikimedia/at-ease", +- "version": "v2.1.0", +- "source": { +- "type": "git", +- "url": "https://github.com/wikimedia/at-ease.git", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.2.9" +- }, +- "require-dev": { +- "mediawiki/mediawiki-codesniffer": "35.0.0", +- "mediawiki/minus-x": "1.1.1", +- "ockcyp/covers-validator": "1.3.3", +- "php-parallel-lint/php-console-highlighter": "0.5.0", +- "php-parallel-lint/php-parallel-lint": "1.2.0", +- "phpunit/phpunit": "^8.5" +- }, +- "type": "library", +- "autoload": { +- "psr-4": { +- "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" +- }, +- "files": [ +- "src/Wikimedia/Functions.php" +- ] +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "GPL-2.0-or-later" +- ], +- "authors": [ +- { +- "name": "Tim Starling", +- "email": "tstarling@wikimedia.org" +- }, +- { +- "name": "MediaWiki developers", +- "email": "wikitech-l@lists.wikimedia.org" +- } +- ], +- "description": "Safe replacement to @ for suppressing warnings.", +- "homepage": "https://www.mediawiki.org/wiki/at-ease", +- "time": "2021-02-27T15:53:37+00:00" +- } +- ], +- "aliases": [], +- "minimum-stability": "dev", +- "stability-flags": [], +- "prefer-stable": false, +- "prefer-lowest": false, +- "platform": [], +- "platform-dev": [], +- "plugin-api-version": "1.1.0" +-} +diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json +index 42fd60e00d..1c92d19d5f 100644 +--- a/packages/js/e2e-utils/package.json ++++ b/packages/js/e2e-utils/package.json +@@ -13,7 +13,7 @@ + "dependencies": { + "@automattic/puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50", + "@wordpress/deprecated": "^3.2.3", +- "@wordpress/e2e-test-utils": "^4.16.1", ++ "@wordpress/e2e-test-utils": "wp-5.8", + "config": "3.3.3", + "fishery": "^1.2.0" + }, +diff --git a/packages/js/eslint-plugin/CHANGELOG.md b/packages/js/eslint-plugin/CHANGELOG.md +index 9ca5ec429e..36e0b56cc5 100644 +--- a/packages/js/eslint-plugin/CHANGELOG.md ++++ b/packages/js/eslint-plugin/CHANGELOG.md +@@ -1,8 +1,15 @@ +-## [2.1.0](https://www.npmjs.com/package/@woocommerce/eslint-plugin/v/2.1.0) - 2022-06-14 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [2.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/eslint-plugin/v/2.2.0) - 2022-07-08 ++ ++- Minor - Allow unused destructured variables in lint rules #35548 ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [2.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/eslint-plugin/v/2.1.0) - 2022-06-14 + + - Minor - Add Jetpack Changelogger + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/eslint-plugin/CHANGELOG.md). +diff --git a/packages/js/eslint-plugin/changelog/dev-update-eslint-config b/packages/js/eslint-plugin/changelog/dev-update-eslint-config +new file mode 100644 +index 0000000000..f5d9350502 +--- /dev/null ++++ b/packages/js/eslint-plugin/changelog/dev-update-eslint-config +@@ -0,0 +1,4 @@ ++Significance: minor ++Type: update ++ ++Update i18n-text-domain rule to only allow woocommerce text domain +diff --git a/packages/js/eslint-plugin/composer.json b/packages/js/eslint-plugin/composer.json +index a78db409b6..8d62a41f97 100644 +--- a/packages/js/eslint-plugin/composer.json ++++ b/packages/js/eslint-plugin/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/eslint-plugin/composer.lock b/packages/js/eslint-plugin/composer.lock +index b8c4fc2c82..25a718f685 100644 +--- a/packages/js/eslint-plugin/composer.lock ++++ b/packages/js/eslint-plugin/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "4a85f8878e384320f16de1e619e0a14f", ++ "content-hash": "cfc0b63277f38526f4ff5300cfa22eca", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/eslint-plugin/configs/recommended.js b/packages/js/eslint-plugin/configs/recommended.js +index acadbb1a92..69f06935ed 100644 +--- a/packages/js/eslint-plugin/configs/recommended.js ++++ b/packages/js/eslint-plugin/configs/recommended.js +@@ -16,6 +16,12 @@ module.exports = { + yoda: [ 'error', 'never' ], + // temporary conversion to warnings until the below are all handled. + '@wordpress/i18n-translator-comments': 'warn', ++ '@wordpress/i18n-text-domain': [ ++ 'error', ++ { ++ allowedTextDomain: 'woocommerce', ++ }, ++ ], + '@wordpress/valid-sprintf': 'warn', + '@wordpress/no-unsafe-wp-apis': 'warn', + '@wordpress/no-global-active-element': 'warn', +@@ -35,10 +41,11 @@ module.exports = { + ], + }, + ], +- 'no-unused-vars': [ ++ '@typescript-eslint/no-unused-vars': [ + 'error', + { + varsIgnorePattern: 'createElement', ++ ignoreRestSiblings: true, + }, + ], + 'react/react-in-jsx-scope': 'error', +diff --git a/packages/js/eslint-plugin/package.json b/packages/js/eslint-plugin/package.json +index 2dded10ed7..7faed5c5bb 100644 +--- a/packages/js/eslint-plugin/package.json ++++ b/packages/js/eslint-plugin/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/eslint-plugin", +- "version": "2.1.0", ++ "version": "2.2.0", + "description": "ESLint plugin for WooCommerce development.", + "author": "Automattic", + "license": "GPL-2.0-or-later", +@@ -36,9 +36,9 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", +- "lint": "eslint ./rules ./configs", ++ "lint": "node ./node_modules/require-turbo && eslint ./rules ./configs", + "lint:fix": "eslint ./rules ./configs --fix" + }, + "devDependencies": { +@@ -46,6 +46,7 @@ + "eslint": "^8.11.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/experimental/CHANGELOG.md b/packages/js/experimental/CHANGELOG.md +index 92089969af..50de636bc6 100644 +--- a/packages/js/experimental/CHANGELOG.md ++++ b/packages/js/experimental/CHANGELOG.md +@@ -1,4 +1,12 @@ +-## [3.1.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.1.0) - 2022-06-14 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [3.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/experimental/v/3.2.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [3.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/experimental/v/3.1.0) - 2022-06-14 + + - Minor - Add Jetpack Changelogger + - Minor - Update TaskItem props type definition. +@@ -7,6 +15,4 @@ + - Minor - Added Typescript type declarations. #32615 + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/experimental/CHANGELOG.md). +diff --git a/packages/js/experimental/composer.json b/packages/js/experimental/composer.json +index f3196db2f2..e3476415ee 100644 +--- a/packages/js/experimental/composer.json ++++ b/packages/js/experimental/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/experimental/composer.lock b/packages/js/experimental/composer.lock +index 65e1c603fc..c96ea1e492 100644 +--- a/packages/js/experimental/composer.lock ++++ b/packages/js/experimental/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "50d3584ba5b768bcd92e9111061b02ce", ++ "content-hash": "8de7a23a39e8b1299465d2c26a261cf7", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json +index 9473aafe90..65fa856783 100644 +--- a/packages/js/experimental/package.json ++++ b/packages/js/experimental/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/experimental", +- "version": "3.1.0", ++ "version": "3.2.0", + "description": "WooCommerce experimental components.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -62,6 +62,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "postcss-loader": "^3.0.0", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "sass-loader": "^10.2.1", + "ts-jest": "^27.1.3", +@@ -74,17 +75,17 @@ + "react-dom": "^17.0.0" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "pnpm run build:js && pnpm run build:css", ++ "build": "node ./node_modules/require-turbo && pnpm run build:js && pnpm run build:css", + "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:css": "webpack", + "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { +diff --git a/packages/js/experimental/src/experimental-list/collapsible-list/index.tsx b/packages/js/experimental/src/experimental-list/collapsible-list/index.tsx +index 6a0ad1cb12..ab36e90d48 100644 +--- a/packages/js/experimental/src/experimental-list/collapsible-list/index.tsx ++++ b/packages/js/experimental/src/experimental-list/collapsible-list/index.tsx +@@ -129,10 +129,8 @@ export const ExperimentalCollapsibleList: React.FC< CollapsibleListProps > = ( { + ...listProps + } ): JSX.Element => { + const [ isCollapsed, setCollapsed ] = useState( collapsed ); +- const [ +- isTransitionComponentCollapsed, +- setTransitionComponentCollapsed, +- ] = useState( collapsed ); ++ const [ isTransitionComponentCollapsed, setTransitionComponentCollapsed ] = ++ useState( collapsed ); + const [ footerLabels, setFooterLabels ] = useState( { + collapse: collapseLabel, + expand: expandLabel, +@@ -277,7 +275,8 @@ export const ExperimentalCollapsibleList: React.FC< CollapsibleListProps > = ( { + classNames="woocommerce-list__item" + > + { cloneElement( child, { +- animation: animationProp, ++ animation: ++ animationProp, + ...remainingProps, + } ) } + +diff --git a/packages/js/experimental/src/inbox-note/inbox-dismiss-confirmation-modal.tsx b/packages/js/experimental/src/inbox-note/inbox-dismiss-confirmation-modal.tsx +index e3933aac98..cacbaf1b60 100644 +--- a/packages/js/experimental/src/inbox-note/inbox-dismiss-confirmation-modal.tsx ++++ b/packages/js/experimental/src/inbox-note/inbox-dismiss-confirmation-modal.tsx +@@ -11,7 +11,9 @@ type ConfirmationModalProps = { + buttonLabel?: string; + }; + +-export const InboxDismissConfirmationModal: React.FC< ConfirmationModalProps > = ( { ++export const InboxDismissConfirmationModal: React.FC< ++ ConfirmationModalProps ++> = ( { + onClose, + onDismiss, + buttonLabel = __( "Yes, I'm sure", 'woocommerce' ), +diff --git a/packages/js/explat/CHANGELOG.md b/packages/js/explat/CHANGELOG.md +index d5155c0094..6749cf7762 100644 +--- a/packages/js/explat/CHANGELOG.md ++++ b/packages/js/explat/CHANGELOG.md +@@ -1,12 +1,19 @@ +-## [2.2.0](https://www.npmjs.com/package/@woocommerce/explat/v/2.2.0) - 2022-06-15 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [2.3.0](https://www.npmjs.com/package/@woocommerce/packages/js/explat/v/2.3.0) - 2022-07-08 ++ ++- Patch - Fix fetchExperimentAssignment response ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [2.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/explat/v/2.2.0) - 2022-06-15 + + - Patch - Added useExperiment example ++- Patch - Standardize lint scripts: add lint:fix + - Minor - Add Jetpack Changelogger + - Minor - Update dependency `@wordpress/hooks` to ^3.5.0 + - Minor - Added Typescript type declarations. #32615 + - Minor - Updated README with useExperiment example +-- Patch - Standardize lint scripts: add lint:fix +- +---- + +-[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/experimental/CHANGELOG.md). ++[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/explat/CHANGELOG.md). +diff --git a/packages/js/explat/changelog/add-33331-wcadmin-install-timestamp-explat-default b/packages/js/explat/changelog/add-33331-wcadmin-install-timestamp-explat-default +new file mode 100644 +index 0000000000..0da7eeee87 +--- /dev/null ++++ b/packages/js/explat/changelog/add-33331-wcadmin-install-timestamp-explat-default +@@ -0,0 +1,4 @@ ++Significance: minor ++Type: add ++ ++Include the woocommerce_admin_install_timestamp option value in the explat assignment query string as woo_wcadmin_install_timestamp. #33574 +diff --git a/packages/js/explat/changelog/fix-explat-fetch-assignment b/packages/js/explat/changelog/fix-explat-fetch-assignment +deleted file mode 100644 +index ba3b48533b..0000000000 +--- a/packages/js/explat/changelog/fix-explat-fetch-assignment ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: patch +-Type: fix +- +-Fix fetchExperimentAssignment response +diff --git a/packages/js/explat/composer.json b/packages/js/explat/composer.json +index 95ce720aa8..c743758573 100644 +--- a/packages/js/explat/composer.json ++++ b/packages/js/explat/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/explat/composer.lock b/packages/js/explat/composer.lock +index 32d09d5ccd..6d46ada4c3 100644 +--- a/packages/js/explat/composer.lock ++++ b/packages/js/explat/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "396588f4b60a92bf5a04029675ad307c", ++ "content-hash": "c62661e12843ad431e9056ecdcfa696b", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json +index f6b8445735..9ffa693eb9 100644 +--- a/packages/js/explat/package.json ++++ b/packages/js/explat/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/explat", +- "version": "2.2.0", ++ "version": "2.3.0", + "description": "WooCommerce component and utils for A/B testing.", + "author": "Automattic", + "license": "GPL-2.0-or-later", +@@ -42,20 +42,21 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { +diff --git a/packages/js/explat/src/assignment.ts b/packages/js/explat/src/assignment.ts +index f05d214960..8aed6c147d 100644 +--- a/packages/js/explat/src/assignment.ts ++++ b/packages/js/explat/src/assignment.ts +@@ -13,6 +13,7 @@ type QueryParams = { + experiment_name: string; + anon_id: string | null; + woo_country_code: string; ++ woo_wcadmin_install_timestamp: string; + }; + + const isValidQueryParams = ( +@@ -20,7 +21,10 @@ const isValidQueryParams = ( + ): queryParams is QueryParams => { + return ( + ( queryParams as QueryParams ).hasOwnProperty( 'experiment_name' ) && +- ( queryParams as QueryParams ).hasOwnProperty( 'woo_country_code' ) ++ ( queryParams as QueryParams ).hasOwnProperty( 'woo_country_code' ) && ++ ( queryParams as QueryParams ).hasOwnProperty( ++ 'woo_wcadmin_install_timestamp' ++ ) + ); + }; + +@@ -52,6 +56,9 @@ const getRequestQueryParams = ( { + ?.woocommerce_default_country || + window.wcSettings?.admin?.preloadSettings?.general + ?.woocommerce_default_country, ++ woo_wcadmin_install_timestamp: ++ window.wcSettings?.admin?.preloadOptions ++ ?.woocommerce_admin_install_timestamp, + } ); + + if ( ! isValidQueryParams( queryParams ) ) { +diff --git a/packages/js/explat/src/index.ts b/packages/js/explat/src/index.ts +index 22a9a1b1db..5aca3f70ab 100644 +--- a/packages/js/explat/src/index.ts ++++ b/packages/js/explat/src/index.ts +@@ -37,16 +37,11 @@ const exPlatClient = createExPlatClient( { + isDevelopmentMode, + } ); + +-export const { +- loadExperimentAssignment, +- dangerouslyGetExperimentAssignment, +-} = exPlatClient; ++export const { loadExperimentAssignment, dangerouslyGetExperimentAssignment } = ++ exPlatClient; + +-export const { +- useExperiment, +- Experiment, +- ProvideExperimentData, +-} = createExPlatClientReactHelpers( exPlatClient ); ++export const { useExperiment, Experiment, ProvideExperimentData } = ++ createExPlatClientReactHelpers( exPlatClient ); + + // Create another auth client that send request to wpcom as auth user. + const exPlatClientWithAuth = createExPlatClient( { +@@ -58,7 +53,8 @@ const exPlatClientWithAuth = createExPlatClient( { + + export const { + loadExperimentAssignment: loadExperimentAssignmentWithAuth, +- dangerouslyGetExperimentAssignment: dangerouslyGetExperimentAssignmentWithAuth, ++ dangerouslyGetExperimentAssignment: ++ dangerouslyGetExperimentAssignmentWithAuth, + } = exPlatClientWithAuth; + + export const { +diff --git a/packages/js/explat/src/test/assignment-test.js b/packages/js/explat/src/test/assignment-test.js +index 73151e6ae9..424039726f 100644 +--- a/packages/js/explat/src/test/assignment-test.js ++++ b/packages/js/explat/src/test/assignment-test.js +@@ -85,6 +85,28 @@ describe( 'fetchExperimentAssignment', () => { + } ); + await expect( assignment ).toEqual( data ); + } ); ++ ++ it( 'adds woo_wcadmin_install_timestamp to request args', () => { ++ const filterArgs = { args: {} }; ++ addFilter( ++ 'woocommerce_explat_request_args', ++ 'woo_wcadmin_install_timestamp_test', ++ function ( args ) { ++ filterArgs.args = args; ++ return args; ++ } ++ ); ++ ++ const fetchPromise = fetchExperimentAssignmentWithAuth( { ++ experimentName: '123', ++ anonId: 'abc', ++ } ); ++ Promise.resolve( fetchPromise ); ++ ++ expect( filterArgs.args ).toHaveProperty( ++ 'woo_wcadmin_install_timestamp' ++ ); ++ } ); + } ); + + describe( 'fetchExperimentAssignmentWithAuth', () => { +@@ -114,4 +136,26 @@ describe( 'fetchExperimentAssignmentWithAuth', () => { + } + ); + } ); ++ ++ it( 'adds woo_wcadmin_install_timestamp to request args', () => { ++ const filterArgs = { args: {} }; ++ addFilter( ++ 'woocommerce_explat_request_args', ++ 'woo_wcadmin_install_timestamp_test', ++ function ( args ) { ++ filterArgs.args = args; ++ return args; ++ } ++ ); ++ ++ const fetchPromise = fetchExperimentAssignmentWithAuth( { ++ experimentName: '123', ++ anonId: 'abc', ++ } ); ++ Promise.resolve( fetchPromise ); ++ ++ expect( filterArgs.args ).toHaveProperty( ++ 'woo_wcadmin_install_timestamp' ++ ); ++ } ); + } ); +diff --git a/packages/js/explat/src/utils.ts b/packages/js/explat/src/utils.ts +index c67735d788..c5f0ccfff1 100644 +--- a/packages/js/explat/src/utils.ts ++++ b/packages/js/explat/src/utils.ts +@@ -10,8 +10,14 @@ interface generalSettings { + interface preloadSettings { + general: generalSettings; + } ++ ++interface preloadOptions { ++ woocommerce_admin_install_timestamp: string; ++} ++ + interface admin { + preloadSettings: preloadSettings; ++ preloadOptions: preloadOptions; + } + + interface wcSettings { +diff --git a/packages/js/extend-cart-checkout-block/changelog/fix-changelogger-phpcs b/packages/js/extend-cart-checkout-block/changelog/fix-changelogger-phpcs +new file mode 100644 +index 0000000000..10fdefc7d2 +--- /dev/null ++++ b/packages/js/extend-cart-checkout-block/changelog/fix-changelogger-phpcs +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: PHPCS violation fixes ++ ++ +diff --git a/packages/js/extend-cart-checkout-block/composer.json b/packages/js/extend-cart-checkout-block/composer.json +index 9d8dd2dbe5..9c44b2a742 100644 +--- a/packages/js/extend-cart-checkout-block/composer.json ++++ b/packages/js/extend-cart-checkout-block/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/extend-cart-checkout-block/composer.lock b/packages/js/extend-cart-checkout-block/composer.lock +index fe3200376a..4885651ee1 100644 +--- a/packages/js/extend-cart-checkout-block/composer.lock ++++ b/packages/js/extend-cart-checkout-block/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "0e9fa5375785055d26669bc4beaf57cd", ++ "content-hash": "6bd29bd29a67b60a2199c7f520eada56", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/internal-e2e-builds/build.js b/packages/js/internal-e2e-builds/build.js +index e0928272e4..999f63fd96 100755 +--- a/packages/js/internal-e2e-builds/build.js ++++ b/packages/js/internal-e2e-builds/build.js +@@ -126,8 +126,10 @@ function buildPackage( packagePath ) { + + let packageName; + try { +- packageName = require( path.resolve( PACKAGE_DIR, 'package.json' ) ) +- .name; ++ packageName = require( path.resolve( ++ PACKAGE_DIR, ++ 'package.json' ++ ) ).name; + } catch ( e ) { + packageName = PACKAGE_DIR.split( path.sep ).pop(); + } +diff --git a/packages/js/internal-e2e-builds/changelog b/packages/js/internal-e2e-builds/changelog +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/internal-e2e-builds/changelog +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/internal-e2e-builds/package.json b/packages/js/internal-e2e-builds/package.json +index eab654445d..2fa6a0a1c1 100644 +--- a/packages/js/internal-e2e-builds/package.json ++++ b/packages/js/internal-e2e-builds/package.json +@@ -17,7 +17,7 @@ + }, + "homepage": "https://github.com/woocommerce/woocommerce#readme", + "scripts": { +- "lint": "eslint build.js", ++ "node ./node_modules/require-turbo && lint": "eslint build.js", + "lint:fix": "eslint build.js --fix" + }, + "devDependencies": { +@@ -27,7 +27,8 @@ + "eslint": "^8.12.0", + "glob": "^7.2.0", + "mkdirp": "^1.0.4", +- "lodash": "^4.17.21" ++ "lodash": "^4.17.21", ++ "require-turbo": "workspace:*" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ +diff --git a/packages/js/internal-js-tests/changelog b/packages/js/internal-js-tests/changelog +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/internal-js-tests/changelog +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/internal-js-tests/package.json b/packages/js/internal-js-tests/package.json +index 7a0c3d89a4..2de6535838 100644 +--- a/packages/js/internal-js-tests/package.json ++++ b/packages/js/internal-js-tests/package.json +@@ -17,11 +17,11 @@ + "main": "build/util/index.js", + "module": "build-module/util/index.js", + "scripts": { +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "ts:check": "tsc --noEmit --project ./tsconfig.json", + "clean": "pnpm exec rimraf *.tsbuildinfo build build-*", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix" + }, + "dependencies": { +@@ -38,6 +38,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/internal-style-build/changelog/add-require-turbo b/packages/js/internal-style-build/changelog/add-require-turbo +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/internal-style-build/changelog/add-require-turbo +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/internal-style-build/package.json b/packages/js/internal-style-build/package.json +index 5c912e8331..86db461884 100644 +--- a/packages/js/internal-style-build/package.json ++++ b/packages/js/internal-style-build/package.json +@@ -29,7 +29,7 @@ + "webpack-rtl-plugin": "^2.0.0" + }, + "scripts": { +- "lint": "eslint index.js", ++ "lint": "node ./node_modules/require-turbo && eslint index.js", + "lint:fix": "eslint index.js --fix" + }, + "private": true, +@@ -39,6 +39,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2", +diff --git a/packages/js/navigation/CHANGELOG.md b/packages/js/navigation/CHANGELOG.md +index 74a32fa9da..4df354fdbe 100644 +--- a/packages/js/navigation/CHANGELOG.md ++++ b/packages/js/navigation/CHANGELOG.md +@@ -1,19 +1,19 @@ +-## [8.0.0](https://www.npmjs.com/package/@woocommerce/navigation/v/8.0.0) - 2022-06-15 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [8.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/navigation/v/8.1.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [8.0.0](https://www.npmjs.com/package/@woocommerce/packages/js/navigation/v/8.0.0) - 2022-06-15 + + - Minor - Add Jetpack Changelogger +-- Patch - Standardize lint scripts: add lint:fix +-- Patch - Update dependency history to ^5.3.0 +-- Major [ **BREAKING CHANGE** ] - Upgraded react-router-dom to v6, which itself causes breaking changes. This upgrade will require consumers to also upgrade their react-router-dom to v6. #33156 + - Minor - Update dependency `@wordpress/hooks` to ^3.5.0 + - Minor - Added Typescript type declarations. #32615 + - Minor - Update dependency `history` to ^5.3.0 +- +- BREAKING CHANGE: +- +- - the returned object from getHistory() has methods changed: goBack() -> back() and goForward() -> forward() +- - the listen() method from the returned object of getHistory() now takes a listener with an object parameter, ({location, action}) instead of (location, action) +- - location.pathname is now validated and makes a warning if it is not a string +- +---- ++- Patch - Standardize lint scripts: add lint:fix ++- Patch - Update dependency history to ^5.3.0 ++- - Upgraded react-router-dom to v6, which itself causes breaking changes. This upgrade will require consumers to also upgrade their react-router-dom to v6. #33156 + + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/navigation/CHANGELOG.md). +diff --git a/packages/js/navigation/composer.json b/packages/js/navigation/composer.json +index 89b3ae9fba..b7f58a6164 100644 +--- a/packages/js/navigation/composer.json ++++ b/packages/js/navigation/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/navigation/composer.lock b/packages/js/navigation/composer.lock +index 261e5abfba..28dcb6647a 100644 +--- a/packages/js/navigation/composer.lock ++++ b/packages/js/navigation/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "d8cb19d42a1c9ecabebeab72510c6935", ++ "content-hash": "0fc4e9b9f69b0b3f85fbc39b55f230d2", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json +index c03f52779c..c613d8b175 100644 +--- a/packages/js/navigation/package.json ++++ b/packages/js/navigation/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/navigation", +- "version": "8.0.0", ++ "version": "8.1.0", + "description": "WooCommerce navigation utilities.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -39,15 +39,15 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "devDependencies": { +@@ -57,6 +57,7 @@ + "@babel/runtime": "^7.17.2", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/navigation/src/history.ts b/packages/js/navigation/src/history.ts +index 00ea876580..3acdd612d7 100644 +--- a/packages/js/navigation/src/history.ts ++++ b/packages/js/navigation/src/history.ts +@@ -56,7 +56,7 @@ function getHistory(): WooBrowserHistory { + console.warn( + `Query path parameter should be a string but instead was: ${ query.path }, undefined behaviour may occur.` + ); +- pathname = ( query.path as unknown ) as string; // ts override only, no coercion going on ++ pathname = query.path as unknown as string; // ts override only, no coercion going on + } else { + pathname = '/'; + } +diff --git a/packages/js/navigation/src/test/index.js b/packages/js/navigation/src/test/index.js +index ce0513033d..b0c705e560 100644 +--- a/packages/js/navigation/src/test/index.js ++++ b/packages/js/navigation/src/test/index.js +@@ -214,9 +214,7 @@ describe( 'getIdsFromQuery', () => { + } ); + it( 'should ignore non numbers entries in the coma-separated list', () => { + expect( getIdsFromQuery( '77,,8,foo,null,9' ) ).toEqual( [ +- 77, +- 8, +- 9, ++ 77, 8, 9, + ] ); + } ); + } ); +diff --git a/packages/js/notices/changelog b/packages/js/notices/changelog +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/packages/js/notices/changelog +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json +index e019f5a245..c12e93bb11 100644 +--- a/packages/js/notices/package.json ++++ b/packages/js/notices/package.json +@@ -38,10 +38,10 @@ + "private": true, + "scripts": { + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix" + }, + "devDependencies": { +@@ -52,6 +52,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "redux": "^4.2.0", ++ "require-turbo": "workspace*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/notices/src/store/selectors.ts b/packages/js/notices/src/store/selectors.ts +index 8daed4e3c9..dcc7a75144 100644 +--- a/packages/js/notices/src/store/selectors.ts ++++ b/packages/js/notices/src/store/selectors.ts +@@ -15,7 +15,7 @@ import { State } from './reducer'; + * + * @type {Array} + */ +-const DEFAULT_NOTICES: [ ] = []; ++const DEFAULT_NOTICES: [] = []; + + /** + * @typedef {Object} WPNotice Notice object. +diff --git a/packages/js/notices/src/store/utils/on-sub-key.ts b/packages/js/notices/src/store/utils/on-sub-key.ts +index cb29221c7b..923e864e5c 100644 +--- a/packages/js/notices/src/store/utils/on-sub-key.ts ++++ b/packages/js/notices/src/store/utils/on-sub-key.ts +@@ -17,27 +17,28 @@ import { Action } from '../actions'; + * + * @return {Function} Higher-order reducer. + */ +-export const onSubKey = ( actionProperty: keyof Action ) => ( +- reducer: Reducer< Notices, Action > +-) => ( state: State = {}, action: Action ) => { +- // Retrieve subkey from action. Do not track if undefined; useful for cases +- // where reducer is scoped by action shape. +- const key = action[ actionProperty ]; +- if ( key === undefined ) { +- return state; +- } ++export const onSubKey = ++ ( actionProperty: keyof Action ) => ++ ( reducer: Reducer< Notices, Action > ) => ++ ( state: State = {}, action: Action ) => { ++ // Retrieve subkey from action. Do not track if undefined; useful for cases ++ // where reducer is scoped by action shape. ++ const key = action[ actionProperty ]; ++ if ( key === undefined ) { ++ return state; ++ } + +- // Avoid updating state if unchanged. Note that this also accounts for a +- // reducer which returns undefined on a key which is not yet tracked. +- const nextKeyState = reducer( state[ key ], action ); +- if ( nextKeyState === state[ key ] ) { +- return state; +- } ++ // Avoid updating state if unchanged. Note that this also accounts for a ++ // reducer which returns undefined on a key which is not yet tracked. ++ const nextKeyState = reducer( state[ key ], action ); ++ if ( nextKeyState === state[ key ] ) { ++ return state; ++ } + +- return { +- ...state, +- [ key ]: nextKeyState, ++ return { ++ ...state, ++ [ key ]: nextKeyState, ++ }; + }; +-}; + + export default onSubKey; +diff --git a/packages/js/number/CHANGELOG.md b/packages/js/number/CHANGELOG.md +index 7dcdf92835..a7283b0a90 100644 +--- a/packages/js/number/CHANGELOG.md ++++ b/packages/js/number/CHANGELOG.md +@@ -1,10 +1,16 @@ +-## [2.3.0](https://www.npmjs.com/package/@woocommerce/number/v/2.3.0) - 2022-06-15 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [2.4.0](https://www.npmjs.com/package/@woocommerce/packages/js/number/v/2.4.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [2.3.0](https://www.npmjs.com/package/@woocommerce/packages/js/number/v/2.3.0) - 2022-06-15 + + - Minor - Update readme code example. + - Minor - Add Jetpack Changelogger + - Patch - Migrate @woocommerce/number to TS + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/number/CHANGELOG.md). +diff --git a/packages/js/number/composer.json b/packages/js/number/composer.json +index 3c7b55f071..caf1ed82b6 100644 +--- a/packages/js/number/composer.json ++++ b/packages/js/number/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/number/composer.lock b/packages/js/number/composer.lock +index 4bbd6438c4..15f9588eb1 100644 +--- a/packages/js/number/composer.lock ++++ b/packages/js/number/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "94fca77665e585bb173bbdea8bc75374", ++ "content-hash": "f823beb8ba53e2ce3eb222a7225b9c81", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/number/package.json b/packages/js/number/package.json +index 08ff35b9c8..875c2b3697 100644 +--- a/packages/js/number/package.json ++++ b/packages/js/number/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/number", +- "version": "2.3.0", ++ "version": "2.4.0", + "description": "Number formatting utilities for WooCommerce.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -27,15 +27,15 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix", +- "test": "jest --config ./jest.config.json", ++ "test": "node ./node_modules/require-turbo && jest --config ./jest.config.json", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "devDependencies": { +@@ -45,6 +45,7 @@ + "@babel/runtime": "^7.17.2", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/packages/js/onboarding/CHANGELOG.md b/packages/js/onboarding/CHANGELOG.md +index e92884604f..fe7f04ba1c 100644 +--- a/packages/js/onboarding/CHANGELOG.md ++++ b/packages/js/onboarding/CHANGELOG.md +@@ -1,15 +1,22 @@ +-## [3.1.0](https://www.npmjs.com/package/@woocommerce/onboarding/v/3.1.0) - 2022-06-15 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [3.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/onboarding/v/3.2.0) - 2022-07-08 ++ ++- Minor - Add WCPayBanner & WCPayBenefits components ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [3.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/onboarding/v/3.1.0) - 2022-06-15 + + - Minor - Add ExPlat dependency and product task experiment logic + - Minor - Add Jetpack Changelogger +-- Patch - Migrate @woocommerce/onboarding to TS +-- Patch - Standardize lint scripts: add lint:fix +-- Patch - Add task_view tracks prop for experimental products #32933 + - Minor - Changed task_view experimental_product key to variant (technically a breaking change but since it was introduced in the same version it is fine) #32944 + - Minor - Removed experimental product hook and instead poll the slot's fill for variant metadata. To be removed when experiment concludes! #33052 + - Minor - Update TaskList types. + - Minor - Added Typescript type declarations. #32615 +- +---- ++- Patch - Migrate @woocommerce/onboarding to TS ++- Patch - Standardize lint scripts: add lint:fix ++- Patch - Add task_view tracks prop for experimental products #32933 + + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/onboarding/CHANGELOG.md). +diff --git a/packages/js/onboarding/composer.json b/packages/js/onboarding/composer.json +index 12128df706..20ce9b2853 100644 +--- a/packages/js/onboarding/composer.json ++++ b/packages/js/onboarding/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/onboarding/composer.lock b/packages/js/onboarding/composer.lock +index 4e9773bb2e..6fca3c5d6a 100644 +--- a/packages/js/onboarding/composer.lock ++++ b/packages/js/onboarding/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "fd4543418c8acf107bb86dda6647ad70", ++ "content-hash": "24717ec0e0fb36f9ba425aa9c7f77ebf", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json +index 787aa626ec..7bd577f1c0 100644 +--- a/packages/js/onboarding/package.json ++++ b/packages/js/onboarding/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/onboarding", +- "version": "3.1.0", ++ "version": "3.2.0", + "description": "Onboarding utilities.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -47,6 +47,7 @@ + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "postcss-loader": "^3.0.0", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "sass-loader": "^10.2.1", + "ts-jest": "^27.1.3", +@@ -55,15 +56,15 @@ + "webpack-cli": "^3.3.12" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "pnpm run build:js && pnpm run build:css", ++ "build": "node ./node_modules/require-turbo && pnpm run build:js && pnpm run build:css", + "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:css": "webpack", + "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix" + }, + "lint-staged": { +diff --git a/plugins/woocommerce-admin/client/payments-welcome/cards/amex.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/amex.js +similarity index 98% +rename from plugins/woocommerce-admin/client/payments-welcome/cards/amex.js +rename to packages/js/onboarding/src/components/WCPayBanner/Icons/amex.js +index 82f0b26434..564be6610f 100644 +--- a/plugins/woocommerce-admin/client/payments-welcome/cards/amex.js ++++ b/packages/js/onboarding/src/components/WCPayBanner/Icons/amex.js +@@ -1,3 +1,8 @@ ++/** ++ * External dependencies ++ */ ++import { createElement } from '@wordpress/element'; ++ + export const Amex = () => ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( + ( + /* eslint-disable */ + ( + /* eslint-disable */ + ( ++
++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ { __( '& more.', 'woocommerce' ) } ++ ++
++); +diff --git a/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss +new file mode 100644 +index 0000000000..65e962e4b2 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss +@@ -0,0 +1,69 @@ ++.woocommerce-recommended-payments-banner { ++ margin: 0 15px 10px 0; ++ min-width: 750px; ++ animation: isLoaded; ++ animation-duration: 250ms; ++ ++ &.components-card { ++ box-shadow: none; ++ border: 1px solid $table-border; ++ border-radius: 2px; ++ } ++ ++ .woocommerce-recommended-payments-banner__body { ++ display: flex; ++ align-items: center; ++ justify-content: center; ++ padding-bottom: 0; ++ padding: 30px 0 0 0; ++ } ++ ++ .woocommerce-recommended-payments__header-title { ++ color: $studio-gray-90; ++ line-height: 32px; ++ } ++ ++ .woocommerce-recommended-payments__header-heading { ++ color: $studio-gray-60; ++ } ++ ++ .woocommerce-recommended-payments-banner__image_container { ++ display: flex; ++ } ++ ++ .woocommerce-recommended-payments-banner__text_container { ++ width: 46%; ++ margin-inline: 24px; ++ ++ a { ++ margin-top: 0; ++ } ++ ++ * { ++ margin-block: 1rem; ++ } ++ } ++ ++ .woocommerce-recommended-payments-banner__footer { ++ display: flex; ++ align-items: center; ++ justify-content: center; ++ padding: 20px 0 30px 0; ++ ++ p { ++ color: #757575; ++ font-style: normal; ++ font-weight: 400; ++ } ++ } ++ ++ .woocommerce-recommended-payments-banner__footer_icon_container { ++ display: flex; ++ align-items: center; ++ } ++ ++ .woocommerce-recommended-payments-banner__footer_icon_container > svg { ++ height: 28px; ++ width: 51px; ++ } ++} +diff --git a/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx +new file mode 100644 +index 0000000000..b3912c4d91 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx +@@ -0,0 +1,122 @@ ++/** ++ * External dependencies ++ */ ++import { createElement, Fragment } from '@wordpress/element'; ++import { __ } from '@wordpress/i18n'; ++import { Card, CardFooter, CardBody } from '@wordpress/components'; ++import { Text } from '@woocommerce/experimental'; ++import interpolateComponents from '@automattic/interpolate-components'; ++import { Link } from '@woocommerce/components'; ++ ++/** ++ * Internal dependencies ++ */ ++import { PaymentMethodsIcons } from './PaymentMethodsIcons'; ++import { WCPayBannerImage } from './WCPayBannerImage'; ++import { WCPayBannerImageCut } from './WCPayBannerImageCut'; ++ ++export const WCPayBannerFooter: React.VFC = () => ( ++ ++
++ ++ { __( 'Accepted payment methods include:', 'woocommerce' ) } ++ ++
++ ++
++); ++ ++export const WCPayBannerText: React.VFC< { ++ actionButton: React.ReactNode; ++} > = ( { actionButton } ) => { ++ return ( ++
++ ++ { __( ++ 'Accept Payments and manage your business.', ++ 'woocommerce' ++ ) } ++ ++ ++ { interpolateComponents( { ++ mixedString: __( ++ 'By using WooCommerce Payments you agree to be bound by our {{tosLink}}Terms of Service{{/tosLink}} and acknowledge that you have read our {{privacyLink}}Privacy Policy{{/privacyLink}} ', ++ 'woocommerce' ++ ), ++ components: { ++ tosLink: ( ++ ++ <> ++ ++ ), ++ privacyLink: ( ++ ++ <> ++ ++ ), ++ }, ++ } ) } ++ ++ { actionButton } ++
++ ); ++}; ++ ++export const WCPayBannerBody: React.VFC< { ++ textPosition: 'left' | 'right'; ++ actionButton: React.ReactNode; ++ bannerImage?: React.ReactNode; ++} > = ( { ++ actionButton, ++ textPosition, ++ bannerImage = , ++} ) => { ++ return ( ++ ++ { textPosition === 'left' ? ( ++ <> ++ ++
++ { bannerImage } ++
++ ++ ) : ( ++ <> ++
++ { bannerImage } ++
++ ++ ++ ) } ++
++ ); ++}; ++ ++export const WCPayBanner: React.FC = ( { children } ) => { ++ return ( ++ ++ { children } ++ ++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/payments/wcpay-banner-image.js b/packages/js/onboarding/src/components/WCPayBanner/WCPayBannerImage.tsx +similarity index 99% +rename from plugins/woocommerce-admin/client/payments/wcpay-banner-image.js +rename to packages/js/onboarding/src/components/WCPayBanner/WCPayBannerImage.tsx +index 62fd76f2a8..b52d6a4372 100644 +--- a/plugins/woocommerce-admin/client/payments/wcpay-banner-image.js ++++ b/packages/js/onboarding/src/components/WCPayBanner/WCPayBannerImage.tsx +@@ -3,7 +3,7 @@ + */ + import { createElement } from '@wordpress/element'; + +-export default () => ( ++export const WCPayBannerImage = () => ( + ( ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++); +diff --git a/packages/js/onboarding/src/components/WCPayBanner/index.ts b/packages/js/onboarding/src/components/WCPayBanner/index.ts +new file mode 100644 +index 0000000000..452a111532 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBanner/index.ts +@@ -0,0 +1,5 @@ ++export * from './Icons'; ++export * from './WCPayBannerImage'; ++export * from './WCPayBannerImageCut'; ++export * from './PaymentMethodsIcons'; ++export * from './WCPayBanner'; +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.scss b/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.scss +new file mode 100644 +index 0000000000..910009670d +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.scss +@@ -0,0 +1,24 @@ ++.woocommerce-wcpay-benefits { ++ background-color: #fff; ++ border: 1px solid $table-border; ++ border-radius: 2px; ++ padding: 40px 30px 30px; ++ width: 680px; ++ height: 344px; ++ ++ .woocommerce-wcpay-benefits-benefit { ++ flex-direction: column; ++ justify-content: start; ++ margin-left: 0; ++ width: 186px; ++ } ++ ++ p { ++ font-style: normal; ++ font-weight: 400; ++ font-size: 16px; ++ line-height: 24px; ++ text-align: center; ++ color: $studio-gray-90; ++ } ++} +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx b/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx +new file mode 100644 +index 0000000000..29c96fbf26 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/WCPayBenefits.tsx +@@ -0,0 +1,50 @@ ++/** ++ * External dependencies ++ */ ++import { createElement } from '@wordpress/element'; ++import { __ } from '@wordpress/i18n'; ++import { Text } from '@woocommerce/experimental'; ++import { Flex } from '@wordpress/components'; ++ ++/** ++ * Internal dependencies ++ */ ++import { ++ PaymentCardIcon, ++ InternationalMarketIcon, ++ EarnManageIcon, ++} from './icons'; ++ ++export const WCPayBenefits: React.VFC = () => { ++ return ( ++ ++ ++ ++ ++ { __( ++ 'Offer your customers their preferred way to pay including debit and credit card payments, Apple Pay, Sofort, SEPA, iDeal and many more.', ++ 'woocommerce' ++ ) } ++ ++ ++ ++ ++ ++ { __( ++ 'Sell to international markets and accept more than 135 currencies with local payment methods.', ++ 'woocommerce' ++ ) } ++ ++ ++ ++ ++ ++ { __( ++ 'Earn and manage recurring revenue and get automatic deposits into your nominated bank account.', ++ 'woocommerce' ++ ) } ++ ++ ++ ++ ); ++}; +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/icons/International-market.js b/packages/js/onboarding/src/components/WCPayBenefits/icons/International-market.js +new file mode 100644 +index 0000000000..5cd087ae40 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/icons/International-market.js +@@ -0,0 +1,101 @@ ++/** ++ * External dependencies ++ */ ++import { createElement } from '@wordpress/element'; ++ ++export const InternationalMarketIcon = () => ( ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++); +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/icons/earn-manage.js b/packages/js/onboarding/src/components/WCPayBenefits/icons/earn-manage.js +new file mode 100644 +index 0000000000..0993dbad0d +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/icons/earn-manage.js +@@ -0,0 +1,180 @@ ++/** ++ * External dependencies ++ */ ++import { createElement } from '@wordpress/element'; ++ ++export const EarnManageIcon = () => ( ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++); +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/icons/index.ts b/packages/js/onboarding/src/components/WCPayBenefits/icons/index.ts +new file mode 100644 +index 0000000000..4c6bb496f1 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/icons/index.ts +@@ -0,0 +1,3 @@ ++export * from './payment-card'; ++export * from './International-market'; ++export * from './earn-manage'; +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/icons/payment-card.js b/packages/js/onboarding/src/components/WCPayBenefits/icons/payment-card.js +new file mode 100644 +index 0000000000..022abd0ea4 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/icons/payment-card.js +@@ -0,0 +1,119 @@ ++/** ++ * External dependencies ++ */ ++import { createElement } from '@wordpress/element'; ++ ++export const PaymentCardIcon = () => ( ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++); +diff --git a/packages/js/onboarding/src/components/WCPayBenefits/index.ts b/packages/js/onboarding/src/components/WCPayBenefits/index.ts +new file mode 100644 +index 0000000000..2cfe85e656 +--- /dev/null ++++ b/packages/js/onboarding/src/components/WCPayBenefits/index.ts +@@ -0,0 +1 @@ ++export * from './WCPayBenefits'; +diff --git a/packages/js/onboarding/src/index.ts b/packages/js/onboarding/src/index.ts +index 3a06e8fa22..074ffab5da 100644 +--- a/packages/js/onboarding/src/index.ts ++++ b/packages/js/onboarding/src/index.ts +@@ -1,4 +1,6 @@ + export * from './components/WCPayCard'; ++export * from './components/WCPayBanner'; ++export * from './components/WCPayBenefits'; + export * from './components/RecommendedRibbon'; + export * from './components/SetupRequired'; + export * from './components/WCPayAcceptedMethods'; +diff --git a/packages/js/onboarding/src/style.scss b/packages/js/onboarding/src/style.scss +index 101ec17b3b..358f3c4aef 100644 +--- a/packages/js/onboarding/src/style.scss ++++ b/packages/js/onboarding/src/style.scss +@@ -1,2 +1,4 @@ + @import 'components/WCPayCard/WCPayCard.scss'; ++@import 'components/WCPayBanner/WCPayBanner.scss'; ++@import 'components/WCPayBenefits/WCPayBenefits.scss'; + @import 'components/RecommendedRibbon/RecommendedRibbon.scss'; +diff --git a/packages/js/tracks/CHANGELOG.md b/packages/js/tracks/CHANGELOG.md +index 8d9aa7ddb4..b74ad4ff9a 100644 +--- a/packages/js/tracks/CHANGELOG.md ++++ b/packages/js/tracks/CHANGELOG.md +@@ -1,9 +1,15 @@ +-## [1.2.0](https://www.npmjs.com/package/@woocommerce/tracks/v/1.2.0) - 2022-06-15 ++# Changelog ++ ++This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ++ ++## [1.3.0](https://www.npmjs.com/package/@woocommerce/packages/js/tracks/v/1.3.0) - 2022-07-08 ++ ++- Minor - Remove PHP and Composer dependencies for packaged JS packages ++ ++## [1.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/tracks/v/1.2.0) - 2022-06-15 + + - Minor - Add Jetpack Changelogger + - Minor - Convert package to Typescript. + - Patch - Standardize lint scripts: add lint:fix + +---- +- + [See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/tracks/CHANGELOG.md). +diff --git a/packages/js/tracks/composer.json b/packages/js/tracks/composer.json +index 493380b808..8c8141d9e2 100644 +--- a/packages/js/tracks/composer.json ++++ b/packages/js/tracks/composer.json +@@ -5,7 +5,7 @@ + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "platform": { +@@ -15,7 +15,7 @@ + "extra": { + "changelogger": { + "formatter": { +- "filename": "../../../tools/changelogger/PackageFormatter.php" ++ "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/packages/js/tracks/composer.lock b/packages/js/tracks/composer.lock +index 6220939710..d046d98a4e 100644 +--- a/packages/js/tracks/composer.lock ++++ b/packages/js/tracks/composer.lock +@@ -4,32 +4,32 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "02cdc5b017a61e45591ba98dfad85bb2", ++ "content-hash": "c1c9ce8ab810d38191077a10b9438963", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -38,7 +38,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -60,9 +60,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "psr/log", +@@ -265,6 +265,7 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, + { +@@ -333,7 +334,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/main" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +diff --git a/packages/js/tracks/package.json b/packages/js/tracks/package.json +index a3a3230e93..12094a3bc3 100644 +--- a/packages/js/tracks/package.json ++++ b/packages/js/tracks/package.json +@@ -1,6 +1,6 @@ + { + "name": "@woocommerce/tracks", +- "version": "1.2.0", ++ "version": "1.3.0", + "description": "WooCommerce user event tracking utilities for Automattic based projects.", + "author": "Automattic", + "license": "GPL-3.0-or-later", +@@ -28,13 +28,13 @@ + "access": "public" + }, + "scripts": { +- "postinstall": "composer install", ++ "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", +- "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", ++ "build": "node ./node_modules/require-turbo && tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "start": "tsc --build --watch", + "prepack": "pnpm run clean && pnpm run build", +- "lint": "eslint src", ++ "lint": "node ./node_modules/require-turbo && eslint src", + "lint:fix": "eslint src --fix" + }, + "devDependencies": { +@@ -44,6 +44,7 @@ + "eslint": "^8.12.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^4.6.2" +diff --git a/phpcs.xml b/phpcs.xml +index 70bf2d649c..9103caf833 100644 +--- a/phpcs.xml ++++ b/phpcs.xml +@@ -117,12 +117,6 @@ + src/Admin/ + + +- +- +- plugins/woocommerce/src/Internal/Admin/ +- src/Admin/ +- +- + + + src/Internal/Admin/ +@@ -133,11 +127,11 @@ + + src/Internal/Admin/ + src/Admin/ +- ++ + + + + src/Internal/Admin/ + src/Admin/ +- ++ + +diff --git a/plugins/woocommerce-admin/changelogs/add-require-turbo b/plugins/woocommerce-admin/changelogs/add-require-turbo +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/plugins/woocommerce-admin/changelogs/add-require-turbo +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/plugins/woocommerce-admin/client/activity-panel/activity-card/placeholder.js b/plugins/woocommerce-admin/client/activity-panel/activity-card/placeholder.js +index 9a1fbd7e7b..5ddcbeb160 100644 +--- a/plugins/woocommerce-admin/client/activity-panel/activity-card/placeholder.js ++++ b/plugins/woocommerce-admin/client/activity-panel/activity-card/placeholder.js +@@ -8,13 +8,8 @@ import { range } from 'lodash'; + + class ActivityCardPlaceholder extends Component { + render() { +- const { +- className, +- hasAction, +- hasDate, +- hasSubtitle, +- lines, +- } = this.props; ++ const { className, hasAction, hasDate, hasSubtitle, lines } = ++ this.props; + const cardClassName = classnames( + 'woocommerce-activity-card is-loading', + className +diff --git a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js +index 96780f348d..97a6bee04b 100644 +--- a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js ++++ b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js +@@ -184,9 +184,8 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { + thingsToDoCount + ), + thingsToDoNextCount: thingsToDoCount, +- requestingTaskListOptions: ! hasFinishedResolution( +- 'getTaskLists' +- ), ++ requestingTaskListOptions: ++ ! hasFinishedResolution( 'getTaskLists' ), + setupTaskListComplete: setupList?.isComplete, + setupTaskListHidden: isSetupTaskListHidden, + setupTasksCount: setupVisibleTasks.length, +diff --git a/plugins/woocommerce-admin/client/activity-panel/display-options/index.js b/plugins/woocommerce-admin/client/activity-panel/display-options/index.js +index d8e8605191..38bfb4d9dc 100644 +--- a/plugins/woocommerce-admin/client/activity-panel/display-options/index.js ++++ b/plugins/woocommerce-admin/client/activity-panel/display-options/index.js +@@ -51,27 +51,22 @@ const LAYOUTS = [ + ]; + + export const DisplayOptions = () => { +- const { +- defaultHomescreenLayout, +- taskListComplete, +- isTaskListHidden, +- } = useSelect( ( select ) => { +- const { getOption } = select( OPTIONS_STORE_NAME ); +- const { getTaskList } = select( ONBOARDING_STORE_NAME ); +- const taskList = getTaskList( 'setup' ); ++ const { defaultHomescreenLayout, taskListComplete, isTaskListHidden } = ++ useSelect( ( select ) => { ++ const { getOption } = select( OPTIONS_STORE_NAME ); ++ const { getTaskList } = select( ONBOARDING_STORE_NAME ); ++ const taskList = getTaskList( 'setup' ); + +- return { +- defaultHomescreenLayout: +- getOption( 'woocommerce_default_homepage_layout' ) || +- 'single_column', +- taskListComplete: taskList?.isComplete, +- isTaskListHidden: taskList?.isHidden, +- }; +- } ); +- const { +- updateUserPreferences, +- homepage_layout: layout, +- } = useUserPreferences(); ++ return { ++ defaultHomescreenLayout: ++ getOption( 'woocommerce_default_homepage_layout' ) || ++ 'single_column', ++ taskListComplete: taskList?.isComplete, ++ isTaskListHidden: taskList?.isHidden, ++ }; ++ } ); ++ const { updateUserPreferences, homepage_layout: layout } = ++ useUserPreferences(); + + const shouldShowStoreLinks = taskListComplete || isTaskListHidden; + const hasTwoColumnContent = +@@ -118,7 +113,8 @@ export const DisplayOptions = () => { + recordEvent( + 'homescreen_display_option', + { +- display_option: newLayout, ++ display_option: ++ newLayout, + } + ); + } } +diff --git a/plugins/woocommerce-admin/client/activity-panel/panels/help.js b/plugins/woocommerce-admin/client/activity-panel/panels/help.js +index a27469c37c..b853bf7cd0 100644 +--- a/plugins/woocommerce-admin/client/activity-panel/panels/help.js ++++ b/plugins/woocommerce-admin/client/activity-panel/panels/help.js +@@ -30,33 +30,27 @@ function getHomeItems() { + return [ + { + title: __( 'Get Support', 'woocommerce' ), +- link: +- 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product', ++ link: 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product', + }, + { + title: __( 'Home Screen', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/home-screen/?utm_medium=product', ++ link: 'https://woocommerce.com/document/home-screen/?utm_medium=product', + }, + { + title: __( 'Inbox', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-2', ++ link: 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-2', + }, + { + title: __( 'Stats Overview', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-4', ++ link: 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-4', + }, + { + title: __( 'Store Management', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-5', ++ link: 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-5', + }, + { + title: __( 'Store Setup Checklist', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/woocommerce-setup-wizard?utm_medium=product#store-setup-checklist', ++ link: 'https://woocommerce.com/document/woocommerce-setup-wizard?utm_medium=product#store-setup-checklist', + }, + ]; + } +@@ -68,21 +62,18 @@ function getAppearanceItems() { + 'Showcase your products and tailor your shopping experience using Blocks', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/woocommerce-blocks/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/woocommerce-blocks/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( + 'Manage Store Notice, Catalog View and Product Images', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/woocommerce-customizer/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/woocommerce-customizer/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'How to choose and change a theme', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/choose-change-theme/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/choose-change-theme/?utm_source=help_panel&utm_medium=product', + }, + ]; + } +@@ -97,8 +88,7 @@ function getMarketingItems( props ) { + }, + activePlugins.includes( 'google-listings-and-ads' ) && { + title: __( 'Set up Google Listing & Ads', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product#get-started', ++ link: 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product#get-started', + }, + activePlugins.includes( 'pinterest-for-woocommerce' ) && { + title: __( 'Set up Pinterest for WooCommerce', 'woocommerce' ), +@@ -106,8 +96,7 @@ function getMarketingItems( props ) { + }, + activePlugins.includes( 'mailchimp-for-woocommerce' ) && { + title: __( 'Connect Mailchimp for WooCommerce', 'woocommerce' ), +- link: +- 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/', ++ link: 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/', + }, + activePlugins.includes( 'creative-mail-by-constant-contact' ) && { + title: __( 'Set up Creative Mail for WooCommerce', 'woocommerce' ), +@@ -122,66 +111,54 @@ function getPaymentGatewaySuggestions( props ) { + return [ + { + title: __( 'Which Payment Option is Right for Me?', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/premium-payment-gateway-extensions/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/premium-payment-gateway-extensions/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.woocommerce_payments && { + title: __( 'WooCommerce Payments Start Up Guide', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/payments/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/payments/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.woocommerce_payments && { + title: __( 'WooCommerce Payments FAQs', 'woocommerce' ), +- link: +- 'https://woocommerce.com/documentation/woocommerce-payments/woocommerce-payments-faqs/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/documentation/woocommerce-payments/woocommerce-payments-faqs/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.stripe && { + title: __( 'Stripe Setup and Configuration', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/stripe/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/stripe/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions[ 'ppcp-gateway' ] && { + title: __( + 'PayPal Checkout Setup and Configuration', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/2-0/woocommerce-paypal-payments/?utm_medium=product#section-3', ++ link: 'https://woocommerce.com/document/2-0/woocommerce-paypal-payments/?utm_medium=product#section-3', + }, + paymentGatewaySuggestions.square_credit_card && { + title: __( 'Square - Get started', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/woocommerce-square/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/woocommerce-square/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.kco && { + title: __( 'Klarna - Introduction', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/klarna-checkout/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/klarna-checkout/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.klarna_payments && { + title: __( 'Klarna - Introduction', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/klarna-payments/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/klarna-payments/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.payfast && { + title: __( 'PayFast Setup and Configuration', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/payfast-payment-gateway/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/payfast-payment-gateway/?utm_source=help_panel&utm_medium=product', + }, + paymentGatewaySuggestions.eway && { + title: __( 'Eway Setup and Configuration', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/eway/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/eway/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'Direct Bank Transfer (BACS)', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/bacs/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/bacs/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'Cash on Delivery', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/cash-on-delivery/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/cash-on-delivery/?utm_source=help_panel&utm_medium=product', + }, + ].filter( Boolean ); + } +@@ -190,26 +167,22 @@ function getProductsItems() { + return [ + { + title: __( 'Adding and Managing Products', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/managing-products/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/managing-products/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( + 'Import products using the CSV Importer and Exporter', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/product-csv-importer-exporter/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/product-csv-importer-exporter/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'Migrate products using Cart2Cart', 'woocommerce' ), +- link: +- 'https://woocommerce.com/products/cart2cart/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/products/cart2cart/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'Learn more about setting up products', 'woocommerce' ), +- link: +- 'https://woocommerce.com/documentation/plugins/woocommerce/getting-started/setup-products/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/documentation/plugins/woocommerce/getting-started/setup-products/?utm_source=help_panel&utm_medium=product', + }, + ]; + } +@@ -221,34 +194,29 @@ function getShippingItems( { activePlugins, countryCode } ) { + return [ + { + title: __( 'Setting up Shipping Zones', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/setting-up-shipping-zones/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/setting-up-shipping-zones/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'Core Shipping Options', 'woocommerce' ), +- link: +- 'https://woocommerce.com/documentation/plugins/woocommerce/getting-started/shipping/core-shipping-options/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/documentation/plugins/woocommerce/getting-started/shipping/core-shipping-options/?utm_source=help_panel&utm_medium=product', + }, + { + title: __( 'Product Shipping Classes', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/product-shipping-classes/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/product-shipping-classes/?utm_source=help_panel&utm_medium=product', + }, + showWCS && { + title: __( + 'WooCommerce Shipping setup and configuration', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/woocommerce-shipping-and-tax/?utm_source=help_panel&utm_medium=product#section-3', ++ link: 'https://woocommerce.com/document/woocommerce-shipping-and-tax/?utm_source=help_panel&utm_medium=product#section-3', + }, + { + title: __( + 'Learn more about configuring your shipping settings', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/plugins/woocommerce/getting-started/shipping/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/plugins/woocommerce/getting-started/shipping/?utm_source=help_panel&utm_medium=product', + }, + ].filter( Boolean ); + } +@@ -276,16 +244,14 @@ function getTaxItems( props ) { + return [ + { + title: __( 'Setting up Taxes in WooCommerce', 'woocommerce' ), +- link: +- 'https://woocommerce.com/document/setting-up-taxes-in-woocommerce/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/document/setting-up-taxes-in-woocommerce/?utm_source=help_panel&utm_medium=product', + }, + showWCS && { + title: __( + 'Automated Tax calculation using WooCommerce Tax', + 'woocommerce' + ), +- link: +- 'https://woocommerce.com/document/woocommerce-services/?utm_source=help_panel&utm_medium=product#section-10', ++ link: 'https://woocommerce.com/document/woocommerce-services/?utm_source=help_panel&utm_medium=product#section-10', + }, + ].filter( Boolean ); + } +@@ -329,8 +295,7 @@ function getListItems( props ) { + const itemsByType = getItems( props ); + const genericDocsLink = { + title: __( 'WooCommerce Docs', 'woocommerce' ), +- link: +- 'https://woocommerce.com/documentation/?utm_source=help_panel&utm_medium=product', ++ link: 'https://woocommerce.com/documentation/?utm_source=help_panel&utm_medium=product', + }; + itemsByType.push( genericDocsLink ); + +diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js +index 13b01295c2..584b990393 100644 +--- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js ++++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js +@@ -30,8 +30,7 @@ export default [ + _links: { + product: [ + { +- href: +- 'https://example.com/wp-json/wc-analytics/products/20', ++ href: 'https://example.com/wp-json/wc-analytics/products/20', + }, + ], + }, +@@ -45,8 +44,7 @@ export default [ + _links: { + product: [ + { +- href: +- 'https://example.com/wp-json/wc-analytics/products/22', ++ href: 'https://example.com/wp-json/wc-analytics/products/22', + }, + ], + }, +@@ -60,8 +58,7 @@ export default [ + _links: { + product: [ + { +- href: +- 'https://example.com/wp-json/wc-analytics/products/23', ++ href: 'https://example.com/wp-json/wc-analytics/products/23', + }, + ], + }, +@@ -75,8 +72,7 @@ export default [ + _links: { + product: [ + { +- href: +- 'https://example.com/wp-json/wc-analytics/products/24', ++ href: 'https://example.com/wp-json/wc-analytics/products/24', + }, + ], + }, +@@ -90,8 +86,7 @@ export default [ + _links: { + product: [ + { +- href: +- 'https://example.com/wp-json/wc-analytics/products/25', ++ href: 'https://example.com/wp-json/wc-analytics/products/25', + }, + ], + }, +diff --git a/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js b/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js +index 60df98c079..0c5be89092 100644 +--- a/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js ++++ b/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js +@@ -126,10 +126,8 @@ class ReportFilters extends Component { + query, + defaultDateRange + ); +- const { +- primary: primaryDate, +- secondary: secondaryDate, +- } = getCurrentDates( query, defaultDateRange ); ++ const { primary: primaryDate, secondary: secondaryDate } = ++ getCurrentDates( query, defaultDateRange ); + const dateQuery = { + period, + compare, +diff --git a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js +index 0402da497a..9a40653b9a 100644 +--- a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js ++++ b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js +@@ -317,7 +317,7 @@ const ReportTable = ( props ) => { + label: ( + +diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/table.js b/plugins/woocommerce-admin/client/analytics/report/categories/table.js +index c96529c3b4..a5d03127f1 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/categories/table.js ++++ b/plugins/woocommerce-admin/client/analytics/report/categories/table.js +@@ -225,9 +225,8 @@ export default compose( + return {}; + } + +- const { getItems, getItemsError, isResolving } = select( +- ITEMS_STORE_NAME +- ); ++ const { getItems, getItemsError, isResolving } = ++ select( ITEMS_STORE_NAME ); + const tableQuery = { + per_page: -1, + }; +diff --git a/plugins/woocommerce-admin/client/analytics/report/customers/table.js b/plugins/woocommerce-admin/client/analytics/report/customers/table.js +index 5afe0f79fe..73f58fb42c 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/customers/table.js ++++ b/plugins/woocommerce-admin/client/analytics/report/customers/table.js +@@ -26,9 +26,8 @@ function CustomersReportTable( { + } ) { + const context = useContext( CurrencyContext ); + const { countries, loadingCountries } = useSelect( ( select ) => { +- const { getCountries, hasFinishedResolution } = select( +- COUNTRIES_STORE_NAME +- ); ++ const { getCountries, hasFinishedResolution } = ++ select( COUNTRIES_STORE_NAME ); + return { + countries: getCountries(), + loadingCountries: ! hasFinishedResolution( 'getCountries' ), +diff --git a/plugins/woocommerce-admin/client/analytics/report/downloads/table.js b/plugins/woocommerce-admin/client/analytics/report/downloads/table.js +index af3c20b4b4..7f95bc9236 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/downloads/table.js ++++ b/plugins/woocommerce-admin/client/analytics/report/downloads/table.js +@@ -86,10 +86,8 @@ class DownloadsReportTable extends Component { + username, + } = download; + +- const { +- code: errorCode, +- name: productName, +- } = _embedded.product[ 0 ]; ++ const { code: errorCode, name: productName } = ++ _embedded.product[ 0 ]; + let productDisplay, productValue; + + // Handle deleted products. +diff --git a/plugins/woocommerce-admin/client/analytics/report/products/index.js b/plugins/woocommerce-admin/client/analytics/report/products/index.js +index 2846337f76..b6caf794cc 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/products/index.js ++++ b/plugins/woocommerce-admin/client/analytics/report/products/index.js +@@ -22,11 +22,8 @@ import ReportFilters from '../../components/report-filters'; + + class ProductsReport extends Component { + getChartMeta() { +- const { +- query, +- isSingleProductView, +- isSingleProductVariable, +- } = this.props; ++ const { query, isSingleProductView, isSingleProductVariable } = ++ this.props; + const isCompareView = + query.filter === 'compare-products' && + query.products && +@@ -54,13 +51,8 @@ class ProductsReport extends Component { + + render() { + const { compareObject, itemsLabel, mode } = this.getChartMeta(); +- const { +- path, +- query, +- isError, +- isRequesting, +- isSingleProductVariable, +- } = this.props; ++ const { path, query, isError, isRequesting, isSingleProductVariable } = ++ this.props; + + if ( isError ) { + return ; +@@ -143,9 +135,8 @@ export default compose( + query.products && + query.products.split( ',' ).length === 1; + +- const { getItems, isResolving, getItemsError } = select( +- ITEMS_STORE_NAME +- ); ++ const { getItems, isResolving, getItemsError } = ++ select( ITEMS_STORE_NAME ); + + if ( isRequesting ) { + return { +diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table.js b/plugins/woocommerce-admin/client/analytics/report/products/table.js +index 80f319fd64..addeb14f70 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/products/table.js ++++ b/plugins/woocommerce-admin/client/analytics/report/products/table.js +@@ -371,9 +371,8 @@ export default compose( + withSelect( ( select, props ) => { + const { query, isRequesting } = props; + +- const { getItems, getItemsError, isResolving } = select( +- ITEMS_STORE_NAME +- ); ++ const { getItems, getItemsError, isResolving } = ++ select( ITEMS_STORE_NAME ); + + if ( + isRequesting || +diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/table.js b/plugins/woocommerce-admin/client/analytics/report/revenue/table.js +index df1b133f24..80385cfd18 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/revenue/table.js ++++ b/plugins/woocommerce-admin/client/analytics/report/revenue/table.js +@@ -353,9 +353,8 @@ export default compose( + SETTINGS_STORE_NAME + ).getSetting( 'wc_admin', 'wcAdminSettings' ); + const datesFromQuery = getCurrentDates( query, defaultDateRange ); +- const { getReportStats, getReportStatsError, isResolving } = select( +- REPORTS_STORE_NAME +- ); ++ const { getReportStats, getReportStatsError, isResolving } = ++ select( REPORTS_STORE_NAME ); + + const tableQuery = formatTableQuery( + query.order || 'desc', +diff --git a/plugins/woocommerce-admin/client/analytics/report/variations/table.js b/plugins/woocommerce-admin/client/analytics/report/variations/table.js +index 28166312e9..6b2ea7311e 100644 +--- a/plugins/woocommerce-admin/client/analytics/report/variations/table.js ++++ b/plugins/woocommerce-admin/client/analytics/report/variations/table.js +@@ -132,7 +132,7 @@ class VariationsReportTable extends Component { + return [ + { + display: deleted ? ( +- name + ' ' + __( '(Deleted)', ' woocommerce-admin' ) ++ name + ' ' + __( '(Deleted)', 'woocommerce' ) + ) : ( + + { name } +diff --git a/plugins/woocommerce-admin/client/analytics/settings/default-date.js b/plugins/woocommerce-admin/client/analytics/settings/default-date.js +index 3c17112266..d5737e1864 100644 +--- a/plugins/woocommerce-admin/client/analytics/settings/default-date.js ++++ b/plugins/woocommerce-admin/client/analytics/settings/default-date.js +@@ -14,9 +14,8 @@ const DefaultDate = ( { value, onChange } ) => { + const { wcAdminSettings } = useSettings( 'wc_admin', [ + 'wcAdminSettings', + ] ); +- const { +- woocommerce_default_date_range: defaultDateRange, +- } = wcAdminSettings; ++ const { woocommerce_default_date_range: defaultDateRange } = ++ wcAdminSettings; + const change = ( query ) => { + onChange( { + target: { +diff --git a/plugins/woocommerce-admin/client/analytics/settings/historical-data/actions.js b/plugins/woocommerce-admin/client/analytics/settings/historical-data/actions.js +index f2442e9566..3c7579a229 100644 +--- a/plugins/woocommerce-admin/client/analytics/settings/historical-data/actions.js ++++ b/plugins/woocommerce-admin/client/analytics/settings/historical-data/actions.js +@@ -191,10 +191,8 @@ export default compose( [ + withSelect( ( select ) => { + const { getFormSettings } = select( IMPORT_STORE_NAME ); + +- const { +- period: selectedPeriod, +- skipPrevious: skipChecked, +- } = getFormSettings(); ++ const { period: selectedPeriod, skipPrevious: skipChecked } = ++ getFormSettings(); + + return { + selectedPeriod, +@@ -202,9 +200,8 @@ export default compose( [ + }; + } ), + withDispatch( ( dispatch ) => { +- const { updateImportation, setImportStarted } = dispatch( +- IMPORT_STORE_NAME +- ); ++ const { updateImportation, setImportStarted } = ++ dispatch( IMPORT_STORE_NAME ); + const { createNotice } = dispatch( 'core/notices' ); + return { + createNotice, +diff --git a/plugins/woocommerce-admin/client/analytics/settings/historical-data/index.js b/plugins/woocommerce-admin/client/analytics/settings/historical-data/index.js +index 35be383283..ad7ca923e2 100644 +--- a/plugins/woocommerce-admin/client/analytics/settings/historical-data/index.js ++++ b/plugins/woocommerce-admin/client/analytics/settings/historical-data/index.js +@@ -30,16 +30,13 @@ class HistoricalData extends Component { + + this.onImportFinished = this.onImportFinished.bind( this ); + this.onImportStarted = this.onImportStarted.bind( this ); +- this.clearStatusAndTotalsCache = this.clearStatusAndTotalsCache.bind( +- this +- ); ++ this.clearStatusAndTotalsCache = ++ this.clearStatusAndTotalsCache.bind( this ); + this.stopImport = this.stopImport.bind( this ); +- this.startStatusCheckInterval = this.startStatusCheckInterval.bind( +- this +- ); +- this.cancelStatusCheckInterval = this.cancelStatusCheckInterval.bind( +- this +- ); ++ this.startStatusCheckInterval = ++ this.startStatusCheckInterval.bind( this ); ++ this.cancelStatusCheckInterval = ++ this.cancelStatusCheckInterval.bind( this ); + } + + startStatusCheckInterval() { +@@ -144,9 +141,8 @@ class HistoricalData extends Component { + export default compose( [ + withSelect( ( select ) => { + const { getNotes } = select( NOTES_STORE_NAME ); +- const { getImportStarted, getFormSettings } = select( +- IMPORT_STORE_NAME +- ); ++ const { getImportStarted, getFormSettings } = ++ select( IMPORT_STORE_NAME ); + + const notesQuery = { + page: 1, +@@ -156,10 +152,8 @@ export default compose( [ + }; + const notes = getNotes( notesQuery ); + const { activeImport, lastImportStartTimestamp } = getImportStarted(); +- const { +- period: selectedPeriod, +- skipPrevious: skipChecked, +- } = getFormSettings(); ++ const { period: selectedPeriod, skipPrevious: skipChecked } = ++ getFormSettings(); + + return { + activeImport, +@@ -171,9 +165,8 @@ export default compose( [ + } ), + withDispatch( ( dispatch ) => { + const { updateNote } = dispatch( NOTES_STORE_NAME ); +- const { invalidateResolution, setImportStarted } = dispatch( +- IMPORT_STORE_NAME +- ); ++ const { invalidateResolution, setImportStarted } = ++ dispatch( IMPORT_STORE_NAME ); + + return { + invalidateResolution, +diff --git a/plugins/woocommerce-admin/client/analytics/settings/historical-data/layout.js b/plugins/woocommerce-admin/client/analytics/settings/historical-data/layout.js +index 882d448e5b..03b617019f 100644 +--- a/plugins/woocommerce-admin/client/analytics/settings/historical-data/layout.js ++++ b/plugins/woocommerce-admin/client/analytics/settings/historical-data/layout.js +@@ -104,9 +104,8 @@ class HistoricalDataLayout extends Component { + } + + export default withSelect( ( select, props ) => { +- const { getImportError, getImportStatus, getImportTotals } = select( +- IMPORT_STORE_NAME +- ); ++ const { getImportError, getImportStatus, getImportTotals } = ++ select( IMPORT_STORE_NAME ); + const { + activeImport, + cacheNeedsClearing, +@@ -120,9 +119,8 @@ export default withSelect( ( select, props ) => { + } = props; + + const params = formatParams( dateFormat, period, skipChecked ); +- const { customers, orders, lastImportStartTimestamp } = getImportTotals( +- params +- ); ++ const { customers, orders, lastImportStartTimestamp } = ++ getImportTotals( params ); + + const { + customers: customersStatus, +diff --git a/plugins/woocommerce-admin/client/analytics/settings/historical-data/test/utils.js b/plugins/woocommerce-admin/client/analytics/settings/historical-data/test/utils.js +index 6f851bb64a..e537bbff4f 100644 +--- a/plugins/woocommerce-admin/client/analytics/settings/historical-data/test/utils.js ++++ b/plugins/woocommerce-admin/client/analytics/settings/historical-data/test/utils.js +@@ -24,9 +24,9 @@ describe( 'formatParams', () => { + } ); + + it( 'returns correct days param based on period label', () => { +- expect( +- formatParams( 'YYYY-MM-DD', { label: '30' }, false ) +- ).toEqual( { days: 30 } ); ++ expect( formatParams( 'YYYY-MM-DD', { label: '30' }, false ) ).toEqual( ++ { days: 30 } ++ ); + } ); + + it( 'returns correct days param based on period date', () => { +diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js +index 7433e931a9..b55b39548e 100644 +--- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js ++++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js +@@ -117,7 +117,7 @@ function CustomerEffortScoreTracks( { + + return ( + { +- const { +- getJetpackConnectUrl, +- isPluginsRequesting, +- getPluginsError, +- } = select( PLUGINS_STORE_NAME ); ++ const { getJetpackConnectUrl, isPluginsRequesting, getPluginsError } = ++ select( PLUGINS_STORE_NAME ); + + const queryArgs = { + redirect_url: props.redirectUrl || window.location.href, +diff --git a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx +index 8a3c8d3d78..6a1349be68 100644 +--- a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx ++++ b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx +@@ -163,40 +163,44 @@ export const normalizeState = ( state: string ): string => { + * @param {string} normalizedAutofillState The value of the autofillState field. + * @return {Function} filter function. + */ +-export const getStateFilter = ( +- isStateAbbreviation: boolean, +- normalizedAutofillState: string +-): ( ( option: Option ) => boolean ) => ( option: Option ) => { +- const countryStateArray = isStateAbbreviation +- ? option.key.split( ':' ) +- : option.label.split( '—' ); +- +- // No region options in the country +- if ( countryStateArray.length <= 1 ) { +- return false; +- } ++export const getStateFilter = ++ ( ++ isStateAbbreviation: boolean, ++ normalizedAutofillState: string ++ ): ( ( option: Option ) => boolean ) => ++ ( option: Option ) => { ++ const countryStateArray = isStateAbbreviation ++ ? option.key.split( ':' ) ++ : option.label.split( '—' ); ++ ++ // No region options in the country ++ if ( countryStateArray.length <= 1 ) { ++ return false; ++ } + +- const state = countryStateArray[ 1 ]; +- // Handle special case, for example: China — Beijing / 北京 +- if ( state.includes( '/' ) ) { +- const stateStrList = state.split( '/' ); +- return ( +- normalizeState( stateStrList[ 0 ] ) === normalizedAutofillState || +- normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState +- ); +- } ++ const state = countryStateArray[ 1 ]; ++ // Handle special case, for example: China — Beijing / 北京 ++ if ( state.includes( '/' ) ) { ++ const stateStrList = state.split( '/' ); ++ return ( ++ normalizeState( stateStrList[ 0 ] ) === ++ normalizedAutofillState || ++ normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState ++ ); ++ } + +- // Handle special case, for example: Iran — Alborz (البرز) +- if ( state.includes( '(' ) && state.includes( ')' ) ) { +- const stateStrList = state.replace( ')', '' ).split( '(' ); +- return ( +- normalizeState( stateStrList[ 0 ] ) === normalizedAutofillState || +- normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState +- ); +- } ++ // Handle special case, for example: Iran — Alborz (البرز) ++ if ( state.includes( '(' ) && state.includes( ')' ) ) { ++ const stateStrList = state.replace( ')', '' ).split( '(' ); ++ return ( ++ normalizeState( stateStrList[ 0 ] ) === ++ normalizedAutofillState || ++ normalizeState( stateStrList[ 1 ] ) === normalizedAutofillState ++ ); ++ } + +- return normalizeState( state ) === normalizedAutofillState; +-}; ++ return normalizeState( state ) === normalizedAutofillState; ++ }; + + /** + * Get the autofill countryState fields and set value from filtered options. +@@ -348,24 +352,22 @@ export function StoreAddress( { + setValue, + }: StoreAddressProps ): JSX.Element { + const countryState = getInputProps( 'countryState' ).value; +- const { +- locale, +- hasFinishedResolution, +- countries, +- loadingCountries, +- } = useSelect( ( select ) => { +- const { +- getLocale, +- getCountries, +- hasFinishedResolution: hasFinishedCountryResolution, +- } = select( COUNTRIES_STORE_NAME ); +- return { +- locale: getLocale( countryState ), +- countries: getCountries(), +- loadingCountries: ! hasFinishedCountryResolution( 'getCountries' ), +- hasFinishedResolution: hasFinishedCountryResolution( 'getLocales' ), +- }; +- } ); ++ const { locale, hasFinishedResolution, countries, loadingCountries } = ++ useSelect( ( select ) => { ++ const { ++ getLocale, ++ getCountries, ++ hasFinishedResolution: hasFinishedCountryResolution, ++ } = select( COUNTRIES_STORE_NAME ); ++ return { ++ locale: getLocale( countryState ), ++ countries: getCountries(), ++ loadingCountries: ++ ! hasFinishedCountryResolution( 'getCountries' ), ++ hasFinishedResolution: ++ hasFinishedCountryResolution( 'getLocales' ), ++ }; ++ } ); + const countryStateOptions = useMemo( + () => getCountryStateOptions( countries ), + [ countries ] +diff --git a/plugins/woocommerce-admin/client/dashboard/customizable.js b/plugins/woocommerce-admin/client/dashboard/customizable.js +index ee2d4bb382..57731267b7 100644 +--- a/plugins/woocommerce-admin/client/dashboard/customizable.js ++++ b/plugins/woocommerce-admin/client/dashboard/customizable.js +@@ -240,10 +240,8 @@ const CustomizableDashboard = ( { defaultDateRange, path, query } ) => { + query, + defaultDateRange + ); +- const { +- primary: primaryDate, +- secondary: secondaryDate, +- } = getCurrentDates( query, defaultDateRange ); ++ const { primary: primaryDate, secondary: secondaryDate } = ++ getCurrentDates( query, defaultDateRange ); + const dateQuery = { + period, + compare, +diff --git a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js +index 76b511d295..ee10bdfc52 100644 +--- a/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js ++++ b/plugins/woocommerce-admin/client/dashboard/dashboard-charts/block.js +@@ -35,14 +35,8 @@ class ChartBlock extends Component { + } + + render() { +- const { +- charts, +- endpoint, +- path, +- query, +- selectedChart, +- filters, +- } = this.props; ++ const { charts, endpoint, path, query, selectedChart, filters } = ++ this.props; + + if ( ! selectedChart ) { + return null; +diff --git a/plugins/woocommerce-admin/client/dashboard/section-controls.js b/plugins/woocommerce-admin/client/dashboard/section-controls.js +index 14b24b51bd..9cd6031abb 100644 +--- a/plugins/woocommerce-admin/client/dashboard/section-controls.js ++++ b/plugins/woocommerce-admin/client/dashboard/section-controls.js +@@ -56,7 +56,7 @@ class SectionControls extends Component { + + } +- label={ __( 'Move up' ) } ++ label={ __( 'Move up', 'woocommerce' ) } + size={ 20 } + className="icon-control" + /> +@@ -68,7 +68,7 @@ class SectionControls extends Component { + } + size={ 20 } +- label={ __( 'Move down' ) } ++ label={ __( 'Move down', 'woocommerce' ) } + className="icon-control" + /> + { __( 'Move down', 'woocommerce' ) } +@@ -78,7 +78,7 @@ class SectionControls extends Component { + + { __( 'Remove section', 'woocommerce' ) } +diff --git a/plugins/woocommerce-admin/client/dashboard/store-performance/utils.js b/plugins/woocommerce-admin/client/dashboard/store-performance/utils.js +index e4f3672c40..ff5a2e3cbb 100644 +--- a/plugins/woocommerce-admin/client/dashboard/store-performance/utils.js ++++ b/plugins/woocommerce-admin/client/dashboard/store-performance/utils.js +@@ -74,9 +74,8 @@ export const getIndicatorValues = ( { + }; + + export const getIndicatorData = ( select, indicators, query, filters ) => { +- const { getReportItems, getReportItemsError, isResolving } = select( +- REPORTS_STORE_NAME +- ); ++ const { getReportItems, getReportItemsError, isResolving } = ++ select( REPORTS_STORE_NAME ); + const { woocommerce_default_date_range: defaultDateRange } = select( + SETTINGS_STORE_NAME + ).getSetting( 'wc_admin', 'wcAdminSettings' ); +diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js +index bef2888d91..d74d3acac3 100644 +--- a/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js ++++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js +@@ -257,53 +257,55 @@ function OrdersPanel( { unreadOrdersCount, orderStatuses } ) { + return currencyContext.formatAmount( total ); + }; + +- const { orders = [], isRequesting, isError, customerItems } = useSelect( +- ( select ) => { +- const { getOrders, hasFinishedResolution, getOrdersError } = select( +- ORDERS_STORE_NAME +- ); +- // eslint-disable-next-line @wordpress/no-unused-vars-before-return +- const { getItems } = select( ITEMS_STORE_NAME ); +- +- if ( ! orderStatuses.length && unreadOrdersCount === 0 ) { +- return { isRequesting: false }; +- } +- +- /* eslint-disable @wordpress/no-unused-vars-before-return */ +- const actionableOrders = getOrders( actionableOrdersQuery, null ); ++ const { ++ orders = [], ++ isRequesting, ++ isError, ++ customerItems, ++ } = useSelect( ( select ) => { ++ const { getOrders, hasFinishedResolution, getOrdersError } = ++ select( ORDERS_STORE_NAME ); ++ // eslint-disable-next-line @wordpress/no-unused-vars-before-return ++ const { getItems } = select( ITEMS_STORE_NAME ); + +- const isRequestingActionable = hasFinishedResolution( 'getOrders', [ +- actionableOrdersQuery, +- ] ); ++ if ( ! orderStatuses.length && unreadOrdersCount === 0 ) { ++ return { isRequesting: false }; ++ } + +- if ( +- isRequestingActionable || +- unreadOrdersCount === null || +- actionableOrders === null +- ) { +- return { +- isError: Boolean( getOrdersError( actionableOrdersQuery ) ), +- isRequesting: true, +- orderStatuses, +- }; +- } ++ /* eslint-disable @wordpress/no-unused-vars-before-return */ ++ const actionableOrders = getOrders( actionableOrdersQuery, null ); + +- const customers = getItems( 'customers', { +- users: actionableOrders +- .map( ( order ) => order.customer_id ) +- .filter( ( id ) => id !== 0 ), +- _fields: [ 'id', 'name', 'country', 'user_id' ], +- } ); ++ const isRequestingActionable = hasFinishedResolution( 'getOrders', [ ++ actionableOrdersQuery, ++ ] ); + ++ if ( ++ isRequestingActionable || ++ unreadOrdersCount === null || ++ actionableOrders === null ++ ) { + return { +- orders: actionableOrders, +- isError: Boolean( getOrdersError( actionableOrders ) ), +- isRequesting: isRequestingActionable, ++ isError: Boolean( getOrdersError( actionableOrdersQuery ) ), ++ isRequesting: true, + orderStatuses, +- customerItems: customers, + }; + } +- ); ++ ++ const customers = getItems( 'customers', { ++ users: actionableOrders ++ .map( ( order ) => order.customer_id ) ++ .filter( ( id ) => id !== 0 ), ++ _fields: [ 'id', 'name', 'country', 'user_id' ], ++ } ); ++ ++ return { ++ orders: actionableOrders, ++ isError: Boolean( getOrdersError( actionableOrders ) ), ++ isRequesting: isRequestingActionable, ++ orderStatuses, ++ customerItems: customers, ++ }; ++ } ); + + if ( isError ) { + if ( ! orderStatuses.length && window.wcAdminFeatures.analytics ) { +diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/utils.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/utils.js +index 2344474b9d..b525c023b6 100644 +--- a/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/utils.js ++++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/utils.js +@@ -9,9 +9,8 @@ import { SETTINGS_STORE_NAME, ITEMS_STORE_NAME } from '@woocommerce/data'; + import { DEFAULT_ACTIONABLE_STATUSES } from '../../../analytics/settings/config'; + + export function getUnreadOrders( select, orderStatuses ) { +- const { getItemsTotalCount, getItemsError, isResolving } = select( +- ITEMS_STORE_NAME +- ); ++ const { getItemsTotalCount, getItemsError, isResolving } = ++ select( ITEMS_STORE_NAME ); + + if ( ! orderStatuses.length ) { + return 0; +@@ -51,7 +50,8 @@ export function getUnreadOrders( select, orderStatuses ) { + export function getOrderStatuses( select ) { + const { getSetting: getMutableSetting } = select( SETTINGS_STORE_NAME ); + const { +- woocommerce_actionable_order_statuses: orderStatuses = DEFAULT_ACTIONABLE_STATUSES, ++ woocommerce_actionable_order_statuses: ++ orderStatuses = DEFAULT_ACTIONABLE_STATUSES, + } = getMutableSetting( 'wc_admin', 'wcAdminSettings', {} ); + return orderStatuses; + } +@@ -65,9 +65,8 @@ export const getLowStockCountQuery = { + }; + + export function getLowStockCount( select ) { +- const { getItemsTotalCount, getItemsError, isResolving } = select( +- ITEMS_STORE_NAME +- ); ++ const { getItemsTotalCount, getItemsError, isResolving } = ++ select( ITEMS_STORE_NAME ); + + const defaultValue = null; + +diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/index.js +index 82590074f3..a95643960c 100644 +--- a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/index.js ++++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/index.js +@@ -49,12 +49,8 @@ class ReviewsPanel extends Component { + } + + deleteReview( reviewId ) { +- const { +- deleteReview, +- createNotice, +- updateReview, +- clearReviewsCache, +- } = this.props; ++ const { deleteReview, createNotice, updateReview, clearReviewsCache } = ++ this.props; + if ( reviewId ) { + deleteReview( reviewId ) + .then( () => { +@@ -370,9 +366,8 @@ export { ReviewsPanel }; + export default compose( [ + withSelect( ( select, props ) => { + const { hasUnapprovedReviews } = props; +- const { getReviews, getReviewsError, isResolving } = select( +- REVIEWS_STORE_NAME +- ); ++ const { getReviews, getReviewsError, isResolving } = ++ select( REVIEWS_STORE_NAME ); + let reviews = []; + let isError = false; + let isRequesting = false; +@@ -389,9 +384,8 @@ export default compose( [ + }; + } ), + withDispatch( ( dispatch, props ) => { +- const { deleteReview, updateReview, invalidateResolution } = dispatch( +- REVIEWS_STORE_NAME +- ); ++ const { deleteReview, updateReview, invalidateResolution } = ++ dispatch( REVIEWS_STORE_NAME ); + const { createNotice } = dispatch( 'core/notices' ); + + const clearReviewsCache = () => { +diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js +index fb8c93bc76..f72e08364e 100644 +--- a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js ++++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js +@@ -37,8 +37,7 @@ const REVIEW = { + date_created_gmt: '2020-11-20T17:28:47', + date_modified: '2020-11-20T17:28:47', + date_modified_gmt: '2020-11-20T17:28:47', +- src: +- 'https://one.wordpress.test/wp-content/uploads/2020/11/cap-2-1.jpg', ++ src: 'https://one.wordpress.test/wp-content/uploads/2020/11/cap-2-1.jpg', + name: 'cap-2-1.jpg', + alt: '', + }, +diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/utils.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/utils.js +index 1dda58d9c9..00bbd1e40f 100644 +--- a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/utils.js ++++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/utils.js +@@ -13,9 +13,8 @@ export const unapprovedReviewsQuery = { + _fields: [ 'id' ], + }; + export function getUnapprovedReviews( select ) { +- const { getReviewsTotalCount, getReviewsError, isResolving } = select( +- REVIEWS_STORE_NAME +- ); ++ const { getReviewsTotalCount, getReviewsError, isResolving } = ++ select( REVIEWS_STORE_NAME ); + + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const totalReviews = getReviewsTotalCount( unapprovedReviewsQuery ); +diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/stock/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/stock/index.js +index b2044aebe1..88d89e05a9 100644 +--- a/plugins/woocommerce-admin/client/homescreen/activity-panel/stock/index.js ++++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/stock/index.js +@@ -40,11 +40,8 @@ export class StockPanel extends Component { + } + + async updateStock( product, quantity ) { +- const { +- invalidateResolution, +- updateProductStock, +- products, +- } = this.props; ++ const { invalidateResolution, updateProductStock, products } = ++ this.props; + + const success = await updateProductStock( product, quantity ); + +@@ -80,12 +77,8 @@ export class StockPanel extends Component { + } + + render() { +- const { +- lowStockProductsCount, +- isError, +- isRequesting, +- products, +- } = this.props; ++ const { lowStockProductsCount, isError, isRequesting, products } = ++ this.props; + + if ( isError ) { + const title = __( +@@ -111,16 +104,16 @@ export class StockPanel extends Component { + // Show placeholders only for the first products fetch. + if ( isRequesting || ! products.length ) { + const numPlaceholders = Math.min( 5, lowStockProductsCount ?? 1 ); +- const placeholders = Array.from( +- new Array( numPlaceholders ) +- ).map( ( v, idx ) => ( +- +- ) ); ++ const placeholders = Array.from( new Array( numPlaceholders ) ).map( ++ ( v, idx ) => ( ++ ++ ) ++ ); + + return
{ placeholders }
; + } +@@ -144,9 +137,8 @@ StockPanel.defaultProps = { + + export default compose( + withSelect( ( select ) => { +- const { getItems, getItemsError, isResolving } = select( +- ITEMS_STORE_NAME +- ); ++ const { getItems, getItemsError, isResolving } = ++ select( ITEMS_STORE_NAME ); + + const products = Array.from( + getItems( 'products/low-in-stock', productsQuery ).values() +@@ -162,9 +154,8 @@ export default compose( + return { products, isError, isRequesting }; + } ), + withDispatch( ( dispatch ) => { +- const { invalidateResolution, updateProductStock } = dispatch( +- ITEMS_STORE_NAME +- ); ++ const { invalidateResolution, updateProductStock } = ++ dispatch( ITEMS_STORE_NAME ); + const { createNotice } = dispatch( 'core/notices' ); + + return { +diff --git a/plugins/woocommerce-admin/client/homescreen/hooks/use-headercard-experiment-hook.js b/plugins/woocommerce-admin/client/homescreen/hooks/use-headercard-experiment-hook.js +index ea090c189c..1ebd8aa5f4 100644 +--- a/plugins/woocommerce-admin/client/homescreen/hooks/use-headercard-experiment-hook.js ++++ b/plugins/woocommerce-admin/client/homescreen/hooks/use-headercard-experiment-hook.js +@@ -10,19 +10,15 @@ export const useHeadercardExperimentHook = ( + installTimestampHasResolved, + installTimestamp + ) => { +- const [ +- isLoadingExperimentAssignment, +- setIsLoadingExperimentAssignment, +- ] = useState( true ); ++ const [ isLoadingExperimentAssignment, setIsLoadingExperimentAssignment ] = ++ useState( true ); + const [ + isLoadingTwoColExperimentAssignment, + setIsLoadingTwoColExperimentAssignment, + ] = useState( true ); + const [ experimentAssignment, setExperimentAssignment ] = useState( null ); +- const [ +- twoColExperimentAssignment, +- setTwoColExperimentAssignment, +- ] = useState( null ); ++ const [ twoColExperimentAssignment, setTwoColExperimentAssignment ] = ++ useState( null ); + + useEffect( () => { + if ( installTimestampHasResolved && installTimestamp ) { +diff --git a/plugins/woocommerce-admin/client/homescreen/layout.js b/plugins/woocommerce-admin/client/homescreen/layout.js +index df1f31137d..398b82cc07 100644 +--- a/plugins/woocommerce-admin/client/homescreen/layout.js ++++ b/plugins/woocommerce-admin/client/homescreen/layout.js +@@ -49,16 +49,6 @@ const Tasks = lazy( () => + } ) ) + ); + +-const TwoColumnTasks = lazy( () => +- import( /* webpackChunkName: "two-column-tasks" */ '../two-column-tasks' ) +-); +- +-const TwoColumnTasksExtended = lazy( () => +- import( +- /* webpackChunkName: "two-column-tasks-extended" */ '../two-column-tasks/extended-task' +- ) +-); +- + export const Layout = ( { + defaultHomescreenLayout, + isBatchUpdating, +@@ -80,44 +70,8 @@ export const Layout = ( { + const isDashboardShown = ! query.task; + const activeSetupTaskList = useActiveSetupTasklist(); + +- const { +- isLoadingExperimentAssignment, +- isLoadingTwoColExperimentAssignment, +- experimentAssignment, +- twoColExperimentAssignment, +- } = { +- isLoadingExperimentAssignment: false, +- isLoadingTwoColExperimentAssignment: false, +- experimentAssignment: null, +- twoColExperimentAssignment: null, +- }; +- +- const isRunningTwoColumnExperiment = +- twoColExperimentAssignment?.variationName === 'treatment'; +- +- // New TaskList UI is enabled when either experiment is treatment. +- const isRunningTaskListExperiment = +- experimentAssignment?.variationName === 'treatment' || +- isRunningTwoColumnExperiment; +- +- // Override defaultHomescreenLayout if store is in the experiment. +- const defaultHomescreenLayoutOverride = () => { +- if ( +- isLoadingExperimentAssignment || +- isLoadingTwoColExperimentAssignment +- ) { +- return defaultHomescreenLayout; // Experiments are still loading, don't override. +- } +- +- if ( ! isRunningTaskListExperiment ) { +- return defaultHomescreenLayout; // Not in the experiment, don't override. +- } +- +- return isRunningTwoColumnExperiment ? 'two_columns' : 'single_column'; +- }; +- + const twoColumns = +- ( userPrefs.homepage_layout || defaultHomescreenLayoutOverride() ) === ++ ( userPrefs.homepage_layout || defaultHomescreenLayout ) === + 'two_columns' && hasTwoColumnContent; + + if ( isBatchUpdating && ! showInbox ) { +@@ -144,23 +98,16 @@ export const Layout = ( { + return ( + <> + +- { ! isLoadingExperimentAssignment && +- ! isLoadingTwoColExperimentAssignment && +- ! isRunningTaskListExperiment && +- ! isLoadingTaskLists && +- ! showingProgressHeader && ( +- +- ) } ++ { ! isLoadingTaskLists && ! showingProgressHeader && ( ++ ++ ) } + { } + { hasTaskList && renderTaskList() } + +@@ -174,33 +121,6 @@ export const Layout = ( { + }; + + const renderTaskList = () => { +- if ( twoColumns && isRunningTaskListExperiment ) { +- return ( +- // When running the two-column experiment, we still need to render +- // the component in the left column for the extended task list. +- +- +- +- ); +- } else if ( +- ! twoColumns && +- isRunningTaskListExperiment && +- ! isLoadingExperimentAssignment +- ) { +- return ( +- +- <> +- +- +- +- +- ); +- } +- + return ( + }> + { activeSetupTaskList && +@@ -215,15 +135,6 @@ export const Layout = ( { + + return ( + <> +- { twoColumns && isRunningTaskListExperiment && ( +- +- +- +- ) } +
{ + const { isNotesRequesting } = select( NOTES_STORE_NAME ); +- const { getOption, hasFinishedResolution } = select( +- OPTIONS_STORE_NAME +- ); ++ const { getOption, hasFinishedResolution } = ++ select( OPTIONS_STORE_NAME ); + const { + getTaskList, + getTaskLists, +@@ -313,9 +223,8 @@ export default compose( + 'getOption', + [ WELCOME_FROM_CALYPSO_MODAL_DISMISSED_OPTION_NAME ] + ); +- const fromCalypsoUrlArgIsPresent = !! window.location.search.match( +- 'from-calypso' +- ); ++ const fromCalypsoUrlArgIsPresent = ++ !! window.location.search.match( 'from-calypso' ); + + const shouldShowWelcomeFromCalypsoModal = + welcomeFromCalypsoModalDismissedResolved && +diff --git a/plugins/woocommerce-admin/client/homescreen/stats-overview/install-jetpack-cta.js b/plugins/woocommerce-admin/client/homescreen/stats-overview/install-jetpack-cta.js +index 9b9cc13e68..24196fb594 100644 +--- a/plugins/woocommerce-admin/client/homescreen/stats-overview/install-jetpack-cta.js ++++ b/plugins/woocommerce-admin/client/homescreen/stats-overview/install-jetpack-cta.js +@@ -76,9 +76,8 @@ export const InstallJetpackCTA = () => { + const { updateUserPreferences, ...userPrefs } = useUserPreferences(); + const { canUserInstallPlugins, jetpackInstallState, isBusy } = useSelect( + ( select ) => { +- const { getPluginInstallState, isPluginsRequesting } = select( +- PLUGINS_STORE_NAME +- ); ++ const { getPluginInstallState, isPluginsRequesting } = ++ select( PLUGINS_STORE_NAME ); + const installState = getPluginInstallState( 'jetpack' ); + const busyState = + isPluginsRequesting( 'getJetpackConnectUrl' ) || +diff --git a/plugins/woocommerce-admin/client/homescreen/stats-overview/test/stats-list.js b/plugins/woocommerce-admin/client/homescreen/stats-overview/test/stats-list.js +index 6c30eaced0..bb3affcf53 100644 +--- a/plugins/woocommerce-admin/client/homescreen/stats-overview/test/stats-list.js ++++ b/plugins/woocommerce-admin/client/homescreen/stats-overview/test/stats-list.js +@@ -27,8 +27,7 @@ const data = { + _links: { + api: [ + { +- href: +- 'http://tangaroa.test/wp-json/wc-analytics/reports/revenue/stats', ++ href: 'http://tangaroa.test/wp-json/wc-analytics/reports/revenue/stats', + }, + ], + report: [ { href: '/analytics/revenue' } ], +@@ -43,8 +42,7 @@ const data = { + _links: { + api: [ + { +- href: +- 'http://tangaroa.test/wp-json/wc-analytics/reports/orders/stats', ++ href: 'http://tangaroa.test/wp-json/wc-analytics/reports/orders/stats', + }, + ], + report: [ { href: '/analytics/orders' } ], +diff --git a/plugins/woocommerce-admin/client/hooks/useIsScrolled.js b/plugins/woocommerce-admin/client/hooks/useIsScrolled.js +index 975d5c422b..0c02eb63af 100644 +--- a/plugins/woocommerce-admin/client/hooks/useIsScrolled.js ++++ b/plugins/woocommerce-admin/client/hooks/useIsScrolled.js +@@ -12,9 +12,8 @@ export default function useIsScrolled() { + }; + + const scrollListener = () => { +- rafHandle.current = window.requestAnimationFrame( +- updateIsScrolled +- ); ++ rafHandle.current = ++ window.requestAnimationFrame( updateIsScrolled ); + }; + + window.addEventListener( 'scroll', scrollListener ); +diff --git a/plugins/woocommerce-admin/client/inbox-panel/dismiss-all-modal.js b/plugins/woocommerce-admin/client/inbox-panel/dismiss-all-modal.js +index 5d836c9abd..3fc82831df 100644 +--- a/plugins/woocommerce-admin/client/inbox-panel/dismiss-all-modal.js ++++ b/plugins/woocommerce-admin/client/inbox-panel/dismiss-all-modal.js +@@ -10,9 +10,8 @@ import { __ } from '@wordpress/i18n'; + const DismissAllModal = ( { onClose } ) => { + const { createNotice } = useDispatch( 'core/notices' ); + +- const { batchUpdateNotes, removeAllNotes } = useDispatch( +- NOTES_STORE_NAME +- ); ++ const { batchUpdateNotes, removeAllNotes } = ++ useDispatch( NOTES_STORE_NAME ); + + const dismissAllNotes = async () => { + recordEvent( 'wcadmin_inbox_action_dismissall', {} ); +diff --git a/plugins/woocommerce-admin/client/inbox-panel/index.js b/plugins/woocommerce-admin/client/inbox-panel/index.js +index 4edb881cbb..cc5e8b889f 100644 +--- a/plugins/woocommerce-admin/client/inbox-panel/index.js ++++ b/plugins/woocommerce-admin/client/inbox-panel/index.js +@@ -173,18 +173,13 @@ const INBOX_QUERY = { + + const InboxPanel = ( { showHeader = true } ) => { + const { createNotice } = useDispatch( 'core/notices' ); +- const { removeNote, updateNote, triggerNoteAction } = useDispatch( +- NOTES_STORE_NAME +- ); ++ const { removeNote, updateNote, triggerNoteAction } = ++ useDispatch( NOTES_STORE_NAME ); + + const { isError, isResolvingNotes, isBatchUpdating, notes } = useSelect( + ( select ) => { +- const { +- getNotes, +- getNotesError, +- isResolving, +- isNotesRequesting, +- } = select( NOTES_STORE_NAME ); ++ const { getNotes, getNotesError, isResolving, isNotesRequesting } = ++ select( NOTES_STORE_NAME ); + const WC_VERSION_61_RELEASE_DATE = moment( + '2022-01-11', + 'YYYY-MM-DD' +diff --git a/plugins/woocommerce-admin/client/index.js b/plugins/woocommerce-admin/client/index.js +index 102bae1a65..bd077ed409 100644 +--- a/plugins/woocommerce-admin/client/index.js ++++ b/plugins/woocommerce-admin/client/index.js +@@ -62,9 +62,8 @@ if ( appRoot ) { + } )( HydratedPageLayout ); + } + if ( hydrateUser ) { +- HydratedPageLayout = withCurrentUserHydration( hydrateUser )( +- HydratedPageLayout +- ); ++ HydratedPageLayout = ++ withCurrentUserHydration( hydrateUser )( HydratedPageLayout ); + } + render( , appRoot ); + } else if ( embeddedRoot ) { +@@ -73,9 +72,8 @@ if ( appRoot ) { + window.wcSettings.admin + )( EmbedLayout ); + if ( hydrateUser ) { +- HydratedEmbedLayout = withCurrentUserHydration( hydrateUser )( +- HydratedEmbedLayout +- ); ++ HydratedEmbedLayout = ++ withCurrentUserHydration( hydrateUser )( HydratedEmbedLayout ); + } + // Render the header. + render( , embeddedRoot ); +diff --git a/plugins/woocommerce-admin/client/layout/controller.js b/plugins/woocommerce-admin/client/layout/controller.js +index c905873464..fbd8b468c6 100644 +--- a/plugins/woocommerce-admin/client/layout/controller.js ++++ b/plugins/woocommerce-admin/client/layout/controller.js +@@ -358,9 +358,9 @@ window.wpNavMenuUrlUpdate = function ( query ) { + const nextQuery = getPersistedQuery( query ); + const excludedScreens = getQueryExcludedScreens(); + +- Array.from( +- document.querySelectorAll( '#adminmenu a' ) +- ).forEach( ( item ) => updateLinkHref( item, nextQuery, excludedScreens ) ); ++ Array.from( document.querySelectorAll( '#adminmenu a' ) ).forEach( ++ ( item ) => updateLinkHref( item, nextQuery, excludedScreens ) ++ ); + }; + + // When the route changes, we need to update wp-admin's menu with the correct section & current link +diff --git a/plugins/woocommerce-admin/client/layout/index.js b/plugins/woocommerce-admin/client/layout/index.js +index 62d9d54db8..a38674e3a9 100644 +--- a/plugins/woocommerce-admin/client/layout/index.js ++++ b/plugins/woocommerce-admin/client/layout/index.js +@@ -293,11 +293,8 @@ const Layout = compose( + return; + } + +- const { +- getActivePlugins, +- getInstalledPlugins, +- isJetpackConnected, +- } = select( PLUGINS_STORE_NAME ); ++ const { getActivePlugins, getInstalledPlugins, isJetpackConnected } = ++ select( PLUGINS_STORE_NAME ); + + return { + activePlugins: getActivePlugins(), +diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/index.js b/plugins/woocommerce-admin/client/layout/transient-notices/index.js +index 8f842abfb9..d4920632b5 100644 +--- a/plugins/woocommerce-admin/client/layout/transient-notices/index.js ++++ b/plugins/woocommerce-admin/client/layout/transient-notices/index.js +@@ -19,10 +19,8 @@ const QUEUED_NOTICE_FILTER = 'woocommerce_admin_queued_notice_filter'; + + function TransientNotices( props ) { + const { removeNotice: onRemove } = useDispatch( 'core/notices' ); +- const { +- createNotice: createNotice2, +- removeNotice: onRemove2, +- } = useDispatch( 'core/notices2' ); ++ const { createNotice: createNotice2, removeNotice: onRemove2 } = ++ useDispatch( 'core/notices2' ); + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + const { + currentUser = {}, +@@ -36,9 +34,8 @@ function TransientNotices( props ) { + currentUser: select( USER_STORE_NAME ).getCurrentUser(), + notices: select( 'core/notices' ).getNotices(), + notices2: select( 'core/notices2' ).getNotices(), +- noticesQueue: select( OPTIONS_STORE_NAME ).getOption( +- QUEUE_OPTION +- ), ++ noticesQueue: ++ select( OPTIONS_STORE_NAME ).getOption( QUEUE_OPTION ), + }; + } ); + +diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/snackbar/index.js b/plugins/woocommerce-admin/client/layout/transient-notices/snackbar/index.js +index 8ad412f191..b1c71f2114 100644 +--- a/plugins/woocommerce-admin/client/layout/transient-notices/snackbar/index.js ++++ b/plugins/woocommerce-admin/client/layout/transient-notices/snackbar/index.js +@@ -120,7 +120,11 @@ function Snackbar( + tabIndex="0" + role={ ! explicitDismiss ? 'button' : '' } + onKeyPress={ ! explicitDismiss ? dismissMe : noop } +- aria-label={ ! explicitDismiss ? __( 'Dismiss this notice' ) : '' } ++ aria-label={ ++ ! explicitDismiss ++ ? __( 'Dismiss this notice', 'woocommerce' ) ++ : '' ++ } + > +
+ { icon && ( +diff --git a/plugins/woocommerce-admin/client/marketing/components/knowledge-base/index.js b/plugins/woocommerce-admin/client/marketing/components/knowledge-base/index.js +index 6a2ab23e5f..e5a369cff0 100644 +--- a/plugins/woocommerce-admin/client/marketing/components/knowledge-base/index.js ++++ b/plugins/woocommerce-admin/client/marketing/components/knowledge-base/index.js +@@ -239,9 +239,8 @@ export { KnowledgeBase }; + + export default compose( + withSelect( ( select, props ) => { +- const { getBlogPosts, getBlogPostsError, isResolving } = select( +- STORE_KEY +- ); ++ const { getBlogPosts, getBlogPostsError, isResolving } = ++ select( STORE_KEY ); + + return { + posts: getBlogPosts( props.category ), +diff --git a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js b/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js +index 0bcc7aea8a..2ee875f8ba 100644 +--- a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js ++++ b/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js +@@ -73,9 +73,8 @@ InstalledExtensions.propTypes = { + + export default compose( + withSelect( ( select ) => { +- const { getInstalledPlugins, getActivatingPlugins } = select( +- STORE_KEY +- ); ++ const { getInstalledPlugins, getActivatingPlugins } = ++ select( STORE_KEY ); + + return { + plugins: getInstalledPlugins(), +diff --git a/plugins/woocommerce-admin/client/mobile-banner/banner.js b/plugins/woocommerce-admin/client/mobile-banner/banner.js +index 468db05ca2..0d4d837141 100644 +--- a/plugins/woocommerce-admin/client/mobile-banner/banner.js ++++ b/plugins/woocommerce-admin/client/mobile-banner/banner.js +@@ -23,9 +23,8 @@ export const Banner = ( { onInstall, onDismiss } ) => { + const isVisible = platform() === ANDROID_PLATFORM && ! isActioned; + + useEffect( () => { +- const layout = document.getElementsByClassName( +- 'woocommerce-layout' +- )[ 0 ]; ++ const layout = ++ document.getElementsByClassName( 'woocommerce-layout' )[ 0 ]; + + if ( isVisible && layout ) { + // This is a hack to allow the mobile banner to work in the context of the header which is +diff --git a/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js b/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js +index 20da835391..4a9f5456d7 100644 +--- a/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js ++++ b/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js +@@ -13,22 +13,19 @@ import { HighlightTooltip } from '~/activity-panel/highlight-tooltip'; + const tooltipHiddenOption = 'woocommerce_navigation_favorites_tooltip_hidden'; + + export const FavoritesTooltip = () => { +- const { +- isFavoritesResolving, +- isOptionResolving, +- isTooltipHidden, +- } = useSelect( ( select ) => { +- const { getOption, isResolving } = select( OPTIONS_STORE_NAME ); +- return { +- isFavoritesResolving: select( NAVIGATION_STORE_NAME ).isResolving( +- 'getFavorites' +- ), +- isOptionResolving: isResolving( 'getOption', [ +- tooltipHiddenOption, +- ] ), +- isTooltipHidden: getOption( tooltipHiddenOption ) === 'yes', +- }; +- } ); ++ const { isFavoritesResolving, isOptionResolving, isTooltipHidden } = ++ useSelect( ( select ) => { ++ const { getOption, isResolving } = select( OPTIONS_STORE_NAME ); ++ return { ++ isFavoritesResolving: select( ++ NAVIGATION_STORE_NAME ++ ).isResolving( 'getFavorites' ), ++ isOptionResolving: isResolving( 'getOption', [ ++ tooltipHiddenOption, ++ ] ), ++ isTooltipHidden: getOption( tooltipHiddenOption ) === 'yes', ++ }; ++ } ); + + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + +diff --git a/plugins/woocommerce-admin/client/navigation/components/header/index.js b/plugins/woocommerce-admin/client/navigation/components/header/index.js +index 89981a0e91..1da93e2c76 100644 +--- a/plugins/woocommerce-admin/client/navigation/components/header/index.js ++++ b/plugins/woocommerce-admin/client/navigation/components/header/index.js +@@ -96,7 +96,9 @@ const Header = () => { + } ); + + if ( siteIconUrl ) { +- buttonIcon = {; ++ buttonIcon = ( ++ { ++ ); + } else if ( isRequestingSiteIcon ) { + buttonIcon = null; + } +diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js b/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js +index c2bd2bc834..e2fb7b2ef4 100644 +--- a/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js ++++ b/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js +@@ -28,9 +28,8 @@ export const IntroModal = () => { + + const { isDismissed, isResolving, isWelcomeModalShown } = useSelect( + ( select ) => { +- const { getOption, isResolving: isOptionResolving } = select( +- OPTIONS_STORE_NAME +- ); ++ const { getOption, isResolving: isOptionResolving } = ++ select( OPTIONS_STORE_NAME ); + const dismissedOption = getOption( + INTRO_MODAL_DISMISSED_OPTION_NAME + ); +diff --git a/plugins/woocommerce-admin/client/navigation/test/utils.js b/plugins/woocommerce-admin/client/navigation/test/utils.js +index afe4e4134e..477f635b6a 100644 +--- a/plugins/woocommerce-admin/client/navigation/test/utils.js ++++ b/plugins/woocommerce-admin/client/navigation/test/utils.js +@@ -43,14 +43,12 @@ const sampleMenuItems = [ + { + id: 'multiple-args-plus-one', + title: 'Page with same multiple arguments plus an additional one', +- url: +- 'admin.php?page=wc-admin&path=/test-path§ion=section-name&version=22', ++ url: 'admin.php?page=wc-admin&path=/test-path§ion=section-name&version=22', + }, + { + id: 'hash-and-multiple-args', + title: 'Page with multiple arguments and a hash', +- url: +- 'admin.php?page=wc-admin&path=/test-path§ion=section-name#anchor', ++ url: 'admin.php?page=wc-admin&path=/test-path§ion=section-name#anchor', + }, + ]; + +diff --git a/plugins/woocommerce-admin/client/payments-welcome/exit-survey-modal.tsx b/plugins/woocommerce-admin/client/payments-welcome/exit-survey-modal.tsx +index f12c08ba16..fb4d8cef57 100644 +--- a/plugins/woocommerce-admin/client/payments-welcome/exit-survey-modal.tsx ++++ b/plugins/woocommerce-admin/client/payments-welcome/exit-survey-modal.tsx +@@ -31,9 +31,8 @@ function ExitSurveyModal( { + const [ isInstallChecked, setInstallChecked ] = useState( false ); + const [ isMoreInfoChecked, setMoreInfoChecked ] = useState( false ); + const [ isAnotherTimeChecked, setAnotherTimeChecked ] = useState( false ); +- const [ isSomethingElseChecked, setSomethingElseChecked ] = useState( +- false +- ); ++ const [ isSomethingElseChecked, setSomethingElseChecked ] = ++ useState( false ); + const [ comments, setComments ] = useState( '' ); + + const closeModal = () => { +diff --git a/plugins/woocommerce-admin/client/payments-welcome/index.tsx b/plugins/woocommerce-admin/client/payments-welcome/index.tsx +index 44bfc59943..1d5c06c0d0 100644 +--- a/plugins/woocommerce-admin/client/payments-welcome/index.tsx ++++ b/plugins/woocommerce-admin/client/payments-welcome/index.tsx +@@ -13,12 +13,6 @@ import { recordEvent } from '@woocommerce/tracks'; + import { useDispatch, useSelect } from '@wordpress/data'; + import { OPTIONS_STORE_NAME, PluginsStoreActions } from '@woocommerce/data'; + import apiFetch from '@wordpress/api-fetch'; +- +-/** +- * Internal dependencies +- */ +-import strings from './strings'; +-import Banner from './banner'; + import { + Visa, + MasterCard, +@@ -30,7 +24,13 @@ import { + Discover, + JCB, + UnionPay, +-} from './cards'; ++} from '@woocommerce/onboarding'; ++ ++/** ++ * Internal dependencies ++ */ ++import strings from './strings'; ++import Banner from './banner'; + import './style.scss'; + import FrequentlyAskedQuestions from './faq'; + import ExitSurveyModal from './exit-survey-modal'; +@@ -238,9 +238,8 @@ const ConnectAccountPage = () => { + : false; + + return { +- isJetpackConnected: select( +- 'wc/admin/plugins' +- ).isJetpackConnected(), ++ isJetpackConnected: ++ select( 'wc/admin/plugins' ).isJetpackConnected(), + connectUrl: + 'admin.php?wcpay-connect=1&_wpnonce=' + + getAdminSetting( 'wcpay_welcome_page_connect_nonce' ), +diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.scss b/plugins/woocommerce-admin/client/payments/payment-recommendations.scss +index c77e452e26..8a7ce28c9e 100644 +--- a/plugins/woocommerce-admin/client/payments/payment-recommendations.scss ++++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.scss +@@ -1,45 +1,3 @@ +-.woocommerce-recommended-payments-banner { +- margin: 0 15px 10px 0; +- min-width: 750px; +- animation: isLoaded; +- animation-duration: 250ms; +- +- .woocommerce-recommended-payments-banner__body { +- display: flex; +- align-items: center; +- justify-content: center; +- padding-bottom: 0; +- } +- +- .woocommerce-recommended-payments-banner__text_container { +- width: 46%; +- margin-inline: 24px; +- a { +- margin-top: 0; +- } +- +- * { +- margin-block: 1rem; +- } +- } +- +- .woocommerce-recommended-payments-banner__footer { +- display: flex; +- align-items: center; +- justify-content: center; +- } +- +- .woocommerce-recommended-payments-banner__footer_icon_container { +- display: flex; +- align-items: center; +- } +- +- .woocommerce-recommended-payments-banner__footer_icon_container > svg { +- height: 28px; +- width: 51px; +- } +-} +- + .woocommerce-recommended-payments-card { + margin: 0 15px 10px 0; + animation: isLoaded; +diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +index 17ec126cf7..babef4e867 100644 +--- a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx ++++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +@@ -38,10 +38,8 @@ const PaymentRecommendations: React.FC = () => { + ); + const [ isDismissed, setIsDismissed ] = useState< boolean >( false ); + const [ isInstalled, setIsInstalled ] = useState< boolean >( false ); +- const { +- installAndActivatePlugins, +- dismissRecommendedPlugins, +- } = useDispatch( PLUGINS_STORE_NAME ); ++ const { installAndActivatePlugins, dismissRecommendedPlugins } = ++ useDispatch( PLUGINS_STORE_NAME ); + const { createNotice } = useDispatch( 'core/notices' ); + + const { +@@ -210,7 +208,7 @@ const PaymentRecommendations: React.FC = () => { + size="20" + lineHeight="28px" + > +- { __( 'Additional ways to get paid', 'woocommerce' ) } ++ { __( 'Recommended payment providers', 'woocommerce' ) } + + { + + + + +diff --git a/plugins/woocommerce-admin/client/payments/payment-settings-banner.tsx b/plugins/woocommerce-admin/client/payments/payment-settings-banner.tsx +index a3b0d0cd5d..3831557698 100644 +--- a/plugins/woocommerce-admin/client/payments/payment-settings-banner.tsx ++++ b/plugins/woocommerce-admin/client/payments/payment-settings-banner.tsx +@@ -1,147 +1,52 @@ + /** + * External dependencies + */ +-import { Card, CardFooter, CardBody, Button } from '@wordpress/components'; +-import { Text } from '@woocommerce/experimental'; ++import { Button } from '@wordpress/components'; + import { __ } from '@wordpress/i18n'; + import { getAdminLink } from '@woocommerce/settings'; +-import interpolateComponents from '@automattic/interpolate-components'; +-import { Link } from '@woocommerce/components'; ++import { ++ WCPayBanner, ++ WCPayBannerBody, ++ WCPayBannerFooter, ++} from '@woocommerce/onboarding'; + + /** + * Internal dependencies + */ +-import { +- Visa, +- MasterCard, +- Amex, +- ApplePay, +- Giropay, +- GooglePay, +- CB, +- DinersClub, +- Discover, +- UnionPay, +- JCB, +- Sofort, +-} from '../payments-welcome/cards'; +-import WCPayBannerImage from './wcpay-banner-image'; + import './payment-recommendations.scss'; + import { getAdminSetting } from '~/utils/admin-settings'; + import { usePaymentExperiment } from './use-payments-experiment'; + +-export const PaymentMethodsIcons = () => ( +-
+- +- +- +- +- +- +- +- +- +- +- +- +-
+-); +- +-const WcPayBanner = () => { ++const WCPaySettingBanner = () => { + const WC_PAY_SETUP_URL = getAdminLink( + 'admin.php?wcpay-connect=1&_wpnonce=' + + getAdminSetting( 'wcpay_welcome_page_connect_nonce' ) + ); + + return ( +- +- +-
+- +-
+-
+- +- { __( +- 'Accept Payments and manage your business.', +- 'woocommerce' +- ) } +- +- +- { interpolateComponents( { +- mixedString: __( +- 'By using WooCommerce Payments you agree to be bound by our {{tosLink}}Terms of Service{{/tosLink}} and acknowledge that you have read our {{privacyLink}}Privacy Policy{{/privacyLink}} ', +- 'woocommerce' +- ), +- components: { +- tosLink: ( +- +- <> +- +- ), +- privacyLink: ( +- +- <> +- +- ), +- }, +- } ) } +- ++ ++ + { __( 'Get started', 'woocommerce' ) } + +-
+-
+- +-
+- +- { __( +- 'Accepted payment methods include:', +- 'woocommerce' +- ) } +- +-
+-
+- +-
+-
+- +- { __( '& more.', 'woocommerce' ) } +- +-
+-
+-
++ } ++ /> ++ ++ + ); + }; + + const DefaultPaymentMethodsHeaderText = () => ( + <> +-

Payment Methods

++

{ __( 'Payment Methods', 'woocommerce' ) }

+
+

+- Installed payment methods are listed below and can be sorted to +- control their display order on the frontend. ++ { __( ++ 'Installed payment methods are listed below and can be sorted to control their display order on the frontend.', ++ 'woocommerce' ++ ) } +

+
+ +@@ -155,7 +60,7 @@ export const PaymentsBannerWrapper = () => { + ! isLoadingExperiment && + experimentAssignment?.variationName === 'treatment' + ) { +- return ; ++ return ; + } + return ; + }; +diff --git a/plugins/woocommerce-admin/client/profile-wizard/header.js b/plugins/woocommerce-admin/client/profile-wizard/header.js +index dd925b7c4b..2a1590b414 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/header.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/header.js +@@ -23,10 +23,10 @@ export default class ProfileWizardHeader extends Component { + + shouldWarnForUnsavedChanges( step ) { + if ( typeof this.props.stepValueChanges[ step ] !== 'undefined' ) { +- const initialValues = this.props.stepValueChanges[ step ] +- .initialValues; +- const currentValues = this.props.stepValueChanges[ step ] +- .currentValues; ++ const initialValues = ++ this.props.stepValueChanges[ step ].initialValues; ++ const currentValues = ++ this.props.stepValueChanges[ step ].currentValues; + + if ( + Array.isArray( initialValues ) && +diff --git a/plugins/woocommerce-admin/client/profile-wizard/index.js b/plugins/woocommerce-admin/client/profile-wizard/index.js +index 3b3a919803..5ef6e2e42a 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/index.js +@@ -43,20 +43,16 @@ class ProfileWizard extends Component { + this.cachedActivePlugins = props.activePlugins; + this.goToNextStep = this.goToNextStep.bind( this ); + this.trackStepValueChanges = this.trackStepValueChanges.bind( this ); +- this.updateCurrentStepValues = this.updateCurrentStepValues.bind( +- this +- ); ++ this.updateCurrentStepValues = ++ this.updateCurrentStepValues.bind( this ); + this.stepValueChanges = {}; + } + + componentDidUpdate( prevProps ) { + const { step: prevStep } = prevProps.query; + const { step } = this.props.query; +- const { +- isError, +- isGetProfileItemsRequesting, +- createNotice, +- } = this.props; ++ const { isError, isGetProfileItemsRequesting, createNotice } = ++ this.props; + + const isRequestError = + ! isGetProfileItemsRequesting && prevProps.isRequesting && isError; +@@ -318,11 +314,8 @@ export default compose( + const { getProfileItems, getOnboardingError } = select( + ONBOARDING_STORE_NAME + ); +- const { +- getActivePlugins, +- getPluginsError, +- isJetpackConnected, +- } = select( PLUGINS_STORE_NAME ); ++ const { getActivePlugins, getPluginsError, isJetpackConnected } = ++ select( PLUGINS_STORE_NAME ); + + const profileItems = getProfileItems(); + +@@ -345,10 +338,8 @@ export default compose( + }; + } ), + withDispatch( ( dispatch ) => { +- const { +- connectToJetpackWithFailureRedirect, +- createErrorNotice, +- } = dispatch( PLUGINS_STORE_NAME ); ++ const { connectToJetpackWithFailureRedirect, createErrorNotice } = ++ dispatch( PLUGINS_STORE_NAME ); + const { updateNote } = dispatch( NOTES_STORE_NAME ); + const { updateOptions } = dispatch( OPTIONS_STORE_NAME ); + const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME ); +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +index bcad24b6ea..8629597deb 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +@@ -121,6 +121,9 @@ export const isSellingElsewhere = ( selectedOption ) => + export const isSellingOtherPlatformInPerson = ( selectedOption ) => + [ 'other', 'brick-mortar-other' ].includes( selectedOption ); + ++export const PERSIST_FREE_FEATURES_DATA_STORAGE_KEY = ++ 'wc-admin-free-features-tab-values'; ++ + class BusinessDetails extends Component { + constructor( props ) { + super(); +@@ -129,16 +132,19 @@ class BusinessDetails extends Component { + isPopoverVisible: false, + isValid: false, + currentTab: 'business-details', +- savedValues: null, ++ savedValues: props.initialValues, + }; + + this.onContinue = this.onContinue.bind( this ); + this.validate = this.validate.bind( this ); ++ this.persistValues = this.persistValues.bind( this ); ++ this.persistProfileItems.bind( this ); ++ + props.trackStepValueChanges( + props.step.key, +- { ...( this.state.savedValues || props.initialValues ) }, +- this.savedValues || props.initialValues, +- this.persistProfileItems.bind( this ) ++ props.initialValues, ++ this.state.savedValues, ++ this.persistValues + ); + } + +@@ -146,11 +152,8 @@ class BusinessDetails extends Component { + extensionInstallationOptions, + installableExtensionsData + ) { +- const { +- createNotice, +- goToNextStep, +- installAndActivatePlugins, +- } = this.props; ++ const { createNotice, goToNextStep, installAndActivatePlugins } = ++ this.props; + + const alreadyActivatedExtensions = installableExtensionsData.reduce( + ( actExtensions, bundle ) => { +@@ -194,10 +197,11 @@ class BusinessDetails extends Component { + .then( ( response ) => { + const totalInstallationTime = + window.performance.now() - installationStartTime; +- const installedExtensionsData = prepareExtensionTrackingInstallationData( +- extensionInstallationOptions, +- response +- ); ++ const installedExtensionsData = ++ prepareExtensionTrackingInstallationData( ++ extensionInstallationOptions, ++ response ++ ); + + recordEvent( + 'storeprofiler_store_business_features_installed_and_activated', +@@ -242,8 +246,28 @@ class BusinessDetails extends Component { + } ); + } + ++ async persistValues() { ++ await this.persistProfileItems(); ++ ++ try { ++ window.localStorage.setItem( ++ PERSIST_FREE_FEATURES_DATA_STORAGE_KEY, ++ JSON.stringify( this.state.savedValues.freeFeaturesTab ) ++ ); ++ } catch ( error ) { ++ this.props.createNotice( ++ 'error', ++ __( ++ 'There was a problem saving free features state', ++ 'woocommerce' ++ ) ++ ); ++ } ++ } ++ + async persistProfileItems( additions = {} ) { + const { updateProfileItems, createNotice } = this.props; ++ const { businessDetailsTab = {} } = this.state.savedValues; + + const { + number_employees: numberEmployees, +@@ -253,7 +277,7 @@ class BusinessDetails extends Component { + revenue, + selling_venues: sellingVenues, + setup_client: isSetupClient, +- } = this.state.savedValues; ++ } = businessDetailsTab; + + const updates = { + number_employees: numberEmployees, +@@ -404,12 +428,13 @@ class BusinessDetails extends Component { + + return ( + { + this.setState( { +- savedValues: values, ++ savedValues: { ++ ...this.state.savedValues, ++ businessDetailsTab: values, ++ }, + currentTab: BUSINESS_FEATURES_TAB_NAME, + } ); + +@@ -420,10 +445,17 @@ class BusinessDetails extends Component { + } ); + } } + onChange={ ( _, values, isValid ) => { +- this.setState( { savedValues: values, isValid } ); ++ const savedValues = { ++ ...this.state.savedValues, ++ businessDetailsTab: values, ++ }; ++ this.setState( { ++ savedValues, ++ isValid, ++ } ); + this.props.updateCurrentStepValues( + this.props.step.key, +- values ++ savedValues + ); + } } + validate={ this.validate } +@@ -614,6 +646,9 @@ class BusinessDetails extends Component { + + renderFreeFeaturesStep() { + const { isInstallingActivating, settings, profileItems } = this.props; ++ const { ++ savedValues: { freeFeaturesTab }, ++ } = this.state; + const country = settings.woocommerce_default_country + ? settings.woocommerce_default_country + : null; +@@ -649,15 +684,34 @@ class BusinessDetails extends Component { + country={ country } + industry={ profileItems.industry } + productTypes={ profileItems.product_types } ++ installExtensionOptions={ ++ freeFeaturesTab.installExtensionOptions ++ } ++ setInstallExtensionOptions={ ( ++ installExtensionOptions ++ ) => { ++ const savedValues = { ++ ...this.state.savedValues, ++ freeFeaturesTab: { ++ ...freeFeaturesTab, ++ installExtensionOptions, ++ }, ++ }; ++ this.setState( { ++ savedValues, ++ } ); ++ this.props.updateCurrentStepValues( ++ this.props.step.key, ++ savedValues ++ ); ++ } } + /> + + ); + } + + render() { +- const { initialValues } = this.props; +- +- // There is a hack here to help us manage the selected tab programatically. ++ // There is a hack here to help us manage the selected tab programmatically. + // We set the tab name "current-tab". when its the one we want selected. This tricks + // the logic in the TabPanel and allows us to switch which tab has the name "current-tab" + // and force it to re-render with a different tab selected. +@@ -670,7 +724,8 @@ class BusinessDetails extends Component { + this.setState( { + currentTab: tabName, + savedValues: +- this.state.savedValues || initialValues, ++ this.state.savedValues || ++ this.props.initialValues, + } ); + recordEvent( 'storeprofiler_step_view', { + step: tabName, +@@ -719,9 +774,8 @@ export const BusinessFeaturesList = compose( + const { getProfileItems, getOnboardingError } = select( + ONBOARDING_STORE_NAME + ); +- const { getPluginsError, isPluginsRequesting } = select( +- PLUGINS_STORE_NAME +- ); ++ const { getPluginsError, isPluginsRequesting } = ++ select( PLUGINS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); + + return { +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js +index 945b46cf4c..d0b2396c28 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/index.js +@@ -211,22 +211,27 @@ export const ExtensionSection = ( { + ); + }; + +-export const createInstallExtensionOptions = ( installableExtensions ) => { +- return installableExtensions.reduce( +- ( acc, curr ) => { +- const plugins = curr.plugins.reduce( ( pluginAcc, plugin ) => { +- return { +- ...pluginAcc, +- [ plugin.key ]: true, +- }; +- }, {} ); ++export const createInstallExtensionOptions = ( ++ installableExtensions, ++ prevInstallExtensionOptions ++) => { ++ return installableExtensions.reduce( ( acc, curr ) => { ++ const plugins = curr.plugins.reduce( ( pluginAcc, plugin ) => { ++ if ( acc.hasOwnProperty( plugin.key ) ) { ++ return pluginAcc; ++ } ++ + return { +- ...acc, +- ...plugins, ++ ...pluginAcc, ++ [ plugin.key ]: true, + }; +- }, +- { install_extensions: true } +- ); ++ }, {} ); ++ ++ return { ++ ...acc, ++ ...plugins, ++ }; ++ }, prevInstallExtensionOptions ); + }; + + export const SelectiveExtensionsBundle = ( { +@@ -235,23 +240,20 @@ export const SelectiveExtensionsBundle = ( { + country, + productTypes, + industry, ++ setInstallExtensionOptions, ++ installExtensionOptions = { install_extensions: true }, + } ) => { + const [ showExtensions, setShowExtensions ] = useState( false ); +- const [ installExtensionOptions, setInstallExtensionOptions ] = useState( { +- install_extensions: true, +- } ); +- const { +- freeExtensions: freeExtensionBundleByCategory, +- isResolving, +- } = useSelect( ( select ) => { +- const { getFreeExtensions, hasFinishedResolution } = select( +- ONBOARDING_STORE_NAME +- ); +- return { +- freeExtensions: getFreeExtensions(), +- isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), +- }; +- } ); ++ const { freeExtensions: freeExtensionBundleByCategory, isResolving } = ++ useSelect( ( select ) => { ++ const { getFreeExtensions, hasFinishedResolution } = select( ++ ONBOARDING_STORE_NAME ++ ); ++ return { ++ freeExtensions: getFreeExtensions(), ++ isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), ++ }; ++ } ); + + const { invalidateResolutionForStoreSelector } = useDispatch( + ONBOARDING_STORE_NAME +@@ -283,11 +285,15 @@ export const SelectiveExtensionsBundle = ( { + }, [ freeExtensionBundleByCategory, productTypes, country ] ); + + useEffect( () => { +- if ( ! isInstallingActivating ) { +- setInstallExtensionOptions( () => +- createInstallExtensionOptions( installableExtensions ) +- ); ++ if ( isInstallingActivating || installableExtensions.length === 0 ) { ++ return; + } ++ setInstallExtensionOptions( ++ createInstallExtensionOptions( ++ installableExtensions, ++ installExtensionOptions ++ ) ++ ); + // Disable reason: This effect should only called when the installableExtensions are changed. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ installableExtensions ] ); +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/test/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/test/index.js +index 7145bed459..213a688e39 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/test/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/selective-extensions-bundle/test/index.js +@@ -96,9 +96,25 @@ describe( 'Selective extensions bundle', () => { + } ); + + it( 'should list installable free extensions from obw/basics and obw/grow', () => { ++ const mockSetInstallExtensionOptions = jest.fn(); ++ // Render once to get installExtensionOptions ++ render( ++ ++ ); ++ + const { getByText, queryByText } = render( +- ++ + ); ++ + expect( + getByText( new RegExp( pluginNames.mailpoet ) ) + ).toBeInTheDocument(); +@@ -117,7 +133,10 @@ describe( 'Selective extensions bundle', () => { + + it( 'should list installable extensions when dropdown is clicked', () => { + const { getAllByRole, getByText, queryByText } = render( +- ++ + ); + const collapseButton = getAllByRole( 'button' ).find( + ( item ) => item.textContent === '' +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/index.js +index dd4a640caa..06b4c21b1d 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/index.js +@@ -2,13 +2,17 @@ + * External dependencies + */ + import { useSelect } from '@wordpress/data'; ++import { useMemo } from '@wordpress/element'; + import { Spinner } from '@woocommerce/components'; + import { ONBOARDING_STORE_NAME, SETTINGS_STORE_NAME } from '@woocommerce/data'; + + /** + * Internal dependencies + */ +-import { BusinessFeaturesList } from './flows/selective-bundle'; ++import { ++ BusinessFeaturesList, ++ PERSIST_FREE_FEATURES_DATA_STORAGE_KEY, ++} from './flows/selective-bundle'; + import './style.scss'; + + export const BusinessDetailsStep = ( props ) => { +@@ -18,13 +22,30 @@ export const BusinessDetailsStep = ( props ) => { + ! select( ONBOARDING_STORE_NAME ).hasFinishedResolution( + 'getProfileItems' + ) || +- ! select( +- SETTINGS_STORE_NAME +- ).hasFinishedResolution( 'getSettings', [ 'general' ] ), ++ ! select( SETTINGS_STORE_NAME ).hasFinishedResolution( ++ 'getSettings', ++ [ 'general' ] ++ ), + profileItems: select( ONBOARDING_STORE_NAME ).getProfileItems(), + }; + } ); + ++ const freeFeaturesTabValues = useMemo( () => { ++ try { ++ const values = JSON.parse( ++ window.localStorage.getItem( ++ PERSIST_FREE_FEATURES_DATA_STORAGE_KEY ++ ) ++ ); ++ if ( values ) { ++ return values; ++ } ++ } catch ( _e ) { ++ // Skip errors ++ } ++ return { install_extensions: true }; ++ }, [] ); ++ + if ( isLoading ) { + return ( +
+@@ -34,13 +55,16 @@ export const BusinessDetailsStep = ( props ) => { + } + + const initialValues = { +- number_employees: profileItems.number_employees || '', +- other_platform: profileItems.other_platform || '', +- other_platform_name: profileItems.other_platform_name || '', +- product_count: profileItems.product_count || '', +- selling_venues: profileItems.selling_venues || '', +- revenue: profileItems.revenue || '', +- setup_client: profileItems.setup_client || false, ++ businessDetailsTab: { ++ number_employees: profileItems.number_employees || '', ++ other_platform: profileItems.other_platform || '', ++ other_platform_name: profileItems.other_platform_name || '', ++ product_count: profileItems.product_count || '', ++ selling_venues: profileItems.selling_venues || '', ++ revenue: profileItems.revenue || '', ++ setup_client: profileItems.setup_client || false, ++ }, ++ freeFeaturesTab: freeFeaturesTabValues, + }; + + return ( +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/test/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/test/index.js +index 6b3001239c..752d39a7be 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/test/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/test/index.js +@@ -60,9 +60,8 @@ describe( 'BusinessDetails', () => { + install_wcpay: true, + }; + +- const installedExtensions = prepareExtensionTrackingData( +- extensions +- ); ++ const installedExtensions = ++ prepareExtensionTrackingData( extensions ); + + expect( installedExtensions ).toEqual( expectedExtensions ); + } ); +@@ -170,7 +169,8 @@ describe( 'BusinessDetails', () => { + ]; + + const values = createInstallExtensionOptions( +- installableExtensions ++ installableExtensions, ++ { install_extensions: true } + ); + + expect( values ).toEqual( +@@ -192,12 +192,10 @@ describe( 'BusinessDetails', () => { + expect( notSellingElsewhere ).toBeFalsy(); + } ); + test( 'isSellingOtherPlatformInPerson', () => { +- const sellingAnotherPlatformAndInPerson = isSellingOtherPlatformInPerson( +- 'brick-mortar-other' +- ); +- const notSellingAnotherPlatformAndInPerson = isSellingOtherPlatformInPerson( +- 'no' +- ); ++ const sellingAnotherPlatformAndInPerson = ++ isSellingOtherPlatformInPerson( 'brick-mortar-other' ); ++ const notSellingAnotherPlatformAndInPerson = ++ isSellingOtherPlatformInPerson( 'no' ); + + expect( sellingAnotherPlatformAndInPerson ).toBeTruthy(); + expect( notSellingAnotherPlatformAndInPerson ).toBeFalsy(); +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/industry.js b/plugins/woocommerce-admin/client/profile-wizard/steps/industry.js +index cb82f489c2..f869bb4e16 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/industry.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/industry.js +@@ -287,11 +287,8 @@ class Industry extends Component { + + export default compose( + withSelect( ( select ) => { +- const { +- getProfileItems, +- getOnboardingError, +- isOnboardingRequesting, +- } = select( ONBOARDING_STORE_NAME ); ++ const { getProfileItems, getOnboardingError, isOnboardingRequesting } = ++ select( ONBOARDING_STORE_NAME ); + const { getSettings } = select( SETTINGS_STORE_NAME ); + const { general: locationSettings = {} } = getSettings( 'general' ); + +@@ -299,9 +296,8 @@ export default compose( + isError: Boolean( getOnboardingError( 'updateProfileItems' ) ), + profileItems: getProfileItems(), + locationSettings, +- isProfileItemsRequesting: isOnboardingRequesting( +- 'updateProfileItems' +- ), ++ isProfileItemsRequesting: ++ isOnboardingRequesting( 'updateProfileItems' ), + }; + } ), + withDispatch( ( dispatch ) => { +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js +index 73166b5354..b6c31bc07f 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/product-types/index.js +@@ -198,12 +198,8 @@ export class ProductTypes extends Component { + + render() { + const { productTypes = [] } = this.props; +- const { +- error, +- isMonthlyPricing, +- isWCPayInstalled, +- selected, +- } = this.state; ++ const { error, isMonthlyPricing, isWCPayInstalled, selected } = ++ this.state; + const { + countryCode, + isInstallingActivating, +@@ -356,26 +352,23 @@ export default compose( + isOnboardingRequesting, + } = select( ONBOARDING_STORE_NAME ); + const { getSettings } = select( SETTINGS_STORE_NAME ); +- const { getInstalledPlugins, isPluginsRequesting } = select( +- PLUGINS_STORE_NAME +- ); ++ const { getInstalledPlugins, isPluginsRequesting } = ++ select( PLUGINS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); + + return { + isError: Boolean( getOnboardingError( 'updateProfileItems' ) ), + profileItems: getProfileItems(), +- isProfileItemsRequesting: isOnboardingRequesting( +- 'updateProfileItems' +- ), ++ isProfileItemsRequesting: ++ isOnboardingRequesting( 'updateProfileItems' ), + installedPlugins: getInstalledPlugins(), + isInstallingActivating: + isPluginsRequesting( 'installPlugins' ) || + isPluginsRequesting( 'activatePlugins' ), + countryCode: getCountryCode( settings.woocommerce_default_country ), + productTypes: getProductTypes(), +- isProductTypesRequesting: ! hasFinishedResolution( +- 'getProductTypes' +- ), ++ isProductTypesRequesting: ++ ! hasFinishedResolution( 'getProductTypes' ), + }; + } ), + withDispatch( ( dispatch ) => { +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js +index 6883494dc8..fb13fb23d9 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js +@@ -346,10 +346,8 @@ export class StoreDetails extends Component { + if ( skipping ) { + skipProfiler(); + } else { +- this.onContinue( +- values +- ).then( () => +- this.props.goToNextStep() ++ this.onContinue( values ).then( ++ () => this.props.goToNextStep() + ); + } + } } +@@ -470,11 +468,8 @@ StoreDetails.contextType = CurrencyContext; + + export default compose( + withSelect( ( select ) => { +- const { +- getSettings, +- getSettingsError, +- isUpdateSettingsRequesting, +- } = select( SETTINGS_STORE_NAME ); ++ const { getSettings, getSettingsError, isUpdateSettingsRequesting } = ++ select( SETTINGS_STORE_NAME ); + const { + getProfileItems, + isOnboardingRequesting, +@@ -547,13 +542,10 @@ export default compose( + } ), + withDispatch( ( dispatch ) => { + const { createNotice } = dispatch( 'core/notices' ); +- const { +- invalidateResolutionForStoreSelector, +- updateProfileItems, +- } = dispatch( ONBOARDING_STORE_NAME ); +- const { updateAndPersistSettingsForGroup } = dispatch( +- SETTINGS_STORE_NAME +- ); ++ const { invalidateResolutionForStoreSelector, updateProfileItems } = ++ dispatch( ONBOARDING_STORE_NAME ); ++ const { updateAndPersistSettingsForGroup } = ++ dispatch( SETTINGS_STORE_NAME ); + + return { + createNotice, +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/theme/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/theme/index.js +index 1ea656e083..f74e33c7b4 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/theme/index.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/theme/index.js +@@ -342,7 +342,8 @@ class Theme extends Component { + render() { + const { activeTab, chosen, demo } = this.state; + const themes = this.getThemes( activeTab ); +- const activeThemeSupportsWooCommerce = this.doesActiveThemeSupportWooCommerce(); ++ const activeThemeSupportsWooCommerce = ++ this.doesActiveThemeSupportWooCommerce(); + + return ( + +@@ -419,17 +420,13 @@ class Theme extends Component { + + export default compose( + withSelect( ( select ) => { +- const { +- getProfileItems, +- getOnboardingError, +- isOnboardingRequesting, +- } = select( ONBOARDING_STORE_NAME ); ++ const { getProfileItems, getOnboardingError, isOnboardingRequesting } = ++ select( ONBOARDING_STORE_NAME ); + + return { + isError: Boolean( getOnboardingError( 'updateProfileItems' ) ), +- isUpdatingProfileItems: isOnboardingRequesting( +- 'updateProfileItems' +- ), ++ isUpdatingProfileItems: ++ isOnboardingRequesting( 'updateProfileItems' ), + profileItems: getProfileItems(), + }; + } ), +diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/usage-modal.js b/plugins/woocommerce-admin/client/profile-wizard/steps/usage-modal.js +index b6facf7539..79fc55613c 100644 +--- a/plugins/woocommerce-admin/client/profile-wizard/steps/usage-modal.js ++++ b/plugins/woocommerce-admin/client/profile-wizard/steps/usage-modal.js +@@ -22,13 +22,8 @@ class UsageModal extends Component { + } + + async componentDidUpdate( prevProps, prevState ) { +- const { +- hasErrors, +- isRequesting, +- onClose, +- onContinue, +- createNotice, +- } = this.props; ++ const { hasErrors, isRequesting, onClose, onContinue, createNotice } = ++ this.props; + const { isLoadingScripts, isRequestStarted } = this.state; + + // We can't rely on isRequesting props only because option update might be triggered by other component. +diff --git a/plugins/woocommerce-admin/client/settings-recommendations/dismissable-list.tsx b/plugins/woocommerce-admin/client/settings-recommendations/dismissable-list.tsx +index e23cadda3e..cfeecd18c5 100644 +--- a/plugins/woocommerce-admin/client/settings-recommendations/dismissable-list.tsx ++++ b/plugins/woocommerce-admin/client/settings-recommendations/dismissable-list.tsx +@@ -56,9 +56,8 @@ export const DismissableList: React.FC< { + className?: string; + } > = ( { children, className, dismissOptionName } ) => { + const isVisible = useSelect( ( select ) => { +- const { getOption, hasFinishedResolution } = select( +- OPTIONS_STORE_NAME +- ); ++ const { getOption, hasFinishedResolution } = ++ select( OPTIONS_STORE_NAME ); + + const hasFinishedResolving = hasFinishedResolution( 'getOption', [ + dismissOptionName, +diff --git a/plugins/woocommerce-admin/client/settings-recommendations/recommendations-eligibility-wrapper.tsx b/plugins/woocommerce-admin/client/settings-recommendations/recommendations-eligibility-wrapper.tsx +index d9bd277566..047afd7e57 100644 +--- a/plugins/woocommerce-admin/client/settings-recommendations/recommendations-eligibility-wrapper.tsx ++++ b/plugins/woocommerce-admin/client/settings-recommendations/recommendations-eligibility-wrapper.tsx +@@ -11,9 +11,8 @@ const RecommendationsEligibilityWrapper: React.FC = ( { children } ) => { + const { currentUserCan } = useUser(); + + const isMarketplaceSuggestionsEnabled = useSelect( ( select ) => { +- const { getOption, hasFinishedResolution } = select( +- OPTIONS_STORE_NAME +- ); ++ const { getOption, hasFinishedResolution } = ++ select( OPTIONS_STORE_NAME ); + + const hasFinishedResolving = hasFinishedResolution( 'getOption', [ + SHOW_MARKETPLACE_SUGGESTION_OPTION, +diff --git a/plugins/woocommerce-admin/client/shipping/experimental-shipping-recommendations.tsx b/plugins/woocommerce-admin/client/shipping/experimental-shipping-recommendations.tsx +new file mode 100644 +index 0000000000..d4d9598b21 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/shipping/experimental-shipping-recommendations.tsx +@@ -0,0 +1,77 @@ ++/** ++ * External dependencies ++ */ ++import { useSelect } from '@wordpress/data'; ++ ++import { ++ PLUGINS_STORE_NAME, ++ SETTINGS_STORE_NAME, ++ ONBOARDING_STORE_NAME, ++} from '@woocommerce/data'; ++ ++/** ++ * Internal dependencies ++ */ ++import { getCountryCode } from '~/dashboard/utils'; ++import WooCommerceServicesItem from './experimental-woocommerce-services-item'; ++import { ShippingRecommendationsList } from './shipping-recommendations'; ++import './shipping-recommendations.scss'; ++ ++const ShippingRecommendations: React.FC = () => { ++ const { ++ activePlugins, ++ installedPlugins, ++ countryCode, ++ isJetpackConnected, ++ isSellingDigitalProductsOnly, ++ } = useSelect( ( select ) => { ++ const settings = select( SETTINGS_STORE_NAME ).getSettings< { ++ general?: { ++ woocommerce_default_country: string; ++ }; ++ } >( 'general' ); ++ ++ const { ++ getActivePlugins, ++ getInstalledPlugins, ++ isJetpackConnected: _isJetpackConnected, ++ } = select( PLUGINS_STORE_NAME ); ++ ++ const profileItems = select( ONBOARDING_STORE_NAME ).getProfileItems() ++ .product_types; ++ ++ return { ++ activePlugins: getActivePlugins(), ++ installedPlugins: getInstalledPlugins(), ++ countryCode: getCountryCode( ++ settings.general?.woocommerce_default_country ++ ), ++ isJetpackConnected: _isJetpackConnected(), ++ isSellingDigitalProductsOnly: ++ profileItems?.length === 1 && profileItems[ 0 ] === 'downloads', ++ }; ++ } ); ++ ++ if ( ++ activePlugins.includes( 'woocommerce-services' ) && ++ isJetpackConnected ++ ) { ++ return null; ++ } ++ ++ if ( countryCode !== 'US' || isSellingDigitalProductsOnly ) { ++ return null; ++ } ++ ++ return ( ++ ++ ++ ++ ); ++}; ++ ++export default ShippingRecommendations; +diff --git a/plugins/woocommerce-admin/client/shipping/experimental-woocommerce-services-item.tsx b/plugins/woocommerce-admin/client/shipping/experimental-woocommerce-services-item.tsx +new file mode 100644 +index 0000000000..558e07be21 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/shipping/experimental-woocommerce-services-item.tsx +@@ -0,0 +1,72 @@ ++/** ++ * External dependencies ++ */ ++import { __ } from '@wordpress/i18n'; ++import { Button, ExternalLink } from '@wordpress/components'; ++import { Pill } from '@woocommerce/components'; ++import { getNewPath, navigateTo } from '@woocommerce/navigation'; ++import { recordEvent } from '@woocommerce/tracks'; ++import { useContext, useMemo } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import './woocommerce-services-item.scss'; ++import WooIcon from './woo-icon.svg'; ++import { LayoutContext } from '~/layout'; ++ ++const WooCommerceServicesItem: React.FC< { ++ isWCSInstalled: boolean | undefined; ++} > = ( { isWCSInstalled } ) => { ++ const layoutContext = useContext( LayoutContext ); ++ const updatedLayoutContext = useMemo( ++ () => layoutContext.getExtendedContext( 'wc-settings' ), ++ [ layoutContext ] ++ ); ++ const handleSetupClick = () => { ++ recordEvent( 'tasklist_click', { ++ task_name: 'shipping-recommendation', ++ context: updatedLayoutContext.toString(), ++ } ); ++ navigateTo( { ++ url: getNewPath( { task: 'shipping-recommendation' }, '/', {} ), ++ } ); ++ }; ++ ++ return ( ++
++
++ WooCommerce Service Logo ++
++
++ ++ { __( 'WooCommerce Shipping', 'woocommerce' ) } ++ { __( 'Recommended', 'woocommerce' ) } ++ ++ ++ { __( ++ 'Print USPS and DHL Express labels straight from your WooCommerce dashboard and save on shipping.', ++ 'woocommerce' ++ ) } ++
++ ++ { __( 'Learn more', 'woocommerce' ) } ++ ++
++
++
++ ++
++
++ ); ++}; ++ ++export default WooCommerceServicesItem; +diff --git a/plugins/woocommerce-admin/client/shipping/shipping-recommendations-wrapper.tsx b/plugins/woocommerce-admin/client/shipping/shipping-recommendations-wrapper.tsx +index 5c315885ae..fbfa17c53d 100644 +--- a/plugins/woocommerce-admin/client/shipping/shipping-recommendations-wrapper.tsx ++++ b/plugins/woocommerce-admin/client/shipping/shipping-recommendations-wrapper.tsx +@@ -8,13 +8,19 @@ import { lazy, Suspense } from '@wordpress/element'; + */ + import { EmbeddedBodyProps } from '../embedded-body-layout/embedded-body-props'; + import RecommendationsEligibilityWrapper from '../settings-recommendations/recommendations-eligibility-wrapper'; ++import { ShippingTour } from './shipping-tour'; + +-const ShippingRecommendationsLoader = lazy( +- () => +- import( +- /* webpackChunkName: "shipping-recommendations" */ './shipping-recommendations' +- ) +-); ++const ShippingRecommendationsLoader = lazy( () => { ++ if ( window.wcAdminFeatures[ 'shipping-smart-defaults' ] ) { ++ return import( ++ /* webpackChunkName: "shipping-recommendations" */ './experimental-shipping-recommendations' ++ ); ++ } ++ ++ return import( ++ /* webpackChunkName: "shipping-recommendations" */ './shipping-recommendations' ++ ); ++} ); + + export const ShippingRecommendations: React.FC< EmbeddedBodyProps > = ( { + page, +@@ -41,6 +47,9 @@ export const ShippingRecommendations: React.FC< EmbeddedBodyProps > = ( { + return ( + + ++ { window.wcAdminFeatures[ 'shipping-setting-tour' ] && ( ++ ++ ) } + + + +diff --git a/plugins/woocommerce-admin/client/shipping/shipping-recommendations.tsx b/plugins/woocommerce-admin/client/shipping/shipping-recommendations.tsx +index 7968e46ad0..67a2aab7e7 100644 +--- a/plugins/woocommerce-admin/client/shipping/shipping-recommendations.tsx ++++ b/plugins/woocommerce-admin/client/shipping/shipping-recommendations.tsx +@@ -52,7 +52,7 @@ const useInstallPlugin = () => { + return [ pluginsBeingSetup, handleSetup ] as const; + }; + +-const ShippingRecommendationsList: React.FC = ( { children } ) => ( ++export const ShippingRecommendationsList: React.FC = ( { children } ) => ( + { ++ const { ++ hasCreatedDefaultShippingZones, ++ hasReviewedDefaultShippingOptions, ++ isLoading, ++ } = useSelect( ( select ) => { ++ const { hasFinishedResolution, getOption } = ++ select( OPTIONS_STORE_NAME ); ++ ++ return { ++ isLoading: ++ ! hasFinishedResolution( 'getOption', [ ++ CREATED_DEFAULTS_OPTION, ++ ] ) && ++ ! hasFinishedResolution( 'getOption', [ ++ REVIEWED_DEFAULTS_OPTION, ++ ] ), ++ hasCreatedDefaultShippingZones: ++ getOption( CREATED_DEFAULTS_OPTION ) === 'yes', ++ hasReviewedDefaultShippingOptions: ++ getOption( REVIEWED_DEFAULTS_OPTION ) === 'yes', ++ }; ++ } ); ++ ++ return { ++ isLoading, ++ show: ++ ! isLoading && ++ hasCreatedDefaultShippingZones && ++ ! hasReviewedDefaultShippingOptions, ++ }; ++}; ++ ++type NonEmptySelectorArray = readonly [ string, ...string[] ]; ++ ++const computeDims = ( elementsSelectors: NonEmptySelectorArray ) => { ++ const rects = elementsSelectors.map( ( elementSelector ) => { ++ const rect = document ++ ?.querySelector( elementSelector ) ++ ?.getBoundingClientRect(); ++ ++ if ( ! rect ) { ++ throw new Error( ++ "Shipping tour: Couldn't find element with selector: " + ++ elementSelector ++ ); ++ } ++ return rect; ++ } ); ++ ++ const originCoords = document ++ .querySelector( `.${ FLOATER_WRAPPER_CLASS }` ) ++ ?.getBoundingClientRect() || { top: 0, left: 0 }; ++ ++ const top = Math.min( ...rects.map( ( rect ) => rect.top ) ); ++ const left = Math.min( ...rects.map( ( rect ) => rect.left ) ); ++ const right = Math.max( ...rects.map( ( rect ) => rect.right ) ); ++ const bottom = Math.max( ...rects.map( ( rect ) => rect.bottom ) ); ++ const width = right - left; ++ const height = bottom - top; ++ ++ // offset top and left from origin ++ const topOffset = top - originCoords.top; ++ const leftOffset = left - originCoords.left; ++ ++ return { left: leftOffset, top: topOffset, width, height }; ++}; ++ ++const TourFloater = ( { dims }: { dims: Partial< DOMRect > } ) => { ++ return ( ++
++ ); ++}; ++ ++// this is defines the elements to be spotlit for each step ++const spotlitElementsSelectors: Array< NonEmptySelectorArray > = [ ++ [ ++ // just use bottom right element and top left element instead of all rects ++ // top left = table header cell for sort handles ++ 'th.wc-shipping-zone-sort', ++ // bottom right = worldwide region cell ++ 'tr.wc-shipping-zone-worldwide > td.wc-shipping-zone-region', ++ ], ++ [ ++ // selectors for rightmost column (shipping methods) ++ 'th.wc-shipping-zone-methods', ++ 'tr.wc-shipping-zone-worldwide > td.wc-shipping-zone-methods', ++ ], ++]; ++ ++const TourFloaterWrapper = ( { step }: { step: number } ) => { ++ const thisRef = useRef< HTMLDivElement >( null ); ++ useLayoutEffect( () => { ++ // this moves the element to the correct place which is right before the table element ++ if ( thisRef.current?.parentElement ) { ++ thisRef.current.parentElement.insertBefore( ++ thisRef.current, ++ document.querySelector( 'table.wc-shipping-zones' ) ++ ); ++ } ++ }, [] ); ++ ++ const currentStepSelectors = ++ spotlitElementsSelectors[ step ] ?? ++ spotlitElementsSelectors[ spotlitElementsSelectors.length - 1 ]; ++ ++ const [ dims, setDims ] = useState( computeDims( currentStepSelectors ) ); ++ useEffect( () => { ++ setDims( computeDims( currentStepSelectors ) ); ++ const observer = new ResizeObserver( () => { ++ setDims( computeDims( currentStepSelectors ) ); ++ } ); ++ ++ const shippingSettingsTableElement = document.querySelector( ++ SHIPPING_ZONES_SETTINGS_TABLE_CLASS ++ ); ++ ++ if ( ! shippingSettingsTableElement ) { ++ throw new Error( ++ "Shipping tour: Couldn't find shipping settings table element with selector: " + ++ SHIPPING_ZONES_SETTINGS_TABLE_CLASS ++ ); ++ } ++ ++ observer.observe( shippingSettingsTableElement ); ++ ++ return () => { ++ observer.disconnect(); ++ }; ++ }, [ currentStepSelectors ] ); ++ ++ const shippingSettingsTableParentElement = document.querySelector( ++ SHIPPING_ZONES_SETTINGS_TABLE_CLASS ++ )?.parentElement; ++ ++ if ( ! shippingSettingsTableParentElement ) { ++ throw new Error( ++ "Shipping tour: Couldn't find shipping settings table parent element with selector: " + ++ SHIPPING_ZONES_SETTINGS_TABLE_CLASS ++ ); ++ } ++ /** ++ * use ReactDOM.createPortal to inject our element into non-React controlled DOM ++ * unfortunately createPortal uses .appendChild which puts it in the wrong place, ++ * we want it to be before the table ++ */ ++ return createPortal( ++
++ { } ++
, ++ shippingSettingsTableParentElement ++ ); ++}; ++ ++export const ShippingTour = () => { ++ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); ++ const { show: showTour } = useShowShippingTour(); ++ ++ const [ step, setStepNumber ] = useState( 0 ); ++ ++ const tourConfig: TourKitTypes.WooConfig = { ++ placement: 'auto', ++ options: { ++ effects: { ++ spotlight: { ++ interactivity: { ++ enabled: false, ++ }, ++ }, ++ liveResize: { ++ mutation: true, ++ resize: true, ++ }, ++ }, ++ callbacks: { ++ onNextStep: ( currentStepIndex ) => ++ setStepNumber( currentStepIndex + 1 ), ++ onPreviousStep: ( currentStepIndex ) => ++ setStepNumber( currentStepIndex - 1 ), ++ }, ++ }, ++ steps: [ ++ { ++ referenceElements: { ++ desktop: `.${ FLOATER_CLASS }`, ++ }, ++ meta: { ++ name: 'shipping-zones', ++ heading: __( 'Shipping zones', 'woocommerce' ), ++ descriptions: { ++ desktop: ( ++ <> ++ ++ { __( ++ 'We added a few shipping zones for you based on your location, but you can manage them at any time.', ++ 'woocommerce' ++ ) } ++ ++
++ ++ { __( ++ 'A shipping zone is a geographic area where a certain set of shipping methods are offered.', ++ 'woocommerce' ++ ) } ++ ++ ++ ), ++ }, ++ }, ++ }, ++ { ++ referenceElements: { ++ desktop: `.${ FLOATER_CLASS }`, ++ }, ++ meta: { ++ name: 'shipping-methods', ++ heading: __( 'Shipping methods', 'woocommerce' ), ++ descriptions: { ++ desktop: __( ++ 'We defaulted to some recommended shipping methods based on your store location, but you can manage them at any time within each shipping zone settings.', ++ 'woocommerce' ++ ), ++ }, ++ }, ++ }, ++ ], ++ closeHandler: () => { ++ updateOptions( { ++ [ REVIEWED_DEFAULTS_OPTION ]: 'yes', ++ } ); ++ }, ++ }; ++ ++ const isWcsSectionPresent = document.querySelector( WCS_LINK_SELECTOR ); ++ ++ const isShippingRecommendationsPresent = document.querySelector( ++ SHIPPING_RECOMMENDATIONS_SELECTOR ++ ); ++ ++ if ( isWcsSectionPresent ) { ++ tourConfig.steps.push( { ++ referenceElements: { ++ desktop: WCS_LINK_SELECTOR, ++ }, ++ meta: { ++ name: 'woocommerce-shipping', ++ heading: __( 'WooCommerce Shipping', 'woocommerce' ), ++ descriptions: { ++ desktop: __( ++ 'Print USPS and DHL labels straight from your WooCommerce dashboard and save on shipping thanks to discounted rates. You can manage WooCommerce Shipping in this section.', ++ 'woocommerce' ++ ), ++ }, ++ }, ++ } ); ++ } ++ ++ if ( isShippingRecommendationsPresent ) { ++ tourConfig.steps.push( { ++ referenceElements: { ++ desktop: SHIPPING_RECOMMENDATIONS_SELECTOR, ++ }, ++ meta: { ++ name: 'shipping-recommendations', ++ heading: __( 'WooCommerce Shipping', 'woocommerce' ), ++ descriptions: { ++ desktop: __( ++ 'If you’d like to speed up your process and print your shipping label straight from your WooCommerce dashboard, WooCommerce Shipping may be for you! ', ++ 'woocommerce' ++ ), ++ }, ++ }, ++ } ); ++ } ++ ++ if ( showTour ) { ++ return ( ++
++ ++ ++
++ ); ++ } ++ ++ return null; ++}; +diff --git a/plugins/woocommerce-admin/client/shipping/test/experimental-shipping-recommendations.tsx b/plugins/woocommerce-admin/client/shipping/test/experimental-shipping-recommendations.tsx +new file mode 100644 +index 0000000000..e4a82ebc87 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/shipping/test/experimental-shipping-recommendations.tsx +@@ -0,0 +1,96 @@ ++/** ++ * External dependencies ++ */ ++import { render, screen } from '@testing-library/react'; ++import { useSelect } from '@wordpress/data'; ++ ++/** ++ * Internal dependencies ++ */ ++import ShippingRecommendations from '../experimental-shipping-recommendations'; ++ ++jest.mock( '@wordpress/data', () => ( { ++ ...jest.requireActual( '@wordpress/data' ), ++ useSelect: jest.fn(), ++} ) ); ++jest.mock( '../../settings-recommendations/dismissable-list', () => ( { ++ DismissableList: ( ( { children } ) => children ) as React.FC, ++ DismissableListHeading: ( ( { children } ) => children ) as React.FC, ++} ) ); ++ ++const defaultSelectReturn = { ++ getActivePlugins: () => [], ++ getInstalledPlugins: () => [], ++ isJetpackConnected: () => false, ++ getSettings: () => ( { ++ general: { ++ woocommerce_default_country: 'US', ++ }, ++ } ), ++ getProfileItems: () => ( {} ), ++}; ++ ++describe( 'ShippingRecommendations', () => { ++ beforeEach( () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => ( { ...defaultSelectReturn } ) ) ++ ); ++ } ); ++ ++ it( 'should not render when WCS is already installed and Jetpack is connected', () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => ( { ++ ...defaultSelectReturn, ++ getActivePlugins: () => [ 'woocommerce-services' ], ++ isJetpackConnected: () => true, ++ } ) ) ++ ); ++ render( ); ++ ++ expect( ++ screen.queryByText( 'WooCommerce Shipping' ) ++ ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should not render when store location is not US', () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => ( { ++ ...defaultSelectReturn, ++ getSettings: () => ( { ++ general: { ++ woocommerce_default_country: 'JP', ++ }, ++ } ), ++ } ) ) ++ ); ++ render( ); ++ ++ expect( ++ screen.queryByText( 'WooCommerce Shipping' ) ++ ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should not render when store sells digital products only', () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => ( { ++ ...defaultSelectReturn, ++ getProfileItems: () => ( { ++ product_types: [ 'downloads' ], ++ } ), ++ } ) ) ++ ); ++ render( ); ++ ++ expect( ++ screen.queryByText( 'WooCommerce Shipping' ) ++ ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should render WCS when not installed', () => { ++ render( ); ++ ++ expect( ++ screen.queryByText( 'WooCommerce Shipping' ) ++ ).toBeInTheDocument(); ++ } ); ++} ); +diff --git a/plugins/woocommerce-admin/client/shipping/test/experimental-woocommerce-services-item.tsx b/plugins/woocommerce-admin/client/shipping/test/experimental-woocommerce-services-item.tsx +new file mode 100644 +index 0000000000..c1db856958 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/shipping/test/experimental-woocommerce-services-item.tsx +@@ -0,0 +1,50 @@ ++/** ++ * External dependencies ++ */ ++import { render, screen } from '@testing-library/react'; ++import { recordEvent } from '@woocommerce/tracks'; ++ ++/** ++ * Internal dependencies ++ */ ++import WooCommerceServicesItem from '../experimental-woocommerce-services-item'; ++jest.mock( '@woocommerce/tracks', () => ( { ++ ...jest.requireActual( '@woocommerce/tracks' ), ++ recordEvent: jest.fn(), ++} ) ); ++ ++describe( 'WooCommerceServicesItem', () => { ++ it( 'should render WCS item with CTA = "Get started" when WCS is not installed', () => { ++ render( ); ++ ++ expect( ++ screen.queryByText( 'WooCommerce Shipping' ) ++ ).toBeInTheDocument(); ++ ++ expect( ++ screen.queryByRole( 'button', { name: 'Get started' } ) ++ ).toBeInTheDocument(); ++ } ); ++ ++ it( 'should render WCS item with CTA = "Activate" when WCS is installed', () => { ++ render( ); ++ ++ expect( ++ screen.queryByText( 'WooCommerce Shipping' ) ++ ).toBeInTheDocument(); ++ ++ expect( ++ screen.queryByRole( 'button', { name: 'Activate' } ) ++ ).toBeInTheDocument(); ++ } ); ++ ++ it( 'should record track when clicking setup button', () => { ++ render( ); ++ ++ screen.queryByRole( 'button', { name: 'Get started' } )?.click(); ++ expect( recordEvent ).toHaveBeenCalledWith( 'tasklist_click', { ++ context: 'root/wc-settings', ++ task_name: 'shipping-recommendation', ++ } ); ++ } ); ++} ); +diff --git a/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations-wrapper.test.tsx b/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations-wrapper.test.tsx +new file mode 100644 +index 0000000000..8c46c4ad43 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations-wrapper.test.tsx +@@ -0,0 +1,116 @@ ++/** ++ * External dependencies ++ */ ++import { render } from '@testing-library/react'; ++import { useSelect } from '@wordpress/data'; ++ ++/** ++ * Internal dependencies ++ */ ++import { ShippingRecommendations } from '../shipping-recommendations-wrapper'; ++ ++jest.mock( '@wordpress/data', () => ( { ++ ...jest.requireActual( '@wordpress/data' ), ++ useSelect: jest.fn(), ++ useDispatch: jest.fn(), ++} ) ); ++ ++jest.mock( '@wordpress/element', () => ( { ++ ...jest.requireActual( '@wordpress/element' ), ++ Suspense: () =>
WooCommerce Shipping
, ++} ) ); ++ ++const eligibleSelectReturn = { ++ getOption: () => 'yes', ++ getCurrentUser: () => ( { ++ is_super_admin: true, ++ } ), ++ hasStartedResolution: () => true, ++ hasFinishedResolution: () => true, ++}; ++ ++describe( 'ShippingRecommendations', () => { ++ beforeEach( () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => eligibleSelectReturn ) ++ ); ++ } ); ++ ++ it( 'should not render when section is not empty', () => { ++ const { queryByText } = render( ++ ++ ); ++ ++ expect( queryByText( 'WooCommerce Shipping' ) ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should not render when zone_id is not empty', () => { ++ const { queryByText } = render( ++ ++ ); ++ ++ expect( queryByText( 'WooCommerce Shipping' ) ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should not render when woocommerce_show_marketplace_suggestions is "no"', () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => ( { ++ ...eligibleSelectReturn, ++ getOption: () => 'no', ++ } ) ) ++ ); ++ const { queryByText } = render( ++ ++ ); ++ expect( queryByText( 'WooCommerce Shipping' ) ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should not render when user is not allowed', () => { ++ ( useSelect as jest.Mock ).mockImplementation( ( fn ) => ++ fn( () => ( { ++ ...eligibleSelectReturn, ++ getCurrentUser: () => ( { ++ is_super_admin: false, ++ capabilities: {}, ++ } ), ++ } ) ) ++ ); ++ const { queryByText } = render( ++ ++ ); ++ expect( queryByText( 'WooCommerce Shipping' ) ).not.toBeInTheDocument(); ++ } ); ++ ++ it( 'should render WCS', async () => { ++ const { getByText } = render( ++ ++ ); ++ ++ expect( getByText( 'WooCommerce Shipping' ) ).toBeInTheDocument(); ++ } ); ++} ); +diff --git a/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations.test.js b/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations.test.js +index 5cc220e4dc..af3f68aa71 100644 +--- a/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations.test.js ++++ b/plugins/woocommerce-admin/client/shipping/test/shipping-recommendations.test.js +@@ -46,7 +46,7 @@ describe( 'ShippingRecommendations', () => { + render( ); + + expect( +- screen.queryByText( 'Woocommerce Shipping' ) ++ screen.queryByText( 'WooCommerce Shipping' ) + ).not.toBeInTheDocument(); + } ); + +@@ -54,7 +54,7 @@ describe( 'ShippingRecommendations', () => { + render( ); + + expect( +- screen.queryByText( 'Woocommerce Shipping' ) ++ screen.queryByText( 'WooCommerce Shipping' ) + ).toBeInTheDocument(); + } ); + +diff --git a/plugins/woocommerce-admin/client/shipping/woocommerce-services-item.tsx b/plugins/woocommerce-admin/client/shipping/woocommerce-services-item.tsx +index ce39b0b31b..44496a5a22 100644 +--- a/plugins/woocommerce-admin/client/shipping/woocommerce-services-item.tsx ++++ b/plugins/woocommerce-admin/client/shipping/woocommerce-services-item.tsx +@@ -57,7 +57,7 @@ const WooCommerceServicesItem: React.FC< { +
+
+ +- { __( 'Woocommerce Shipping', 'woocommerce' ) } ++ { __( 'WooCommerce Shipping', 'woocommerce' ) } + { __( 'Recommended', 'woocommerce' ) } + + +diff --git a/plugins/woocommerce-admin/client/tasks/fills/Marketing/Plugin.tsx b/plugins/woocommerce-admin/client/tasks/fills/Marketing/Plugin.tsx +index 40ebcc455d..161743c9ff 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/Marketing/Plugin.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/Marketing/Plugin.tsx +@@ -81,7 +81,7 @@ export const Plugin: React.FC< PluginProps > = ( { + onManage( slug ); + } } + > +- { __( 'Manage', 'woocommmerce-admin' ) } ++ { __( 'Manage', 'woocommerce' ) } + + ) } + { isInstalled && ! isActive && ( +@@ -91,7 +91,7 @@ export const Plugin: React.FC< PluginProps > = ( { + isSecondary + onClick={ () => installAndActivate( slug ) } + > +- { __( 'Activate', 'woocommmerce-admin' ) } ++ { __( 'Activate', 'woocommerce' ) } + + ) } + { ! isInstalled && ( +@@ -101,7 +101,7 @@ export const Plugin: React.FC< PluginProps > = ( { + isSecondary + onClick={ () => installAndActivate( slug ) } + > +- { __( 'Get started', 'woocommmerce-admin' ) } ++ { __( 'Get started', 'woocommerce' ) } + + ) } +
+diff --git a/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx +index 1514ed436d..1eb1a9eb5a 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/Marketing/index.tsx +@@ -32,14 +32,8 @@ export const transformExtensionToPlugin = ( + activePlugins: string[], + installedPlugins: string[] + ): PluginProps => { +- const { +- description, +- image_url, +- is_built_by_wc, +- key, +- manage_url, +- name, +- } = extension; ++ const { description, image_url, is_built_by_wc, key, manage_url, name } = ++ extension; + const slug = getPluginSlug( key ); + return { + description, +@@ -60,7 +54,11 @@ export const getMarketingExtensionLists = ( + ): [ PluginProps[], PluginListProps[] ] => { + const installed: PluginProps[] = []; + const lists: PluginListProps[] = []; +- freeExtensions.forEach( ( list ) => { ++ const freeExtensionsRandomized: ExtensionList[] = freeExtensions.sort( ++ () => Math.random() - 0.5 ++ ); // Randomize the order sections appear. ++ ++ freeExtensionsRandomized.forEach( ( list ) => { + if ( ! ALLOWED_PLUGIN_LISTS.includes( list.key ) ) { + return; + } +@@ -103,26 +101,21 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => { + ); + const { actionTask } = useDispatch( ONBOARDING_STORE_NAME ); + const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); +- const { +- activePlugins, +- freeExtensions, +- installedPlugins, +- isResolving, +- } = useSelect( ( select ) => { +- const { getActivePlugins, getInstalledPlugins } = select( +- PLUGINS_STORE_NAME +- ); +- const { getFreeExtensions, hasFinishedResolution } = select( +- ONBOARDING_STORE_NAME +- ); +- +- return { +- activePlugins: getActivePlugins(), +- freeExtensions: getFreeExtensions(), +- installedPlugins: getInstalledPlugins(), +- isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), +- }; +- } ); ++ const { activePlugins, freeExtensions, installedPlugins, isResolving } = ++ useSelect( ( select ) => { ++ const { getActivePlugins, getInstalledPlugins } = ++ select( PLUGINS_STORE_NAME ); ++ const { getFreeExtensions, hasFinishedResolution } = select( ++ ONBOARDING_STORE_NAME ++ ); ++ ++ return { ++ activePlugins: getActivePlugins(), ++ freeExtensions: getFreeExtensions(), ++ installedPlugins: getInstalledPlugins(), ++ isResolving: ! hasFinishedResolution( 'getFreeExtensions' ), ++ }; ++ } ); + + const [ installedExtensions, pluginLists ] = useMemo( + () => +@@ -144,6 +137,9 @@ const Marketing: React.FC< MarketingProps > = ( { onComplete } ) => { + installed_extensions: installedExtensions.map( + ( extension ) => extension.slug + ), ++ section_order: pluginLists ++ .map( ( list ) => list.key ) ++ .join( ', ' ), + } ); + + createNoticesFromResponse( response ); +diff --git a/plugins/woocommerce-admin/client/tasks/fills/Marketing/test/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/Marketing/test/index.tsx +index addbdb2a1d..27a665ce4b 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/Marketing/test/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/Marketing/test/index.tsx +@@ -117,10 +117,11 @@ describe( 'getMarketingExtensionLists', () => { + [], + [] + ); ++ const listKeys = lists.map( ( list ) => list.key ); + + expect( lists.length ).toBe( 2 ); +- expect( lists[ 0 ].key ).toBe( 'task-list/reach' ); +- expect( lists[ 1 ].key ).toBe( 'task-list/grow' ); ++ expect( listKeys ).toContain( 'task-list/reach' ); ++ expect( listKeys ).toContain( 'task-list/grow' ); + } ); + + test( 'should separate installed plugins', () => { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js +index 289243ed3c..bd03f21442 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js +@@ -12,6 +12,7 @@ import './List.scss'; + + export const List = ( { + heading, ++ headingDescription, + markConfigured, + recommendation, + paymentGateways, +@@ -19,7 +20,16 @@ export const List = ( { + } ) => { + return ( + +- { heading && { heading } } ++ { heading && ( ++ ++ { heading } ++ { headingDescription && ( ++

++ { headingDescription } ++

++ ) } ++
++ ) } + { paymentGateways.map( ( paymentGateway ) => { + const { id } = paymentGateway; + return ( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Setup.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Setup.js +index 333e14a4c3..89fbf12524 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Setup.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Setup.js +@@ -45,24 +45,22 @@ export const Setup = ( { markConfigured, paymentGateway } ) => { + PAYMENT_GATEWAYS_STORE_NAME + ); + +- const { +- isOptionUpdating, +- isPaymentGatewayResolving, +- needsPluginInstall, +- } = useSelect( ( select ) => { +- const { isOptionsUpdating } = select( OPTIONS_STORE_NAME ); +- const { isResolving } = select( PAYMENT_GATEWAYS_STORE_NAME ); +- const activePlugins = select( PLUGINS_STORE_NAME ).getActivePlugins(); +- const pluginsToInstall = plugins.filter( +- ( m ) => ! activePlugins.includes( m ) +- ); ++ const { isOptionUpdating, isPaymentGatewayResolving, needsPluginInstall } = ++ useSelect( ( select ) => { ++ const { isOptionsUpdating } = select( OPTIONS_STORE_NAME ); ++ const { isResolving } = select( PAYMENT_GATEWAYS_STORE_NAME ); ++ const activePlugins = ++ select( PLUGINS_STORE_NAME ).getActivePlugins(); ++ const pluginsToInstall = plugins.filter( ++ ( m ) => ! activePlugins.includes( m ) ++ ); + +- return { +- isOptionUpdating: isOptionsUpdating(), +- isPaymentGatewayResolving: isResolving( 'getPaymentGateways' ), +- needsPluginInstall: !! pluginsToInstall.length, +- }; +- } ); ++ return { ++ isOptionUpdating: isOptionsUpdating(), ++ isPaymentGatewayResolving: isResolving( 'getPaymentGateways' ), ++ needsPluginInstall: !! pluginsToInstall.length, ++ }; ++ } ); + + useEffect( () => { + if ( needsPluginInstall ) { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js +index ee4ad87a86..61786ba8f7 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js +@@ -2,16 +2,12 @@ + * External dependencies + */ + import { __ } from '@wordpress/i18n'; +-import interpolateComponents from '@automattic/interpolate-components'; +-import { Link, Pill } from '@woocommerce/components'; +-import { recordEvent } from '@woocommerce/tracks'; +-import { Text } from '@woocommerce/experimental'; + import { +- WCPayCard, +- WCPayCardHeader, +- WCPayCardFooter, +- WCPayCardBody, +- SetupRequired, ++ WCPayBanner, ++ WCPayBannerFooter, ++ WCPayBannerBody, ++ WCPayBenefits, ++ WCPayBannerImageCut, + } from '@woocommerce/onboarding'; + import { useDispatch } from '@wordpress/data'; + +@@ -21,27 +17,10 @@ import { useDispatch } from '@wordpress/data'; + + import { Action } from '../Action'; + import { connectWcpay } from './utils'; +- +-const TosPrompt = () => +- interpolateComponents( { +- mixedString: __( +- 'Upon clicking "Get started", you agree to the {{link}}Terms of service{{/link}}. Next we’ll ask you to share a few details about your business to create your account.', +- 'woocommerce' +- ), +- components: { +- link: ( +- +- ), +- }, +- } ); ++import './suggestion.scss'; + + export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { + const { +- description, + id, + needsSetup, + installed, +@@ -62,40 +41,31 @@ export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { + } + + return ( +- +- +- { installed && needsSetup ? ( +- +- ) : ( +- { __( 'Recommended', 'woocommerce' ) } +- ) } +- +- +- { +- recordEvent( 'tasklist_payment_learn_more' ); +- } } +- /> +- +- +- <> +- +- +- +- +- +- +- ++
++ ++ ++ } ++ bannerImage={ } ++ /> ++ ++ ++ ++
+ ); + }; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/suggestion.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/suggestion.scss +new file mode 100644 +index 0000000000..7568be8200 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/suggestion.scss +@@ -0,0 +1,33 @@ ++.woocommerce-wcpay-suggestion { ++ .woocommerce-recommended-payments-banner { ++ min-width: 100%; ++ margin-bottom: 24px; ++ ++ .woocommerce-recommended-payments-banner__body { ++ padding: 30px 0 0 40px; ++ justify-content: space-between; ++ } ++ ++ .woocommerce-recommended-payments-banner__text_container { ++ width: 305px; ++ margin-left: 0; ++ } ++ ++ .woocommerce-recommended-payments-banner__footer { ++ flex-direction: column; ++ align-items: flex-start; ++ padding: 20px 38.2px 30px 40px; ++ } ++ ++ .woocommerce-recommended-payments-banner__footer_icon_container { ++ margin-top: 12px; ++ margin-left: -5px; ++ ++ > svg { ++ width: 38px; ++ height: 24px; ++ margin-right: 8px; ++ } ++ } ++ } ++} +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +index 894f567399..524ff50767 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +@@ -14,7 +14,6 @@ import { useMemo, useCallback, useEffect } from '@wordpress/element'; + import { registerPlugin } from '@wordpress/plugins'; + import { WooOnboardingTask } from '@woocommerce/onboarding'; + import { getNewPath } from '@woocommerce/navigation'; +-import { getAdminLink } from '@woocommerce/settings'; + import { Button } from '@wordpress/components'; + import ExternalIcon from 'gridicons/dist/external'; + +@@ -204,29 +203,53 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { + ); + } + ++ let additionalSectionHeading = __( ++ 'Choose a payment provider', ++ 'woocommerce' ++ ); ++ let additionalSectionHeadingDescription = __( ++ 'To start accepting online payments', ++ 'woocommerce' ++ ); ++ if ( isWCPaySupported ) { ++ if ( isWCPayOrOtherCategoryDoneSetup ) { ++ additionalSectionHeading = __( ++ 'Additional payment options', ++ 'woocommerce' ++ ); ++ additionalSectionHeadingDescription = __( ++ 'Give your customers additional choices in ways to pay.', ++ 'woocommerce' ++ ); ++ } else { ++ additionalSectionHeading = __( ++ 'Other payment providers', ++ 'woocommerce' ++ ); ++ additionalSectionHeadingDescription = __( ++ 'Try one of the alternative payment providers.', ++ 'woocommerce' ++ ); ++ } ++ } ++ + const additionalSection = !! additionalGateways.length && ( + +- { __( 'See more', 'woocommerce' ) } +- +- +- ) ++ + } + > + ); +@@ -248,7 +271,10 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { + <> + + + { additionalSection } +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss +index 4f72919997..465f700a26 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss +@@ -20,6 +20,16 @@ + font-weight: 400; + line-height: 28px; + margin: 0; ++ display: flex; ++ flex-direction: column; ++ align-items: flex-start; ++ } ++ ++ .woocommerce-task-payment-header__description { ++ margin: 0; ++ color: $gray-700; ++ font-weight: 400; ++ font-size: 14px; + } + + .components-card__footer { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +index c1d710c865..b56e28545a 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +@@ -26,8 +26,7 @@ const paymentGatewaySuggestions = [ + title: 'Stripe', + content: + 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/stripe.png', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/stripe.png', + plugins: [ 'woocommerce-gateway-stripe' ], + is_visible: true, + recommendation_priority: 3, +@@ -39,8 +38,7 @@ const paymentGatewaySuggestions = [ + title: 'PayPal Payments', + content: + "Safe and secure payments using credit cards or your customer's PayPal account.", +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', + plugins: [ 'woocommerce-paypal-payments' ], + is_visible: true, + category_other: [ 'US' ], +@@ -50,8 +48,7 @@ const paymentGatewaySuggestions = [ + id: 'cod', + title: 'Cash on delivery', + content: 'Take payments in cash upon delivery.', +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/cod.svg', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/cod.svg', + is_visible: true, + is_offline: true, + }, +@@ -59,8 +56,7 @@ const paymentGatewaySuggestions = [ + id: 'bacs', + title: 'Direct bank transfer', + content: 'Take payments via bank transfer.', +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/bacs.svg', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/bacs.svg', + is_visible: true, + is_offline: true, + }, +@@ -69,8 +65,7 @@ const paymentGatewaySuggestions = [ + title: 'WooCommerce Payments', + content: + 'Manage transactions without leaving your WordPress Dashboard. Only with WooCommerce Payments.', +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/wcpay.svg', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/wcpay.svg', + plugins: [ 'woocommerce-payments' ], + description: + 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your store’s dashboard - with no setup costs or monthly fees.', +@@ -82,8 +77,7 @@ const paymentGatewaySuggestions = [ + title: 'Eway', + content: + 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/eway.png', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/eway.png', + plugins: [ 'woocommerce-gateway-eway' ], + is_visible: true, + category_other: [ 'US' ], +@@ -124,8 +118,12 @@ describe( 'PaymentGatewaySuggestions', () => { + expect( paymentTitles ).toEqual( [] ); + + expect( +- container.getElementsByTagName( 'title' )[ 0 ].textContent +- ).toBe( 'WooCommerce Payments' ); ++ container ++ .querySelector( ++ '.woocommerce-recommended-payments__header-heading' ++ ) ++ .textContent.includes( 'WooCommerce Payments' ) ++ ).toBe( true ); + } ); + + test( 'should render all payment gateways if no WCPay', () => { +@@ -145,6 +143,10 @@ describe( 'PaymentGatewaySuggestions', () => { + /> + ); + ++ expect( ++ screen.getByText( 'Choose a payment provider' ) ++ ).toBeInTheDocument(); ++ + const paymentTitleElements = container.querySelectorAll( + '.woocommerce-task-payment__title > span:first-child' + ); +@@ -201,8 +203,7 @@ describe( 'PaymentGatewaySuggestions', () => { + title: 'PayPal Payments', + content: + "Safe and secure payments using credit cards or your customer's PayPal account.", +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', + plugins: [ 'woocommerce-paypal-payments' ], + is_visible: true, + }, +@@ -245,6 +246,10 @@ describe( 'PaymentGatewaySuggestions', () => { + /> + ); + ++ expect( ++ screen.getByText( 'Additional payment options' ) ++ ).toBeInTheDocument(); ++ + const paymentTitleElements = container.querySelectorAll( + '.woocommerce-task-payment__title' + ); +@@ -274,8 +279,7 @@ describe( 'PaymentGatewaySuggestions', () => { + title: 'PayPal Payments', + content: + "Safe and secure payments using credit cards or your customer's PayPal account.", +- image: +- 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', ++ image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', + plugins: [ 'woocommerce-paypal-payments' ], + is_visible: true, + }, +@@ -295,7 +299,7 @@ describe( 'PaymentGatewaySuggestions', () => { + } ); + } ); + +- test( 'should record event correctly when other payment methods is clicked', () => { ++ test( 'should record event correctly when Other payment providers is clicked', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { +@@ -313,7 +317,7 @@ describe( 'PaymentGatewaySuggestions', () => { + /> + ); + +- fireEvent.click( screen.getByText( 'Other payment methods' ) ); ++ fireEvent.click( screen.getByText( 'Other payment providers' ) ); + + // By default it's hidden, so when toggle it shows. + // Second call after "tasklist_payments_options". +@@ -323,7 +327,7 @@ describe( 'PaymentGatewaySuggestions', () => { + 'tasklist_payment_show_toggle', + { + toggle: 'show', +- payment_method_count: paymentGatewaySuggestions.length - 1, // Minus one for WCPay since it's not counted in "other payment methods". ++ payment_method_count: paymentGatewaySuggestions.length - 1, // Minus one for WCPay since it's not counted in "Other payment providers". + }, + ] ); + } ); +@@ -346,7 +350,7 @@ describe( 'PaymentGatewaySuggestions', () => { + /> + ); + +- fireEvent.click( screen.getByText( 'Other payment methods' ) ); ++ fireEvent.click( screen.getByText( 'Other payment providers' ) ); + fireEvent.click( screen.getByText( 'See more' ) ); + expect( + recordEvent.mock.calls[ recordEvent.mock.calls.length - 1 ] +diff --git a/plugins/woocommerce-admin/client/tasks/fills/appearance.js b/plugins/woocommerce-admin/client/tasks/fills/appearance.js +index 5b12d7c987..cf89d248ba 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/appearance.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/appearance.js +@@ -248,13 +248,8 @@ class Appearance extends Component { + } + + getSteps() { +- const { +- isDirty, +- isPending, +- logo, +- storeNoticeText, +- isUpdatingLogo, +- } = this.state; ++ const { isDirty, isPending, logo, storeNoticeText, isUpdatingLogo } = ++ this.state; + + const steps = [ + { +@@ -378,12 +373,8 @@ class Appearance extends Component { + } + + render() { +- const { +- isPending, +- stepIndex, +- isUpdatingLogo, +- isUpdatingNotice, +- } = this.state; ++ const { isPending, stepIndex, isUpdatingLogo, isUpdatingNotice } = ++ this.state; + const currentStep = this.getSteps()[ stepIndex ].key; + + return ( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/components/load-sample-product-modal.tsx b/plugins/woocommerce-admin/client/tasks/fills/components/load-sample-product-modal.tsx +index 13bdd208a7..daeaa3224b 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/components/load-sample-product-modal.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/components/load-sample-product-modal.tsx +@@ -21,10 +21,13 @@ const LoadSampleProductModal: React.FC = () => { + > + + +- { __( 'Loading sample products' ) } ++ { __( 'Loading sample products', 'woocommerce' ) } + + +- { __( 'We are loading 9 sample products into your store' ) } ++ { __( ++ 'We are loading 9 sample products into your store', ++ 'woocommerce' ++ ) } + + + ); +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/index.tsx +index 09aa7569dd..e53171764d 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/index.tsx +@@ -44,14 +44,12 @@ export const Products = () => { + [ recordCompletionTime ] + ); + +- const { +- loadSampleProduct, +- isLoadingSampleProducts, +- } = useLoadSampleProducts( { +- redirectUrlAfterSuccess: getAdminLink( +- 'edit.php?post_type=product&wc_onboarding_active_task=products' +- ), +- } ); ++ const { loadSampleProduct, isLoadingSampleProducts } = ++ useLoadSampleProducts( { ++ redirectUrlAfterSuccess: getAdminLink( ++ 'edit.php?post_type=product&wc_onboarding_active_task=products' ++ ), ++ } ); + + const productTypeListItems = useProductTypeListItems( + getProductTypes( { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/test/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/test/index.tsx +index fcfe17c62e..8982298d7b 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/test/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-import-products/test/index.tsx +@@ -44,8 +44,7 @@ describe( 'Products', () => { + + userEvent.click( + getByRole( 'menuitem', { +- name: +- 'FROM A CSV FILE Import all products at once by uploading a CSV file.', ++ name: 'FROM A CSV FILE Import all products at once by uploading a CSV file.', + } ) + ); + await waitFor( () => +@@ -63,8 +62,7 @@ describe( 'Products', () => { + + userEvent.click( + getByRole( 'menuitem', { +- name: +- 'FROM CART2CART Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. Learn more (opens in a new tab)', ++ name: 'FROM CART2CART Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. Learn more (opens in a new tab)', + } ) + ); + await waitFor( () => +@@ -92,8 +90,7 @@ describe( 'Products', () => { + + userEvent.click( + getByRole( 'menuitem', { +- name: +- 'FROM A CSV FILE Import all products at once by uploading a CSV file.', ++ name: 'FROM A CSV FILE Import all products at once by uploading a CSV file.', + } ) + ); + await waitFor( () => { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/card-layout.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/card-layout.tsx +index fa63449ee9..1ec937d537 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/card-layout.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/card-layout.tsx +@@ -30,7 +30,8 @@ const CardLayout: React.FC< CardProps > = ( { items } ) => { + + { interpolateComponents( { + mixedString: __( +- '{{sbLink}}Start blank{{/sbLink}} or select a product type:' ++ '{{sbLink}}Start blank{{/sbLink}} or select a product type:', ++ 'woocommerce' + ), + components: { + sbLink: ( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/constants.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/constants.tsx +index d12ac2767c..6d29f88fc3 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/constants.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/constants.tsx +@@ -76,7 +76,8 @@ export const LoadSampleProductType = { + key: 'load-sample-product' as const, + title: __( 'can’t decide?', 'woocommerce' ), + content: __( +- 'Load sample products and see what they look like in your store.' ++ 'Load sample products and see what they look like in your store.', ++ 'woocommerce' + ), + before: , + after: , +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/footer.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/footer.tsx +index bf9216aadd..90831371c4 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/footer.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/footer.tsx +@@ -25,7 +25,8 @@ const Footer: React.FC = () => { + + { interpolateComponents( { + mixedString: __( +- '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}} or {{_3rdLink}}use a 3rd party migration plugin{{/_3rdLink}}.' ++ '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}} or {{_3rdLink}}use a 3rd party migration plugin{{/_3rdLink}}.', ++ 'woocommerce' + ), + components: { + importCSVLink: ( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/index.tsx +index 6dcbb24e54..6e36365e2c 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/index.tsx +@@ -59,10 +59,8 @@ export const Products = () => { + isConfirmingLoadSampleProducts, + setIsConfirmingLoadSampleProducts, + ] = useState( false ); +- const { +- isLoading: isLoadingExperiment, +- experimentLayout, +- } = useProductTaskExperiment(); ++ const { isLoading: isLoadingExperiment, experimentLayout } = ++ useProductTaskExperiment(); + + const { isStoreInUS } = useSelect( ( select ) => { + const { getSettings } = select( SETTINGS_STORE_NAME ); +@@ -105,14 +103,12 @@ export const Products = () => { + [ recordCompletionTime, productTypes ] + ); + +- const { +- loadSampleProduct, +- isLoadingSampleProducts, +- } = useLoadSampleProducts( { +- redirectUrlAfterSuccess: getAdminLink( +- 'edit.php?post_type=product&wc_onboarding_active_task=products' +- ), +- } ); ++ const { loadSampleProduct, isLoadingSampleProducts } = ++ useLoadSampleProducts( { ++ redirectUrlAfterSuccess: getAdminLink( ++ 'edit.php?post_type=product&wc_onboarding_active_task=products' ++ ), ++ } ); + + const visibleProductTypes = useMemo( () => { + const surfacedProductTypes = productTypesWithTimeRecord.filter( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/test/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/test/index.tsx +index 97fa632201..2abcc17be7 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/test/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/test/index.tsx +@@ -150,7 +150,7 @@ describe( 'Products', () => { + expect( queryByText( 'Subscription product' ) ).toBeInTheDocument(); + } ); + +- it( 'clicking on suggested product should fire event tasklist_product_template_selection with is_suggested:true and task_completion_time', () => { ++ it( 'clicking on suggested product should fire event tasklist_add_product with method: product_template, tasklist_product_template_selection with is_suggested:true and task_completion_time', () => { + ( getAdminSetting as jest.Mock ).mockImplementation( () => ( { + profile: { + product_types: [ 'downloads' ], +@@ -160,24 +160,28 @@ describe( 'Products', () => { + + userEvent.click( + getByRole( 'menuitem', { +- name: +- 'Digital product A digital product like service, downloadable book, music or video.', ++ name: 'Digital product A digital product like service, downloadable book, music or video.', + } ) + ); + + expect( recordEvent ).toHaveBeenNthCalledWith( + 1, ++ 'tasklist_add_product', ++ { method: 'product_template' } ++ ); ++ expect( recordEvent ).toHaveBeenNthCalledWith( ++ 2, + 'tasklist_product_template_selection', + { is_suggested: true, product_type: 'digital' } + ); + expect( recordEvent ).toHaveBeenNthCalledWith( +- 2, ++ 3, + 'task_completion_time', + { task_name: 'products', time: '0-2s' } + ); + } ); + +- it( 'clicking on not-suggested product should fire event tasklist_product_template_selection with is_suggested:false and task_completion_time', async () => { ++ it( 'clicking on not-suggested product should fire event tasklist_add_product with method: product_template, tasklist_product_template_selection with is_suggested:false and task_completion_time', async () => { + ( getAdminSetting as jest.Mock ).mockImplementation( () => ( { + profile: { + product_types: [ 'downloads' ], +@@ -208,11 +212,16 @@ describe( 'Products', () => { + ); + expect( recordEvent ).toHaveBeenNthCalledWith( + 2, ++ 'tasklist_add_product', ++ { method: 'product_template' } ++ ); ++ expect( recordEvent ).toHaveBeenNthCalledWith( ++ 3, + 'tasklist_product_template_selection', + { is_suggested: false, product_type: 'grouped' } + ); + expect( recordEvent ).toHaveBeenNthCalledWith( +- 3, ++ 4, + 'task_completion_time', + { task_name: 'products', time: '0-2s' } + ); +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts +index bc0cd43d52..feb90c5217 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-create-product-by-type.ts +@@ -27,7 +27,7 @@ export const useCreateProductByType = () => { + setIsRequesting( true ); + try { + const data: { +- id?: string; ++ id?: number; + } = await createProductFromTemplate( + { + template_name: type, +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-layout-experiment.ts b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-layout-experiment.ts +index 61abdfaf43..e3d440cdad 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-layout-experiment.ts ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-layout-experiment.ts +@@ -23,15 +23,15 @@ export const getProductLayoutExperiment = async (): Promise< Layout > => { + return 'control'; + }; + +-export const isProductTaskExperimentTreatment = async (): Promise< boolean > => { +- return ( await getProductLayoutExperiment() ) !== 'control'; +-}; ++export const isProductTaskExperimentTreatment = ++ async (): Promise< boolean > => { ++ return ( await getProductLayoutExperiment() ) !== 'control'; ++ }; + + export const useProductTaskExperiment = () => { + const [ isLoading, setIsLoading ] = useState< boolean >( true ); +- const [ experimentLayout, setExperimentLayout ] = useState< Layout >( +- 'control' +- ); ++ const [ experimentLayout, setExperimentLayout ] = ++ useState< Layout >( 'control' ); + + useEffect( () => { + getProductLayoutExperiment().then( ( layout ) => { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-types-list-items.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-types-list-items.tsx +index c8282263df..2c37d21f18 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-types-list-items.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-products/use-product-types-list-items.tsx +@@ -27,6 +27,9 @@ const useProductTypeListItems = ( + ...productType, + onClick: () => { + createProductByType( productType.key ); ++ recordEvent( 'tasklist_add_product', { ++ method: 'product_template', ++ } ); + recordEvent( 'tasklist_product_template_selection', { + product_type: productType.key, + is_suggested: suggestedProductTypes.includes( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/connect.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/connect.tsx +new file mode 100644 +index 0000000000..2bed128cff +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/connect.tsx +@@ -0,0 +1,28 @@ ++/** ++ * External dependencies ++ */ ++import { __ } from '@wordpress/i18n'; ++import { recordEvent } from '@woocommerce/tracks'; ++ ++/** ++ * Internal dependencies ++ */ ++import { default as ConnectForm } from '~/dashboard/components/connect'; ++ ++type ConnectProps = { ++ onConnect: () => void; ++}; ++ ++export const Connect: React.FC< ConnectProps > = ( { onConnect } ) => { ++ return ( ++ { ++ recordEvent( 'tasklist_shipping_recommendation_connect_store', { ++ connect: true, ++ } ); ++ onConnect?.(); ++ } } ++ /> ++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugins.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugins.tsx +new file mode 100644 +index 0000000000..5189ecd231 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugins.tsx +@@ -0,0 +1,126 @@ ++/** ++ * External dependencies ++ */ ++import { __ } from '@wordpress/i18n'; ++import interpolateComponents from '@automattic/interpolate-components'; ++import { Link, Plugins as PluginInstaller } from '@woocommerce/components'; ++import { OPTIONS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data'; ++import { recordEvent } from '@woocommerce/tracks'; ++import { Text } from '@woocommerce/experimental'; ++import { useDispatch, useSelect } from '@wordpress/data'; ++import { useEffect } from '@wordpress/element'; ++ ++/** ++ * Internal dependencies ++ */ ++import { createNoticesFromResponse } from '~/lib/notices'; ++ ++const isWcConnectOptions = ( ++ wcConnectOptions: unknown ++): wcConnectOptions is { ++ [ key: string ]: unknown; ++} => typeof wcConnectOptions === 'object' && wcConnectOptions !== null; ++ ++type Props = { ++ nextStep: () => void; ++ pluginsToActivate: string[]; ++}; ++ ++export const Plugins: React.FC< Props > = ( { ++ nextStep, ++ pluginsToActivate, ++} ) => { ++ const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); ++ const { isResolving, tosAccepted } = useSelect( ( select ) => { ++ const { getOption, hasFinishedResolution } = ++ select( OPTIONS_STORE_NAME ); ++ const wcConnectOptions = getOption( 'wc_connect_options' ); ++ ++ return { ++ isResolving: ++ ! hasFinishedResolution( 'getOption', [ ++ 'woocommerce_setup_jetpack_opted_in', ++ ] ) || ++ ! hasFinishedResolution( 'getOption', [ ++ 'wc_connect_options', ++ ] ), ++ tosAccepted: ++ ( isWcConnectOptions( wcConnectOptions ) && ++ wcConnectOptions?.tos_accepted ) || ++ getOption( 'woocommerce_setup_jetpack_opted_in' ) === '1', ++ }; ++ } ); ++ ++ useEffect( () => { ++ if ( ! tosAccepted || pluginsToActivate.length ) { ++ return; ++ } ++ ++ nextStep(); ++ }, [ nextStep, pluginsToActivate, tosAccepted ] ); ++ const agreementText = pluginsToActivate.includes( 'woocommerce-services' ) ++ ? __( ++ 'By installing Jetpack and WooCommerce Shipping you agree to the {{link}}Terms of Service{{/link}}.', ++ 'woocommerce' ++ ) ++ : __( ++ 'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.', ++ 'woocommerce' ++ ); ++ ++ if ( isResolving ) { ++ return null; ++ } ++ ++ return ( ++ <> ++ { ++ createNoticesFromResponse( response ); ++ recordEvent( ++ 'tasklist_shipping_recommendation_install_extensions', ++ { ++ install_extensions: true, ++ } ++ ); ++ updateOptions( { ++ woocommerce_setup_jetpack_opted_in: true, ++ } ); ++ nextStep(); ++ } } ++ onError={ ( errors: unknown, response: unknown ) => ++ createNoticesFromResponse( response ) ++ } ++ pluginSlugs={ pluginsToActivate } ++ /> ++ { ! tosAccepted && ( ++ ++ { interpolateComponents( { ++ mixedString: agreementText, ++ components: { ++ link: ( ++ ++ <> ++ ++ ), ++ }, ++ } ) } ++ ++ ) } ++ ++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/shipstation-banner.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/shipstation-banner.tsx +new file mode 100644 +index 0000000000..20db717fe7 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/shipstation-banner.tsx +@@ -0,0 +1,79 @@ ++/** ++ * External dependencies ++ */ ++import { __ } from '@wordpress/i18n'; ++ ++/** ++ * Internal dependencies ++ */ ++import ShipStationImage from '../images/shipstation.svg'; ++import TimerImage from '../images/timer.svg'; ++import StarImage from '../images/star.svg'; ++import DiscountImage from '../images/discount.svg'; ++import './wcs-banner.scss'; ++ ++export const ShipStationBanner = () => { ++ return ( ++
++
++ ++
++
++
++
++ ++
++
++
++ ++ { __( 'Save your time', 'woocommerce' ) } ++ ++
++
++ { __( ++ 'Import your orders automatically, no matter where you sell.', ++ 'woocommerce' ++ ) } ++
++
++
++
++
++ ++
++
++
++ ++ { __( 'Save your money', 'woocommerce' ) } ++ ++
++
++ { __( ++ 'Live shipping rates amongst all the carrier choices.', ++ 'woocommerce' ++ ) } ++
++
++
++
++
++ ++
++
++
++ ++ { __( 'Wow your shoppers', 'woocommerce' ) } ++ ++
++
++ { __( ++ 'Customize notification emails, packing slips, shipping labels.', ++ 'woocommerce' ++ ) } ++
++
++
++
++
++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/store-location.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/store-location.tsx +new file mode 100644 +index 0000000000..1d34401519 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/store-location.tsx +@@ -0,0 +1,65 @@ ++/** ++ * External dependencies ++ */ ++import { SETTINGS_STORE_NAME } from '@woocommerce/data'; ++import { recordEvent } from '@woocommerce/tracks'; ++import { useEffect, useState } from '@wordpress/element'; ++import { useSelect, useDispatch } from '@wordpress/data'; ++ ++/** ++ * Internal dependencies ++ */ ++import { getCountryCode } from '~/dashboard/utils'; ++import { hasCompleteAddress, SettingsSelector } from '../../tax/utils'; ++import { default as StoreLocationForm } from '~/tasks/fills/steps/location'; ++ ++export const StoreLocation: React.FC< { ++ nextStep: () => void; ++ onLocationComplete: () => void; ++} > = ( { nextStep, onLocationComplete } ) => { ++ const { createNotice } = useDispatch( 'core/notices' ); ++ const { updateAndPersistSettingsForGroup } = ++ useDispatch( SETTINGS_STORE_NAME ); ++ const { generalSettings, isResolving } = useSelect( ( select ) => { ++ const { getSettings, hasFinishedResolution } = select( ++ SETTINGS_STORE_NAME ++ ) as SettingsSelector; ++ ++ return { ++ generalSettings: getSettings( 'general' )?.general, ++ isResolving: ! hasFinishedResolution( 'getSettings', [ ++ 'general', ++ ] ), ++ }; ++ } ); ++ ++ useEffect( () => { ++ if ( isResolving || ! hasCompleteAddress( generalSettings ) ) { ++ return; ++ } ++ onLocationComplete(); ++ }, [ generalSettings, onLocationComplete, isResolving ] ); ++ ++ if ( isResolving ) { ++ return null; ++ } ++ ++ return ( ++ { ++ const country = getCountryCode( values.countryState ); ++ recordEvent( 'tasklist_shipping_recommendation_set_location', { ++ country, ++ } ); ++ nextStep(); ++ } } ++ isSettingsRequesting={ false } ++ settings={ generalSettings } ++ updateAndPersistSettingsForGroup={ ++ updateAndPersistSettingsForGroup ++ } ++ createNotice={ createNotice } ++ isSettingsError={ false } ++ /> ++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/wcs-banner.scss b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/wcs-banner.scss +new file mode 100644 +index 0000000000..4b464ed661 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/wcs-banner.scss +@@ -0,0 +1,32 @@ ++.woocommerce-task-shipping-recommendation__plugins-install { ++ display: flex; ++ padding: $gap-large $gap; ++ border: 1px solid #ddd; ++ border-radius: 3px; ++ justify-content: space-around; ++ margin-bottom: $gap-large; ++ ++ .plugins-install__wcs-image { ++ display: flex; ++ ++ img { ++ width: 100%; ++ } ++ } ++ ++ .plugins-install__list { ++ display: flex; ++ flex-direction: column; ++ justify-content: space-around; ++ gap: $gap; ++ } ++ ++ .plugins-install__list-item { ++ display: flex; ++ align-items: center; ++ } ++ ++ .plugins-install__list-icon { ++ margin-right: $gap-small; ++ } ++} +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/wcs-banner.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/wcs-banner.tsx +new file mode 100644 +index 0000000000..71369cb51a +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/wcs-banner.tsx +@@ -0,0 +1,82 @@ ++/** ++ * External dependencies ++ */ ++import { __ } from '@wordpress/i18n'; ++ ++/** ++ * Internal dependencies ++ */ ++import WCSImage from '../images/wcs.svg'; ++import PrinterImage from '../images/printer.svg'; ++import PaperImage from '../images/paper.svg'; ++import DiscountImage from '../images/discount.svg'; ++import './wcs-banner.scss'; ++ ++export const WCSBanner = () => { ++ return ( ++
++
++ ++
++
++
++
++ ++
++
++
++ ++ { __( ++ 'Buy postage when you need it', ++ 'woocommerce' ++ ) } ++ ++
++
++ { __( ++ 'No need to wonder where that stampbook went.', ++ 'woocommerce' ++ ) } ++
++
++
++
++
++ ++
++
++
++ ++ { __( 'Print at home', 'woocommerce' ) } ++ ++
++
++ { __( ++ 'Pick up an order, then just pay, print, package and post.', ++ 'woocommerce' ++ ) } ++
++
++
++
++
++ ++
++
++
++ ++ { __( 'Discounted rates', 'woocommerce' ) } ++ ++
++
++ { __( ++ 'Access discounted shipping rates with DHL and USPS.', ++ 'woocommerce' ++ ) } ++
++
++
++
++
++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/discount.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/discount.svg +new file mode 100644 +index 0000000000..2855be644b +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/discount.svg +@@ -0,0 +1,6 @@ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/paper.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/paper.svg +new file mode 100644 +index 0000000000..8ae1e048fd +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/paper.svg +@@ -0,0 +1,10 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/printer.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/printer.svg +new file mode 100644 +index 0000000000..653bd5bcbe +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/printer.svg +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/shipstation.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/shipstation.svg +new file mode 100644 +index 0000000000..aaef0b59fa +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/shipstation.svg +@@ -0,0 +1,48 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/star.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/star.svg +new file mode 100644 +index 0000000000..2283a6f2cb +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/star.svg +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/timer.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/timer.svg +new file mode 100644 +index 0000000000..afb0cdc184 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/timer.svg +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/wcs.svg b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/wcs.svg +new file mode 100644 +index 0000000000..84da16a5f0 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/images/wcs.svg +@@ -0,0 +1,58 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/index.tsx +new file mode 100644 +index 0000000000..ae49f21480 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/index.tsx +@@ -0,0 +1,62 @@ ++/** ++ * External dependencies ++ */ ++import { ++ OPTIONS_STORE_NAME, ++ PLUGINS_STORE_NAME, ++ SETTINGS_STORE_NAME, ++} from '@woocommerce/data'; ++import { withSelect } from '@wordpress/data'; ++import { registerPlugin } from '@wordpress/plugins'; ++import { WooOnboardingTask } from '@woocommerce/onboarding'; ++import { compose } from '@wordpress/compose'; ++ ++/** ++ * Internal dependencies ++ */ ++import { SettingsSelector } from '../tax/utils'; ++import { ShippingRecommendation } from './shipping-recommendation'; ++import { TaskProps } from './types'; ++ ++const ShippingRecommendationWrapper = compose( ++ withSelect( ( select ) => { ++ const { getSettings } = select( ++ SETTINGS_STORE_NAME ++ ) as SettingsSelector; ++ const { hasFinishedResolution } = select( OPTIONS_STORE_NAME ); ++ const { getActivePlugins } = select( PLUGINS_STORE_NAME ); ++ ++ return { ++ activePlugins: getActivePlugins(), ++ generalSettings: getSettings( 'general' )?.general, ++ isJetpackConnected: ++ select( PLUGINS_STORE_NAME ).isJetpackConnected(), ++ isResolving: ++ ! hasFinishedResolution( 'getOption', [ ++ 'woocommerce_setup_jetpack_opted_in', ++ ] ) || ++ ! hasFinishedResolution( 'getOption', [ ++ 'wc_connect_options', ++ ] ) || ++ ! select( PLUGINS_STORE_NAME ).hasFinishedResolution( ++ 'isJetpackConnected' ++ ), ++ }; ++ } ) ++)( ShippingRecommendation ); ++ ++registerPlugin( 'wc-admin-onboarding-task-shipping-recommendation', { ++ // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. ++ scope: 'woocommerce-tasks', ++ render: () => ( ++ ++ { ( { onComplete, query, task }: TaskProps ) => ( ++ ++ ) } ++ ++ ), ++} ); +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/shipping-recommendation.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/shipping-recommendation.tsx +new file mode 100644 +index 0000000000..f74c1997e0 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/shipping-recommendation.tsx +@@ -0,0 +1,150 @@ ++/** ++ * External dependencies ++ */ ++import { __ } from '@wordpress/i18n'; ++import { difference } from 'lodash'; ++import { useCallback, useEffect, useState } from '@wordpress/element'; ++import { Stepper } from '@woocommerce/components'; ++import { Card, CardBody, Button } from '@wordpress/components'; ++ ++/** ++ * Internal dependencies ++ */ ++import { Connect } from './components/connect'; ++import { Plugins } from './components/plugins'; ++import { StoreLocation } from './components/store-location'; ++import { WCSBanner } from './components/wcs-banner'; ++import { TaskProps, ShippingRecommendationProps } from './types'; ++import { redirectToWCSSettings } from './utils'; ++ ++/** ++ * Plugins required to automate shipping. ++ */ ++const AUTOMATION_PLUGINS = [ 'jetpack', 'woocommerce-services' ]; ++ ++export const ShippingRecommendation: React.FC< ++ TaskProps & ShippingRecommendationProps ++> = ( { activePlugins, isJetpackConnected, isResolving } ) => { ++ const [ pluginsToActivate, setPluginsToActivate ] = useState< string[] >( ++ [] ++ ); ++ const [ stepIndex, setStepIndex ] = useState( 0 ); ++ const [ isRedirecting, setIsRedirecting ] = useState( false ); ++ const [ locationStepRedirected, setLocationStepRedirected ] = ++ useState( false ); ++ ++ const nextStep = () => { ++ setStepIndex( stepIndex + 1 ); ++ }; ++ ++ const redirect = () => { ++ setIsRedirecting( true ); ++ redirectToWCSSettings(); ++ }; ++ ++ const viewLocationStep = () => { ++ setStepIndex( 0 ); ++ }; ++ ++ // Skips to next step only once. ++ const onLocationComplete = () => { ++ if ( locationStepRedirected ) { ++ return; ++ } ++ setLocationStepRedirected( true ); ++ nextStep(); ++ }; ++ ++ useEffect( () => { ++ const remainingPlugins = difference( ++ AUTOMATION_PLUGINS, ++ activePlugins ++ ); ++ ++ // Force redirect when all steps are completed. ++ if ( ++ ! isResolving && ++ remainingPlugins.length === 0 && ++ isJetpackConnected ++ ) { ++ redirect(); ++ } ++ ++ if ( remainingPlugins.length <= pluginsToActivate.length ) { ++ return; ++ } ++ setPluginsToActivate( remainingPlugins ); ++ }, [ activePlugins, isJetpackConnected, isResolving, pluginsToActivate ] ); ++ ++ const steps = [ ++ { ++ key: 'store_location', ++ label: __( 'Set store location', 'woocommerce' ), ++ description: __( ++ 'The address from which your business operates', ++ 'woocommerce' ++ ), ++ content: ( ++ ++ ), ++ onClick: viewLocationStep, ++ }, ++ { ++ key: 'plugins', ++ label: pluginsToActivate.includes( 'woocommerce-services' ) ++ ? __( ++ 'Install Jetpack and WooCommerce Shipping', ++ 'woocommerce' ++ ) ++ : __( 'Install Jetpack', 'woocommerce' ), ++ description: __( ++ 'Enable shipping label printing and discounted rates', ++ 'woocommerce' ++ ), ++ content: ( ++
++ ++ ++
++ ), ++ }, ++ { ++ key: 'connect', ++ label: __( 'Connect your store', 'woocommerce' ), ++ description: __( ++ 'Connect your store to WordPress.com to enable WooCommerce Shipping', ++ 'woocommerce' ++ ), ++ content: isJetpackConnected ? ( ++ ++ ) : ( ++ ++ ), ++ }, ++ ]; ++ ++ const step = steps[ stepIndex ]; ++ ++ return ( ++
++ ++ ++ ++ ++ ++
++ ); ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/test/shipping-recommendation.tsx b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/test/shipping-recommendation.tsx +new file mode 100644 +index 0000000000..8ebec7be8f +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/test/shipping-recommendation.tsx +@@ -0,0 +1,136 @@ ++/** ++ * External dependencies ++ */ ++import { render } from '@testing-library/react'; ++import { TaskType } from '@woocommerce/data'; ++ ++/** ++ * Internal dependencies ++ */ ++import { ShippingRecommendation as _ShippingRecommendation } from '../shipping-recommendation'; ++import { ShippingRecommendationProps, TaskProps } from '../types'; ++import { redirectToWCSSettings } from '../utils'; ++ ++jest.mock( '../../tax/utils', () => ( { ++ hasCompleteAddress: jest.fn().mockReturnValue( true ), ++} ) ); ++ ++jest.mock( '../utils', () => ( { ++ redirectToWCSSettings: jest.fn(), ++} ) ); ++ ++jest.mock( '@wordpress/data', () => ( { ++ ...jest.requireActual( '@wordpress/data' ), ++ useSelect: jest.fn().mockImplementation( ( fn ) => ++ fn( () => ( { ++ getSettings: () => ( { ++ general: { ++ woocommerce_default_country: 'US', ++ }, ++ } ), ++ getCountries: () => [], ++ getLocales: () => [], ++ getLocale: () => 'en', ++ hasFinishedResolution: () => true, ++ getOption: ( key: string ) => { ++ return { ++ wc_connect_options: { ++ tos_accepted: true, ++ }, ++ woocommerce_setup_jetpack_opted_in: 1, ++ }[ key ]; ++ }, ++ } ) ) ++ ), ++} ) ); ++ ++const taskProps: TaskProps = { ++ onComplete: () => {}, ++ query: {}, ++ task: { ++ id: 'shipping-recommendation', ++ } as TaskType, ++}; ++ ++const ShippingRecommendation = ( props: ShippingRecommendationProps ) => { ++ return <_ShippingRecommendation { ...taskProps } { ...props } />; ++}; ++ ++describe( 'ShippingRecommendation', () => { ++ test( 'should show plugins step when jetpack is not installed and activated', () => { ++ const { getByRole } = render( ++ ++ ); ++ expect( ++ getByRole( 'button', { name: 'Install & enable' } ) ++ ).toBeInTheDocument(); ++ } ); ++ ++ test( 'should show plugins step when woocommerce-services is not installed and activated', () => { ++ const { getByRole } = render( ++ ++ ); ++ expect( ++ getByRole( 'button', { name: 'Install & enable' } ) ++ ).toBeInTheDocument(); ++ } ); ++ ++ test( 'should show connect step when both plugins are activated', () => { ++ const { getByRole } = render( ++ ++ ); ++ expect( ++ getByRole( 'button', { name: 'Connect' } ) ++ ).toBeInTheDocument(); ++ } ); ++ ++ test( 'should show "complete task" button when both plugins are activated and jetpack is connected', () => { ++ const { getByRole } = render( ++ ++ ); ++ expect( ++ getByRole( 'button', { name: 'Complete task' } ) ++ ).toBeInTheDocument(); ++ } ); ++ ++ test( 'should automatically be redirected when all steps are completed', () => { ++ render( ++ ++ ); ++ ++ expect( redirectToWCSSettings ).toHaveBeenCalled(); ++ } ); ++ ++ test( 'should allow location step to be manually navigated', () => { ++ const { getByText } = render( ++ ++ ); ++ ++ getByText( 'Set store location' ).click(); ++ expect( getByText( 'Address line 1' ) ).toBeInTheDocument(); ++ } ); ++} ); +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/types.ts b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/types.ts +new file mode 100644 +index 0000000000..8697dedb0c +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/types.ts +@@ -0,0 +1,16 @@ ++/** ++ * External dependencies ++ */ ++import { TaskType } from '@woocommerce/data'; ++ ++export type TaskProps = { ++ onComplete: () => void; ++ query: Record< string, string >; ++ task: TaskType; ++}; ++ ++export type ShippingRecommendationProps = { ++ activePlugins: string[]; ++ isJetpackConnected: boolean; ++ isResolving: boolean; ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/utils.ts b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/utils.ts +new file mode 100644 +index 0000000000..95616a7496 +--- /dev/null ++++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/utils.ts +@@ -0,0 +1,13 @@ ++/** ++ * External dependencies ++ */ ++ ++import { getAdminLink } from '@woocommerce/settings'; ++ ++export const redirectToWCSSettings = () => { ++ if ( window?.location ) { ++ window.location.href = getAdminLink( ++ 'admin.php?page=wc-settings&tab=shipping§ion=woocommerce-services-settings' ++ ); ++ } ++}; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/index.js b/plugins/woocommerce-admin/client/tasks/fills/index.js +index 3454543fc7..690a8203b3 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/index.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/index.js +@@ -34,3 +34,10 @@ if ( + } else { + import( './products' ); + } ++ ++if ( ++ window.wcAdminFeatures && ++ window.wcAdminFeatures[ 'shipping-smart-defaults' ] ++) { ++ import( './experimental-shipping-recommendation' ); ++} +diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js b/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js +index 96cc70b137..2ca78139c1 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/products/product-template-modal.js +@@ -159,7 +159,7 @@ export default function ProductTemplateModal( { onClose } ) { + + return ( + onClose() } + className="woocommerce-product-template-modal" +@@ -193,7 +193,7 @@ export default function ProductTemplateModal( { onClose } ) { + disabled={ ! selectedTemplate || isRedirecting } + onClick={ createTemplate } + > +- { __( 'Go' ) } ++ { __( 'Go', 'woocommerce' ) } + +
+
+diff --git a/plugins/woocommerce-admin/client/tasks/fills/shipping/index.js b/plugins/woocommerce-admin/client/tasks/fills/shipping/index.js +index bb13b7a3e0..e4c4100c87 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/shipping/index.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/shipping/index.js +@@ -21,6 +21,7 @@ import { + import { recordEvent } from '@woocommerce/tracks'; + import { registerPlugin } from '@wordpress/plugins'; + import { WooOnboardingTask } from '@woocommerce/onboarding'; ++import { Text } from '@woocommerce/experimental'; + + /** + * Internal dependencies +@@ -29,6 +30,8 @@ import Connect from '../../../dashboard/components/connect'; + import { getCountryCode } from '../../../dashboard/utils'; + import StoreLocation from '../steps/location'; + import ShippingRates from './rates'; ++import { WCSBanner } from '../experimental-shipping-recommendation/components/wcs-banner'; ++import { ShipStationBanner } from '../experimental-shipping-recommendation/components/shipstation-banner'; + import { createNoticesFromResponse } from '../../../lib/notices'; + import './shipping.scss'; + +@@ -46,6 +49,12 @@ export class Shipping extends Component { + this.activePlugins = props.activePlugins; + this.state = this.initialState; + this.completeStep = this.completeStep.bind( this ); ++ ++ this.shippingSmartDefaultsEnabled = ++ window.wcAdminFeatures && ++ window.wcAdminFeatures[ 'shipping-smart-defaults' ]; ++ ++ this.storeLocationCompleted = false; + } + + componentDidMount() { +@@ -141,7 +150,15 @@ export class Shipping extends Component { + ); + + if ( step === 'store_location' && isCompleteAddress ) { +- this.completeStep(); ++ if ( ++ this.shippingSmartDefaultsEnabled && ++ ! this.storeLocationCompleted ++ ) { ++ this.completeStep(); ++ this.storeLocationCompleted = true; ++ } else if ( ! this.shippingSmartDefaultsEnabled ) { ++ this.completeStep(); ++ } + } + } + +@@ -195,7 +212,7 @@ export class Shipping extends Component { + const requiresJetpackConnection = + ! isJetpackConnected && countryCode === 'US'; + +- const steps = [ ++ let steps = [ + { + key: 'store_location', + label: __( 'Set store location', 'woocommerce' ), +@@ -217,7 +234,11 @@ export class Shipping extends Component { + recordEvent( 'tasklist_shipping_set_location', { + country, + } ); ++ + // Don't need to trigger completeStep here as it's triggered by the address updates in the componentDidUpdate function. ++ if ( this.shippingSmartDefaultsEnabled ) { ++ this.completeStep(); ++ } + } } + /> + ), +@@ -328,12 +349,188 @@ export class Shipping extends Component { + }, + ]; + ++ // Override the step fields for the smart shipping defaults. ++ if ( this.shippingSmartDefaultsEnabled ) { ++ const agreementText = pluginsToActivate.includes( ++ 'woocommerce-services' ++ ) ++ ? __( ++ 'By installing Jetpack and WooCommerce Shipping you agree to the {{link}}Terms of Service{{/link}}.', ++ 'woocommerce' ++ ) ++ : __( ++ 'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.', ++ 'woocommerce' ++ ); ++ const shippingSmartDefaultsSteps = { ++ rates: { ++ label: __( 'Review your shipping options', 'woocommerce' ), ++ description: __( ++ 'We recommend the following shipping options based on your location. You can manage your shipping options again at any time in WooCommerce Shipping settings.', ++ 'woocommerce' ++ ), ++ onClick: ++ this.state.step !== 'rates' ++ ? () => { ++ this.setState( { step: 'rates' } ); ++ } ++ : undefined, ++ content: ( ++ { ++ const { id } = task; ++ optimisticallyCompleteTask( id ); ++ invalidateResolutionForStoreSelector(); ++ this.completeStep(); ++ } } ++ createNotice={ createNotice } ++ /> ++ ), ++ }, ++ label_printing: { ++ label: __( ++ 'Enable shipping label printing and discounted rates', ++ 'woocommerce' ++ ), ++ description: pluginsToActivate.includes( ++ 'woocommerce-shipstation-integration' ++ ) ++ ? interpolateComponents( { ++ mixedString: __( ++ 'We recommend using ShipStation to save time at the post office by printing your shipping ' + ++ 'labels at home. Try ShipStation free for 30 days. {{link}}Learn more{{/link}}.', ++ 'woocommerce' ++ ), ++ components: { ++ link: ( ++ ++ ), ++ }, ++ } ) ++ : __( ++ 'Save time and fulfill your orders with WooCommerce Shipping. You can manage it at any time in WooCommerce Shipping Settings.', ++ 'woocommerce' ++ ), ++ ++ content: ( ++ <> ++ { pluginsToActivate.includes( ++ 'woocommerce-services' ++ ) ? ( ++ ++ ) : ( ++ ++ ) } ++ { ++ createNoticesFromResponse( response ); ++ recordEvent( ++ 'tasklist_shipping_label_printing', ++ { ++ install: true, ++ plugins_to_activate: ++ pluginsToActivate, ++ } ++ ); ++ this.completeStep(); ++ } } ++ onError={ ( errors, response ) => ++ createNoticesFromResponse( response ) ++ } ++ onSkip={ () => { ++ recordEvent( ++ 'tasklist_shipping_label_printing', ++ { ++ install: false, ++ plugins_to_activate: ++ pluginsToActivate, ++ } ++ ); ++ getHistory().push( ++ getNewPath( {}, '/', {} ) ++ ); ++ onComplete(); ++ } } ++ pluginSlugs={ pluginsToActivate } ++ /> ++ { ! isJetpackConnected && ++ pluginsToActivate.includes( ++ 'woocommerce-services' ++ ) && ( ++ ++ { interpolateComponents( { ++ mixedString: agreementText, ++ components: { ++ link: ( ++ ++ <> ++ ++ ), ++ }, ++ } ) } ++ ++ ) } ++ ++ ), ++ }, ++ store_location: { ++ label: __( 'Set your store location', 'woocommerce' ), ++ description: __( ++ 'Add your store location to help us calculate shipping rates and the best shipping options for you. You can manage your store location again at any time in WooCommerce Settings General.', ++ 'woocommerce' ++ ), ++ onClick: ++ this.state.step !== 'store_location' ++ ? () => { ++ this.setState( { step: 'store_location' } ); ++ } ++ : undefined, ++ buttonText: __( 'Save store location', 'woocommerce' ), ++ }, ++ }; ++ ++ steps = steps.map( ( step ) => { ++ if ( shippingSmartDefaultsSteps.hasOwnProperty( step.key ) ) { ++ step = { ++ ...step, ++ ...shippingSmartDefaultsSteps[ step.key ], ++ }; ++ } ++ // Empty description field if it's not the current step. ++ if ( step.key !== this.state.step ) { ++ step.description = ''; ++ } ++ return step; ++ } ); ++ } + return filter( steps, ( step ) => step.visible ); + } + + render() { + const { isPending, step } = this.state; + const { isUpdateSettingsRequesting } = this.props; ++ const steps = this.getSteps(); + + return ( +
+@@ -345,7 +542,7 @@ export class Shipping extends Component { + } + isVertical + currentStep={ step } +- steps={ this.getSteps() } ++ steps={ steps } + /> + + +@@ -356,12 +553,10 @@ export class Shipping extends Component { + + const ShippingWrapper = compose( + withSelect( ( select ) => { +- const { getSettings, isUpdateSettingsRequesting } = select( +- SETTINGS_STORE_NAME +- ); +- const { getActivePlugins, isJetpackConnected } = select( +- PLUGINS_STORE_NAME +- ); ++ const { getSettings, isUpdateSettingsRequesting } = ++ select( SETTINGS_STORE_NAME ); ++ const { getActivePlugins, isJetpackConnected } = ++ select( PLUGINS_STORE_NAME ); + const { getCountry } = select( COUNTRIES_STORE_NAME ); + + const { general: settings = {} } = getSettings( 'general' ); +@@ -384,9 +579,8 @@ const ShippingWrapper = compose( + } ), + withDispatch( ( dispatch ) => { + const { createNotice } = dispatch( 'core/notices' ); +- const { updateAndPersistSettingsForGroup } = dispatch( +- SETTINGS_STORE_NAME +- ); ++ const { updateAndPersistSettingsForGroup } = ++ dispatch( SETTINGS_STORE_NAME ); + const { + invalidateResolutionForStoreSelector, + optimisticallyCompleteTask, +diff --git a/plugins/woocommerce-admin/client/tasks/fills/steps/location.js b/plugins/woocommerce-admin/client/tasks/fills/steps/location.js +index 94f5388d03..a4cce788a9 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/steps/location.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/steps/location.js +@@ -23,6 +23,7 @@ const StoreLocation = ( { + isSettingsRequesting, + updateAndPersistSettingsForGroup, + settings, ++ buttonText = __( 'Continue', 'woocommerce' ), + } ) => { + const { getLocale, hasFinishedResolution } = useSelect( ( select ) => { + const countryStore = select( COUNTRIES_STORE_NAME ); +@@ -101,7 +102,7 @@ const StoreLocation = ( { + setValue={ setValue } + /> + + + ) } +diff --git a/plugins/woocommerce-admin/client/tasks/fills/tax/components/store-location.tsx b/plugins/woocommerce-admin/client/tasks/fills/tax/components/store-location.tsx +index 018cf9f53b..42d7292854 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/tax/components/store-location.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/tax/components/store-location.tsx +@@ -18,9 +18,8 @@ export const StoreLocation: React.FC< { + nextStep: () => void; + } > = ( { nextStep } ) => { + const { createNotice } = useDispatch( 'core/notices' ); +- const { updateAndPersistSettingsForGroup } = useDispatch( +- SETTINGS_STORE_NAME +- ); ++ const { updateAndPersistSettingsForGroup } = ++ useDispatch( SETTINGS_STORE_NAME ); + const { generalSettings, isResolving } = useSelect( ( select ) => { + const { getSettings, hasFinishedResolution } = select( + SETTINGS_STORE_NAME +diff --git a/plugins/woocommerce-admin/client/tasks/fills/tax/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/tax/index.tsx +index 3218365805..c2ad5e7831 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/tax/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/tax/index.tsx +@@ -55,9 +55,8 @@ const Tax: React.FC< TaxProps > = ( { onComplete, query, task } ) => { + const [ isPending, setIsPending ] = useState( false ); + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + const { createNotice } = useDispatch( 'core/notices' ); +- const { updateAndPersistSettingsForGroup } = useDispatch( +- SETTINGS_STORE_NAME +- ); ++ const { updateAndPersistSettingsForGroup } = ++ useDispatch( SETTINGS_STORE_NAME ); + const { generalSettings, isResolving, taxSettings } = useSelect( + ( select ) => { + const { getSettings, hasFinishedResolution } = select( +diff --git a/plugins/woocommerce-admin/client/tasks/fills/tax/utils.ts b/plugins/woocommerce-admin/client/tasks/fills/tax/utils.ts +index 553f6a0d90..1c64602a55 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/tax/utils.ts ++++ b/plugins/woocommerce-admin/client/tasks/fills/tax/utils.ts +@@ -43,18 +43,14 @@ export const redirectToTaxSettings = (): void => { + * Types for settings selectors. + */ + export type SettingsSelector = WPDataSelectors & { +- getSettings: ( +- type: string +- ) => { ++ getSettings: ( type: string ) => { + general: { + woocommerce_default_country?: string; + woocommerce_calc_taxes?: string; + }; + tax: { [ key: string ]: string }; + }; +- getOption: ( +- type: string +- ) => { ++ getOption: ( type: string ) => { + tos_accepted?: boolean; + }; + }; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/index.tsx +index 45b57eb9e7..55138f5dc4 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/index.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/index.tsx +@@ -34,21 +34,20 @@ export const WooCommerceTax: React.FC< TaxChildProps > = ( { + const { getSettings } = select( + SETTINGS_STORE_NAME + ) as SettingsSelector; +- const { getActivePlugins, hasFinishedResolution } = select( +- PLUGINS_STORE_NAME +- ); ++ const { getActivePlugins, hasFinishedResolution } = ++ select( PLUGINS_STORE_NAME ); + const activePlugins = getActivePlugins(); + + return { + generalSettings: getSettings( 'general' ).general, +- isJetpackConnected: select( +- PLUGINS_STORE_NAME +- ).isJetpackConnected(), ++ isJetpackConnected: ++ select( PLUGINS_STORE_NAME ).isJetpackConnected(), + isResolving: + ! hasFinishedResolution( 'isJetpackConnected' ) || +- ! select( +- SETTINGS_STORE_NAME +- ).hasFinishedResolution( 'getSettings', [ 'general' ] ) || ++ ! select( SETTINGS_STORE_NAME ).hasFinishedResolution( ++ 'getSettings', ++ [ 'general' ] ++ ) || + ! hasFinishedResolution( 'getActivePlugins' ), + pluginsToActivate: difference( AUTOMATION_PLUGINS, activePlugins ), + }; +diff --git a/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/plugins.tsx b/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/plugins.tsx +index 340f1aa9e0..fe2a2590a8 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/plugins.tsx ++++ b/plugins/woocommerce-admin/client/tasks/fills/tax/woocommerce-tax/plugins.tsx +@@ -30,9 +30,8 @@ export const Plugins: React.FC< SetupStepProps > = ( { + } ) => { + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + const { isResolving, tosAccepted } = useSelect( ( select ) => { +- const { getOption, hasFinishedResolution } = select( +- OPTIONS_STORE_NAME +- ); ++ const { getOption, hasFinishedResolution } = ++ select( OPTIONS_STORE_NAME ); + const wcConnectOptions = getOption( 'wc_connect_options' ); + + return { +diff --git a/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js b/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js +index e6af8c900d..b41112cab1 100644 +--- a/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js ++++ b/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js +@@ -1,11 +1,15 @@ + /** + * External dependencies + */ +-import React from 'react'; ++import React, { useEffect } from 'react'; + import { registerPlugin } from '@wordpress/plugins'; +-import { WooOnboardingTaskListItem } from '@woocommerce/onboarding'; ++import { ++ WooOnboardingTaskListItem, ++ WooOnboardingTask, ++} from '@woocommerce/onboarding'; + import { PLUGINS_STORE_NAME } from '@woocommerce/data'; + import { useDispatch } from '@wordpress/data'; ++import { Spinner } from '@woocommerce/components'; + + /** + * Internal dependencies +@@ -20,6 +24,7 @@ const WoocommercePaymentsTaskItem = () => { + + { ( { defaultTaskItem: DefaultTaskItem } ) => ( + { + return new Promise( ( resolve, reject ) => { + return installActivateAndConnectWcpay( +@@ -39,3 +44,47 @@ registerPlugin( 'woocommerce-admin-task-wcpay', { + scope: 'woocommerce-tasks', + render: WoocommercePaymentsTaskItem, + } ); ++ ++const ReadyWcPay = () => { ++ const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); ++ const { createNotice } = useDispatch( 'core/notices' ); ++ ++ useEffect( () => { ++ new Promise( ( resolve, reject ) => { ++ return installActivateAndConnectWcpay( ++ reject, ++ createNotice, ++ installAndActivatePlugins ++ ); ++ } ); ++ }, [ createNotice, installAndActivatePlugins ] ); ++ ++ return ( ++
++ ++
++ Preparing payment settings... ++
++
++ ); ++}; ++ ++// shows up at http://host/wp-admin/admin.php?page=wc-admin&task=woocommerce-payments which is the default url for woocommerce-payments task ++const WoocommercePaymentsTaskPage = () => ( ++ ++ ++ ++); ++ ++registerPlugin( 'woocommerce-admin-task-wcpay-page', { ++ scope: 'woocommerce-tasks', ++ render: WoocommercePaymentsTaskPage, ++} ); +diff --git a/plugins/woocommerce-admin/client/tasks/task-list-item.tsx b/plugins/woocommerce-admin/client/tasks/task-list-item.tsx +index 854c9e2e9c..debdb1dd5d 100644 +--- a/plugins/woocommerce-admin/client/tasks/task-list-item.tsx ++++ b/plugins/woocommerce-admin/client/tasks/task-list-item.tsx +@@ -66,7 +66,7 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( { + + const onDismiss = useCallback( () => { + dismissTask( id ); +- createNotice( 'success', __( 'Task dismissed' ), { ++ createNotice( 'success', __( 'Task dismissed', 'woocommerce' ), { + actions: [ + { + label: __( 'Undo', 'woocommerce' ), +diff --git a/plugins/woocommerce-admin/client/tasks/task.tsx b/plugins/woocommerce-admin/client/tasks/task.tsx +index 2de0f295f5..79ce9e91df 100644 +--- a/plugins/woocommerce-admin/client/tasks/task.tsx ++++ b/plugins/woocommerce-admin/client/tasks/task.tsx +@@ -25,10 +25,8 @@ export const Task: React.FC< TaskProps > = ( { query, task } ) => { + // eslint-enable-next-line no-console + } + +- const { +- invalidateResolutionForStoreSelector, +- optimisticallyCompleteTask, +- } = useDispatch( ONBOARDING_STORE_NAME ); ++ const { invalidateResolutionForStoreSelector, optimisticallyCompleteTask } = ++ useDispatch( ONBOARDING_STORE_NAME ); + + const updateBadge = useCallback( () => { + const badgeElement: HTMLElement | null = document.querySelector( +diff --git a/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx +index 2082d98dfd..26eada0409 100644 +--- a/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx ++++ b/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx +@@ -46,53 +46,48 @@ function getStoreAgeInWeeks( adminInstallTimestamp: number ) { + return storeAgeInWeeks; + } + +-export const TaskListCompletedHeader: React.FC< TaskListCompletedHeaderProps > = ( { +- hideTasks, +- keepTasks, +- customerEffortScore, +-} ) => { ++export const TaskListCompletedHeader: React.FC< ++ TaskListCompletedHeaderProps ++> = ( { hideTasks, keepTasks, customerEffortScore } ) => { + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + const [ showCesModal, setShowCesModal ] = useState( false ); + const [ hasSubmittedScore, setHasSubmittedScore ] = useState( false ); + const [ score, setScore ] = useState( NaN ); +- const [ hideCustomerEffortScore, setHideCustomerEffortScore ] = useState( +- false +- ); +- const { +- storeAgeInWeeks, +- cesShownForActions, +- canShowCustomerEffortScore, +- } = useSelect( ( select: WCDataSelector ) => { +- const { getOption, hasFinishedResolution } = select( +- OPTIONS_STORE_NAME +- ); ++ const [ hideCustomerEffortScore, setHideCustomerEffortScore ] = ++ useState( false ); ++ const { storeAgeInWeeks, cesShownForActions, canShowCustomerEffortScore } = ++ useSelect( ( select: WCDataSelector ) => { ++ const { getOption, hasFinishedResolution } = ++ select( OPTIONS_STORE_NAME ); + +- if ( customerEffortScore ) { +- const allowTracking = getOption( ALLOW_TRACKING_OPTION_NAME ); +- const adminInstallTimestamp: number = +- getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0; +- const cesActions = getOption< string[] >( +- SHOWN_FOR_ACTIONS_OPTION_NAME +- ); +- const loadingOptions = +- ! hasFinishedResolution( 'getOption', [ +- SHOWN_FOR_ACTIONS_OPTION_NAME, +- ] ) || +- ! hasFinishedResolution( 'getOption', [ +- ADMIN_INSTALL_TIMESTAMP_OPTION_NAME, +- ] ); +- return { +- storeAgeInWeeks: getStoreAgeInWeeks( adminInstallTimestamp ), +- cesShownForActions: cesActions, +- canShowCustomerEffortScore: +- ! loadingOptions && +- allowTracking && +- ! ( cesActions || [] ).includes( 'store_setup' ), +- loading: loadingOptions, +- }; +- } +- return {}; +- } ); ++ if ( customerEffortScore ) { ++ const allowTracking = getOption( ALLOW_TRACKING_OPTION_NAME ); ++ const adminInstallTimestamp: number = ++ getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0; ++ const cesActions = getOption< string[] >( ++ SHOWN_FOR_ACTIONS_OPTION_NAME ++ ); ++ const loadingOptions = ++ ! hasFinishedResolution( 'getOption', [ ++ SHOWN_FOR_ACTIONS_OPTION_NAME, ++ ] ) || ++ ! hasFinishedResolution( 'getOption', [ ++ ADMIN_INSTALL_TIMESTAMP_OPTION_NAME, ++ ] ); ++ return { ++ storeAgeInWeeks: getStoreAgeInWeeks( ++ adminInstallTimestamp ++ ), ++ cesShownForActions: cesActions, ++ canShowCustomerEffortScore: ++ ! loadingOptions && ++ allowTracking && ++ ! ( cesActions || [] ).includes( 'store_setup' ), ++ loading: loadingOptions, ++ }; ++ } ++ return {}; ++ } ); + + useEffect( () => { + if ( hasSubmittedScore ) { +diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/woocommerce-payments.js b/plugins/woocommerce-admin/client/two-column-tasks/headers/woocommerce-payments.js +index a05f8cd46c..b47ae6f423 100644 +--- a/plugins/woocommerce-admin/client/two-column-tasks/headers/woocommerce-payments.js ++++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/woocommerce-payments.js +@@ -89,7 +89,7 @@ const WoocommercePaymentsHeader = ( { task, trackClick } ) => { + +

+ Timer{ ' ' } +- { __( '2 minutes' ) } ++ { __( '2 minutes', 'woocommerce' ) } +

+
+ +diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +index b69ea01cd8..147757bdae 100644 +--- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx ++++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +@@ -26,7 +26,8 @@ type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { + title: string | React.ReactNode | undefined; + onToggle?: ( isOpen: boolean ) => void; + }; +-const PanelBodyWithUpdatedType = PanelBody as React.ComponentType< PanelBodyProps >; ++const PanelBodyWithUpdatedType = ++ PanelBody as React.ComponentType< PanelBodyProps >; + + export const SectionedTaskList: React.FC< TaskListProps > = ( { + query, +@@ -45,10 +46,8 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { + profileItems: getProfileItems(), + }; + } ); +- const { +- hideTaskList, +- keepCompletedTaskList: keepCompletedTasks, +- } = useDispatch( ONBOARDING_STORE_NAME ); ++ const { hideTaskList, keepCompletedTaskList: keepCompletedTasks } = ++ useDispatch( ONBOARDING_STORE_NAME ); + const [ openPanel, setOpenPanel ] = useState< string | null >( + sections?.find( ( section ) => ! section.isComplete )?.id || null + ); +diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item-two-column.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list-item-two-column.tsx +index 28dd5055d7..211853559e 100644 +--- a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item-two-column.tsx ++++ b/plugins/woocommerce-admin/client/two-column-tasks/task-list-item-two-column.tsx +@@ -46,7 +46,7 @@ export const TaskListItemTwoColumn: React.FC< TaskListItemProps > = ( { + + const onDismissTask = ( onDismiss?: () => void ) => { + dismissTask( taskId ); +- createNotice( 'success', __( 'Task dismissed' ), { ++ createNotice( 'success', __( 'Task dismissed', 'woocommerce' ), { + actions: [ + { + label: __( 'Undo', 'woocommerce' ), +diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx +index a92f0e9a25..bc280ce4a1 100644 +--- a/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx ++++ b/plugins/woocommerce-admin/client/two-column-tasks/task-list-item.tsx +@@ -97,7 +97,7 @@ export const TaskListItem: React.FC< TaskListItemProps > = ( { + + const onDismiss = useCallback( () => { + dismissTask( task.id ); +- createNotice( 'success', __( 'Task dismissed' ), { ++ createNotice( 'success', __( 'Task dismissed', 'woocommerce' ), { + actions: [ + { + label: __( 'Undo', 'woocommerce' ), +diff --git a/plugins/woocommerce-admin/client/typings/global.d.ts b/plugins/woocommerce-admin/client/typings/global.d.ts +index 9244a30dcc..5e9a1c1d5c 100644 +--- a/plugins/woocommerce-admin/client/typings/global.d.ts ++++ b/plugins/woocommerce-admin/client/typings/global.d.ts +@@ -28,6 +28,8 @@ declare global { + 'wc-pay-promotion': boolean; + 'wc-pay-welcome-page': boolean; + 'wc-pay-subscriptions-page': boolean; ++ 'shipping-smart-defaults': boolean; ++ 'shipping-setting-tour': boolean; + }; + } + } +diff --git a/plugins/woocommerce-admin/client/utils/admin-settings.js b/plugins/woocommerce-admin/client/utils/admin-settings.js +index ea68fefcc1..5572e29213 100644 +--- a/plugins/woocommerce-admin/client/utils/admin-settings.js ++++ b/plugins/woocommerce-admin/client/utils/admin-settings.js +@@ -40,7 +40,10 @@ export function getAdminSetting( + ) { + if ( mutableSources.includes( name ) ) { + throw new Error( +- __( 'Mutable settings should be accessed via data store.' ) ++ __( ++ 'Mutable settings should be accessed via data store.', ++ 'woocommerce' ++ ) + ); + } + const value = ADMIN_SETTINGS_SOURCE.hasOwnProperty( name ) +@@ -75,7 +78,10 @@ export const ORDER_STATUSES = getAdminSetting( 'orderStatuses' ); + export function setAdminSetting( name, value, filter = ( val ) => val ) { + if ( mutableSources.includes( name ) ) { + throw new Error( +- __( 'Mutable settings should be mutated via data store.' ) ++ __( ++ 'Mutable settings should be mutated via data store.', ++ 'woocommerce' ++ ) + ); + } + ADMIN_SETTINGS_SOURCE[ name ] = filter( value ); +diff --git a/plugins/woocommerce-admin/client/utils/index.js b/plugins/woocommerce-admin/client/utils/index.js +index 576cc99ece..5015e295a7 100644 +--- a/plugins/woocommerce-admin/client/utils/index.js ++++ b/plugins/woocommerce-admin/client/utils/index.js +@@ -29,9 +29,11 @@ export function getUrlParams( locationSearch ) { + */ + export function getScreenName() { + let screenName = ''; +- const { page, path, post_type: postType } = getUrlParams( +- window.location.search +- ); ++ const { ++ page, ++ path, ++ post_type: postType, ++ } = getUrlParams( window.location.search ); + if ( page ) { + const currentPage = page === 'wc-admin' ? 'home_screen' : page; + screenName = path +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-load-sample-products-notice/index.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-load-sample-products-notice/index.ts +index 976fbc7545..cca96b2b50 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-load-sample-products-notice/index.ts ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-load-sample-products-notice/index.ts +@@ -8,7 +8,7 @@ import { getAdminLink } from '@woocommerce/settings'; + + domReady( () => { + dispatch( 'core/notices' ).createSuccessNotice( +- __( 'Sample products added' ), ++ __( 'Sample products added', 'woocommerce' ), + { + id: 'WOOCOMMERCE_ONBOARDING_LOAD_SAMPLE_PRODUCTS_NOTICE', + actions: [ +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx b/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx +index 5858cdff16..f94c28d4be 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx +@@ -57,9 +57,8 @@ export const PaymentPromotionRow: React.FC< PaymentPromotionRowProps > = ( { + const { updatePaymentGateway } = useDispatch( PAYMENT_GATEWAYS_STORE_NAME ); + const { gatewayIsActive, paymentGateway } = useSelect( ( select ) => { + const { getPaymentGateway } = select( PAYMENT_GATEWAYS_STORE_NAME ); +- const activePlugins: string[] = select( +- PLUGINS_STORE_NAME +- ).getActivePlugins(); ++ const activePlugins: string[] = ++ select( PLUGINS_STORE_NAME ).getActivePlugins(); + const isActive = activePlugins && activePlugins.includes( pluginSlug ); + let paymentGatewayData; + if ( isActive ) { +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js +index 59d9da2b0d..4a41285e9c 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/shipping-banner/index.js +@@ -481,11 +481,8 @@ ShippingBanner.propTypes = { + + export default compose( + withSelect( ( select ) => { +- const { +- isPluginsRequesting, +- isJetpackConnected, +- getActivePlugins, +- } = select( PLUGINS_STORE_NAME ); ++ const { isPluginsRequesting, isJetpackConnected, getActivePlugins } = ++ select( PLUGINS_STORE_NAME ); + + const isRequesting = + isPluginsRequesting( 'activatePlugins' ) || +@@ -498,9 +495,8 @@ export default compose( + }; + } ), + withDispatch( ( dispatch ) => { +- const { activatePlugins, installPlugins } = dispatch( +- PLUGINS_STORE_NAME +- ); ++ const { activatePlugins, installPlugins } = ++ dispatch( PLUGINS_STORE_NAME ); + + return { + activatePlugins, +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/style.scss b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/style.scss +index 3034460124..166282d0bb 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/style.scss ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/style.scss +@@ -73,10 +73,6 @@ + max-width: 100%; + + .components-modal__header { +- border-bottom: 0; +- margin: 4px 0; +- padding: 0; +- height: 50px; + .components-button.has-icon { + left: 15px; + svg { +@@ -96,14 +92,12 @@ + .wc-admin-shipping-banner__dismiss-modal-help-text { + font-size: 16px; + line-height: 24px; +- margin: 0 60px 1em 0; + } + + .wc-admin-shipping-banner__dismiss-modal-actions { + text-align: right; + + .components-button.is-primary { +- height: 30px; + padding-left: 15px; + padding-right: 15px; + text-align: center; +@@ -120,7 +114,7 @@ + } + } + +-@media (max-width: 1080px) { ++@media ( max-width: 1080px ) { + #woocommerce-admin-print-label { + text-align: center; + +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-product-step-change.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-product-step-change.ts +index c2d4800fb2..9c0a02626d 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-product-step-change.ts ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-product-step-change.ts +@@ -23,9 +23,10 @@ const getInputValue = ( id: string ) => { + + const getTinyMceValue = ( id: string ) => { + const iframe = document.querySelector< HTMLIFrameElement >( id ); +- const tinymce = iframe?.contentWindow?.document.querySelector< HTMLElement >( +- '#tinymce' +- ); ++ const tinymce = ++ iframe?.contentWindow?.document.querySelector< HTMLElement >( ++ '#tinymce' ++ ); + return tinymce?.innerHTML || ''; + }; + +@@ -91,24 +92,23 @@ export const useProductStepChange = () => { + Partial< Record< ProductTourStepName, string > > + >( {} ); + const [ isLoaded, setIsLoaded ] = useState( false ); +- const getValues: () => Partial< +- Record< ProductTourStepName, string > +- > = useCallback( () => { +- return { +- 'product-name': getInputValue( '#title' ), +- 'product-description': getProductDescriptionValue( +- isContentEditorTmceActive +- ), +- // For product data, we're just going to detect change if price is changed. +- 'product-data': getInputValue( '#_regular_price' ), +- 'product-short-description': getProductShortDescriptionValue( +- isExcerptEditorTmceActive +- ), +- 'product-image': getProductImageValue(), +- 'product-tags': getProductTagsValue(), +- 'product-categories': getProductCategoriesValue(), +- }; +- }, [ isContentEditorTmceActive, isExcerptEditorTmceActive ] ); ++ const getValues: () => Partial< Record< ProductTourStepName, string > > = ++ useCallback( () => { ++ return { ++ 'product-name': getInputValue( '#title' ), ++ 'product-description': getProductDescriptionValue( ++ isContentEditorTmceActive ++ ), ++ // For product data, we're just going to detect change if price is changed. ++ 'product-data': getInputValue( '#_regular_price' ), ++ 'product-short-description': getProductShortDescriptionValue( ++ isExcerptEditorTmceActive ++ ), ++ 'product-image': getProductImageValue(), ++ 'product-tags': getProductTagsValue(), ++ 'product-categories': getProductCategoriesValue(), ++ }; ++ }, [ isContentEditorTmceActive, isExcerptEditorTmceActive ] ); + + // If value has changed and isn't empty, returns as changed. + const hasUpdatedInfo: ( key: ProductTourStepName ) => boolean = useCallback( +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-tmce-iframe-focus-style.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-tmce-iframe-focus-style.ts +index 3f12dad2d7..2b0aff1319 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-tmce-iframe-focus-style.ts ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tour/use-tmce-iframe-focus-style.ts +@@ -14,9 +14,8 @@ const addClassToIframeWhenChildFocus = ( { + childSelector, + className, + }: addClassToIframeWhenChildFocusProps ) => { +- const iframe = document.querySelector< HTMLIFrameElement >( +- iframeSelector +- ); ++ const iframe = ++ document.querySelector< HTMLIFrameElement >( iframeSelector ); + const innerDoc = + iframe?.contentDocument || + ( iframe?.contentWindow && iframe?.contentWindow.document ); +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/products-list.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/products-list.ts +index 28678222de..5a9708f702 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/products-list.ts ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/products-list.ts +@@ -9,9 +9,8 @@ const initTracks = () => { + '#bulk-action-selector-top' + ) as HTMLInputElement; + const bulkActionsButton = document.querySelector( '#doaction' ); +- const bulkActionsCancelButton = document.querySelector( +- '#bulk-edit .cancel' +- ); ++ const bulkActionsCancelButton = ++ document.querySelector( '#bulk-edit .cancel' ); + const bulkActionsUpdateButton = document.querySelector( '#bulk_edit' ); + const featuredButtons = document.querySelectorAll( + '#the-list .featured a' +@@ -60,8 +59,8 @@ const initTracks = () => { + + bulkActionsUpdateButton?.addEventListener( 'click', function () { + recordEvent( 'products_list_bulk_edit_update', { +- product_number: document.querySelector( '#bulk-titles' )?.children +- .length, ++ product_number: ++ document.querySelector( '#bulk-titles' )?.children.length, + product_categories: + document.querySelectorAll( + '[name="tax_input[product_cat][]"]:checked' +@@ -119,9 +118,9 @@ const initTracks = () => { + + featuredButtons.forEach( ( button ) => { + button.addEventListener( 'click', function ( event ) { +- const willFeature = ( event.target as HTMLElement ).classList.contains( +- 'not-featured' +- ); ++ const willFeature = ( ++ event.target as HTMLElement ++ ).classList.contains( 'not-featured' ); + + recordEvent( 'products_list_featured_click', { + featured: willFeature ? 'yes' : 'no', +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts +index 36771619e8..3dd03a9e1d 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts +@@ -17,18 +17,18 @@ const getProductData = () => { + return { + product_id: ( document.querySelector( '#post_ID' ) as HTMLInputElement ) + ?.value, +- product_type: ( document.querySelector( +- '#product-type' +- ) as HTMLInputElement )?.value, +- is_downloadable: ( document.querySelector( +- '#_downloadable' +- ) as HTMLInputElement )?.value, +- is_virtual: ( document.querySelector( +- '#_virtual' +- ) as HTMLInputElement )?.value, +- manage_stock: ( document.querySelector( +- '#_manage_stock' +- ) as HTMLInputElement )?.value, ++ product_type: ( ++ document.querySelector( '#product-type' ) as HTMLInputElement ++ )?.value, ++ is_downloadable: ( ++ document.querySelector( '#_downloadable' ) as HTMLInputElement ++ )?.value, ++ is_virtual: ( ++ document.querySelector( '#_virtual' ) as HTMLInputElement ++ )?.value, ++ manage_stock: ( ++ document.querySelector( '#_manage_stock' ) as HTMLInputElement ++ )?.value, + }; + }; + +@@ -39,21 +39,21 @@ const getProductData = () => { + * @return string + */ + const getPublishDate = ( prefix = '' ) => { +- const month = ( document.querySelector( +- `#${ prefix }mm` +- ) as HTMLInputElement )?.value; +- const day = ( document.querySelector( +- `#${ prefix }jj` +- ) as HTMLInputElement )?.value; +- const year = ( document.querySelector( +- `#${ prefix }aa` +- ) as HTMLInputElement )?.value; +- const hours = ( document.querySelector( +- `#${ prefix }hh` +- ) as HTMLInputElement )?.value; +- const seconds = ( document.querySelector( +- `#${ prefix }mn` +- ) as HTMLInputElement )?.value; ++ const month = ( ++ document.querySelector( `#${ prefix }mm` ) as HTMLInputElement ++ )?.value; ++ const day = ( ++ document.querySelector( `#${ prefix }jj` ) as HTMLInputElement ++ )?.value; ++ const year = ( ++ document.querySelector( `#${ prefix }aa` ) as HTMLInputElement ++ )?.value; ++ const hours = ( ++ document.querySelector( `#${ prefix }hh` ) as HTMLInputElement ++ )?.value; ++ const seconds = ( ++ document.querySelector( `#${ prefix }mn` ) as HTMLInputElement ++ )?.value; + + return `${ month }-${ day }-${ year } ${ hours }:${ seconds }`; + }; +@@ -67,13 +67,17 @@ const getPublishingWidgetData = () => { + return { + status: ( document.querySelector( '#post_status' ) as HTMLInputElement ) + ?.value, +- visibility: ( document.querySelector( +- 'input[name="visibility"]:checked' +- ) as HTMLInputElement )?.value, ++ visibility: ( ++ document.querySelector( ++ 'input[name="visibility"]:checked' ++ ) as HTMLInputElement ++ )?.value, + date: getPublishDate() !== getPublishDate( 'hidden_' ) ? 'yes' : 'no', +- catalog_visibility: ( document.querySelector( +- 'input[name="_visibility"]:checked' +- ) as HTMLInputElement )?.value, ++ catalog_visibility: ( ++ document.querySelector( ++ 'input[name="_visibility"]:checked' ++ ) as HTMLInputElement ++ )?.value, + featured: ( document.querySelector( '#_featured' ) as HTMLInputElement ) + ?.checked, + }; +diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/utils.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/utils.ts +index 573ba443d0..1139c1e563 100644 +--- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/utils.ts ++++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/utils.ts +@@ -1,9 +1,9 @@ + /** + * Recursive function that waits up to 3 seconds until an element is found, then calls the callback. + * +- * @param {string} query query of the element. +- * @param {Function} func callback called when element is found. +- * @param {number} tries used internally to limit the number of tries. ++ * @param {string} query query of the element. ++ * @param {Function} func callback called when element is found. ++ * @param {number} tries used internally to limit the number of tries. + */ + export function waitUntilElementIsPresent( + query: string, +diff --git a/plugins/woocommerce-admin/docs/examples/extensions/README.md b/plugins/woocommerce-admin/docs/examples/extensions/README.md +index 91746db38e..3141c7c7fd 100644 +--- a/plugins/woocommerce-admin/docs/examples/extensions/README.md ++++ b/plugins/woocommerce-admin/docs/examples/extensions/README.md +@@ -13,7 +13,7 @@ pnpm install + Build the example extension by running the pnpm script and passing the example name. + + ```bash +-WC_EXT= pnpm example --filter=@woocommerce/admin-library ++WC_EXT= pnpm example --filter=woocommerce/client/admin + ``` + + Include the output plugin in your `.wp-env.json` and `.wp-env.override.json` and restart the WordPress instance. WooCommerce Analytics reports will now reflect the changes made by the example extension. +diff --git a/plugins/woocommerce-admin/docs/features/navigation.md b/plugins/woocommerce-admin/docs/features/navigation.md +index 677431085c..0d56e6bfe5 100644 +--- a/plugins/woocommerce-admin/docs/features/navigation.md ++++ b/plugins/woocommerce-admin/docs/features/navigation.md +@@ -10,7 +10,7 @@ This feature is hidden behind a feature flag and can be turned on or off by visi + + The fastest way to get started is by creating an example plugin from WooCommerce Admin. Enter the following command: + +-`WC_EXT=add-navigation-items pnpm example --filter=@woocommerce/admin-library` ++`WC_EXT=add-navigation-items pnpm example --filter=woocommerce/client/admin` + + This will create a new plugin that covers various features of the navigation and helps to register some intial items and categories within the new navigation menu. After running the command above, you can make edits directly to the files at `docs/examples/extensions/add-navigation-items` and they will be built and copied to your `wp-content/add-navigation-items` folder on save. + +diff --git a/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md b/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md +index bed4ad07a4..d65ce68127 100644 +--- a/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md ++++ b/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md +@@ -10,7 +10,7 @@ Gateway suggestions are retreived from a REST API and can be added via a remote + + To quickly get started with an example plugin, run the following: + +-`WC_EXT=payment-gateway-suggestions pnpm example --filter=@woocommerce/admin-library` ++`WC_EXT=payment-gateway-suggestions pnpm example --filter=woocommerce/client/admin` + + This will create a new plugin that when activated will add two new gateway suggestions. The first is a simple gateway demonstrating how configuration fields can be pulled from the gateway class to create a configuration form. The second gateway shows a more customized approach via SlotFill. + +diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json +index fcc67ba38a..5d959e3d34 100644 +--- a/plugins/woocommerce-admin/package.json ++++ b/plugins/woocommerce-admin/package.json +@@ -1,5 +1,5 @@ + { +- "name": "@woocommerce/admin-library", ++ "name": "woocommerce/client/admin", + "version": "3.3.0", + "license": "GPL-3.0-or-later", + "author": "Automattic", +@@ -10,9 +10,9 @@ + }, + "scripts": { + "analyze": "cross-env NODE_ENV=production ANALYZE=true webpack", +- "build": "pnpm run install-if-deps-outdated && pnpm run clean && WC_ADMIN_PHASE=core pnpm run build:packages && WC_ADMIN_PHASE=core pnpm run build:feature-config && cross-env NODE_ENV=production WC_ADMIN_PHASE=core webpack", ++ "build": "node ./node_modules/require-turbo && pnpm run install-if-deps-outdated && pnpm run clean && WC_ADMIN_PHASE=core pnpm run build:packages && WC_ADMIN_PHASE=core pnpm run build:feature-config && cross-env NODE_ENV=production WC_ADMIN_PHASE=core webpack", + "build:feature-config": "php ../woocommerce/bin/generate-feature-config.php", +- "build:packages": "cross-env NODE_ENV=production pnpm run:packages -- build", ++ "build:packages": "cross-env NODE_ENV=production pnpm -w exec turbo run build --filter='./packages/js/*'", + "clean": "rimraf ../woocommerce/assets/client/admin/* && pnpm run:packages -- clean --parallel", + "client:watch": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development webpack --watch", + "create-hook-reference": "node ./bin/hook-reference/index.js", +@@ -22,14 +22,14 @@ + "preinstall": "npx only-allow pnpm", + "install-if-deps-outdated": "node bin/install-if-deps-outdated.js", + "install-if-no-packages": "node bin/install-if-no-packages.js", +- "lint": "pnpm run lint:js && pnpm run lint:css", ++ "lint": "node ./node_modules/require-turbo && pnpm run lint:js && pnpm run lint:css", + "lint:fix": "pnpm lint:js-fix && pnpm lint:css-fix", + "lint:css": "stylelint '**/*.scss'", + "lint:css-fix": "stylelint '**/*.scss' --fix --ip 'storybook/wordpress'", +- "lint:js": "wp-scripts lint-js ./client --ext=js,ts,tsx", ++ "lint:js": "eslint ./client --ext=js,ts,tsx", + "lint:js-fix": "pnpm run lint:js -- --fix --ext=js,ts,tsx", +- "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", +- "lint:js-pre-commit": "wp-scripts lint-js --ext=js,ts,tsx", ++ "lint:js-packages": "eslint ../../packages/js --ext=js,ts,tsx", ++ "lint:js-pre-commit": "eslint --ext=js,ts,tsx", + "prepack": "pnpm install && pnpm run lint && pnpm run test && cross-env WC_ADMIN_PHASE=core pnpm run build", + "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", + "packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel", +@@ -42,12 +42,12 @@ + "start": "pnpm run install-if-deps-outdated && cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel\"", + "start:package": "pnpm run:packages -- start --parallel", + "pretest": "pnpm run -s install-if-no-packages", +- "test": "pnpm run test:client", ++ "test": "node ./node_modules/require-turbo && pnpm run test:client", + "test-staged": "pnpm run test:client -- --bail --findRelatedTests", + "test:client": "jest --config client/jest.config.js", + "test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache", + "test:help": "wp-scripts test-unit-js --help", +- "test:packages": "pnpm run --filter ../../packages/js/ --filter !api-core-tests test", ++ "test:packages": "pnpm -w exec turbo run test --filter='./packages/js/*'", + "test:update-snapshots": "pnpm run test:client -- --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", + "test:watch": "pnpm run test:client -- --watch", + "ts:check": "tsc --build ./tsconfig.json --pretty", +@@ -204,6 +204,7 @@ + "raw-loader": "^4.0.2", + "readline-sync": "^1.4.10", + "replace": "^1.2.1", ++ "require-turbo": "workspace:*", + "rimraf": "^3.0.2", + "rtlcss": "^2.6.2", + "sass": "^1.49.9", +@@ -228,8 +229,7 @@ + "pnpm lint:css-fix" + ], + "client/**/*.(t|j)s?(x)": [ +- "pnpm reformat-files", +- "pnpm wp-scripts lint-js", ++ "pnpm lint:js-pre-commit", + "pnpm test-staged" + ] + }, +diff --git a/plugins/woocommerce-beta-tester/CHANGELOG.md b/plugins/woocommerce-beta-tester/CHANGELOG.md +new file mode 100644 +index 0000000000..b70d759e5c +--- /dev/null ++++ b/plugins/woocommerce-beta-tester/CHANGELOG.md +@@ -0,0 +1,57 @@ ++## [2.1](https://github.com/woocommerce/woocommerce/releases) - 2022-06-16 ++ ++- Minor - Add WooCommerce Admin Helper Tester functionality to Beta Tester ++- Minor - Standardize build scripts and create a build:zip script ++- Patch - Standardize lint scripts: Add lint:fix ++- Patch - This is only updating monorepo infrastructure. ++- Minor - Updates the WC sniffs version to latest. ++ ++## [2.0.5](https://github.com/woocommerce/woocommerce/releases) - 2021-12-17 ++ ++- Fix: make WC version comparison case insensitive ++ ++## [2.0.4](https://github.com/woocommerce/woocommerce/releases) - 2021-09-29 ++ ++- Dev: Bump tested to version ++- Fix: enqueue logic for css/js assets ++ ++## [2.0.3](https://github.com/woocommerce/woocommerce/releases) - 2021-09-22 ++ ++- Fix: Bump version to release version including admin.css. ++ ++## [2.0.2](https://github.com/woocommerce/woocommerce/releases) ++ ++- Fix notice for undefined `item` ++- Fix auto_update_plugin filter reference ++- Fix including SSR in bug report ++- Fix style in version modal header ++- Add check for WooCommerce installed in default location ++ ++## [2.0.1](https://github.com/woocommerce/woocommerce/releases) ++ ++- Changes to make this plugin compatible with the upcoming WooCommerce 3.6 ++ ++## [2.0.0](https://github.com/woocommerce/woocommerce/releases) ++ ++- Enhancement - Re-built to pull updates from the WordPress.org repository rather than GitHub. ++- Enhancement - Channel selection; choose to receive RC or beta versions. ++- Enhancement - Admin bar item shows version information, and offers shortcuts to functionality. ++- Enhancement - Shortcut to log GitHub issues. ++- Enhancement - Version switcher; choose which release or prerelease to switch to. ++- Enhancement - Setting to enable auto-updates. ++ ++## [1.0.3](https://github.com/woocommerce/woocommerce/releases) ++ ++- Fix repo URLs and directory renaming. ++ ++## [1.0.2](https://github.com/woocommerce/woocommerce/releases) ++ ++- Updated API URL. ++ ++## [1.0.1](https://github.com/woocommerce/woocommerce/releases) ++ ++- Switched to releases API to get latest release, rather than tag which are not chronological. ++ ++## [1.0](https://github.com/woocommerce/woocommerce/releases) ++ ++- First release. +diff --git a/plugins/woocommerce-beta-tester/bin/build-zip.sh b/plugins/woocommerce-beta-tester/bin/build-zip.sh +index d3523fce96..b580ff8c87 100755 +--- a/plugins/woocommerce-beta-tester/bin/build-zip.sh ++++ b/plugins/woocommerce-beta-tester/bin/build-zip.sh +@@ -12,7 +12,7 @@ mkdir -p "$DEST_PATH" + echo "Installing PHP and JS dependencies..." + pnpm install + echo "Running JS Build..." +-pnpm run build ++pnpm -w exec turbo run build --filter=woocommerce-beta-tester || exit "$?" + + echo "Syncing files..." + rsync -rc --exclude-from="$PROJECT_PATH/.distignore" "$PROJECT_PATH/" "$DEST_PATH/" --delete --delete-excluded +diff --git a/plugins/woocommerce-beta-tester/changelog.txt b/plugins/woocommerce-beta-tester/changelog.txt +deleted file mode 100644 +index ff9d23bb80..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog.txt ++++ /dev/null +@@ -1,11 +0,0 @@ +-== Changelog == +- +-2021-12-17 - version 2.0.5 +-- Fix: make WC version comparison case insensitive +- +-2021-09-29 - version 2.0.4 +-- Dev: Bump tested to version +-- Fix: enqueue logic for css/js assets +- +-2021-09-22 - version 2.0.3 +-- Fix: Bump version to release version including admin.css. +diff --git a/plugins/woocommerce-beta-tester/changelog/add-admin-tester b/plugins/woocommerce-beta-tester/changelog/add-admin-tester +deleted file mode 100644 +index 3702af4071..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog/add-admin-tester ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: add +- +-Add WooCommerce Admin Helper Tester functionality to Beta Tester +diff --git a/plugins/woocommerce-beta-tester/changelog/add-require-turbo b/plugins/woocommerce-beta-tester/changelog/add-require-turbo +new file mode 100644 +index 0000000000..083b604a06 +--- /dev/null ++++ b/plugins/woocommerce-beta-tester/changelog/add-require-turbo +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This PR updates repository tooling and doesn't require a changelog entry. ++ ++ +diff --git a/plugins/woocommerce-beta-tester/changelog/consolidate-beta-tester-config b/plugins/woocommerce-beta-tester/changelog/consolidate-beta-tester-config +new file mode 100644 +index 0000000000..844d7094ab +--- /dev/null ++++ b/plugins/woocommerce-beta-tester/changelog/consolidate-beta-tester-config +@@ -0,0 +1,4 @@ ++Significance: patch ++Type: dev ++ ++Move release config in package json into woorelease object. +diff --git a/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-build-scripts b/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-build-scripts +deleted file mode 100644 +index 6f868f1493..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-build-scripts ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: dev +- +-Standardize build scripts and create a build:zip script +diff --git a/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-preuglify b/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-preuglify +deleted file mode 100644 +index 29749df3c6..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-preuglify ++++ /dev/null +@@ -1,5 +0,0 @@ +-Significance: patch +-Type: dev +-Comment: This is an infrastructure change. +- +- +diff --git a/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-release b/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-release +new file mode 100644 +index 0000000000..963bca4513 +--- /dev/null ++++ b/plugins/woocommerce-beta-tester/changelog/fix-beta-tester-release +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: This only changes a build step. ++ ++ +diff --git a/plugins/woocommerce-beta-tester/changelog/fix-changelogger-phpcs b/plugins/woocommerce-beta-tester/changelog/fix-changelogger-phpcs +new file mode 100644 +index 0000000000..10fdefc7d2 +--- /dev/null ++++ b/plugins/woocommerce-beta-tester/changelog/fix-changelogger-phpcs +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: PHPCS violation fixes ++ ++ +diff --git a/plugins/woocommerce-beta-tester/changelog/fix-standardize-lint-woocommerce-plugin b/plugins/woocommerce-beta-tester/changelog/fix-standardize-lint-woocommerce-plugin +deleted file mode 100644 +index df31acdf3b..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog/fix-standardize-lint-woocommerce-plugin ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: patch +-Type: dev +- +-Standardize lint scripts: Add lint:fix +diff --git a/plugins/woocommerce-beta-tester/changelog/pr-32886-update-sniffs b/plugins/woocommerce-beta-tester/changelog/pr-32886-update-sniffs +deleted file mode 100644 +index 2ce04d613a..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog/pr-32886-update-sniffs ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: minor +-Type: dev +- +-Updates the WC sniffs version to latest. +diff --git a/plugins/woocommerce-beta-tester/changelog/refactor-standardize-postinstall b/plugins/woocommerce-beta-tester/changelog/refactor-standardize-postinstall +deleted file mode 100644 +index a1fa604527..0000000000 +--- a/plugins/woocommerce-beta-tester/changelog/refactor-standardize-postinstall ++++ /dev/null +@@ -1,4 +0,0 @@ +-Significance: patch +-Type: dev +- +-This is only updating monorepo infrastructure. +diff --git a/plugins/woocommerce-beta-tester/composer.json b/plugins/woocommerce-beta-tester/composer.json +index 5d0757c720..34687ba440 100644 +--- a/plugins/woocommerce-beta-tester/composer.json ++++ b/plugins/woocommerce-beta-tester/composer.json +@@ -12,7 +12,7 @@ + "require-dev": { + "phpunit/phpunit": "^6.5 || ^7.5", + "woocommerce/woocommerce-sniffs": "^0.1.3", +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "scripts": { + "test": [ +@@ -36,7 +36,7 @@ + }, + "changelogger": { + "formatter": { +- "filename": "../../tools/changelogger/PluginFormatter.php" ++ "filename": "../../tools/changelogger/class-plugin-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +@@ -48,7 +48,7 @@ + "enhancement": "Improve existing functionality" + }, + "versioning": "wordpress", +- "changelog": "NEXT_CHANGELOG.md" ++ "changelog": "CHANGELOG.md" + } + }, + "config": { +diff --git a/plugins/woocommerce-beta-tester/composer.lock b/plugins/woocommerce-beta-tester/composer.lock +index 0844e65e92..aee0bc495e 100644 +--- a/plugins/woocommerce-beta-tester/composer.lock ++++ b/plugins/woocommerce-beta-tester/composer.lock +@@ -4,7 +4,7 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "34b35d8db26a95c991cf3005979a68e9", ++ "content-hash": "352e9d11e8b6eea9d16b6384f9b1539f", + "packages": [ + { + "name": "composer/installers", +@@ -161,27 +161,27 @@ + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -190,7 +190,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -212,9 +212,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", +@@ -2024,16 +2024,16 @@ + }, + { + "name": "squizlabs/php_codesniffer", +- "version": "3.6.2", ++ "version": "3.7.1", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", +- "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" ++ "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", +- "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", ++ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", ++ "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "shasum": "" + }, + "require": { +@@ -2076,7 +2076,7 @@ + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, +- "time": "2021-12-12T21:44:58+00:00" ++ "time": "2022-06-18T07:21:10+00:00" + }, + { + "name": "symfony/console", +@@ -2228,102 +2228,21 @@ + "type": "tidelift" + } + ], ++ "abandoned": "symfony/error-handler", + "time": "2022-04-12T15:19:55+00:00" + }, +- { +- "name": "symfony/polyfill-ctype", +- "version": "v1.25.0", +- "source": { +- "type": "git", +- "url": "https://github.com/symfony/polyfill-ctype.git", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab" +- }, +- "dist": { +- "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", +- "reference": "30885182c981ab175d4d034db0f6f469898070ab", +- "shasum": "" +- }, +- "require": { +- "php": ">=7.1" +- }, +- "provide": { +- "ext-ctype": "*" +- }, +- "suggest": { +- "ext-ctype": "For best performance" +- }, +- "type": "library", +- "extra": { +- "branch-alias": { +- "dev-main": "1.23-dev" +- }, +- "thanks": { +- "name": "symfony/polyfill", +- "url": "https://github.com/symfony/polyfill" +- } +- }, +- "autoload": { +- "files": [ +- "bootstrap.php" +- ], +- "psr-4": { +- "Symfony\\Polyfill\\Ctype\\": "" +- } +- }, +- "notification-url": "https://packagist.org/downloads/", +- "license": [ +- "MIT" +- ], +- "authors": [ +- { +- "name": "Gert de Pagter", +- "email": "BackEndTea@gmail.com" +- }, +- { +- "name": "Symfony Community", +- "homepage": "https://symfony.com/contributors" +- } +- ], +- "description": "Symfony polyfill for ctype functions", +- "homepage": "https://symfony.com", +- "keywords": [ +- "compatibility", +- "ctype", +- "polyfill", +- "portable" +- ], +- "support": { +- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" +- }, +- "funding": [ +- { +- "url": "https://symfony.com/sponsor", +- "type": "custom" +- }, +- { +- "url": "https://github.com/fabpot", +- "type": "github" +- }, +- { +- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", +- "type": "tidelift" +- } +- ], +- "time": "2021-10-20T20:35:02+00:00" +- }, + { + "name": "symfony/polyfill-mbstring", +- "version": "v1.25.0", ++ "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", +- "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" ++ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", +- "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", ++ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", ++ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { +@@ -2338,7 +2257,7 @@ + "type": "library", + "extra": { + "branch-alias": { +- "dev-main": "1.23-dev" ++ "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", +@@ -2377,7 +2296,7 @@ + "shim" + ], + "support": { +- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" ++ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { +@@ -2393,7 +2312,7 @@ + "type": "tidelift" + } + ], +- "time": "2021-11-30T18:21:41+00:00" ++ "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/process", +@@ -2508,21 +2427,21 @@ + }, + { + "name": "webmozart/assert", +- "version": "1.10.0", ++ "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", +- "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" ++ "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", +- "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", ++ "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", ++ "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { +- "php": "^7.2 || ^8.0", +- "symfony/polyfill-ctype": "^1.8" ++ "ext-ctype": "*", ++ "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", +@@ -2560,9 +2479,9 @@ + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", +- "source": "https://github.com/webmozarts/assert/tree/1.10.0" ++ "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, +- "time": "2021-03-09T10:59:23+00:00" ++ "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "wikimedia/at-ease", +diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json +index 227f668ec7..4570ee348a 100644 +--- a/plugins/woocommerce-beta-tester/package.json ++++ b/plugins/woocommerce-beta-tester/package.json +@@ -7,17 +7,15 @@ + "url": "git://github.com/woocommerce/woocommerce-beta-tester.git" + }, + "title": "WooCommerce Beta Tester", +- "version": "2.0.5", ++ "version": "2.1.0", + "homepage": "http://github.com/woocommerce/woocommerce-beta-tester", +- "config": { +- "build_step": "pnpm run build:zip" +- }, + "devDependencies": { + "@woocommerce/dependency-extraction-webpack-plugin": "workspace:*", + "@woocommerce/eslint-plugin": "workspace:*", + "@wordpress/env": "^4.8.0", + "@wordpress/scripts": "^19.2.4", + "eslint": "5.16.0", ++ "require-turbo": "workspace:*", + "uglify-js": "^3.5.3" + }, + "dependencies": { +@@ -40,7 +38,7 @@ + "scripts": { + "postinstall": "composer install", + "changelog": "composer exec -- changelogger", +- "build": "pnpm run build:admin && pnpm run uglify", ++ "build": "node ./node_modules/require-turbo && pnpm run build:admin && pnpm run uglify", + "build:admin": "wp-scripts build", + "build:zip": "./bin/build-zip.sh", + "build:dev": "pnpm run lint:js && pnpm run build", +@@ -66,12 +64,13 @@ + }, + "woorelease": { + "svn_reauth": "true", +- "wp_org_slug": "woocommerce-beta-tester" ++ "wp_org_slug": "woocommerce-beta-tester", ++ "build_step": "pnpm run build:zip" + }, + "lint-staged": { + "*.php": [ + "php -d display_errors=1 -l", +- "composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit" ++ "composer run-script phpcs-pre-commit" + ], + "*.(t|j)s?(x)": [ + "npm run lint:js:fix" +diff --git a/plugins/woocommerce-beta-tester/phpcs.xml b/plugins/woocommerce-beta-tester/phpcs.xml +deleted file mode 100644 +index ca3d379bb1..0000000000 +--- a/plugins/woocommerce-beta-tester/phpcs.xml ++++ /dev/null +@@ -1,51 +0,0 @@ +- +- +- +- +- +- WooCommerce dev PHP_CodeSniffer ruleset. +- +- +- */node_modules/* +- */vendor/* +- +- +- +- +- +- +- +- +- +- +- +- tests/ +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +diff --git a/plugins/woocommerce-beta-tester/readme.txt b/plugins/woocommerce-beta-tester/readme.txt +index 4d4f2b19f8..444802d612 100644 +--- a/plugins/woocommerce-beta-tester/readme.txt ++++ b/plugins/woocommerce-beta-tester/readme.txt +@@ -3,7 +3,7 @@ Contributors: automattic, bor0, claudiosanches, claudiulodro, kloon, mikejolley, + Tags: woocommerce, woo commerce, beta, beta tester, bleeding edge, testing + Requires at least: 4.7 + Tested up to: 5.6 +-Stable tag: 2.0.5 ++Stable tag: 2.1.0 + License: GPLv3 + License URI: https://www.gnu.org/licenses/gpl-3.0.html + +@@ -53,43 +53,10 @@ See our [contributing guidelines here](https://github.com/woocommerce/woocommerc + + == Changelog == + +-= 2.0.5 - 2021-12-17 = +-* Fix: make WC version comparison case insensitive ++## [2.1](https://github.com/woocommerce/woocommerce/releases/tag/2.1) - 2022-06-16 + +-= 2.0.4 - 2021-09-29 = +-* Dev: Bump tested to version +-* Fix: enqueue logic for css/js assets +- +-= 2.0.3 - 2021-09-22 = +-* Fix: Bump version to release version including admin.css. +- +-= 2.0.2 = +- +-* Fix notice for undefined `item` +-* Fix auto_update_plugin filter reference +-* Fix including SSR in bug report +-* Fix style in version modal header +-* Add check for WooCommerce installed in default location +- +-= 2.0.1 = +-* Changes to make this plugin compatible with the upcoming WooCommerce 3.6 +- +-= 2.0.0 = +-* Enhancement - Re-built to pull updates from the WordPress.org repository rather than GitHub. +-* Enhancement - Channel selection; choose to receive RC or beta versions. +-* Enhancement - Admin bar item shows version information, and offers shortcuts to functionality. +-* Enhancement - Shortcut to log GitHub issues. +-* Enhancement - Version switcher; choose which release or prerelease to switch to. +-* Enhancement - Setting to enable auto-updates. +- +-= 1.0.3 = +-* Fix repo URLs and directory renaming. +- +-= 1.0.2 = +-* Updated API URL. +- +-= 1.0.1 = +-* Switched to releases API to get latest release, rather than tag which are not chronological. +- +-= 1.0 = +-* First release. ++- Minor - Add WooCommerce Admin Helper Tester functionality to Beta Tester ++- Minor - Standardize build scripts and create a build:zip script ++- Patch - Standardize lint scripts: Add lint:fix ++- Patch - This is only updating monorepo infrastructure. ++- Minor - Updates the WC sniffs version to latest. +diff --git a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php +index 33aece11c3..c25bf61119 100644 +--- a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php ++++ b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php +@@ -3,11 +3,11 @@ + * Plugin Name: WooCommerce Beta Tester + * Plugin URI: https://github.com/woocommerce/woocommerce-beta-tester + * Description: Run bleeding edge versions of WooCommerce. This will replace your installed version of WooCommerce with the latest tagged release - use with caution, and not on production sites. +- * Version: 2.0.5 ++ * Version: 2.1.0 + * Author: WooCommerce + * Author URI: http://woocommerce.com/ +- * Requires at least: 4.4 +- * Tested up to: 5.8 ++ * Requires at least: 5.8 ++ * Tested up to: 6.0 + * WC requires at least: 3.6.0 + * WC tested up to: 5.7.0 + * Text Domain: woocommerce-beta-tester +@@ -62,7 +62,7 @@ function _wc_beta_tester_bootstrap() { + add_action( 'admin_init', array( 'WC_Beta_Tester', 'instance' ) ); + } + +- // Load admin ++ // Load admin. + require( 'plugin.php' ); + } + +@@ -76,7 +76,10 @@ function add_extension_register_script() { + $script_asset_path = dirname( __FILE__ ) . '/build/index.asset.php'; + $script_asset = file_exists( $script_asset_path ) + ? require( $script_asset_path ) +- : array( 'dependencies' => array(), 'version' => filemtime( $script_path ) ); ++ : array( ++ 'dependencies' => array(), ++ 'version' => filemtime( $script_path ), ++ ); + $script_url = plugins_url( $script_path, __FILE__ ); + + wp_register_script( +@@ -102,7 +105,7 @@ function add_extension_register_script() { + plugins_url( '/build/index.css', __FILE__ ), + // Add any dependencies styles may have, such as wp-components. + array( +- 'wp-components' ++ 'wp-components', + ), + $css_file_version + ); +diff --git a/plugins/woocommerce/.distignore b/plugins/woocommerce/.distignore +index 7e7886d4f9..a4176f018c 100644 +--- a/plugins/woocommerce/.distignore ++++ b/plugins/woocommerce/.distignore +@@ -8,6 +8,7 @@ + /changelog/ + /node_modules/ + /tests/ ++/e2e/ + babel.config.js + changelog.txt + composer.* +diff --git a/plugins/woocommerce/README.md b/plugins/woocommerce/README.md +index 8cac777258..3bcac057a0 100644 +--- a/plugins/woocommerce/README.md ++++ b/plugins/woocommerce/README.md +@@ -26,7 +26,51 @@ cd plugins/woocommerce + pnpm -- wp-env start + ``` + +-You should now be able to visit http://docker.local:8888/ and access WooCommerce environment. ++You should now be able to visit http://localhost:8888/ and access WooCommerce environment. ++ ++## Building Components ++ ++There are two major client-side components included in WooCommerce Core that can be built, linted, and tested independently. We've organized these components ++in this way to take advantage of caching to prevent unnecessarily performing expensive rebuilds when only working in one of them. ++ ++### `plugins/woocommerce/client/legacy` ++ ++This directory contains the CSS and jQuery code for WooCommerce. ++ ++```bash ++# Build the assets. ++pnpm -- turbo run build --filter=woocommerce/client/legacy ++# Lint the assets. ++pnpm -- turbo run lint --filter=woocommerce/client/legacy ++``` ++ ++### `plugins/woocommerce-admin` ++ ++This directory contains the React-based admin interface. ++ ++```bash ++# Build the React-based admin client. ++pnpm -- turbo run build --filter=woocommerce/client/admin ++# Lint the React-based admin client. ++pnpm -- turbo run lint --filter=woocommerce/client/admin ++# Test the React-based admin client. ++pnpm -- turbo run test --filter=woocommerce/client/admin ++``` ++ ++#### Helper Scripts ++ ++Here is a collection of scripts that can help when developing the React-based admin interface. ++ ++```bash ++# Create a develoment build of the React-based admin client. ++pnpm dev --filter=woocommerce/client/admin ++# Create and watch a development build of the React-based admin client. ++pnpm start --filter=woocommerce/client/admin ++# Watch the tests of the React-based admin client. ++pnpm test:watch --filter=woocommerce/client/admin ++# Run a type check over the React-based admin client's TypeScript files. ++pnpm ts:check --filter=woocommerce/client/admin ++``` + + ## Documentation + * [WooCommerce Documentation](https://docs.woocommerce.com/) +diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock +index 6c82f02706..d05f3d8113 100644 +--- a/plugins/woocommerce/bin/composer/mozart/composer.lock ++++ b/plugins/woocommerce/bin/composer/mozart/composer.lock +@@ -266,16 +266,16 @@ + }, + { + "name": "symfony/console", +- "version": "v5.4.9", ++ "version": "v5.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", +- "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb" ++ "reference": "4d671ab4ddac94ee439ea73649c69d9d200b5000" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/symfony/console/zipball/829d5d1bf60b2efeb0887b7436873becc71a45eb", +- "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb", ++ "url": "https://api.github.com/repos/symfony/console/zipball/4d671ab4ddac94ee439ea73649c69d9d200b5000", ++ "reference": "4d671ab4ddac94ee439ea73649c69d9d200b5000", + "shasum": "" + }, + "require": { +@@ -345,7 +345,7 @@ + "terminal" + ], + "support": { +- "source": "https://github.com/symfony/console/tree/v5.4.9" ++ "source": "https://github.com/symfony/console/tree/v5.4.10" + }, + "funding": [ + { +@@ -361,11 +361,11 @@ + "type": "tidelift" + } + ], +- "time": "2022-05-18T06:17:34+00:00" ++ "time": "2022-06-26T13:00:04+00:00" + }, + { + "name": "symfony/deprecation-contracts", +- "version": "v2.5.1", ++ "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", +@@ -412,7 +412,7 @@ + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { +- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" ++ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, + "funding": [ + { +@@ -987,16 +987,16 @@ + }, + { + "name": "symfony/service-contracts", +- "version": "v2.5.1", ++ "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", +- "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" ++ "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", +- "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", ++ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", ++ "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "shasum": "" + }, + "require": { +@@ -1050,7 +1050,7 @@ + "standards" + ], + "support": { +- "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" ++ "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, + "funding": [ + { +@@ -1066,20 +1066,20 @@ + "type": "tidelift" + } + ], +- "time": "2022-03-13T20:07:29+00:00" ++ "time": "2022-05-30T19:17:29+00:00" + }, + { + "name": "symfony/string", +- "version": "v5.4.9", ++ "version": "v5.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", +- "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99" ++ "reference": "4432bc7df82a554b3e413a8570ce2fea90e94097" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/symfony/string/zipball/985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", +- "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", ++ "url": "https://api.github.com/repos/symfony/string/zipball/4432bc7df82a554b3e413a8570ce2fea90e94097", ++ "reference": "4432bc7df82a554b3e413a8570ce2fea90e94097", + "shasum": "" + }, + "require": { +@@ -1136,7 +1136,7 @@ + "utf8" + ], + "support": { +- "source": "https://github.com/symfony/string/tree/v5.4.9" ++ "source": "https://github.com/symfony/string/tree/v5.4.10" + }, + "funding": [ + { +@@ -1152,7 +1152,7 @@ + "type": "tidelift" + } + ], +- "time": "2022-04-19T10:40:37+00:00" ++ "time": "2022-06-26T15:57:47+00:00" + } + ], + "aliases": [], +@@ -1167,5 +1167,5 @@ + "platform-overrides": { + "php": "7.3" + }, +- "plugin-api-version": "2.3.0" ++ "plugin-api-version": "2.2.0" + } +diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.json b/plugins/woocommerce/bin/composer/phpcs/composer.json +index 232bae80f4..51e920e522 100644 +--- a/plugins/woocommerce/bin/composer/phpcs/composer.json ++++ b/plugins/woocommerce/bin/composer/phpcs/composer.json +@@ -1,10 +1,11 @@ + { + "require-dev": { +- "woocommerce/woocommerce-sniffs": "^0.1.3" ++ "woocommerce/woocommerce-sniffs": "^0.1.3", ++ "sirbrillig/phpcs-changed": "^2.10" + }, + "config": { + "platform": { +- "php": "7.0" ++ "php": "7.4" + }, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true +diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock +index 2d6d2068f4..75ced953a3 100644 +--- a/plugins/woocommerce/bin/composer/phpcs/composer.lock ++++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock +@@ -4,7 +4,7 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "9de5fb089d0fd21b5e15523eb7c07459", ++ "content-hash": "df66db582a7c00ab0a701277e962ecce", + "packages": [], + "packages-dev": [ + { +@@ -254,6 +254,63 @@ + }, + "time": "2021-12-30T16:37:40+00:00" + }, ++ { ++ "name": "sirbrillig/phpcs-changed", ++ "version": "v2.10.0", ++ "source": { ++ "type": "git", ++ "url": "https://github.com/sirbrillig/phpcs-changed.git", ++ "reference": "ba049e6f7da40d64056f7b6c4078e87f0f292d6b" ++ }, ++ "dist": { ++ "type": "zip", ++ "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/ba049e6f7da40d64056f7b6c4078e87f0f292d6b", ++ "reference": "ba049e6f7da40d64056f7b6c4078e87f0f292d6b", ++ "shasum": "" ++ }, ++ "require": { ++ "php": "^7.1 || ^8.0" ++ }, ++ "require-dev": { ++ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", ++ "phpstan/phpstan": "^0.12.33", ++ "phpunit/phpunit": "^6.4 || ^9.5", ++ "sirbrillig/phpcs-import-detection": "^1.1.1", ++ "sirbrillig/phpcs-variable-analysis": "^2.1.3", ++ "squizlabs/php_codesniffer": "^3.2.1" ++ }, ++ "bin": [ ++ "bin/phpcs-changed" ++ ], ++ "type": "library", ++ "autoload": { ++ "files": [ ++ "PhpcsChanged/Cli.php", ++ "PhpcsChanged/SvnWorkflow.php", ++ "PhpcsChanged/GitWorkflow.php", ++ "PhpcsChanged/functions.php" ++ ], ++ "psr-4": { ++ "PhpcsChanged\\": "PhpcsChanged/" ++ } ++ }, ++ "notification-url": "https://packagist.org/downloads/", ++ "license": [ ++ "MIT" ++ ], ++ "authors": [ ++ { ++ "name": "Payton Swick", ++ "email": "payton@foolord.com" ++ } ++ ], ++ "description": "Run phpcs on files, but only report warnings/errors from lines which were changed.", ++ "support": { ++ "issues": "https://github.com/sirbrillig/phpcs-changed/issues", ++ "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.10.0" ++ }, ++ "time": "2022-03-09T18:16:50+00:00" ++ }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.1", +@@ -414,7 +471,7 @@ + "platform": [], + "platform-dev": [], + "platform-overrides": { +- "php": "7.0" ++ "php": "7.4" + }, +- "plugin-api-version": "2.3.0" ++ "plugin-api-version": "2.2.0" + } +diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock +index 19d941bd66..e9447e3c9a 100644 +--- a/plugins/woocommerce/bin/composer/phpunit/composer.lock ++++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock +@@ -1697,5 +1697,5 @@ + "platform-overrides": { + "php": "7.0" + }, +- "plugin-api-version": "2.3.0" ++ "plugin-api-version": "2.2.0" + } +diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock +index 5f2b081571..0a5ef582ad 100644 +--- a/plugins/woocommerce/bin/composer/wp/composer.lock ++++ b/plugins/woocommerce/bin/composer/wp/composer.lock +@@ -434,16 +434,16 @@ + }, + { + "name": "wp-cli/i18n-command", +- "version": "v2.3.0", ++ "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/i18n-command.git", +- "reference": "bcb1a8159679cafdf1da884dbe5830122bae2c4d" ++ "reference": "45bc2b47a4ed103b871cd2ec5b483ab55ad12d99" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/bcb1a8159679cafdf1da884dbe5830122bae2c4d", +- "reference": "bcb1a8159679cafdf1da884dbe5830122bae2c4d", ++ "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/45bc2b47a4ed103b871cd2ec5b483ab55ad12d99", ++ "reference": "45bc2b47a4ed103b871cd2ec5b483ab55ad12d99", + "shasum": "" + }, + "require": { +@@ -457,6 +457,7 @@ + "wp-cli/wp-cli-tests": "^3.1" + }, + "suggest": { ++ "ext-json": "Used for reading and generating JSON translation files", + "ext-mbstring": "Used for calculating include/exclude matches in code extraction" + }, + "type": "wp-cli-package", +@@ -468,7 +469,9 @@ + "commands": [ + "i18n", + "i18n make-pot", +- "i18n make-json" ++ "i18n make-json", ++ "i18n make-mo", ++ "i18n update-po" + ] + }, + "autoload": { +@@ -493,9 +496,9 @@ + "homepage": "https://github.com/wp-cli/i18n-command", + "support": { + "issues": "https://github.com/wp-cli/i18n-command/issues", +- "source": "https://github.com/wp-cli/i18n-command/tree/v2.3.0" ++ "source": "https://github.com/wp-cli/i18n-command/tree/v2.4.0" + }, +- "time": "2022-04-06T15:32:48+00:00" ++ "time": "2022-07-04T21:43:20+00:00" + }, + { + "name": "wp-cli/mustangostang-spyc", +@@ -550,16 +553,16 @@ + }, + { + "name": "wp-cli/php-cli-tools", +- "version": "v0.11.13", ++ "version": "v0.11.14", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/php-cli-tools.git", +- "reference": "a2866855ac1abc53005c102e901553ad5772dc04" ++ "reference": "f8f340e4a87687549d046e2da516242f7f36c934" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/a2866855ac1abc53005c102e901553ad5772dc04", +- "reference": "a2866855ac1abc53005c102e901553ad5772dc04", ++ "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/f8f340e4a87687549d046e2da516242f7f36c934", ++ "reference": "f8f340e4a87687549d046e2da516242f7f36c934", + "shasum": "" + }, + "require": { +@@ -598,9 +601,9 @@ + ], + "support": { + "issues": "https://github.com/wp-cli/php-cli-tools/issues", +- "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.13" ++ "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.14" + }, +- "time": "2021-07-01T15:08:16+00:00" ++ "time": "2022-07-04T21:44:34+00:00" + }, + { + "name": "wp-cli/wp-cli", +@@ -684,5 +687,5 @@ + "platform-overrides": { + "php": "7.0" + }, +- "plugin-api-version": "2.3.0" ++ "plugin-api-version": "2.2.0" + } +diff --git a/plugins/woocommerce/changelog/prep-6.8.1-versions b/plugins/woocommerce/changelog/prep-6.8.1-versions +new file mode 100644 +index 0000000000..96328d0be1 +--- /dev/null ++++ b/plugins/woocommerce/changelog/prep-6.8.1-versions +@@ -0,0 +1,5 @@ ++Significance: patch ++Type: dev ++Comment: Bumped versions for 6.8.2 ++ ++ +diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json +index e4276fd4f2..9f0f36fa3c 100644 +--- a/plugins/woocommerce/client/admin/config/core.json ++++ b/plugins/woocommerce/client/admin/config/core.json +@@ -8,6 +8,8 @@ + "experimental-import-products-task": true, + "experimental-fashion-sample-products": true, + "experimental-product-tour": true, ++ "shipping-smart-defaults": true, ++ "shipping-setting-tour": true, + "homescreen": true, + "marketing": true, + "minified-js": false, +diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json +index 8e1d7a95ab..1a24590591 100644 +--- a/plugins/woocommerce/client/admin/config/development.json ++++ b/plugins/woocommerce/client/admin/config/development.json +@@ -8,6 +8,8 @@ + "experimental-import-products-task": true, + "experimental-fashion-sample-products": true, + "experimental-product-tour": true, ++ "shipping-smart-defaults": true, ++ "shipping-setting-tour": true, + "homescreen": true, + "marketing": true, + "minified-js": true, +diff --git a/plugins/woocommerce/legacy/.browserslistrc b/plugins/woocommerce/client/legacy/.browserslistrc +similarity index 100% +rename from plugins/woocommerce/legacy/.browserslistrc +rename to plugins/woocommerce/client/legacy/.browserslistrc +diff --git a/plugins/woocommerce/legacy/.eslintignore b/plugins/woocommerce/client/legacy/.eslintignore +similarity index 100% +rename from plugins/woocommerce/legacy/.eslintignore +rename to plugins/woocommerce/client/legacy/.eslintignore +diff --git a/plugins/woocommerce/legacy/.eslintrc.js b/plugins/woocommerce/client/legacy/.eslintrc.js +similarity index 100% +rename from plugins/woocommerce/legacy/.eslintrc.js +rename to plugins/woocommerce/client/legacy/.eslintrc.js +diff --git a/plugins/woocommerce/legacy/.gitignore b/plugins/woocommerce/client/legacy/.gitignore +similarity index 100% +rename from plugins/woocommerce/legacy/.gitignore +rename to plugins/woocommerce/client/legacy/.gitignore +diff --git a/plugins/woocommerce/legacy/.stylelintrc b/plugins/woocommerce/client/legacy/.stylelintrc +similarity index 100% +rename from plugins/woocommerce/legacy/.stylelintrc +rename to plugins/woocommerce/client/legacy/.stylelintrc +diff --git a/plugins/woocommerce/legacy/Gruntfile.js b/plugins/woocommerce/client/legacy/Gruntfile.js +similarity index 98% +rename from plugins/woocommerce/legacy/Gruntfile.js +rename to plugins/woocommerce/client/legacy/Gruntfile.js +index e67ea826ec..fa10bb87c6 100644 +--- a/plugins/woocommerce/legacy/Gruntfile.js ++++ b/plugins/woocommerce/client/legacy/Gruntfile.js +@@ -6,11 +6,11 @@ module.exports = function ( grunt ) { + // Setting folder templates. + dirs: { + css: 'css', +- cssDest: '../assets/css', ++ cssDest: '../../assets/css', + fonts: 'assets/fonts', + images: 'assets/images', + js: 'js', +- jsDest: '../assets/js', ++ jsDest: '../../assets/js', + php: 'includes', + }, + +diff --git a/plugins/woocommerce/legacy/css/_animation.scss b/plugins/woocommerce/client/legacy/css/_animation.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/_animation.scss +rename to plugins/woocommerce/client/legacy/css/_animation.scss +diff --git a/plugins/woocommerce/legacy/css/_fonts.scss b/plugins/woocommerce/client/legacy/css/_fonts.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/_fonts.scss +rename to plugins/woocommerce/client/legacy/css/_fonts.scss +diff --git a/plugins/woocommerce/legacy/css/_mixins.scss b/plugins/woocommerce/client/legacy/css/_mixins.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/_mixins.scss +rename to plugins/woocommerce/client/legacy/css/_mixins.scss +diff --git a/plugins/woocommerce/legacy/css/_variables.scss b/plugins/woocommerce/client/legacy/css/_variables.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/_variables.scss +rename to plugins/woocommerce/client/legacy/css/_variables.scss +diff --git a/plugins/woocommerce/legacy/css/activation.scss b/plugins/woocommerce/client/legacy/css/activation.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/activation.scss +rename to plugins/woocommerce/client/legacy/css/activation.scss +diff --git a/plugins/woocommerce/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss +similarity index 99% +rename from plugins/woocommerce/legacy/css/admin.scss +rename to plugins/woocommerce/client/legacy/css/admin.scss +index 36a9d59116..bd32dc08ec 100644 +--- a/plugins/woocommerce/legacy/css/admin.scss ++++ b/plugins/woocommerce/client/legacy/css/admin.scss +@@ -5242,6 +5242,9 @@ img.help_tip { + p.form-field, + fieldset.form-field { + padding: 5px 20px 5px 162px !important; /** Padding for aligning labels left - 12px + 150 label width **/ ++ &._sold_individually_field { ++ padding-right: 0px !important; ++ } + } + + .sale_price_dates_fields { +@@ -5392,6 +5395,13 @@ img.help_tip { + .select2-container { + float: left; + } ++ ++ .inventory_sold_individually { ++ display: flex; ++ .woocommerce-help-tip { ++ align-self: center; ++ } ++ } + } + + #woocommerce-product-data input.dp-applied { +@@ -5857,6 +5867,7 @@ img.tips { + z-index: 8675309; + position: absolute; + top: 0; ++ pointer-events: none; + + /*rtl:ignore*/ + left: 0; +diff --git a/plugins/woocommerce/legacy/css/auth.scss b/plugins/woocommerce/client/legacy/css/auth.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/auth.scss +rename to plugins/woocommerce/client/legacy/css/auth.scss +diff --git a/plugins/woocommerce/legacy/css/dashboard-setup.scss b/plugins/woocommerce/client/legacy/css/dashboard-setup.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/dashboard-setup.scss +rename to plugins/woocommerce/client/legacy/css/dashboard-setup.scss +diff --git a/plugins/woocommerce/legacy/css/dashboard.scss b/plugins/woocommerce/client/legacy/css/dashboard.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/dashboard.scss +rename to plugins/woocommerce/client/legacy/css/dashboard.scss +diff --git a/plugins/woocommerce/legacy/css/helper.scss b/plugins/woocommerce/client/legacy/css/helper.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/helper.scss +rename to plugins/woocommerce/client/legacy/css/helper.scss +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_222222_256x240.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_222222_256x240.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_222222_256x240.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_222222_256x240.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_2e83ff_256x240.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_2e83ff_256x240.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_2e83ff_256x240.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_2e83ff_256x240.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_454545_256x240.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_454545_256x240.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_454545_256x240.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_454545_256x240.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_888888_256x240.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_888888_256x240.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_888888_256x240.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_888888_256x240.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png b/plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png +rename to plugins/woocommerce/client/legacy/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/jquery-ui-rtl.css b/plugins/woocommerce/client/legacy/css/jquery-ui/jquery-ui-rtl.css +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/jquery-ui-rtl.css +rename to plugins/woocommerce/client/legacy/css/jquery-ui/jquery-ui-rtl.css +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.css b/plugins/woocommerce/client/legacy/css/jquery-ui/jquery-ui.css +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.css +rename to plugins/woocommerce/client/legacy/css/jquery-ui/jquery-ui.css +diff --git a/plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.min.css b/plugins/woocommerce/client/legacy/css/jquery-ui/jquery-ui.min.css +similarity index 100% +rename from plugins/woocommerce/legacy/css/jquery-ui/jquery-ui.min.css +rename to plugins/woocommerce/client/legacy/css/jquery-ui/jquery-ui.min.css +diff --git a/plugins/woocommerce/legacy/css/marketplace-suggestions.scss b/plugins/woocommerce/client/legacy/css/marketplace-suggestions.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/marketplace-suggestions.scss +rename to plugins/woocommerce/client/legacy/css/marketplace-suggestions.scss +diff --git a/plugins/woocommerce/legacy/css/menu.scss b/plugins/woocommerce/client/legacy/css/menu.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/menu.scss +rename to plugins/woocommerce/client/legacy/css/menu.scss +diff --git a/plugins/woocommerce/legacy/css/network-order-widget.scss b/plugins/woocommerce/client/legacy/css/network-order-widget.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/network-order-widget.scss +rename to plugins/woocommerce/client/legacy/css/network-order-widget.scss +diff --git a/plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.css b/plugins/woocommerce/client/legacy/css/photoswipe/default-skin/default-skin.css +similarity index 100% +rename from plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.css +rename to plugins/woocommerce/client/legacy/css/photoswipe/default-skin/default-skin.css +diff --git a/plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.png b/plugins/woocommerce/client/legacy/css/photoswipe/default-skin/default-skin.png +similarity index 100% +rename from plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.png +rename to plugins/woocommerce/client/legacy/css/photoswipe/default-skin/default-skin.png +diff --git a/plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.svg b/plugins/woocommerce/client/legacy/css/photoswipe/default-skin/default-skin.svg +similarity index 100% +rename from plugins/woocommerce/legacy/css/photoswipe/default-skin/default-skin.svg +rename to plugins/woocommerce/client/legacy/css/photoswipe/default-skin/default-skin.svg +diff --git a/plugins/woocommerce/legacy/css/photoswipe/default-skin/preloader.gif b/plugins/woocommerce/client/legacy/css/photoswipe/default-skin/preloader.gif +similarity index 100% +rename from plugins/woocommerce/legacy/css/photoswipe/default-skin/preloader.gif +rename to plugins/woocommerce/client/legacy/css/photoswipe/default-skin/preloader.gif +diff --git a/plugins/woocommerce/legacy/css/photoswipe/photoswipe.css b/plugins/woocommerce/client/legacy/css/photoswipe/photoswipe.css +similarity index 100% +rename from plugins/woocommerce/legacy/css/photoswipe/photoswipe.css +rename to plugins/woocommerce/client/legacy/css/photoswipe/photoswipe.css +diff --git a/plugins/woocommerce/legacy/css/prettyPhoto.scss b/plugins/woocommerce/client/legacy/css/prettyPhoto.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/prettyPhoto.scss +rename to plugins/woocommerce/client/legacy/css/prettyPhoto.scss +diff --git a/plugins/woocommerce/legacy/css/privacy.scss b/plugins/woocommerce/client/legacy/css/privacy.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/privacy.scss +rename to plugins/woocommerce/client/legacy/css/privacy.scss +diff --git a/plugins/woocommerce/legacy/css/reports-print.scss b/plugins/woocommerce/client/legacy/css/reports-print.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/reports-print.scss +rename to plugins/woocommerce/client/legacy/css/reports-print.scss +diff --git a/plugins/woocommerce/legacy/css/select2.scss b/plugins/woocommerce/client/legacy/css/select2.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/select2.scss +rename to plugins/woocommerce/client/legacy/css/select2.scss +diff --git a/plugins/woocommerce/legacy/css/twenty-nineteen.scss b/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/twenty-nineteen.scss +rename to plugins/woocommerce/client/legacy/css/twenty-nineteen.scss +diff --git a/plugins/woocommerce/legacy/css/twenty-seventeen.scss b/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/twenty-seventeen.scss +rename to plugins/woocommerce/client/legacy/css/twenty-seventeen.scss +diff --git a/plugins/woocommerce/legacy/css/twenty-twenty-one-admin.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-one-admin.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/twenty-twenty-one-admin.scss +rename to plugins/woocommerce/client/legacy/css/twenty-twenty-one-admin.scss +diff --git a/plugins/woocommerce/legacy/css/twenty-twenty-one.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/twenty-twenty-one.scss +rename to plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss +diff --git a/plugins/woocommerce/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +similarity index 99% +rename from plugins/woocommerce/legacy/css/twenty-twenty-two.scss +rename to plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +index ae5b2594ce..b8baaa4301 100644 +--- a/plugins/woocommerce/legacy/css/twenty-twenty-two.scss ++++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +@@ -95,6 +95,10 @@ $tt2-gray: #f7f7f7; + display: none; + } + ++ .woocommerce-breadcrumb { ++ margin-bottom: 1rem; ++ } ++ + .woocommerce-message, + .woocommerce-error, + .woocommerce-info { +@@ -253,7 +257,12 @@ $tt2-gray: #f7f7f7; + + div.product { + position: relative; +- overflow: auto; ++ ++ &::after { ++ content: ""; ++ display: block; ++ clear: both; ++ } + + > span.onsale { + position: absolute; +@@ -747,7 +756,7 @@ $tt2-gray: #f7f7f7; + } + } + } +- ++ + table.shop_table, + table.shop_table_responsive { + tbody { +diff --git a/plugins/woocommerce/legacy/css/twenty-twenty.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/twenty-twenty.scss +rename to plugins/woocommerce/client/legacy/css/twenty-twenty.scss +diff --git a/plugins/woocommerce/legacy/css/wc-setup.scss b/plugins/woocommerce/client/legacy/css/wc-setup.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/wc-setup.scss +rename to plugins/woocommerce/client/legacy/css/wc-setup.scss +diff --git a/plugins/woocommerce/legacy/css/woocommerce-layout.scss b/plugins/woocommerce/client/legacy/css/woocommerce-layout.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/woocommerce-layout.scss +rename to plugins/woocommerce/client/legacy/css/woocommerce-layout.scss +diff --git a/plugins/woocommerce/legacy/css/woocommerce-smallscreen.scss b/plugins/woocommerce/client/legacy/css/woocommerce-smallscreen.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/woocommerce-smallscreen.scss +rename to plugins/woocommerce/client/legacy/css/woocommerce-smallscreen.scss +diff --git a/plugins/woocommerce/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss +similarity index 100% +rename from plugins/woocommerce/legacy/css/woocommerce.scss +rename to plugins/woocommerce/client/legacy/css/woocommerce.scss +diff --git a/plugins/woocommerce/legacy/js/accounting/accounting.js b/plugins/woocommerce/client/legacy/js/accounting/accounting.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/accounting/accounting.js +rename to plugins/woocommerce/client/legacy/js/accounting/accounting.js +diff --git a/plugins/woocommerce/legacy/js/admin/api-keys.js b/plugins/woocommerce/client/legacy/js/admin/api-keys.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/api-keys.js +rename to plugins/woocommerce/client/legacy/js/admin/api-keys.js +diff --git a/plugins/woocommerce/legacy/js/admin/backbone-modal.js b/plugins/woocommerce/client/legacy/js/admin/backbone-modal.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/backbone-modal.js +rename to plugins/woocommerce/client/legacy/js/admin/backbone-modal.js +diff --git a/plugins/woocommerce/legacy/js/admin/marketplace-suggestions.js b/plugins/woocommerce/client/legacy/js/admin/marketplace-suggestions.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/marketplace-suggestions.js +rename to plugins/woocommerce/client/legacy/js/admin/marketplace-suggestions.js +diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-coupon.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-coupon.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/meta-boxes-coupon.js +rename to plugins/woocommerce/client/legacy/js/admin/meta-boxes-coupon.js +diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js +similarity index 96% +rename from plugins/woocommerce/legacy/js/admin/meta-boxes-order.js +rename to plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js +index 78d3d95d8a..2cb9841c54 100644 +--- a/plugins/woocommerce/legacy/js/admin/meta-boxes-order.js ++++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js +@@ -333,6 +333,20 @@ jQuery( function ( $ ) { + $( '#woocommerce-order-items' ).unblock(); + }, + ++ filter_data: function( handle, data ) { ++ const filteredData = $( '#woocommerce-order-items' ) ++ .triggerHandler( ++ `woocommerce_order_meta_box_${handle}_ajax_data`, ++ [ data ] ++ ); ++ ++ if ( filteredData ) { ++ return filteredData; ++ } ++ ++ return data; ++ }, ++ + reload_items: function() { + var data = { + order_id: woocommerce_admin_meta_boxes.post_id, +@@ -340,6 +354,8 @@ jQuery( function ( $ ) { + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'reload_items', data ); ++ + wc_meta_boxes_order_items.block(); + + $.ajax({ +@@ -462,6 +478,8 @@ jQuery( function ( $ ) { + user_email : user_email + } ); + ++ data = wc_meta_boxes_order_items.filter_data( 'add_coupon', data ); ++ + $.ajax( { + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, +@@ -500,6 +518,8 @@ jQuery( function ( $ ) { + coupon : $this.data( 'code' ) + } ); + ++ data = wc_meta_boxes_order_items.filter_data( 'remove_coupon', data ); ++ + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); +@@ -587,6 +607,8 @@ jQuery( function ( $ ) { + amount : value + } ); + ++ data = wc_meta_boxes_order_items.filter_data( 'add_fee', data ); ++ + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); +@@ -594,7 +616,7 @@ jQuery( function ( $ ) { + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + window.wcTracks.recordEvent( 'order_edit_added_fee', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } else { +@@ -616,11 +638,13 @@ jQuery( function ( $ ) { + dataType : 'json' + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'add_shipping', data ); ++ + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.success ) { + $( 'table.woocommerce_order_items tbody#order_shipping_line_items' ).append( response.data.html ); + window.wcTracks.recordEvent( 'order_edit_add_shipping', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } else { +@@ -683,6 +707,8 @@ jQuery( function ( $ ) { + data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); + } + ++ data = wc_meta_boxes_order_items.filter_data( 'delete_item', data ); ++ + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, +@@ -707,7 +733,7 @@ jQuery( function ( $ ) { + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_remove_item', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } +@@ -727,6 +753,8 @@ jQuery( function ( $ ) { + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'delete_tax', data ); ++ + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, +@@ -797,6 +825,8 @@ jQuery( function ( $ ) { + security: woocommerce_admin_meta_boxes.calc_totals_nonce + } ); + ++ data = wc_meta_boxes_order_items.filter_data( 'recalculate', data ); ++ + $( document.body ).trigger( 'order-totals-recalculate-before', data ); + + $.ajax({ +@@ -815,7 +845,7 @@ jQuery( function ( $ ) { + $( document.body ).trigger( 'order-totals-recalculate-complete', response ); + + window.wcTracks.recordEvent( 'order_edit_recalc_totals', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + ok_cancel: 'OK', + status: $( '#order_status' ).val() + } ); +@@ -840,6 +870,8 @@ jQuery( function ( $ ) { + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'save_line_items', data ); ++ + wc_meta_boxes_order_items.block(); + + $.ajax({ +@@ -866,7 +898,7 @@ jQuery( function ( $ ) { + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_save_line_items', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } +@@ -938,6 +970,8 @@ jQuery( function ( $ ) { + security : woocommerce_admin_meta_boxes.order_item_nonce + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'do_refund', data ); ++ + $.ajax( { + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, +@@ -980,6 +1014,8 @@ jQuery( function ( $ ) { + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'delete_refund', data ); ++ + $.ajax({ + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, +@@ -1192,6 +1228,8 @@ jQuery( function ( $ ) { + data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); + } + ++ data = wc_meta_boxes_order_items.filter_data( 'add_items', data ); ++ + $.ajax({ + type: 'POST', + url: woocommerce_admin_meta_boxes.ajax_url, +@@ -1216,7 +1254,7 @@ jQuery( function ( $ ) { + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_add_products', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + }, +@@ -1248,6 +1286,8 @@ jQuery( function ( $ ) { + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + ++ data = wc_meta_boxes_order_items.filter_data( 'add_tax', data ); ++ + $.ajax({ + url : woocommerce_admin_meta_boxes.ajax_url, + data : data, +@@ -1265,7 +1305,7 @@ jQuery( function ( $ ) { + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_add_tax', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + status: $( '#order_status' ).val() + } ); + } +@@ -1330,7 +1370,7 @@ jQuery( function ( $ ) { + $( '#woocommerce-order-notes' ).unblock(); + $( '#add_order_note' ).val( '' ); + window.wcTracks.recordEvent( 'order_edit_add_order_note', { +- order_id: data.post_id, ++ order_id: woocommerce_admin_meta_boxes.post_id, + note_type: data.note_type || 'private', + status: $( '#order_status' ).val() + } ); +diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +similarity index 99% +rename from plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js +rename to plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +index 79d3e7b322..e6949a5fb4 100644 +--- a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js ++++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +@@ -118,7 +118,13 @@ jQuery( function( $ ) { + // Init TipTip + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); +- $( '.woocommerce_variations .tips, .woocommerce_variations .help_tip, .woocommerce_variations .woocommerce-help-tip', wrapper ) ++ $( ++ '.woocommerce_variations .tips, ' + ++ '.woocommerce_variations .help_tip, ' + ++ '.woocommerce_variations .woocommerce-help-tip, ' + ++ '.toolbar-variations-defaults .woocommerce-help-tip', ++ wrapper ++ ) + .tipTip({ + 'attribute': 'data-tip', + 'fadeIn': 50, +@@ -812,7 +818,7 @@ jQuery( function( $ ) { + default : + $( 'select.variation_actions' ).trigger( do_variation_action ); + data = $( 'select.variation_actions' ).triggerHandler( do_variation_action + '_ajax_data', data ); +- ++ + if ( null === data ) { + return; + } +diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/meta-boxes-product.js +rename to plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/meta-boxes.js +rename to plugins/woocommerce/client/legacy/js/admin/meta-boxes.js +diff --git a/plugins/woocommerce/legacy/js/admin/network-orders.js b/plugins/woocommerce/client/legacy/js/admin/network-orders.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/network-orders.js +rename to plugins/woocommerce/client/legacy/js/admin/network-orders.js +diff --git a/plugins/woocommerce/legacy/js/admin/product-ordering.js b/plugins/woocommerce/client/legacy/js/admin/product-ordering.js +similarity index 90% +rename from plugins/woocommerce/legacy/js/admin/product-ordering.js +rename to plugins/woocommerce/client/legacy/js/admin/product-ordering.js +index c030871a48..b6e358d20e 100644 +--- a/plugins/woocommerce/legacy/js/admin/product-ordering.js ++++ b/plugins/woocommerce/client/legacy/js/admin/product-ordering.js +@@ -66,6 +66,15 @@ jQuery( function( $ ) { + $( this ).removeClass( 'alternate' ); + } + }); +- } ++ }, ++ sort: function (e, ui) { ++ ui.placeholder.find( 'td' ).each( function( key, value ) { ++ if ( ui.helper.find( 'td' ).eq( key ).is( ':visible' ) ) { ++ $( this ).show(); ++ } else { ++ $( this ).hide(); ++ } ++ }); ++ } + }); + }); +diff --git a/plugins/woocommerce/legacy/js/admin/quick-edit.js b/plugins/woocommerce/client/legacy/js/admin/quick-edit.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/quick-edit.js +rename to plugins/woocommerce/client/legacy/js/admin/quick-edit.js +diff --git a/plugins/woocommerce/legacy/js/admin/reports.js b/plugins/woocommerce/client/legacy/js/admin/reports.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/reports.js +rename to plugins/woocommerce/client/legacy/js/admin/reports.js +diff --git a/plugins/woocommerce/legacy/js/admin/settings-views-html-settings-tax.js b/plugins/woocommerce/client/legacy/js/admin/settings-views-html-settings-tax.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/settings-views-html-settings-tax.js +rename to plugins/woocommerce/client/legacy/js/admin/settings-views-html-settings-tax.js +diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/client/legacy/js/admin/settings.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/settings.js +rename to plugins/woocommerce/client/legacy/js/admin/settings.js +diff --git a/plugins/woocommerce/legacy/js/admin/system-status.js b/plugins/woocommerce/client/legacy/js/admin/system-status.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/system-status.js +rename to plugins/woocommerce/client/legacy/js/admin/system-status.js +diff --git a/plugins/woocommerce/legacy/js/admin/term-ordering.js b/plugins/woocommerce/client/legacy/js/admin/term-ordering.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/term-ordering.js +rename to plugins/woocommerce/client/legacy/js/admin/term-ordering.js +diff --git a/plugins/woocommerce/legacy/js/admin/users.js b/plugins/woocommerce/client/legacy/js/admin/users.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/users.js +rename to plugins/woocommerce/client/legacy/js/admin/users.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-clipboard.js b/plugins/woocommerce/client/legacy/js/admin/wc-clipboard.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-clipboard.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-clipboard.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-enhanced-select.js b/plugins/woocommerce/client/legacy/js/admin/wc-enhanced-select.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-enhanced-select.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-enhanced-select.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-orders.js b/plugins/woocommerce/client/legacy/js/admin/wc-orders.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-orders.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-orders.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-product-export.js b/plugins/woocommerce/client/legacy/js/admin/wc-product-export.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-product-export.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-product-export.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-product-import.js b/plugins/woocommerce/client/legacy/js/admin/wc-product-import.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-product-import.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-product-import.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-setup.js b/plugins/woocommerce/client/legacy/js/admin/wc-setup.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-setup.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-setup.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-shipping-classes.js b/plugins/woocommerce/client/legacy/js/admin/wc-shipping-classes.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-shipping-classes.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-shipping-classes.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-shipping-zone-methods.js b/plugins/woocommerce/client/legacy/js/admin/wc-shipping-zone-methods.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-shipping-zone-methods.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-shipping-zone-methods.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-shipping-zones.js b/plugins/woocommerce/client/legacy/js/admin/wc-shipping-zones.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-shipping-zones.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-shipping-zones.js +diff --git a/plugins/woocommerce/legacy/js/admin/wc-status-widget.js b/plugins/woocommerce/client/legacy/js/admin/wc-status-widget.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/wc-status-widget.js +rename to plugins/woocommerce/client/legacy/js/admin/wc-status-widget.js +diff --git a/plugins/woocommerce/legacy/js/admin/woocommerce_admin.js b/plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/admin/woocommerce_admin.js +rename to plugins/woocommerce/client/legacy/js/admin/woocommerce_admin.js +diff --git a/plugins/woocommerce/legacy/js/flexslider/jquery.flexslider.js b/plugins/woocommerce/client/legacy/js/flexslider/jquery.flexslider.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/flexslider/jquery.flexslider.js +rename to plugins/woocommerce/client/legacy/js/flexslider/jquery.flexslider.js +diff --git a/plugins/woocommerce/legacy/js/frontend/add-payment-method.js b/plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/add-payment-method.js +rename to plugins/woocommerce/client/legacy/js/frontend/add-payment-method.js +diff --git a/plugins/woocommerce/legacy/js/frontend/add-to-cart-variation.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/add-to-cart-variation.js +rename to plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js +diff --git a/plugins/woocommerce/legacy/js/frontend/add-to-cart.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/add-to-cart.js +rename to plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js +diff --git a/plugins/woocommerce/legacy/js/frontend/address-i18n.js b/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/address-i18n.js +rename to plugins/woocommerce/client/legacy/js/frontend/address-i18n.js +diff --git a/plugins/woocommerce/legacy/js/frontend/cart-fragments.js b/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/cart-fragments.js +rename to plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js +diff --git a/plugins/woocommerce/legacy/js/frontend/cart.js b/plugins/woocommerce/client/legacy/js/frontend/cart.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/cart.js +rename to plugins/woocommerce/client/legacy/js/frontend/cart.js +diff --git a/plugins/woocommerce/legacy/js/frontend/checkout.js b/plugins/woocommerce/client/legacy/js/frontend/checkout.js +similarity index 99% +rename from plugins/woocommerce/legacy/js/frontend/checkout.js +rename to plugins/woocommerce/client/legacy/js/frontend/checkout.js +index a6b61eaacb..91a33b6932 100644 +--- a/plugins/woocommerce/legacy/js/frontend/checkout.js ++++ b/plugins/woocommerce/client/legacy/js/frontend/checkout.js +@@ -560,7 +560,11 @@ jQuery( function( $ ) { + // Detach the unload handler that prevents a reload / redirect + wc_checkout_form.detachUnloadEventsOnSubmit(); + +- wc_checkout_form.submit_error( '
' + errorThrown + '
' ); ++ wc_checkout_form.submit_error( ++ '
' + ++ ( errorThrown || wc_checkout_params.i18n_checkout_error ) + ++ '
' ++ ); + } + }); + } +diff --git a/plugins/woocommerce/legacy/js/frontend/country-select.js b/plugins/woocommerce/client/legacy/js/frontend/country-select.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/country-select.js +rename to plugins/woocommerce/client/legacy/js/frontend/country-select.js +diff --git a/plugins/woocommerce/legacy/js/frontend/credit-card-form.js b/plugins/woocommerce/client/legacy/js/frontend/credit-card-form.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/credit-card-form.js +rename to plugins/woocommerce/client/legacy/js/frontend/credit-card-form.js +diff --git a/plugins/woocommerce/legacy/js/frontend/geolocation.js b/plugins/woocommerce/client/legacy/js/frontend/geolocation.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/geolocation.js +rename to plugins/woocommerce/client/legacy/js/frontend/geolocation.js +diff --git a/plugins/woocommerce/legacy/js/frontend/lost-password.js b/plugins/woocommerce/client/legacy/js/frontend/lost-password.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/lost-password.js +rename to plugins/woocommerce/client/legacy/js/frontend/lost-password.js +diff --git a/plugins/woocommerce/legacy/js/frontend/password-strength-meter.js b/plugins/woocommerce/client/legacy/js/frontend/password-strength-meter.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/password-strength-meter.js +rename to plugins/woocommerce/client/legacy/js/frontend/password-strength-meter.js +diff --git a/plugins/woocommerce/legacy/js/frontend/price-slider.js b/plugins/woocommerce/client/legacy/js/frontend/price-slider.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/price-slider.js +rename to plugins/woocommerce/client/legacy/js/frontend/price-slider.js +diff --git a/plugins/woocommerce/legacy/js/frontend/single-product.js b/plugins/woocommerce/client/legacy/js/frontend/single-product.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/single-product.js +rename to plugins/woocommerce/client/legacy/js/frontend/single-product.js +diff --git a/plugins/woocommerce/legacy/js/frontend/tokenization-form.js b/plugins/woocommerce/client/legacy/js/frontend/tokenization-form.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/tokenization-form.js +rename to plugins/woocommerce/client/legacy/js/frontend/tokenization-form.js +diff --git a/plugins/woocommerce/legacy/js/frontend/woocommerce.js b/plugins/woocommerce/client/legacy/js/frontend/woocommerce.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/frontend/woocommerce.js +rename to plugins/woocommerce/client/legacy/js/frontend/woocommerce.js +diff --git a/plugins/woocommerce/legacy/js/jquery-blockui/jquery.blockUI.js b/plugins/woocommerce/client/legacy/js/jquery-blockui/jquery.blockUI.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-blockui/jquery.blockUI.js +rename to plugins/woocommerce/client/legacy/js/jquery-blockui/jquery.blockUI.js +diff --git a/plugins/woocommerce/legacy/js/jquery-cookie/jquery.cookie.js b/plugins/woocommerce/client/legacy/js/jquery-cookie/jquery.cookie.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-cookie/jquery.cookie.js +rename to plugins/woocommerce/client/legacy/js/jquery-cookie/jquery.cookie.js +diff --git a/plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.js b/plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.js +rename to plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.js +diff --git a/plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.pie.js b/plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.pie.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.pie.js +rename to plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.pie.js +diff --git a/plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.resize.js b/plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.resize.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.resize.js +rename to plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.resize.js +diff --git a/plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.stack.js b/plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.stack.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.stack.js +rename to plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.stack.js +diff --git a/plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.time.js b/plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.time.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-flot/jquery.flot.time.js +rename to plugins/woocommerce/client/legacy/js/jquery-flot/jquery.flot.time.js +diff --git a/plugins/woocommerce/legacy/js/jquery-payment/jquery.payment.js b/plugins/woocommerce/client/legacy/js/jquery-payment/jquery.payment.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-payment/jquery.payment.js +rename to plugins/woocommerce/client/legacy/js/jquery-payment/jquery.payment.js +diff --git a/plugins/woocommerce/legacy/js/jquery-qrcode/jquery.qrcode.js b/plugins/woocommerce/client/legacy/js/jquery-qrcode/jquery.qrcode.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-qrcode/jquery.qrcode.js +rename to plugins/woocommerce/client/legacy/js/jquery-qrcode/jquery.qrcode.js +diff --git a/plugins/woocommerce/legacy/js/jquery-serializejson/jquery.serializejson.js b/plugins/woocommerce/client/legacy/js/jquery-serializejson/jquery.serializejson.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-serializejson/jquery.serializejson.js +rename to plugins/woocommerce/client/legacy/js/jquery-serializejson/jquery.serializejson.js +diff --git a/plugins/woocommerce/legacy/js/jquery-tiptip/jquery.tipTip.js b/plugins/woocommerce/client/legacy/js/jquery-tiptip/jquery.tipTip.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-tiptip/jquery.tipTip.js +rename to plugins/woocommerce/client/legacy/js/jquery-tiptip/jquery.tipTip.js +diff --git a/plugins/woocommerce/legacy/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js b/plugins/woocommerce/client/legacy/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js +rename to plugins/woocommerce/client/legacy/js/jquery-ui-touch-punch/jquery-ui-touch-punch.js +diff --git a/plugins/woocommerce/legacy/js/js-cookie/js.cookie.js b/plugins/woocommerce/client/legacy/js/js-cookie/js.cookie.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/js-cookie/js.cookie.js +rename to plugins/woocommerce/client/legacy/js/js-cookie/js.cookie.js +diff --git a/plugins/woocommerce/legacy/js/photoswipe/photoswipe-ui-default.js b/plugins/woocommerce/client/legacy/js/photoswipe/photoswipe-ui-default.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/photoswipe/photoswipe-ui-default.js +rename to plugins/woocommerce/client/legacy/js/photoswipe/photoswipe-ui-default.js +diff --git a/plugins/woocommerce/legacy/js/photoswipe/photoswipe.js b/plugins/woocommerce/client/legacy/js/photoswipe/photoswipe.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/photoswipe/photoswipe.js +rename to plugins/woocommerce/client/legacy/js/photoswipe/photoswipe.js +diff --git a/plugins/woocommerce/legacy/js/prettyPhoto/jquery.prettyPhoto.init.js b/plugins/woocommerce/client/legacy/js/prettyPhoto/jquery.prettyPhoto.init.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/prettyPhoto/jquery.prettyPhoto.init.js +rename to plugins/woocommerce/client/legacy/js/prettyPhoto/jquery.prettyPhoto.init.js +diff --git a/plugins/woocommerce/legacy/js/prettyPhoto/jquery.prettyPhoto.js b/plugins/woocommerce/client/legacy/js/prettyPhoto/jquery.prettyPhoto.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/prettyPhoto/jquery.prettyPhoto.js +rename to plugins/woocommerce/client/legacy/js/prettyPhoto/jquery.prettyPhoto.js +diff --git a/plugins/woocommerce/legacy/js/round/round.js b/plugins/woocommerce/client/legacy/js/round/round.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/round/round.js +rename to plugins/woocommerce/client/legacy/js/round/round.js +diff --git a/plugins/woocommerce/legacy/js/select2/select2.full.js b/plugins/woocommerce/client/legacy/js/select2/select2.full.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/select2/select2.full.js +rename to plugins/woocommerce/client/legacy/js/select2/select2.full.js +diff --git a/plugins/woocommerce/legacy/js/select2/select2.js b/plugins/woocommerce/client/legacy/js/select2/select2.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/select2/select2.js +rename to plugins/woocommerce/client/legacy/js/select2/select2.js +diff --git a/plugins/woocommerce/legacy/js/selectWoo/selectWoo.full.js b/plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.full.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/selectWoo/selectWoo.full.js +rename to plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.full.js +diff --git a/plugins/woocommerce/legacy/js/selectWoo/selectWoo.js b/plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/selectWoo/selectWoo.js +rename to plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.js +diff --git a/plugins/woocommerce/legacy/js/stupidtable/stupidtable.js b/plugins/woocommerce/client/legacy/js/stupidtable/stupidtable.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/stupidtable/stupidtable.js +rename to plugins/woocommerce/client/legacy/js/stupidtable/stupidtable.js +diff --git a/plugins/woocommerce/legacy/js/zoom/jquery.zoom.js b/plugins/woocommerce/client/legacy/js/zoom/jquery.zoom.js +similarity index 100% +rename from plugins/woocommerce/legacy/js/zoom/jquery.zoom.js +rename to plugins/woocommerce/client/legacy/js/zoom/jquery.zoom.js +diff --git a/plugins/woocommerce/legacy/package.json b/plugins/woocommerce/client/legacy/package.json +similarity index 78% +rename from plugins/woocommerce/legacy/package.json +rename to plugins/woocommerce/client/legacy/package.json +index f32e485d72..333bc10794 100644 +--- a/plugins/woocommerce/legacy/package.json ++++ b/plugins/woocommerce/client/legacy/package.json +@@ -1,13 +1,13 @@ + { +- "name": "woocommerce-legacy-assets", ++ "name": "woocommerce/client/legacy", + "version": "1.0.0", + "author": "Automattic", + "license": "GPL-2.0-or-later", + "private": true, + "main": "Gruntfile.js", + "scripts": { +- "build": "grunt assets", +- "lint": "grunt eslint stylelint --force" ++ "build": "node ./node_modules/require-turbo && grunt assets", ++ "lint": "node ./node_modules/require-turbo && grunt eslint stylelint --force" + }, + "devDependencies": { + "@wordpress/stylelint-config": "19.1.0", +@@ -29,6 +29,7 @@ + "grunt-sass": "3.1.0", + "grunt-stylelint": "0.16.0", + "gruntify-eslint": "5.0.0", ++ "require-turbo": "workspace:*", + "sass": "^1.45.0", + "stylelint": "13.8.0" + } +diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json +index 0f71ac3831..d6502f2941 100644 +--- a/plugins/woocommerce/composer.json ++++ b/plugins/woocommerce/composer.json +@@ -2,7 +2,7 @@ + "name": "woocommerce/woocommerce", + "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", + "homepage": "https://woocommerce.com/", +- "version": "6.7.0", ++ "version": "6.8.2", + "type": "wordpress-plugin", + "license": "GPL-3.0-or-later", + "prefer-stable": true, +@@ -22,13 +22,13 @@ + "pelago/emogrifier": "^6.0", + "psr/container": "1.0.0", + "woocommerce/action-scheduler": "3.4.2", +- "woocommerce/woocommerce-blocks": "7.8.3" ++ "woocommerce/woocommerce-blocks": "8.0.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "yoast/phpunit-polyfills": "^1.0", + "phpunit/phpunit": "7.5.20", +- "automattic/jetpack-changelogger": "3.0.2" ++ "automattic/jetpack-changelogger": "3.1.3" + }, + "config": { + "optimize-autoloader": true, +@@ -88,7 +88,7 @@ + "phpcs -s -p" + ], + "phpcs-pre-commit": [ +- "phpcs -s -p -n" ++ "phpcs-changed --git -s" + ], + "phpcbf": [ + "phpcbf -p" +@@ -126,7 +126,7 @@ + }, + "changelogger": { + "formatter": { +- "filename": "../../tools/changelogger/PluginFormatter.php" ++ "filename": "../../tools/changelogger/class-legacy-core-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", +diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock +index 0e536f0af1..941fb722be 100644 +--- a/plugins/woocommerce/composer.lock ++++ b/plugins/woocommerce/composer.lock +@@ -4,7 +4,7 @@ + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], +- "content-hash": "aeddc506065c69ac27c581fe81a6c182", ++ "content-hash": "15c3bc105340156959abfb26fc827185", + "packages": [ + { + "name": "automattic/jetpack-autoloader", +@@ -681,16 +681,16 @@ + }, + { + "name": "woocommerce/woocommerce-blocks", +- "version": "v7.8.3", ++ "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-blocks.git", +- "reference": "0d6113c42c7707c0bcae1892b071a9b6253612c7" ++ "reference": "47379270586de9d409200885883ec035dd4aceed" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/0d6113c42c7707c0bcae1892b071a9b6253612c7", +- "reference": "0d6113c42c7707c0bcae1892b071a9b6253612c7", ++ "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/47379270586de9d409200885883ec035dd4aceed", ++ "reference": "47379270586de9d409200885883ec035dd4aceed", + "shasum": "" + }, + "require": { +@@ -734,35 +734,35 @@ + ], + "support": { + "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", +- "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v7.8.3" ++ "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v8.0.0" + }, +- "time": "2022-06-20T13:20:21+00:00" ++ "time": "2022-07-05T15:54:39+00:00" + } + ], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", +- "version": "v3.0.2", ++ "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3" ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0" + }, + "dist": { + "type": "zip", +- "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", +- "reference": "b76f9cb4c22ec08490eff91a2e0e5aa586ee04b3", ++ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", ++ "reference": "cdd256d8ba6369f82d9377de7e9e2598e3e16ae0", + "shasum": "" + }, + "require": { + "php": ">=5.6", +- "symfony/console": "^3.4 | ^5.2", +- "symfony/process": "^3.4 | ^5.2", +- "wikimedia/at-ease": "^1.2 | ^2.0" ++ "symfony/console": "^3.4 || ^5.2", ++ "symfony/process": "^3.4 || ^5.2", ++ "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { +- "wikimedia/testing-access-wrapper": "^1.0 | ^2.0", +- "yoast/phpunit-polyfills": "1.0.2" ++ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", ++ "yoast/phpunit-polyfills": "1.0.3" + }, + "bin": [ + "bin/changelogger" +@@ -771,7 +771,7 @@ + "extra": { + "autotagger": true, + "branch-alias": { +- "dev-master": "3.0.x-dev" ++ "dev-trunk": "3.1.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { +@@ -793,9 +793,9 @@ + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { +- "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.0.2" ++ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.1.3" + }, +- "time": "2021-11-02T14:06:49+00:00" ++ "time": "2022-06-21T07:31:56+00:00" + }, + { + "name": "bamarni/composer-bin-plugin", +@@ -2940,5 +2940,5 @@ + "platform-overrides": { + "php": "7.2" + }, +- "plugin-api-version": "2.3.0" ++ "plugin-api-version": "2.2.0" + } +diff --git a/plugins/woocommerce/e2e/README.md b/plugins/woocommerce/e2e/README.md +new file mode 100644 +index 0000000000..b78838db8e +--- /dev/null ++++ b/plugins/woocommerce/e2e/README.md +@@ -0,0 +1,268 @@ ++# WooCommerce Playwright End to End Tests ++ ++ This is still a work in progress. Feel free to add a test here. We will gradually deprecate [Puppeteer](../tests/e2e). ++ ++## Table of contents ++ ++- [Pre-requisites](#pre-requisites) ++ - [Install Node.js](#install-nodejs) ++ - [Install NVM](#install-nvm) ++ - [Install Docker](#install-pnpm) ++ - [Install Docker](#install-docker) ++- [Configuration](#configuration) ++ - [Test Environment](#test-environment) ++ - [Test Variables](#test-variables) ++- [Running tests](#running-tests) ++ - [Prep work for running tests](#prep-work-for-running-tests) ++ - [How to run tests in headless mode](#how-to-run-tests-in-headless-mode) ++ - [How to run tests in non-headless mode](#how-to-run-tests-in-non-headless-mode) ++ - [How to run tests in debug mode](#how-to-run-tests-in-debug-mode) ++ - [How to run an individual test](#how-to-run-an-individual-test) ++ - [How to skip tests](#how-to-skip-tests) ++ - [How to run tests using custom WordPress, PHP and MariaDB versions](#how-to-run-tests-using-custom-wordpress-php-and-mariadb-versions) ++- [Guide for writing e2e tests](#guide-for-writing-e2e-tests) ++ - [Tools for writing tests](#tools-for-writing-tests) ++ - [Creating test structure](#creating-test-structure) ++ - [Writing the test](#writing-the-test) ++- [Debugging tests](#debugging-tests) ++ ++## Pre-requisites ++ ++### Install Node.js ++ ++Follow [instructions on the node.js site](https://nodejs.org/en/download/) to install Node.js. ++ ++### Install NVM ++ ++Follow instructions in the [NVM repository](https://github.com/nvm-sh/nvm) to install NVM. ++ ++### Install pnpm ++ ++Follow [instructions on pnpm.io site](https://pnpm.io/installation) to install pnpm. ++ ++### Install Docker ++ ++Install Docker Desktop if you don't have it installed: ++ ++- [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/) ++- [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/install/) ++ ++Once installed, you should see `Docker Desktop is running` message with the green light next to it indicating that everything is working as expected. ++ ++Note, that if you install docker through other methods such as homebrew, for example, your steps to set it up will be different. The commands listed in steps below may also vary. ++ ++## Configuration ++ ++This section explains how e2e tests are working behind the scenes. These are not instructions on how to build environment for running e2e tests and run them. If you are looking for instructions on how to run e2e tests, jump to [Running tests](#running-tests). ++ ++### Test Environment ++ ++We recommend using Docker for running tests locally in order for the test environment to match the setup on GitHub CI (where Docker is also used for running tests). [An official WordPress Docker image](https://github.com/docker-library/docs/blob/master/wordpress/README.md) is used to build the site. Once the site using the WP Docker image is built, the current WooCommerce dev branch is mapped into the `plugins` folder of that newly built test site. ++ ++### Test Variables ++ ++The test environment uses the following test variables: ++ ++```json ++{ ++ "url": "http://localhost:8086/", ++ "users": { ++ "admin": { ++ "username": "admin", ++ "password": "password" ++ }, ++ "customer": { ++ "username": "customer", ++ "password": "password" ++ } ++ } ++} ++``` ++ ++If you need to modify the port for your local test environment (eg. port is already in use) or use different playwright config, edit `tests/e2e/config/default.json` and `e2e/playwright.config.js`. ++ ++## Running tests ++ ++If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for End-to-end testing. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below. ++ ++### Prep work for running tests ++ ++Run the following in a terminal/command line window ++ ++- `cd` to the WooCommerce monorepo folder ++ ++- `git checkout trunk` (or the branch where you need to run tests) ++ ++- `nvm use` ++ ++- `pnpm install` ++ ++- `pnpm dlx playwright install chromium` to install chromium. ++ ++- `pnpm -- turbo run build --filter=woocommerce` ++ ++- `pnpm docker:up --filter=woocommerce` (this will build the test site using Docker) ++ ++- Use `docker ps` to confirm that the Docker containers are running. You should see a log similar to one below indicating that everything had been built as expected: ++ ++```bash ++CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ++c380e1964506 env_wordpress-cli "entrypoint.sh" 7 seconds ago Up 5 seconds woocommerce_e2e_wordpress-cli ++2ab8e8439e9f wordpress:5.5.1 "docker-entrypoint.s…" 8 seconds ago Up 7 seconds 0.0.0.0:8086->80/tcp woocommerce_e2e_wordpress-www ++4c1e3f2a49db mariadb:10.5.5 "docker-entrypoint.s…" 10 seconds ago Up 8 seconds 3306/tcp woocommerce_e2e_db ++``` ++ ++Note that by default, Docker will download the latest images available for WordPress, PHP and MariaDB. In the example above, you can see that WordPress 5.5.1 and MariaDB 10.5.5 were used. ++ ++See [How to run tests using custom WordPress, PHP and MariaDB versions](#how-to-run-tests-using-custom-wordpress,-php-and-mariadb-versions) if you'd like to use different versions. ++ ++- Navigate to `http://localhost:8086/` ++ ++If everything went well, you should be able to access the site. If you changed the port to something other than `8086` as per [Test Variables](#test-variables) section, use the appropriate port to access your site. ++ ++As noted in [Test Variables](#test-variables) section, use the following Admin user details to login: ++ ++``` ++Username: admin ++PW: password ++``` ++ ++- Run `pnpm docker:down --filter=woocommerce` when you are done with running e2e tests and before making any changes to test site configuration. ++ ++Note that running `pnpm docker:down --filter=woocommerce` and then `pnpm docker:up --filter=woocommerce` re-initializes the test container. ++ ++### How to run tests in headless mode ++ ++To run e2e tests in headless mode use the following command: ++ ++```bash ++cd plugins/woocommerce ++pnpm playwright test --config=e2e/playwright.config.js ++``` ++ ++### How to run tests in non-headless mode ++ ++Tests run in headless mode by default. However, sometimes it's useful to observe the browser while running or developing tests. To do so, you can run tests in a non-headless (dev) mode: ++ ++```bash ++cd plugins/woocommerce ++pnpm playwright test --config=e2e/playwright.config.js --headed ++``` ++ ++## How to retry failed tests ++ ++Sometimes tests may fail for different reasons such as network issues, or lost connection. To mitigate against test flakiess, failed tests are rerun up to 1 times before being marked as failed. The amount of retry attempts can be adjusted by passing the `--retries` variable when running tests. For example: ++ ++```bash ++cd plugins/woocommerce ++pnpm playwright test --config=e2e/playwright.config.js --retries 3 ++``` ++ ++### How to run tests in debug mode ++ ++Tests run in headless mode by default. While writing tests it may be useful to have the debugger loaded while running a test in non-headless mode. To run tests in debug mode: ++ ++```bash ++cd plugins/woocommerce ++pnpm playwright test --config=e2e/playwright.config.js --debug ++``` ++ ++Playwright Inspector window will be opened and the script will be paused on the first Playwright statement. You can step over each action using the "Step over" action or resume script without further pauses. ++ ++## How to run an individual test ++ ++To run an individual test, use the direct path to the spec. For example: ++ ++```bash ++cd plugins/woocommerce ++pnpm playwright test --config=e2e/playwright.config.js ./e2e/tests/activate-and-setup/basic-setup.spec.js ++``` ++ ++## How to disable parallelization ++ ++To run an individual test, use the direct path to the spec. For example: ++ ++```bash ++cd plugins/woocommerce ++pnpm playwright test --config=e2e/playwright.config.js --workers=1 ++``` ++ ++## How to skip tests ++ ++To skip the tests, use `.only` in the relevant test entry to specify the tests that you do want to run. For example: ++ ++```js ++test.only( 'Can login', async () => {} ++``` ++ ++```js ++test.only( 'Can make sure WooCommerce is activated. If not, activate it', async () => {} ++``` ++ ++You can also use `.skip` in the same fashion. For example: ++ ++```js ++test.skip( 'Can start Setup Wizard', async () => {} ++``` ++ ++### How to run tests using custom WordPress, PHP and MariaDB versions ++ ++The following variables can be used to specify the versions of WordPress, PHP and MariaDB that you'd like to use to build your test site with Docker: ++ ++- `WP_VERSION` ++- `TRAVIS_PHP_VERSION` ++- `TRAVIS_MARIADB_VERSION` ++ ++The full command to build the site will look as follows: ++ ++```bash ++TRAVIS_MARIADB_VERSION=10.5.3 TRAVIS_PHP_VERSION=7.4.5 WP_VERSION=5.4.1 pnpm docker:up --filter=woocommerce ++``` ++ ++## Guide for writing e2e tests ++ ++### Tools for writing tests ++ ++We use the following tools to write e2e tests: ++ ++- [Playwright](https://playwright.dev/docs/intro) – a tool enables reliable end-to-end testing for modern web apps. ++ ++### Creating test structure ++ ++It is a good practice to start working on the test by identifying what needs to be tested on the higher and lower levels. For example, if you are writing a test to verify that merchant can create a virtual product, the overview of the test will be as follows: ++ ++- Merchant can create virtual product ++ - Merchant can log in ++ - Merchant can create virtual product ++ - Merchant can verify that virtual product was created ++ ++Once you identify the structure of the test, you can move on to writing it. ++ ++### Writing the test ++ ++The structure of the test serves as a skeleton for the test itself. You can turn it into a test by using `describe()` and `it()` methods of Playwright: ++ ++- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe) - creates a block that groups together several related tests; ++- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test. ++ ++Based on our example, the test skeleton would look as follows: ++ ++```js ++test.describe( 'Merchant can create virtual product', () => { ++ test( 'merchant can log in', async () => { ++ ++ } ); ++ ++ test( 'merchant can create virtual product', async () => { ++ ++ } ); ++ ++ test( 'merchant can verify that virtual product was created', async () => { ++ ++ } ); ++} ); ++``` ++ ++## Debugging tests ++ ++For Playwright debugging, follow [Playwright's documentation](https://playwright.dev/docs/debug). +diff --git a/plugins/woocommerce/e2e/global-setup.js b/plugins/woocommerce/e2e/global-setup.js +index 85178273c2..6c008efe25 100644 +--- a/plugins/woocommerce/e2e/global-setup.js ++++ b/plugins/woocommerce/e2e/global-setup.js +@@ -28,31 +28,33 @@ module.exports = async ( config ) => { + await adminPage.fill( 'input[name="log"]', 'admin' ); + await adminPage.fill( 'input[name="pwd"]', 'password' ); + await adminPage.click( 'text=Log In' ); +- await adminPage +- .context() +- .storageState( { path: 'e2e/storage/adminState.json' } ); +- // While we're here, let's add a consumer token for API access +- await adminPage.goto( +- `${ baseURL }/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1` +- ); +- await adminPage.fill( '#key_description', 'Key for API access' ); +- await adminPage.selectOption( '#key_permissions', 'read_write' ); +- await adminPage.click( 'text=Generate API key' ); +- process.env.CONSUMER_KEY = await adminPage.inputValue( +- '#key_consumer_key' +- ); +- process.env.CONSUMER_SECRET = await adminPage.inputValue( +- '#key_consumer_secret' +- ); ++ await adminPage.context().storageState( { path: adminState } ); + ++ // While we're here, let's add a consumer token for API access ++ // This step was failing occasionally, and globalsetup doesn't retry, so make it retry ++ const nRetries = 5; ++ for ( let i = 0; i < nRetries; i++ ) { ++ try { ++ await adminPage.goto( ++ `${ baseURL }/wp-admin/admin.php?page=wc-settings&tab=advanced§ion=keys&create-key=1` ++ ); ++ await adminPage.fill( '#key_description', 'Key for API access' ); ++ await adminPage.selectOption( '#key_permissions', 'read_write' ); ++ await adminPage.click( 'text=Generate API key' ); ++ process.env.CONSUMER_KEY = await adminPage.inputValue( ++ '#key_consumer_key' ++ ); ++ process.env.CONSUMER_SECRET = await adminPage.inputValue( ++ '#key_consumer_secret' ++ ); ++ } catch ( e ) {} ++ } + // Sign in as customer user and save state + const customerPage = await browser.newPage(); + await customerPage.goto( `${ baseURL }/wp-admin` ); + await customerPage.fill( 'input[name="log"]', 'customer' ); + await customerPage.fill( 'input[name="pwd"]', 'password' ); + await customerPage.click( 'text=Log In' ); +- await customerPage +- .context() +- .storageState( { path: 'e2e/storage/customerState.json' } ); ++ await customerPage.context().storageState( { path: customerState } ); + await browser.close(); + }; +diff --git a/plugins/woocommerce/e2e/playwright.config.js b/plugins/woocommerce/e2e/playwright.config.js +index 23962b8be3..e8849be8be 100644 +--- a/plugins/woocommerce/e2e/playwright.config.js ++++ b/plugins/woocommerce/e2e/playwright.config.js +@@ -1,12 +1,13 @@ + const { devices } = require( '@playwright/test' ); + + const config = { +- timeout: 20000, ++ timeout: 60 * 1000, + outputDir: './report', + globalSetup: require.resolve( './global-setup' ), + globalTeardown: require.resolve( './global-teardown' ), + testDir: 'tests', +- retries: 1, ++ retries: 2, ++ workers: 4, + reporter: [ + [ 'list' ], + [ 'html', { outputFolder: 'output' } ], +@@ -24,14 +25,6 @@ const config = { + name: 'Chrome', + use: { ...devices[ 'Desktop Chrome' ] }, + }, +- // { +- // name: 'Firefox', +- // use: { ...devices['Desktop Firefox'] }, +- // }, +- // { +- // name: 'Webkit', +- // use: { ...devices['Desktop Webkit'] }, +- // }, + ], + }; + +diff --git a/plugins/woocommerce/e2e/test-data/sample_products.csv b/plugins/woocommerce/e2e/test-data/sample_products.csv +index dfb25e8889..e73f8d1a31 100644 +--- a/plugins/woocommerce/e2e/test-data/sample_products.csv ++++ b/plugins/woocommerce/e2e/test-data/sample_products.csv +@@ -1,26 +1,26 @@ + ID,Type,SKU,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL" +-44,variable,woo-vneck-tee,"V-Neck T-Shirt",1,1,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,24,1,2,1,,,,"Clothing > Tshirts",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Size,"Large, Medium, Small",1,1,1,,,, +-45,variable,woo-hoodie,Hoodie,1,0,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1.5,10,8,3,1,,,,"Clothing > Hoodies",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Logo,"Yes, No",1,0,1,,,, +-46,simple,woo-hoodie-with-logo,"Hoodie with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,10,6,3,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,, +-47,simple,woo-tshirt,T-Shirt,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.8,8,6,1,1,,,18,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, +-48,simple,woo-beanie,Beanie,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,4,5,.5,1,,18,20,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg,,,,,,,,,0,Color,Red,1,1,,,,,1,,,, +-58,simple,woo-belt,Belt,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1.2,12,2,1.5,1,,55,65,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, +-60,simple,woo-cap,Cap,1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,0.6,8,6.5,4,1,,16,18,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg,,,,,,,,,0,Color,Yellow,1,1,,,,,1,,,, +-62,simple,woo-sunglasses,Sunglasses,1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,4,1.4,1,1,,,90,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, +-64,simple,woo-hoodie-with-pocket,"Hoodie with Pocket",1,1,hidden,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,3,10,8,2,1,,35,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, +-66,simple,woo-hoodie-with-zipper,"Hoodie with Zipper",1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,8,6,2,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, +-68,simple,woo-long-sleeve-tee,"Long Sleeve Tee",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1,7,5,1,1,,,25,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg,,,,,,,,,0,Color,Green,1,1,,,,,1,,,, +-70,simple,woo-polo,Polo,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.8,6,5,1,1,,,20,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,, +-73,"simple, downloadable, virtual",woo-album,Album,1,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,,15,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg,1,1,,,,,,,0,,,,,,,,,1,"Single 1",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,"Single 2",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg +-75,"simple, downloadable, virtual",woo-single,Single,1,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,2,3,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg,1,1,,,,,,,0,,,,,,,,,1,Single,https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,, +-76,variation,woo-vneck-tee-red,"V-Neck T-Shirt - Red",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,20,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg,,,woo-vneck-tee,,,,,,0,Color,Red,,1,Size,,,1,,,,, +-77,variation,woo-vneck-tee-green,"V-Neck T-Shirt - Green",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,20,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg,,,woo-vneck-tee,,,,,,0,Color,Green,,1,Size,,,1,,,,, +-78,variation,woo-vneck-tee-blue,"V-Neck T-Shirt - Blue",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,15,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg,,,woo-vneck-tee,,,,,,0,Color,Blue,,1,Size,,,1,,,,, +-79,variation,woo-hoodie-red,"Hoodie - Red, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,42,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg,,,woo-hoodie,,,,,,1,Color,Red,,1,Logo,No,,0,,,,, +-80,variation,woo-hoodie-green,"Hoodie - Green, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg,,,woo-hoodie,,,,,,2,Color,Green,,1,Logo,No,,0,,,,, +-81,variation,woo-hoodie-blue,"Hoodie - Blue, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg,,,woo-hoodie,,,,,,3,Color,Blue,,1,Logo,No,,0,,,,, +-83,simple,Woo-tshirt-logo,"T-Shirt with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,10,12,.5,1,,,18,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, +-85,simple,Woo-beanie-logo,"Beanie with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,6,4,1,1,,18,20,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg,,,,,,,,,0,Color,Red,1,1,,,,,1,,,, +-87,grouped,logo-collection,"Logo Collection",1,0,visible,"This is a grouped product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,,,,,1,,,,Clothing,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,"woo-hoodie-with-logo, woo-tshirt, woo-beanie",,,,,0,,,,,,,,,1,,,, +-89,external,wp-pennant,"WordPress Pennant",1,0,visible,"This is an external product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,,,,,1,,,11.05,Decor,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg,,,,,,,https://mercantile.wordpress.org/product/wordpress-pennant/,"Buy on the WordPress swag store!",0,,,,,,,,,1,,,, +-90,variation,woo-hoodie-blue-logo,"Hoodie - Blue, Yes",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,woo-hoodie,,,,,,0,Color,Blue,,1,Logo,Yes,,0,,,,, ++44,variable,woo-vneck-tee,"Imported V-Neck T-Shirt",1,1,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,24,1,2,1,,,,"Clothing > Tshirts",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Size,"Large, Medium, Small",1,1,1,,,, ++45,variable,woo-hoodie,"Imported Hoodie",1,0,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1.5,10,8,3,1,,,,"Clothing > Hoodies",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Logo,"Yes, No",1,0,1,,,, ++46,simple,woo-hoodie-with-logo,"Imported Hoodie with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,10,6,3,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,, ++47,simple,woo-tshirt,"Imported T-Shirt",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.8,8,6,1,1,,,18,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, ++48,simple,woo-beanie,"Imported Beanie",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,4,5,.5,1,,18,20,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg,,,,,,,,,0,Color,Red,1,1,,,,,1,,,, ++58,simple,woo-belt,"Imported Belt",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1.2,12,2,1.5,1,,55,65,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, ++60,simple,woo-cap,"Imported Cap",1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,0.6,8,6.5,4,1,,16,18,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg,,,,,,,,,0,Color,Yellow,1,1,,,,,1,,,, ++62,simple,woo-sunglasses,"Imported Sunglasses",1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,4,1.4,1,1,,,90,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, ++64,simple,woo-hoodie-with-pocket,"Imported Hoodie with Pocket",1,1,hidden,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,3,10,8,2,1,,35,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, ++66,simple,woo-hoodie-with-zipper,"Imported Hoodie with Zipper",1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,8,6,2,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, ++68,simple,woo-long-sleeve-tee,"Imported Long Sleeve Tee",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1,7,5,1,1,,,25,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg,,,,,,,,,0,Color,Green,1,1,,,,,1,,,, ++70,simple,woo-polo,"Imported Polo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.8,6,5,1,1,,,20,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,, ++73,"simple, downloadable, virtual",woo-album,"Imported Album",1,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,,15,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg,1,1,,,,,,,0,,,,,,,,,1,"Single 1",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,"Single 2",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg ++75,"simple, downloadable, virtual",woo-single,"Imported Single",1,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,2,3,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg,1,1,,,,,,,0,,,,,,,,,1,Single,https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,, ++76,variation,woo-vneck-tee-red,"Imported V-Neck T-Shirt - Red",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,20,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg,,,woo-vneck-tee,,,,,,0,Color,Red,,1,Size,,,1,,,,, ++77,variation,woo-vneck-tee-green,"Imported V-Neck T-Shirt - Green",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,20,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg,,,woo-vneck-tee,,,,,,0,Color,Green,,1,Size,,,1,,,,, ++78,variation,woo-vneck-tee-blue,"Imported V-Neck T-Shirt - Blue",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,15,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg,,,woo-vneck-tee,,,,,,0,Color,Blue,,1,Size,,,1,,,,, ++79,variation,woo-hoodie-red,"Imported Hoodie - Red, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,42,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg,,,woo-hoodie,,,,,,1,Color,Red,,1,Logo,No,,0,,,,, ++80,variation,woo-hoodie-green,"Imported Hoodie - Green, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg,,,woo-hoodie,,,,,,2,Color,Green,,1,Logo,No,,0,,,,, ++81,variation,woo-hoodie-blue,"Imported Hoodie - Blue, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg,,,woo-hoodie,,,,,,3,Color,Blue,,1,Logo,No,,0,,,,, ++83,simple,Woo-tshirt-logo,"Imported T-Shirt with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,10,12,.5,1,,,18,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, ++85,simple,Woo-beanie-logo,"Imported Beanie with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,6,4,1,1,,18,20,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg,,,,,,,,,0,Color,Red,1,1,,,,,1,,,, ++87,grouped,logo-collection,"Imported Logo Collection",1,0,visible,"This is a grouped product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,,,,,1,,,,Clothing,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,"woo-hoodie-with-logo, woo-tshirt, woo-beanie",,,,,0,,,,,,,,,1,,,, ++89,external,wp-pennant,"Imported WordPress Pennant",1,0,visible,"This is an external product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,,,,,1,,,11.05,Decor,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg,,,,,,,https://mercantile.wordpress.org/product/wordpress-pennant/,"Buy on the WordPress swag store!",0,,,,,,,,,1,,,, ++90,variation,woo-hoodie-blue-logo,"Imported Hoodie - Blue, Yes",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,woo-hoodie,,,,,,0,Color,Blue,,1,Logo,Yes,,0,,,,, +diff --git a/plugins/woocommerce/e2e/test-data/sample_products_override.csv b/plugins/woocommerce/e2e/test-data/sample_products_override.csv +index 6b880308a7..295cc62dd0 100644 +--- a/plugins/woocommerce/e2e/test-data/sample_products_override.csv ++++ b/plugins/woocommerce/e2e/test-data/sample_products_override.csv +@@ -1,26 +1,26 @@ + ID,Type,SKU,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL" +-,variable,woo-vneck-tee,V-Neck T-Shirt Override,1,"1","visible","This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".5","24","1","2","1",,,,Clothing > Tshirts,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,"Color","Blue, Green, Red","1","1","Size","Large, Medium, Small","1","1","1",,,, +-,variable,woo-hoodie,Hoodie Override,1,"0","visible","This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","1.5","10","8","3","1",,,,Clothing > Hoodies,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,,,,,,0,"Color","Blue, Green, Red","1","1","Logo","Yes, No","1","0","1",,,, +-,simple,woo-hoodie-with-logo,Hoodie with Logo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","2","10","6","3","1",,,"145",Clothing > Hoodies,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,"Color","Blue","1","1",,,,,"1",,,, +-,simple,woo-tshirt,T-Shirt Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".8","8","6","1","1",,,"118",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg,,,,,,,,,0,"Color","Gray","1","1",,,,,"1",,,, +-,simple,woo-beanie,Beanie Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".2","4","5",".5","1",,"118","120",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg,,,,,,,,,0,"Color","Red","1","1",,,,,"1",,,, +-,simple,woo-belt,Belt Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","1.2","12","2","1.5","1",,"155","165",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg,,,,,,,,,0,,,,,,,,,"1",,,, +-,simple,woo-cap,Cap Override,1,"1","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","0.6","8","6.5","4","1",,"116","118",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg,,,,,,,,,0,"Color","Yellow","1","1",,,,,"1",,,, +-,simple,woo-sunglasses,Sunglasses Override,1,"1","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".2","4","1.4","1","1",,,"190",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg,,,,,,,,,0,,,,,,,,,"1",,,, +-,simple,woo-hoodie-with-pocket,Hoodie with Pocket Override,1,"1","hidden","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","3","10","8","2","1",,"135","145",Clothing > Hoodies,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg,,,,,,,,,0,"Color","Gray","1","1",,,,,"1",,,, +-,simple,woo-hoodie-with-zipper,Hoodie with Zipper Override,1,"1","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","2","8","6","2","1",,,"145",Clothing > Hoodies,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg,,,,,,,,,0,,,,,,,,,"1",,,, +-,simple,woo-long-sleeve-tee,Long Sleeve Tee Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","1","7","5","1","1",,,"125",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg,,,,,,,,,0,"Color","Green","1","1",,,,,"1",,,, +-,simple,woo-polo,Polo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".8","6","5","1","1",,,"120",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg,,,,,,,,,0,"Color","Blue","1","1",,,,,"1",,,, +-,"simple, downloadable, virtual",woo-album,Album Override,1,"0","visible","This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"1",,,"115",Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg,"1","1",,,,,,,0,,,,,,,,,"1","Single 1","https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg","Single 2","https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg" +-,"simple, downloadable, virtual",woo-single,Single Override,1,"0","visible","This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"1",,"12","13",Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg,"1","1",,,,,,,0,,,,,,,,,"1","Single","https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg",, +-,variation,woo-vneck-tee-red,V-Neck T-Shirt - Red Override,1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"120",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg,,,woo-vneck-tee,,,,,,0,"Color","Red",,"1","Size",,,"1",,,,, +-,variation,woo-vneck-tee-green,V-Neck T-Shirt - Green Override,1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"120",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg,,,woo-vneck-tee,,,,,,0,"Color","Green",,"1","Size",,,"1",,,,, +-,variation,woo-vneck-tee-blue,V-Neck T-Shirt - Blue Override,1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"115",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg,,,woo-vneck-tee,,,,,,0,"Color","Blue",,"1","Size",,,"1",,,,, +-,variation,woo-hoodie-red,"Hoodie - Red, No Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,"142","145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg,,,woo-hoodie,,,,,,1,"Color","Red",,"1","Logo","No",,"0",,,,, +-,variation,woo-hoodie-green,"Hoodie - Green, No Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg,,,woo-hoodie,,,,,,2,"Color","Green",,"1","Logo","No",,"0",,,,, +-,variation,woo-hoodie-blue,"Hoodie - Blue, No Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg,,,woo-hoodie,,,,,,3,"Color","Blue",,"1","Logo","No",,"0",,,,, +-,simple,Woo-tshirt-logo,T-Shirt with Logo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".5","10","12",".5","1",,,"118",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg,,,,,,,,,0,"Color","Gray","1","1",,,,,"1",,,, +-,simple,Woo-beanie-logo,Beanie with Logo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".2","6","4","1","1",,"118","120",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg,,,,,,,,,0,"Color","Red","1","1",,,,,"1",,,, +-,grouped,logo-collection,Logo Collection Override,1,"0","visible","This is a grouped product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",,,,,"1",,,,Clothing,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,"woo-hoodie-with-logo, woo-tshirt, woo-beanie",,,,,0,,,,,,,,,"1",,,, +-,external,wp-pennant,WordPress Pennant Override,1,"0","visible","This is an external product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",,,,,"1",,,"111.05",Decor,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg,,,,,,,"https://mercantile.wordpress.org/product/wordpress-pennant/","Buy on the WordPress swag store!",0,,,,,,,,,"1",,,, +-,variation,woo-hoodie-blue-logo,"Hoodie - Blue, Yes Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,woo-hoodie,,,,,,0,"Color","Blue",,"1","Logo","Yes",,"0",,,,, +\ No newline at end of file ++,variable,woo-vneck-tee,Imported V-Neck T-Shirt Override,1,"1","visible","This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".5","24","1","2","1",,,,Clothing > Tshirts,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,"Color","Blue, Green, Red","1","1","Size","Large, Medium, Small","1","1","1",,,, ++,variable,woo-hoodie,Imported Hoodie Override,1,"0","visible","This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","1.5","10","8","3","1",,,,Clothing > Hoodies,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,,,,,,0,"Color","Blue, Green, Red","1","1","Logo","Yes, No","1","0","1",,,, ++,simple,woo-hoodie-with-logo,Imported Hoodie with Logo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","2","10","6","3","1",,,"145",Clothing > Hoodies,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,"Color","Blue","1","1",,,,,"1",,,, ++,simple,woo-tshirt,Imported T-Shirt Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".8","8","6","1","1",,,"118",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg,,,,,,,,,0,"Color","Gray","1","1",,,,,"1",,,, ++,simple,woo-beanie,Imported Beanie Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".2","4","5",".5","1",,"118","120",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg,,,,,,,,,0,"Color","Red","1","1",,,,,"1",,,, ++,simple,woo-belt,Imported Belt Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","1.2","12","2","1.5","1",,"155","165",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg,,,,,,,,,0,,,,,,,,,"1",,,, ++,simple,woo-cap,Imported Cap Override,1,"1","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","0.6","8","6.5","4","1",,"116","118",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg,,,,,,,,,0,"Color","Yellow","1","1",,,,,"1",,,, ++,simple,woo-sunglasses,Imported Sunglasses Override,1,"1","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".2","4","1.4","1","1",,,"190",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg,,,,,,,,,0,,,,,,,,,"1",,,, ++,simple,woo-hoodie-with-pocket,Imported Hoodie with Pocket Override,1,"1","hidden","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","3","10","8","2","1",,"135","145",Clothing > Hoodies,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg,,,,,,,,,0,"Color","Gray","1","1",,,,,"1",,,, ++,simple,woo-hoodie-with-zipper,Imported Hoodie with Zipper Override,1,"1","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","2","8","6","2","1",,,"145",Clothing > Hoodies,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg,,,,,,,,,0,,,,,,,,,"1",,,, ++,simple,woo-long-sleeve-tee,Imported Long Sleeve Tee Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0","1","7","5","1","1",,,"125",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg,,,,,,,,,0,"Color","Green","1","1",,,,,"1",,,, ++,simple,woo-polo,Imported Polo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".8","6","5","1","1",,,"120",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg,,,,,,,,,0,"Color","Blue","1","1",,,,,"1",,,, ++,"simple, downloadable, virtual",woo-album,Imported Album Override,1,"0","visible","This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"1",,,"115",Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg,"1","1",,,,,,,0,,,,,,,,,"1","Single 1","https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg","Single 2","https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg" ++,"simple, downloadable, virtual",woo-single,Imported Single Override,1,"0","visible","This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"1",,"12","13",Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg,"1","1",,,,,,,0,,,,,,,,,"1","Single","https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg",, ++,variation,woo-vneck-tee-red,Imported V-Neck T-Shirt - Red Override,1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"120",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg,,,woo-vneck-tee,,,,,,0,"Color","Red",,"1","Size",,,"1",,,,, ++,variation,woo-vneck-tee-green,Imported V-Neck T-Shirt - Green Override,1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"120",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg,,,woo-vneck-tee,,,,,,0,"Color","Green",,"1","Size",,,"1",,,,, ++,variation,woo-vneck-tee-blue,Imported V-Neck T-Shirt - Blue Override,1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"115",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg,,,woo-vneck-tee,,,,,,0,"Color","Blue",,"1","Size",,,"1",,,,, ++,variation,woo-hoodie-red,"Imported Hoodie - Red, No Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,"142","145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg,,,woo-hoodie,,,,,,1,"Color","Red",,"1","Logo","No",,"0",,,,, ++,variation,woo-hoodie-green,"Imported Hoodie - Green, No Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg,,,woo-hoodie,,,,,,2,"Color","Green",,"1","Logo","No",,"0",,,,, ++,variation,woo-hoodie-blue,"Imported Hoodie - Blue, No Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg,,,woo-hoodie,,,,,,3,"Color","Blue",,"1","Logo","No",,"0",,,,, ++,simple,Woo-tshirt-logo,Imported T-Shirt with Logo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".5","10","12",".5","1",,,"118",Clothing > Tshirts,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg,,,,,,,,,0,"Color","Gray","1","1",,,,,"1",,,, ++,simple,Woo-beanie-logo,Imported Beanie with Logo Override,1,"0","visible","This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",".2","6","4","1","1",,"118","120",Clothing > Accessories,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg,,,,,,,,,0,"Color","Red","1","1",,,,,"1",,,, ++,grouped,logo-collection,Imported Logo Collection Override,1,"0","visible","This is a grouped product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",,,,,"1",,,,Clothing,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,"woo-hoodie-with-logo, woo-tshirt, woo-beanie",,,,,0,,,,,,,,,"1",,,, ++,external,wp-pennant,Imported WordPress Pennant Override,1,"0","visible","This is an external product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,"taxable",,"1",,"0","0",,,,,"1",,,"111.05",Decor,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg,,,,,,,"https://mercantile.wordpress.org/product/wordpress-pennant/","Buy on the WordPress swag store!",0,,,,,,,,,"1",,,, ++,variation,woo-hoodie-blue-logo,"Imported Hoodie - Blue, Yes Override",1,"0","visible",,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,"taxable",,"1",,"0","0",,,,,"0",,,"145",,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,woo-hoodie,,,,,,0,"Color","Blue",,"1","Logo","Yes",,"0",,,,, +diff --git a/plugins/woocommerce/e2e/tests/activate-and-setup/basic-setup.spec.js b/plugins/woocommerce/e2e/tests/activate-and-setup/basic-setup.spec.js +index d70d3de998..dbf9a1398b 100644 +--- a/plugins/woocommerce/e2e/tests/activate-and-setup/basic-setup.spec.js ++++ b/plugins/woocommerce/e2e/tests/activate-and-setup/basic-setup.spec.js +@@ -3,16 +3,16 @@ const { test, expect } = require( '@playwright/test' ); + test.describe( 'Store owner can finish initial store setup', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + test( 'can enable tax rates and calculations', async ( { page } ) => { +- await page.goto( '/wp-admin/admin.php?page=wc-settings' ); ++ await page.goto( 'wp-admin/admin.php?page=wc-settings' ); + // Check the enable taxes checkbox + await page.check( '#woocommerce_calc_taxes' ); + await page.click( 'text=Save changes' ); + // Verify changes have been saved +- expect( page.isChecked( '#woocommerce_calc_taxes' ) ).toBeTruthy(); ++ await expect( page.locator( '#woocommerce_calc_taxes' ) ).toBeChecked(); + } ); + + test( 'can configure permalink settings', async ( { page } ) => { +- await page.goto( '/wp-admin/options-permalink.php' ); ++ await page.goto( 'wp-admin/options-permalink.php' ); + // Select "Post name" option in common settings section + await page.check( 'label >> text=Post name' ); + // Select "Custom base" in product permalinks section +@@ -21,20 +21,14 @@ test.describe( 'Store owner can finish initial store setup', () => { + await page.fill( '#woocommerce_permalink_structure', '/product/' ); + await page.click( '#submit' ); + // Verify that settings have been saved +- await page.waitForLoadState( 'networkidle' ); // not autowaiting for form submission +- const notice = await page.textContent( +- '#setting-error-settings_updated' ++ await expect( ++ page.locator( '#setting-error-settings_updated' ) ++ ).toContainText( 'Permalink structure updated.' ); ++ await expect( page.locator( '#permalink_structure' ) ).toHaveValue( ++ '/%postname%/' + ); +- expect( notice ).toContain( 'Permalink structure updated.' ); +- const postSlug = await page.getAttribute( +- '#permalink_structure', +- 'value' +- ); +- expect( postSlug ).toBe( '/%postname%/' ); +- const wcSlug = await page.getAttribute( +- '#woocommerce_permalink_structure', +- 'value' +- ); +- expect( wcSlug ).toBe( '/product/' ); ++ await expect( ++ page.locator( '#woocommerce_permalink_structure' ) ++ ).toHaveValue( '/product/' ); + } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/activate-and-setup/complete-onboarding-wizard.spec.js b/plugins/woocommerce/e2e/tests/activate-and-setup/complete-onboarding-wizard.spec.js +index 7c78a2a68f..c1d6238515 100644 +--- a/plugins/woocommerce/e2e/tests/activate-and-setup/complete-onboarding-wizard.spec.js ++++ b/plugins/woocommerce/e2e/tests/activate-and-setup/complete-onboarding-wizard.spec.js +@@ -156,8 +156,9 @@ test.describe( 'Store owner can complete onboarding wizard', () => { + } ); + } ); + ++// !Changed from Japanese to Malta store, as Japanese Yen does not use decimals + test.describe( +- 'A japanese store can complete the selective bundle install but does not include WCPay.', ++ 'A Malta store can complete the selective bundle install but does not include WCPay.', + () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + +@@ -171,11 +172,11 @@ test.describe( + await page.click( '#woocommerce-select-control-0__control-input' ); + await page.fill( + '#woocommerce-select-control-0__control-input', +- 'Japan — Hokkaido' ++ 'Malta' + ); +- await page.click( 'button >> text=Japan — Hokkaido' ); +- await page.fill( '#inspector-text-control-2', 'Sapporo' ); +- await page.fill( '#inspector-text-control-3', '007-0852' ); ++ await page.click( 'button >> text=Malta' ); ++ await page.fill( '#inspector-text-control-2', 'Valletta' ); ++ await page.fill( '#inspector-text-control-3', 'VLT 1011' ); + await page.fill( + '#inspector-text-control-4', + 'admin@woocommercecoree2etestsuite.com' +@@ -236,31 +237,37 @@ test.describe( + await page.click( 'button >> text=Continue' ); + } ); + +- test( 'should display the choose payments task, and not the WC Pay task', async ( { ++ // Skipping this test because it's very flaky. Onboarding checklist changed so that the text ++ // changes when a task is completed. ++ test.skip( 'should display the choose payments task, and not the WC Pay task', async ( { + page, + } ) => { +- // Setup ++ // If payment has previously been setup, the setup checklist will show something different ++ // This step resets it ++ await page.goto( ++ 'wp-admin/admin.php?page=wc-settings&tab=checkout' ++ ); ++ // Ensure that all payment methods are disabled ++ await expect( ++ page.locator( '.woocommerce-input-toggle--disabled' ) ++ ).toHaveCount( 3 ); ++ // Checklist shows when completing setup wizard + await page.goto( + 'wp-admin/admin.php?page=wc-admin&path=%2Fsetup-wizard&step=theme' + ); + await page.click( 'button >> text=Continue with my active theme' ); + // Start test +- // If the test is being retried, the modal may have already been dismissed +- await page.locator( '#adminmenumain' ); +- const modalHeading = await page.$( +- 'h2.woocommerce__welcome-modal__page-content__header' +- ); +- if ( modalHeading ) { +- await expect( modalHeading ).toContain( +- 'Welcome to your WooCommerce store’s online HQ!' +- ); +- await page.click( '[aria-label="Close dialog"]' ); +- } +- const listItem = await page.textContent( +- ':nth-match(li[role=button], 3)' +- ); +- expect( listItem ).toContain( 'Set up payments' ); +- expect( listItem ).not.toContain( 'Set up WooCommerce Payments' ); ++ await page.waitForLoadState( 'networkidle' ); ++ await expect( ++ page.locator( ++ ':nth-match(.woocommerce-task-list__item-title, 3)' ++ ) ++ ).toContainText( 'Set up payments' ); ++ await expect( ++ page.locator( ++ ':nth-match(.woocommerce-task-list__item-title, 3)' ++ ) ++ ).not.toContainText( 'Set up WooCommerce Payments' ); + } ); + } + ); +@@ -294,29 +301,16 @@ test.describe( 'Store owner can go through setup Task List', () => { + + test( 'can setup shipping', async ( { page } ) => { + await page.goto( '/wp-admin/admin.php?page=wc-admin' ); +- // Close the welcome dialog if it's present +- await page.waitForLoadState( 'networkidle' ); // explictly wait because the welcome dialog loads last +- const welcomeDialog = await page.$( '.components-modal__header' ); +- if ( welcomeDialog !== null ) { +- await page.click( +- 'div.components-modal__header >> button.components-button' +- ); +- } +- await expect( welcomeDialog ).not.toBeVisible(); +- await page +- .locator( 'li[role="button"]:has-text("Set up shipping1 minute")' ) +- .click(); ++ await page.click( ':nth-match(.woocommerce-task-list__item-title, 5)' ); + +- const shippingPage = await page.textContent( 'h1' ); +- if ( shippingPage === 'Shipping' ) { ++ // check if this is the first time (or if the test is being retried) ++ const currPage = page.url(); ++ if ( currPage.indexOf( 'page=wc-settings&tab=shipping' ) > 0 ) { + // click the Add shipping zone button on the shipping settings page + await page.locator( '.page-title-action' ).click(); +- + await expect( +- page.locator( 'h2', { +- hasText: 'Shipping zones', +- } ) +- ).toBeVisible(); ++ page.locator( 'div.woocommerce > form > h2' ) ++ ).toContainText( 'Shipping zones' ); + } else { + await page.locator( 'button.components-button.is-primary' ).click(); + } +diff --git a/plugins/woocommerce/e2e/tests/admin-analytics/analytics-overview.spec.js b/plugins/woocommerce/e2e/tests/admin-analytics/analytics-overview.spec.js +index 795ad4481d..fc0bdc8338 100644 +--- a/plugins/woocommerce/e2e/tests/admin-analytics/analytics-overview.spec.js ++++ b/plugins/woocommerce/e2e/tests/admin-analytics/analytics-overview.spec.js +@@ -11,6 +11,9 @@ test.describe( 'Analytics pages', () => { + await page.goto( + 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview' + ); ++ ++ await page.waitForLoadState( 'networkidle' ); ++ + // Grab all of the section headings + const sections = await page.$$( + 'h2.woocommerce-section-header__title' +@@ -25,51 +28,6 @@ test.describe( 'Analytics pages', () => { + ); + } ); + +- test( 'should allow a user to remove a section', async ( { page } ) => { +- await page.goto( +- 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview' +- ); +- // clicks the first button to the right of the Performance heading +- await page +- .locator( 'button:right-of(:text("Performance")) >> nth=0' ) +- .click(); +- await page.locator( 'text=Remove section' ).click(); +- // Grab all of the section headings +- const sections = await page.$$( +- 'h2.woocommerce-section-header__title' +- ); +- await expect( sections.length ).toEqual( 2 ); +- +- // clean up +- await page.locator( '//button[@title="Add more sections"]' ).click(); +- await page +- .locator( '//button[@title="Add Performance section"]' ) +- .click(); +- await page.waitForLoadState( 'networkidle' ); +- } ); +- +- test( 'should allow a user to add a section back in', async ( { +- page, +- } ) => { +- await page.goto( +- 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview' +- ); +- // button only shows when not all sections visible, so remove a section +- await page +- .locator( 'button:right-of(:text("Performance")) >> nth=0' ) +- .click(); +- await page.locator( 'text=Remove section' ).click(); +- +- // add section +- await page.locator( '//button[@title="Add more sections"]' ).click(); +- await page +- .locator( '//button[@title="Add Performance section"]' ) +- .click(); +- await expect( +- page.locator( 'h2.woocommerce-section-header__title >> nth=2' ) +- ).toContainText( 'Performance' ); +- } ); +- + test.describe( 'moving sections', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + +@@ -80,20 +38,16 @@ test.describe( 'Analytics pages', () => { + 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview' + ); + // check the top section +- await page +- .locator( +- 'button.components-button.woocommerce-ellipsis-menu__toggle >> nth=0' +- ) +- .click(); ++ await page.click( ++ '//button[@title="Choose which analytics to display and the section name"]' ++ ); + await expect( page.locator( 'text=Move up' ) ).not.toBeVisible(); + await expect( page.locator( 'text=Move down' ) ).toBeVisible(); + + // check the bottom section +- await await page +- .locator( +- 'button.components-button.woocommerce-ellipsis-menu__toggle >> nth=2' +- ) +- .click(); ++ await page.click( ++ '//button[@title="Choose which leaderboards to display and other settings"]' ++ ); + await expect( page.locator( 'text=Move down' ) ).not.toBeVisible(); + await expect( page.locator( 'text=Move up' ) ).toBeVisible(); + } ); +@@ -111,12 +65,10 @@ test.describe( 'Analytics pages', () => { + .locator( 'h2.woocommerce-section-header__title >> nth=1' ) + .innerText(); + +- await page +- .locator( +- 'button.components-button.woocommerce-ellipsis-menu__toggle >> nth=0' +- ) +- .click(); +- await page.locator( 'text=Move down' ).click(); ++ await page.click( ++ 'button.components-button.woocommerce-ellipsis-menu__toggle >> nth=0' ++ ); ++ await page.click( 'text=Move down' ); + + // second section becomes first section, first becomes second + await expect( +@@ -140,12 +92,10 @@ test.describe( 'Analytics pages', () => { + .locator( 'h2.woocommerce-section-header__title >> nth=1' ) + .innerText(); + +- await page +- .locator( +- 'button.components-button.woocommerce-ellipsis-menu__toggle >> nth=1' +- ) +- .click(); +- await page.locator( 'text=Move up' ).click(); ++ await page.click( ++ 'button.components-button.woocommerce-ellipsis-menu__toggle >> nth=1' ++ ); ++ await page.click( 'text=Move up' ); + + // second section becomes first section, first becomes second + await expect( +@@ -156,4 +106,54 @@ test.describe( 'Analytics pages', () => { + ).toHaveText( firstSection ); + } ); + } ); ++ ++ test( 'should allow a user to remove a section', async ( { page } ) => { ++ await page.goto( ++ 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview' ++ ); ++ // clicks the first button to the right of the Performance heading ++ await page.click( 'button:right-of(:text("Performance")) >> nth=0' ); ++ await page.click( 'text=Remove section' ); ++ // Grab all of the section headings ++ const sections = await page.$$( ++ 'h2.woocommerce-section-header__title' ++ ); ++ await expect( sections.length ).toEqual( 2 ); ++ ++ // clean up ++ await page.click( '//button[@title="Add more sections"]' ); ++ await page.click( '//button[@title="Add Performance section"]' ); ++ await page.waitForSelector( 'h2:has-text("Performance")', { ++ state: 'visible', ++ } ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ); ++ ++ test( 'should allow a user to add a section back in', async ( { ++ page, ++ } ) => { ++ await page.goto( ++ 'wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2Foverview' ++ ); ++ // button only shows when not all sections visible, so remove a section ++ await page.click( 'button:right-of(:text("Performance")) >> nth=0' ); ++ await page.click( 'text=Remove section' ); ++ ++ // add section ++ await page.click( '//button[@title="Add more sections"]' ); ++ await page.click( '//button[@title="Add Performance section"]' ); ++ await expect( ++ page.locator( 'h2.woocommerce-section-header__title >> nth=2' ) ++ ).toContainText( 'Performance' ); ++ ++ // clean up by moving performance section back to the top ++ await page.click( ++ '//button[@title="Choose which analytics to display and the section name"]' ++ ); ++ await page.click( 'text=Move up' ); ++ await page.click( ++ '//button[@title="Choose which analytics to display and the section name"]' ++ ); ++ await page.click( 'text=Move up' ); ++ } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/admin-analytics/analytics.spec.js b/plugins/woocommerce/e2e/tests/admin-analytics/analytics.spec.js +index 2e2b294891..44c283ed23 100644 +--- a/plugins/woocommerce/e2e/tests/admin-analytics/analytics.spec.js ++++ b/plugins/woocommerce/e2e/tests/admin-analytics/analytics.spec.js +@@ -24,7 +24,7 @@ test.describe( 'Analytics pages', () => { + `/wp-admin/admin.php?page=wc-admin&path=%2Fanalytics%2F${ urlTitle }` + ); + const pageTitle = page.locator( 'h1' ); +- await expect( pageTitle ).toHaveText( aPages ); ++ await expect( pageTitle ).toContainText( aPages ); + await expect( + page.locator( '#woocommerce-layout__primary' ) + ).toBeVisible(); +diff --git a/plugins/woocommerce/e2e/tests/admin-tasks/payment.spec.js b/plugins/woocommerce/e2e/tests/admin-tasks/payment.spec.js +index e59b432bb6..860d2ebba8 100644 +--- a/plugins/woocommerce/e2e/tests/admin-tasks/payment.spec.js ++++ b/plugins/woocommerce/e2e/tests/admin-tasks/payment.spec.js +@@ -1,4 +1,5 @@ + const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + + test.describe( 'Payment setup task', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); +@@ -12,6 +13,21 @@ test.describe( 'Payment setup task', () => { + await page.waitForLoadState( 'networkidle' ); + } ); + ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.put( 'payment_gateways/bacs', { ++ enabled: false, ++ } ); ++ await api.put( 'payment_gateways/cod', { ++ enabled: false, ++ } ); ++ } ); ++ + test( 'Can visit the payment setup task from the homescreen if the setup wizard has been skipped', async ( { + page, + } ) => { +@@ -23,13 +39,16 @@ test.describe( 'Payment setup task', () => { + test( 'Saving valid bank account transfer details enables the payment method', async ( { + page, + } ) => { ++ // load the bank transfer page + await page.goto( + 'wp-admin/admin.php?page=wc-admin&task=payments&id=bacs' + ); ++ // purposely no await -- close the help dialog if/when it appears + page.locator( '.components-button.is-small.has-icon' ) + .click() + .catch( () => {} ); +- // purposely no await -- close the help dialog if/when it appears ++ ++ // fill in bank transfer form + await page.fill( '//input[@placeholder="Account name"]', 'Savings' ); + await page.fill( '//input[@placeholder="Account number"]', '1234' ); + await page.fill( '//input[@placeholder="Bank name"]', 'Test Bank' ); +@@ -37,46 +56,48 @@ test.describe( 'Payment setup task', () => { + await page.fill( '//input[@placeholder="IBAN"]', '12 3456 7890' ); + await page.fill( '//input[@placeholder="BIC / Swift"]', 'ABBA' ); + await page.click( 'text=Save' ); ++ ++ // check that bank transfers were set up + await expect( + page.locator( 'div.components-snackbar__content' ) +- ).toHaveText( 'Direct bank transfer details added successfully' ); +- await expect( page.locator( 'h1' ) ).toHaveText( 'Set up payments' ); ++ ).toContainText( 'Direct bank transfer details added successfully' ); ++ ++ await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=checkout' ); ++ + await expect( + page.locator( +- 'a:right-of(h3:has-text("Direct bank transfer")) >> nth=0' ++ '//tr[@data-gateway_id="bacs"]/td[@class="status"]/a' + ) +- ).toHaveText( 'Manage' ); +- +- // clean up +- await page.goto( +- 'wp-admin/admin.php?page=wc-settings&tab=checkout§ion=bacs' +- ); +- await page.click( 'text="Enable bank transfer"' ); +- await page.click( 'text="Save changes"' ); ++ ).toHaveClass( 'wc-payment-gateway-method-toggle-enabled' ); + } ); + + test( 'Enabling cash on delivery enables the payment method', async ( { + page, + } ) => { + await page.goto( 'wp-admin/admin.php?page=wc-admin&task=payments' ); ++ ++ // purposely no await -- close the help dialog if/when it appears + page.locator( '.components-button.is-small.has-icon' ) + .click() + .catch( () => {} ); +- // purposely no await -- close the help dialog if/when it appears +- await page.click( 'button:has-text("Enable")' ); // enable COD payment option +- await page.goto( 'wp-admin/admin.php?page=wc-admin&task=payments' ); +- await expect( page.locator( 'h1' ) ).toHaveText( 'Set up payments' ); +- await expect( +- page.locator( +- 'a:right-of(h3:has-text("Cash on delivery")) >> nth=0' +- ) +- ).toHaveText( 'Manage' ); + +- // clean up +- await page.goto( +- 'wp-admin/admin.php?page=wc-settings&tab=checkout§ion=cod' ++ if ( await page.isVisible( 'text=Offline payment methods' ) ) { ++ // other payment methods are already shown ++ } else { ++ // show other payment methods ++ await page.click( 'button.toggle-button' ); ++ } ++ ++ // enable COD payment option ++ await page.click( ++ 'div.woocommerce-task-payment-cod > div.woocommerce-task-payment__footer > button' + ); +- await page.click( 'text="Enable cash on delivery"' ); +- await page.click( 'text="Save changes"' ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=checkout' ); ++ ++ await expect( ++ page.locator( '//tr[@data-gateway_id="cod"]/td[@class="status"]/a' ) ++ ).toHaveClass( 'wc-payment-gateway-method-toggle-enabled' ); + } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/basic.spec.js b/plugins/woocommerce/e2e/tests/basic.spec.js +index 41cee6f192..b18fa3e107 100644 +--- a/plugins/woocommerce/e2e/tests/basic.spec.js ++++ b/plugins/woocommerce/e2e/tests/basic.spec.js +@@ -1,25 +1,32 @@ + const { test, expect } = require( '@playwright/test' ); + +-test( 'Load the home page', async ( { page } ) => { +- await page.goto( '/' ); +- const title = page.locator( 'h1.site-title' ); +- await expect( title ).toHaveText( 'WooCommerce Core E2E Test Suite' ); +-} ); ++test.describe( ++ 'A basic set of tests to ensure WP, wp-admin and my-account load', ++ () => { ++ test( 'Load the home page', async ( { page } ) => { ++ await page.goto( '/' ); ++ const title = page.locator( 'h1.site-title' ); ++ await expect( title ).toHaveText( ++ 'WooCommerce Core E2E Test Suite' ++ ); ++ } ); + +-test.describe( 'Sign in as admin', () => { +- test.use( { storageState: 'e2e/storage/adminState.json' } ); +- test( 'Load wp-admin', async ( { page } ) => { +- await page.goto( '/wp-admin' ); +- const title = page.locator( 'div.wrap > h1' ); +- await expect( title ).toHaveText( 'Dashboard' ); +- } ); +-} ); ++ test.describe( 'Sign in as admin', () => { ++ test.use( { storageState: 'e2e/storage/adminState.json' } ); ++ test( 'Load wp-admin', async ( { page } ) => { ++ await page.goto( '/wp-admin' ); ++ const title = page.locator( 'div.wrap > h1' ); ++ await expect( title ).toHaveText( 'Dashboard' ); ++ } ); ++ } ); + +-test.describe( 'Sign in as customer', () => { +- test.use( { storageState: 'e2e/storage/customerState.json' } ); +- test( 'Load customer my account page', async ( { page } ) => { +- await page.goto( '/my-account' ); +- const title = page.locator( 'h1.entry-title' ); +- await expect( title ).toHaveText( 'My account' ); +- } ); +-} ); ++ test.describe( 'Sign in as customer', () => { ++ test.use( { storageState: 'e2e/storage/customerState.json' } ); ++ test( 'Load customer my account page', async ( { page } ) => { ++ await page.goto( '/my-account' ); ++ const title = page.locator( 'h1.entry-title' ); ++ await expect( title ).toHaveText( 'My account' ); ++ } ); ++ } ); ++ } ++); +diff --git a/plugins/woocommerce/e2e/tests/merchant/create-coupon.spec.js b/plugins/woocommerce/e2e/tests/merchant/create-coupon.spec.js +index 63df4eb5fa..889bbb44a9 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/create-coupon.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/create-coupon.spec.js +@@ -1,14 +1,32 @@ + const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const couponCode = `code-${ new Date().getTime().toString() }`; + + test.describe( 'Add New Coupon Page', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.get( 'coupons' ).then( ( response ) => { ++ for ( let i = 0; i < response.data.length; i++ ) { ++ if ( response.data[ i ].code === couponCode ) { ++ api.delete( `coupons/${ response.data[ i ].id }`, { ++ force: true, ++ } ); ++ } ++ } ++ } ); ++ } ); ++ + test( 'can create new coupon', async ( { page } ) => { + await page.goto( 'wp-admin/post-new.php?post_type=shop_coupon' ); +- await page.fill( +- '#title', +- `code-${ new Date().getTime().toString() }` +- ); ++ await page.fill( '#title', couponCode ); + await page.fill( '#woocommerce-coupon-description', 'test coupon' ); + + await page.fill( '#coupon_amount', '100' ); +@@ -18,8 +36,5 @@ test.describe( 'Add New Coupon Page', () => { + await expect( page.locator( 'div.notice.notice-success' ) ).toHaveText( + 'Coupon updated.Dismiss this notice.' + ); +- +- // delete the coupon +- await page.dispatchEvent( 'a.submitdelete', 'click' ); + } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/create-order.spec.js b/plugins/woocommerce/e2e/tests/merchant/create-order.spec.js +index 451208dffc..b8a9f1f67d 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/create-order.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/create-order.spec.js +@@ -1,6 +1,10 @@ + const { test, expect } = require( '@playwright/test' ); + const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + ++const simpleProductName = 'Add new order simple product'; ++const variableProductName = 'Add new order variable product'; ++const externalProductName = 'Add new order external product'; ++const groupedProductName = 'Add new order grouped product'; + const taxClasses = [ + { + name: 'Tax Class Simple', +@@ -30,13 +34,14 @@ const taxRates = [ + }, + ]; + const taxClassSlugs = []; +-const taxTotals = [ '$10.00', '$60.00', '$240.00' ]; ++const taxTotals = [ '10.00', '20.00', '240.00' ]; + let simpleProductId, + variableProductId, + externalProductId, + subProductAId, + subProductBId, +- groupedProductId; ++ groupedProductId, ++ orderId; + + test.describe( 'WooCommerce Orders > Add new order', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); +@@ -53,41 +58,32 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + value: 'yes', + } ); + // add tax classes +- let a = 0; +- for ( const tax in taxClasses ) { +- api.post( 'taxes/classes', taxClasses[ tax ] ).then( +- ( response ) => { +- taxClassSlugs[ a ] = response.data.slug; +- a++; +- // add tax rates +- for ( const rate in taxRates ) { +- api.post( 'taxes', taxRates[ rate ] ); +- } +- } +- ); ++ for ( let i = 0; i < taxClasses.length; i++ ) { ++ await api ++ .post( 'taxes/classes', taxClasses[ i ] ) ++ .then( ( response ) => { ++ taxClassSlugs[ i ] = response.data.slug; ++ } ); ++ } ++ // attach rates to the classes ++ for ( let i = 0; i < taxRates.length; i++ ) { ++ await api.post( 'taxes', taxRates[ i ] ); + } +- // make sure the taxes are all created before creating products +- api.get( 'taxes/classes' ).then( ( response ) => { +- while ( true ) { +- if ( Object.keys( response.data ).length === 3 ) { +- api.post( 'products', { +- name: 'Simple Product 273722', +- type: 'simple', +- regular_price: '100', +- tax_class: 'Tax Class Simple', +- } ).then( ( resp ) => { +- simpleProductId = resp.data.id; +- } ); +- break; +- } +- } +- } ); + // create simple product +- ++ await api ++ .post( 'products', { ++ name: simpleProductName, ++ type: 'simple', ++ regular_price: '100', ++ tax_class: 'Tax Class Simple', ++ } ) ++ .then( ( resp ) => { ++ simpleProductId = resp.data.id; ++ } ); + // create variable product + const variations = [ + { +- regular_price: '200', ++ regular_price: '100', + attributes: [ + { + name: 'Size', +@@ -101,7 +97,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + tax_class: 'Tax Class Variable', + }, + { +- regular_price: '300', ++ regular_price: '100', + attributes: [ + { + name: 'Size', +@@ -117,7 +113,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + ]; + await api + .post( 'products', { +- name: 'Variable Product 024611', ++ name: variableProductName, + type: 'variable', + tax_class: 'Tax Class Variable', + } ) +@@ -133,7 +129,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + // create external product + await api + .post( 'products', { +- name: 'External product 786794', ++ name: externalProductName, + regular_price: '800', + tax_class: 'Tax Class External', + external_url: 'https://wordpress.org/plugins/woocommerce', +@@ -156,7 +152,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + } ); + await api + .post( 'products', { +- name: 'Grouped Product 858012', ++ name: groupedProductName, + regular_price: '29.99', + grouped_products: [ subProductAId, subProductBId ], + type: 'grouped', +@@ -167,22 +163,26 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + } ); + + test.afterAll( async ( { baseURL } ) => { +- // cleans up all products after run + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); +- await api.delete( `products/${ simpleProductId }`, { force: true } ); +- await api.delete( `products/${ variableProductId }`, { force: true } ); +- await api.delete( `products/${ externalProductId }`, { force: true } ); +- await api.delete( `products/${ subProductAId }`, { force: true } ); +- await api.delete( `products/${ subProductBId }`, { force: true } ); +- await api.delete( `products/${ groupedProductId }`, { force: true } ); ++ // cleans up all products after run ++ await api.post( 'products/batch', { ++ delete: [ ++ simpleProductId, ++ variableProductId, ++ externalProductId, ++ subProductAId, ++ subProductBId, ++ groupedProductId, ++ ], ++ } ); + // clean up tax classes and rates +- for ( const key in taxClassSlugs ) { +- await api.delete( `taxes/classes/${ taxClassSlugs[ key ] }`, { ++ for ( let i = 0; i < taxClassSlugs.length; i++ ) { ++ await api.delete( `taxes/classes/${ taxClassSlugs[ i ] }`, { + force: true, + } ); + } +@@ -190,6 +190,11 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + await api.put( 'settings/general/woocommerce_calc_taxes', { + value: 'no', + } ); ++ ++ // if we're only running the second test, there's no orderId created ++ if ( orderId ) { ++ await api.delete( `orders/${ orderId }`, { force: true } ); ++ } + } ); + + test( 'can create new order', async ( { page } ) => { +@@ -198,6 +203,18 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + 'Add new order' + ); + ++ await page.waitForLoadState( 'networkidle' ); ++ // get order ID from the page ++ const orderHtmlElement = await page.$( ++ 'h2.woocommerce-order-data__heading' ++ ); ++ const orderText = await page.evaluate( ++ ( element ) => element.textContent, ++ orderHtmlElement ++ ); ++ orderId = orderText.match( /([0-9])\w+/ ); ++ orderId = orderId[ 0 ].toString(); ++ + await page.selectOption( '#order_status', 'wc-processing' ); + await page.fill( 'input[name=order_date]', '2018-12-13' ); + await page.fill( 'input[name=order_date_hour]', '18' ); +@@ -231,7 +248,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + await page.click( 'text=Search for a product…' ); + await page.type( + 'input:below(:text("Search for a product…"))', +- 'Simple Product 273722' ++ simpleProductName + ); + await page.click( + 'li.select2-results__option.select2-results__option--highlighted' +@@ -240,7 +257,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + await page.click( 'text=Search for a product…' ); + await page.type( + 'input:below(:text("Search for a product…"))', +- 'Variable Product 024611' ++ variableProductName + ); + await page.click( + 'li.select2-results__option.select2-results__option--highlighted' +@@ -249,7 +266,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + await page.click( 'text=Search for a product…' ); + await page.type( + 'input:below(:text("Search for a product…"))', +- 'Grouped Product 858012' ++ groupedProductName + ); + await page.click( + 'li.select2-results__option.select2-results__option--highlighted' +@@ -258,7 +275,7 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + await page.click( 'text=Search for a product…' ); + await page.type( + 'input:below(:text("Search for a product…"))', +- 'External product 786794' ++ externalProductName + ); + await page.click( + 'li.select2-results__option.select2-results__option--highlighted' +@@ -268,16 +285,16 @@ test.describe( 'WooCommerce Orders > Add new order', () => { + + // assert that products added + await expect( page.locator( 'td.name > a >> nth=0' ) ).toContainText( +- 'Simple Product 273722' ++ simpleProductName + ); + await expect( page.locator( 'td.name > a >> nth=1' ) ).toContainText( +- 'Variable Product 024611' ++ variableProductName + ); + await expect( page.locator( 'td.name > a >> nth=2' ) ).toContainText( +- 'Grouped Product 858012' ++ groupedProductName + ); + await expect( page.locator( 'td.name > a >> nth=3' ) ).toContainText( +- 'External product 786794' ++ externalProductName + ); + + // Recalculate taxes +diff --git a/plugins/woocommerce/e2e/tests/merchant/create-shipping-classes.spec.js b/plugins/woocommerce/e2e/tests/merchant/create-shipping-classes.spec.js +index dfb83bcaa7..f3ad9e2b59 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/create-shipping-classes.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/create-shipping-classes.spec.js +@@ -3,6 +3,23 @@ const { test, expect } = require( '@playwright/test' ); + test.describe( 'Merchant can add shipping classes', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + ++ test.afterEach( async ( { page } ) => { ++ // no api endpoints for shipping classes, so use the UI to cleanup ++ await page.goto( ++ 'wp-admin/admin.php?page=wc-settings&tab=shipping§ion=classes' ++ ); ++ ++ await page.dispatchEvent( ++ '.wc-shipping-class-delete >> nth=0', ++ 'click' ++ ); ++ await page.dispatchEvent( ++ '.wc-shipping-class-delete >> nth=0', ++ 'click' ++ ); ++ await page.dispatchEvent( 'text=Save shipping classes', 'click' ); ++ } ); ++ + test( 'can add shipping classes', async ( { page } ) => { + await page.goto( + 'wp-admin/admin.php?page=wc-settings&tab=shipping§ion=classes' +@@ -54,16 +71,5 @@ test.describe( 'Merchant can add shipping classes', () => { + ).toBeVisible(); + } + } +- +- // clean up +- await page.dispatchEvent( +- '.wc-shipping-class-delete >> nth=0', +- 'click' +- ); +- await page.dispatchEvent( +- '.wc-shipping-class-delete >> nth=0', +- 'click' +- ); +- await page.dispatchEvent( 'text=Save shipping classes', 'click' ); + } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/create-shipping-zones.spec.js b/plugins/woocommerce/e2e/tests/merchant/create-shipping-zones.spec.js +index 9260fad5df..3ef0c998da 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/create-shipping-zones.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/create-shipping-zones.spec.js +@@ -1,38 +1,59 @@ + const { test, expect } = require( '@playwright/test' ); + const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +-const sanFranciscoZIP = '94107'; +-const shippingZoneNameUS = 'US with Flat rate'; +-const shippingZoneNameFL = 'CA with Free shipping'; +-const shippingZoneNameSF = 'SF with Local pickup'; +-let productId; ++const maynePostal = 'V0N 2J0'; ++const shippingZoneNameFlatRate = 'Canada with Flat rate'; ++const shippingZoneNameFreeShip = 'BC with Free shipping'; ++const shippingZoneNameLocalPickup = 'Mayne Island with Local pickup'; + + test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + +- test( 'add shipping zone for San Francisco with free Local pickup', async ( { ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.get( 'shipping/zones' ).then( ( response ) => { ++ for ( let i = 0; i < response.data.length; i++ ) { ++ if ( ++ response.data[ i ].name === shippingZoneNameFlatRate || ++ response.data[ i ].name === shippingZoneNameFreeShip || ++ response.data[ i ].name === shippingZoneNameLocalPickup ++ ) { ++ api.delete( `shipping/zones/${ response.data[ i ].id }`, { ++ force: true, ++ } ); ++ } ++ } ++ } ); ++ } ); ++ ++ test( 'add shipping zone for Mayne Island with free Local pickup', async ( { + page, + } ) => { + await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=shipping' ); +- if ( await page.isVisible( `text=${ shippingZoneNameSF }` ) ) { ++ if ( await page.isVisible( `text=${ shippingZoneNameLocalPickup }` ) ) { + // this shipping zone already exists, don't create it + } else { + await page.goto( + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' + ); +- await page.fill( '#zone_name', shippingZoneNameSF ); ++ await page.fill( '#zone_name', shippingZoneNameLocalPickup ); + + await page.click( '.select2-search__field' ); + await page.type( + '.select2-search__field', +- 'California, United States' ++ 'British Columbia, Canada' + ); + await page.click( + '.select2-results__option.select2-results__option--highlighted' + ); + + await page.click( '.wc-shipping-zone-postcodes-toggle' ); +- await page.fill( '#zone_postcodes', sanFranciscoZIP ); ++ await page.fill( '#zone_postcodes', maynePostal ); + + await page.click( 'text=Add shipping method' ); + +@@ -49,32 +70,32 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { + } + + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( +- /SF with Local pickup.*/ ++ /Mayne Island with Local pickup.*/ + ); + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( +- /California, 94107.*/ ++ /British Columbia, V0N 2J0.*/ + ); + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( + /Local pickup.*/ + ); + } ); + +- test( 'add shipping zone for California with Free shipping', async ( { ++ test( 'add shipping zone for British Columbia with Free shipping', async ( { + page, + } ) => { + await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=shipping' ); +- if ( await page.isVisible( `text=${ shippingZoneNameFL }` ) ) { ++ if ( await page.isVisible( `text=${ shippingZoneNameFreeShip }` ) ) { + // this shipping zone already exists, don't create it + } else { + await page.goto( + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' + ); +- await page.fill( '#zone_name', shippingZoneNameFL ); ++ await page.fill( '#zone_name', shippingZoneNameFreeShip ); + + await page.click( '.select2-search__field' ); + await page.type( + '.select2-search__field', +- 'California, United States' ++ 'British Columbia, Canada' + ); + await page.click( + '.select2-results__option.select2-results__option--highlighted' +@@ -94,28 +115,28 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { + await page.reload(); // Playwright runs so fast, the location shows up as "Everywhere" at first + } + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( +- /CA with Free shipping.*/ ++ /BC with Free shipping.*/ + ); + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( +- /California.*/ ++ /British Columbia.*/ + ); + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( + /Free shipping.*/ + ); + } ); + +- test( 'add shipping zone for the US with Flat rate', async ( { page } ) => { ++ test( 'add shipping zone for Canada with Flat rate', async ( { page } ) => { + await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=shipping' ); +- if ( await page.isVisible( `text=${ shippingZoneNameUS }` ) ) { ++ if ( await page.isVisible( `text=${ shippingZoneNameFlatRate }` ) ) { + // this shipping zone already exists, don't create it + } else { + await page.goto( + 'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new' + ); +- await page.fill( '#zone_name', shippingZoneNameUS ); ++ await page.fill( '#zone_name', shippingZoneNameFlatRate ); + + await page.click( '.select2-search__field' ); +- await page.type( '.select2-search__field', 'United States' ); ++ await page.type( '.select2-search__field', 'Canada' ); + await page.click( + '.select2-results__option.select2-results__option--highlighted' + ); +@@ -138,10 +159,10 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { + await page.reload(); // Playwright runs so fast, the location shows up as "Everywhere" at first + } + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( +- /US with Flat rate*/ ++ /Canada with Flat rate*/ + ); + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( +- /United States \(US\).*/ ++ /Canada.*/ + ); + await expect( page.locator( '.wc-shipping-zones' ) ).toHaveText( + /Flat rate.*/ +@@ -151,109 +172,171 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => { + + test.describe( 'Verifies shipping options from customer perspective', () => { + // note: tests are being run in an unauthenticated state (not as admin) +- test.beforeAll( async () => { ++ let productId, shippingFreeId, shippingFlatId, shippingLocalId; ++ ++ test.beforeAll( async ( { baseURL } ) => { + // need to add a product to the store so that we can order it and check shipping options + const api = new wcApi( { +- url: 'http://localhost:8084', ++ url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); +- api.post( 'products', { +- name: 'Shipping options are the best', +- type: 'simple', +- regular_price: '25.99', +- } ).then( ( response ) => { +- productId = response.data.id; ++ await api ++ .post( 'products', { ++ name: 'Shipping options are the best', ++ type: 'simple', ++ regular_price: '25.99', ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ // create shipping zones ++ await api ++ .post( 'shipping/zones', { ++ name: shippingZoneNameLocalPickup, ++ } ) ++ .then( ( response ) => { ++ shippingLocalId = response.data.id; ++ } ); ++ await api ++ .post( 'shipping/zones', { ++ name: shippingZoneNameFreeShip, ++ } ) ++ .then( ( response ) => { ++ shippingFreeId = response.data.id; ++ } ); ++ await api ++ .post( 'shipping/zones', { ++ name: shippingZoneNameFlatRate, ++ } ) ++ .then( ( response ) => { ++ shippingFlatId = response.data.id; ++ } ); ++ // set shipping zone locations ++ await api.put( `shipping/zones/${ shippingFlatId }/locations`, [ ++ { ++ code: 'CA', ++ }, ++ ] ); ++ await api.put( `shipping/zones/${ shippingFreeId }/locations`, [ ++ { ++ code: 'CA:BC', ++ type: 'state', ++ }, ++ ] ); ++ await api.put( `shipping/zones/${ shippingLocalId }/locations`, [ ++ { ++ code: 'V0N 2J0', ++ type: 'postcode', ++ }, ++ ] ); ++ // set shipping zone methods ++ await api.post( `shipping/zones/${ shippingFlatId }/methods`, { ++ method_id: 'flat_rate', ++ settings: { ++ cost: '10.00', ++ }, + } ); ++ await api.post( `shipping/zones/${ shippingFreeId }/methods`, { ++ method_id: 'free_shipping', ++ } ); ++ await api.post( `shipping/zones/${ shippingLocalId }/methods`, { ++ method_id: 'local_pickup', ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { context, page } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); + } ); + +- test.afterAll( async () => { ++ test.afterAll( async ( { baseURL } ) => { + const api = new wcApi( { +- url: 'http://localhost:8084', ++ url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); +- api.delete( `products/${ productId }`, { force: true } ); ++ await api.delete( `products/${ productId }`, { force: true } ); ++ await api.delete( `shipping/zones/${ shippingFlatId }`, { ++ force: true, ++ } ); ++ await api.delete( `shipping/zones/${ shippingFreeId }`, { ++ force: true, ++ } ); ++ await api.delete( `shipping/zones/${ shippingLocalId }`, { ++ force: true, ++ } ); + } ); + +- test( 'allows customer to benefit from a free Local pickup if in SF', async ( { ++ test( 'allows customer to benefit from a free Local pickup if on Mayne Island', async ( { + page, + } ) => { +- await page.goto( '/shop' ); +- await page.click( 'text=Add to cart' ); +- await page.click( 'text=View cart' ); +- +- await page.click( 'text=Change address' ); +- await page.fill( '#calc_shipping_postcode', '94107' ); ++ await page.goto( 'cart/' ); ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', 'CA' ); ++ await page.selectOption( '#calc_shipping_state', 'BC' ); ++ await page.fill( '#calc_shipping_postcode', maynePostal ); + await page.click( 'button[name=calc_shipping]' ); + await page.waitForSelector( 'button[name=calc_shipping]', { + state: 'hidden', + } ); + +- expect( +- await page.textContent( +- '.shipping ul#shipping_method > li > label' +- ) +- ).toBe( 'Local pickup' ); +- expect( +- await page.textContent( +- 'td[data-title="Total"] > strong > .amount > bdi' +- ) +- ).toBe( '$25.99' ); ++ await expect( ++ page.locator( '.shipping ul#shipping_method > li > label' ) ++ ).toContainText( 'Local pickup' ); ++ await expect( ++ page.locator( 'td[data-title="Total"] > strong > .amount > bdi' ) ++ ).toContainText( '25.99' ); + } ); + +- test( 'allows customer to benefit from a free Free shipping if in CA', async ( { ++ test( 'allows customer to benefit from a free Free shipping if in BC', async ( { + page, + } ) => { +- await page.goto( '/shop' ); +- await page.click( 'text=Add to cart' ); +- await page.click( 'text=View cart' ); ++ await page.goto( 'cart/' ); + +- await page.click( 'text=Change address' ); +- await page.fill( '#calc_shipping_postcode', '94000' ); ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', 'CA' ); ++ await page.selectOption( '#calc_shipping_state', 'BC' ); + await page.click( 'button[name=calc_shipping]' ); + await page.waitForSelector( 'button[name=calc_shipping]', { + state: 'hidden', + } ); + +- expect( +- await page.textContent( +- '.shipping ul#shipping_method > li > label' +- ) +- ).toBe( 'Free shipping' ); +- expect( +- await page.textContent( +- 'td[data-title="Total"] > strong > .amount > bdi' +- ) +- ).toBe( '$25.99' ); ++ await expect( ++ page.locator( '.shipping ul#shipping_method > li > label' ) ++ ).toContainText( 'Free shipping' ); ++ await expect( ++ page.locator( 'td[data-title="Total"] > strong > .amount > bdi' ) ++ ).toContainText( '25.99' ); + } ); + + test( 'allows customer to pay for a Flat rate shipping method', async ( { + page, + } ) => { +- await page.goto( '/shop' ); +- await page.click( 'text=Add to cart' ); +- await page.click( 'text=View cart' ); ++ await page.goto( 'cart/' ); + +- await page.click( 'text=Change address' ); +- await page.selectOption( '#calc_shipping_state', 'NY' ); +- await page.fill( '#calc_shipping_postcode', '10010' ); ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', 'CA' ); ++ await page.selectOption( '#calc_shipping_state', 'AB' ); ++ await page.fill( '#calc_shipping_postcode', 'T2T 1B3' ); + await page.click( 'button[name=calc_shipping]' ); + await page.waitForSelector( 'button[name=calc_shipping]', { + state: 'hidden', + } ); + +- expect( +- await page.textContent( +- '.shipping ul#shipping_method > li > label' +- ) +- ).toBe( 'Flat rate: $10.00' ); +- expect( +- await page.textContent( +- 'td[data-title="Total"] > strong > .amount > bdi' +- ) +- ).toBe( '$35.99' ); ++ await expect( ++ page.locator( '.shipping ul#shipping_method > li > label' ) ++ ).toContainText( 'Flat rate:' ); ++ await expect( ++ page.locator( '.shipping ul#shipping_method > li > label' ) ++ ).toContainText( '10.00' ); ++ await expect( ++ page.locator( 'td[data-title="Total"] > strong > .amount > bdi' ) ++ ).toContainText( '35.99' ); + } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/create-simple-product.spec.js b/plugins/woocommerce/e2e/tests/merchant/create-simple-product.spec.js +index 607de7df93..58fde16c6d 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/create-simple-product.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/create-simple-product.spec.js +@@ -20,10 +20,13 @@ test.describe( 'Add New Simple Product Page', () => { + // and the flat rate shipping method to that zone + await api + .post( 'shipping/zones', { +- name: 'Everywhere', ++ name: 'Somewhere', + } ) + .then( ( response ) => { + shippingZoneId = response.data.id; ++ api.put( `shipping/zones/${ shippingZoneId }/locations`, [ ++ { code: 'CN' }, ++ ] ); + api.post( `shipping/zones/${ shippingZoneId }/methods`, { + method_id: 'flat_rate', + } ); +@@ -65,8 +68,8 @@ test.describe( 'Add New Simple Product Page', () => { + await page.click( '#_virtual' ); + await page.fill( '#_regular_price', productPrice ); + await page.click( '#publish' ); +- await expect( page.locator( 'div.notice-success' ) ).toHaveText( +- 'Product published. View ProductDismiss this notice.' ++ await expect( page.locator( 'div.notice-success > p' ) ).toContainText( ++ 'Product published.' + ); + } ); + +@@ -77,7 +80,7 @@ test.describe( 'Add New Simple Product Page', () => { + await page.click( `h2:has-text("${ virtualProductName }")` ); + await page.click( 'text=Add to cart' ); + await page.click( 'text=View cart' ); +- await expect( page.locator( 'td[data-title=Product]' ) ).toHaveText( ++ await expect( page.locator( 'td[data-title=Product]' ) ).toContainText( + virtualProductName + ); + await expect( +@@ -91,8 +94,8 @@ test.describe( 'Add New Simple Product Page', () => { + await page.fill( '#title', nonVirtualProductName ); + await page.fill( '#_regular_price', productPrice ); + await page.click( '#publish' ); +- await expect( page.locator( 'div.notice-success' ) ).toHaveText( +- 'Product published. View ProductDismiss this notice.' ++ await expect( page.locator( 'div.notice-success > p' ) ).toContainText( ++ 'Product published.' + ); + } ); + +@@ -103,7 +106,7 @@ test.describe( 'Add New Simple Product Page', () => { + await page.click( `h2:has-text("${ nonVirtualProductName }")` ); + await page.click( 'text=Add to cart' ); + await page.click( 'text=View cart' ); +- await expect( page.locator( 'td[data-title=Product]' ) ).toHaveText( ++ await expect( page.locator( 'td[data-title=Product]' ) ).toContainText( + nonVirtualProductName + ); + await expect( +diff --git a/plugins/woocommerce/e2e/tests/merchant/create-variable-product.spec.js b/plugins/woocommerce/e2e/tests/merchant/create-variable-product.spec.js +index 5ec97af7ee..54aeb12f27 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/create-variable-product.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/create-variable-product.spec.js +@@ -34,8 +34,6 @@ test.describe( 'Add New Variable Product Page', () => { + ) { + api.delete( `products/${ product.id }`, { + force: true, +- } ).then( () => { +- // nothing to do here. + } ); + } + } +@@ -290,8 +288,8 @@ test.describe( 'Add New Variable Product Page', () => { + + // remove a variation + page.on( 'dialog', ( dialog ) => dialog.accept() ); +- await page.click( '.remove_variation.delete', { force: true } ); +- await page.click( '.remove_variation.delete' ); // have to do this twice to get the link to appear ++ await page.hover( '.woocommerce_variation' ); ++ await page.click( '.remove_variation.delete' ); + await expect( page.locator( '.woocommerce_variation' ) ).toHaveCount( + 0 + ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/order-coupon.spec.js b/plugins/woocommerce/e2e/tests/merchant/order-coupon.spec.js +index c51a50797e..9e3239e2f1 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/order-coupon.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/order-coupon.spec.js +@@ -4,6 +4,8 @@ const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + let productId, couponId, orderId; + + const productPrice = '9.99'; ++const productName = 'Apply Coupon Product'; ++const couponCode = '5off'; + const couponAmount = '5'; + const discountedPrice = ( productPrice - couponAmount ).toString(); + +@@ -20,7 +22,7 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + // create a simple product + await api + .post( 'products', { +- name: 'Simple Product', ++ name: productName, + type: 'simple', + regular_price: productPrice, + } ) +@@ -30,7 +32,7 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + // create a $5 off coupon + await api + .post( 'coupons', { +- code: '5off', ++ code: couponCode, + discount_type: 'fixed_product', + amount: couponAmount, + } ) +@@ -48,7 +50,7 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + ], + coupon_lines: [ + { +- code: '5off', ++ code: couponCode, + }, + ], + } ) +@@ -81,7 +83,7 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + await page.click( 'text=Search for a product…' ); + await page.type( + 'input:below(:text("Search for a product…"))', +- 'Simple Product' ++ productName + ); + await page.click( + 'li.select2-results__option.select2-results__option--highlighted' +@@ -90,10 +92,10 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + await page.click( 'button#btn-ok' ); + + // apply coupon +- page.on( 'dialog', ( dialog ) => dialog.accept( '5off' ) ); ++ page.on( 'dialog', ( dialog ) => dialog.accept( couponCode ) ); + await page.click( 'button.add-coupon' ); + +- await expect( page.locator( 'text=5off' ) ).toBeVisible(); ++ await expect( page.locator( `text=${ couponCode }` ) ).toBeVisible(); + await expect( + page.locator( '.wc-order-totals td.label >> nth=1' ) + ).toContainText( 'Coupon(s)' ); +@@ -111,7 +113,7 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + test( 'can remove a coupon', async ( { page } ) => { + await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` ); + // assert that there is a coupon on the order +- await expect( page.locator( 'text=5off' ) ).toBeVisible(); ++ await expect( page.locator( `text=${ couponCode }` ) ).toBeVisible(); + await expect( + page.locator( '.wc-order-totals td.label >> nth=1' ) + ).toContainText( 'Coupon(s)' ); +@@ -128,12 +130,14 @@ test.describe( 'WooCommerce Orders > Apply Coupon', () => { + await page.dispatchEvent( 'a.remove-coupon', 'click' ); // have to use dispatchEvent because nothing visible to click on + + // make sure the coupon was removed +- await expect( page.locator( 'text=5off' ) ).not.toBeVisible(); ++ await expect( ++ page.locator( `text=${ couponCode }` ) ++ ).not.toBeVisible(); + await expect( + page.locator( '.wc-order-totals td.label >> nth=1' ) + ).toContainText( 'Order Total' ); + await expect( + page.locator( '.wc-order-totals td.total >> nth=1' ) +- ).toContainText( '$9.99' ); ++ ).toContainText( productPrice ); + } ); + } ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/order-edit.spec.js b/plugins/woocommerce/e2e/tests/merchant/order-edit.spec.js +index 3bbcc62636..2c8b09094c 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/order-edit.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/order-edit.spec.js +@@ -2,11 +2,11 @@ const { test, expect } = require( '@playwright/test' ); + const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + const uuid = require( 'uuid' ); + +-let orderId; +- +-test.describe( 'WooCommerce Orders > Edit order', () => { ++test.describe( 'Edit order', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + ++ let orderId; ++ + test.beforeAll( async ( { baseURL } ) => { + const api = new wcApi( { + url: baseURL, +@@ -86,279 +86,256 @@ test.describe( 'WooCommerce Orders > Edit order', () => { + } ); + } ); + +-test.describe( +- 'WooCommerce Orders > Edit order > Downloadable product permissions', +- () => { +- test.use( { storageState: 'e2e/storage/adminState.json' } ); +- +- const productName = 'TDP 001'; +- const product2Name = 'TDP 002'; +- const customerBilling = { +- email: 'john.doe@example.com', +- }; +- +- let productId, product2Id, noProductOrderId; +- +- test.beforeEach( async ( { baseURL } ) => { +- const api = new wcApi( { +- url: baseURL, +- consumerKey: process.env.CONSUMER_KEY, +- consumerSecret: process.env.CONSUMER_SECRET, +- version: 'wc/v3', +- } ); +- await api +- .post( 'products', { +- name: productName, +- downloadable: true, +- download_limit: -1, +- downloads: [ +- { +- id: uuid.v4(), +- name: 'Single', +- file: +- 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', +- }, +- ], +- } ) +- .then( ( response ) => { +- productId = response.data.id; +- } ); +- await api +- .post( 'products', { +- name: product2Name, +- downloadable: true, +- download_limit: -1, +- downloads: [ +- { +- id: uuid.v4(), +- name: 'Single', +- file: +- 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', +- }, +- ], +- } ) +- .then( ( response ) => { +- product2Id = response.data.id; +- } ); +- await api +- .post( 'orders', { +- status: 'processing', +- line_items: [ +- { +- product_id: productId, +- quantity: 1, +- }, +- ], +- billing: customerBilling, +- } ) +- .then( ( response ) => { +- orderId = response.data.id; +- } ); +- await api +- .post( 'orders', { +- status: 'processing', +- billing: customerBilling, +- } ) +- .then( ( response ) => { +- noProductOrderId = response.data.id; +- } ); +- } ); ++test.describe( 'Edit order > Downloadable product permissions', () => { ++ test.use( { storageState: 'e2e/storage/adminState.json' } ); + +- test.afterEach( async ( { baseURL } ) => { +- const api = new wcApi( { +- url: baseURL, +- consumerKey: process.env.CONSUMER_KEY, +- consumerSecret: process.env.CONSUMER_SECRET, +- version: 'wc/v3', +- } ); +- await api.delete( `products/${ productId }`, { force: true } ); +- await api.delete( `products/${ product2Id }`, { force: true } ); +- await api.delete( `orders/${ orderId }`, { force: true } ); +- await api.delete( `orders/${ noProductOrderId }`, { force: true } ); +- } ); ++ const productName = 'TDP 001'; ++ const product2Name = 'TDP 002'; ++ const customerBilling = { ++ email: 'john.doe@example.com', ++ }; + +- test( 'can add downloadable product permissions to order without product', async ( { +- page, +- } ) => { +- // go to the order with no products +- await page.goto( +- `wp-admin/post.php?post=${ noProductOrderId }&action=edit` +- ); +- +- // add downloadable product permissions +- await page.type( 'input.select2-search__field', productName ); +- await page.click( +- 'li.select2-results__option.select2-results__option--highlighted' +- ); +- await page.click( 'button.grant_access' ); +- +- // verify new downloadable product permission details +- await expect( page.locator( 'h3.fixed' ) ).toContainText( +- productName +- ); +- await expect( +- page.locator( 'input[name="downloads_remaining[1]"]' ) +- ).toHaveAttribute( 'placeholder', 'Unlimited' ); +- await expect( +- page.locator( 'input[name="access_expires[1]"]' ) +- ).toHaveAttribute( 'placeholder', 'Never' ); +- await expect( +- page.locator( 'button.revoke_access' ) +- ).toBeVisible(); +- await expect( +- page.locator( 'a:has-text("Copy link")' ) +- ).toBeVisible(); +- await expect( +- page.locator( 'a:has-text("View report")' ) +- ).toBeVisible(); +- } ); ++ let orderId, productId, product2Id, noProductOrderId; + +- test( 'can add downloadable product permissions to order with product', async ( { +- page, +- } ) => { +- // open the order that already has a product assigned +- await page.goto( +- `wp-admin/post.php?post=${ orderId }&action=edit` +- ); +- +- // add downloadable product permissions +- await page.type( 'input.select2-search__field', product2Name ); +- await page.click( +- 'li.select2-results__option.select2-results__option--highlighted' +- ); +- await page.click( 'button.grant_access' ); +- +- // verify new downloadable product permission details +- await expect( page.locator( 'h3.fixed >> nth=1' ) ).toContainText( +- product2Name +- ); +- await expect( +- page.locator( 'input[name="downloads_remaining[2]"]' ) +- ).toHaveAttribute( 'placeholder', 'Unlimited' ); +- await expect( +- page.locator( 'input[name="access_expires[2]"]' ) +- ).toHaveAttribute( 'placeholder', 'Never' ); ++ test.beforeEach( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', + } ); ++ await api ++ .post( 'products', { ++ name: productName, ++ downloadable: true, ++ download_limit: -1, ++ downloads: [ ++ { ++ id: uuid.v4(), ++ name: 'Single', ++ file: ++ 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', ++ }, ++ ], ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ await api ++ .post( 'products', { ++ name: product2Name, ++ downloadable: true, ++ download_limit: -1, ++ downloads: [ ++ { ++ id: uuid.v4(), ++ name: 'Single', ++ file: ++ 'https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg', ++ }, ++ ], ++ } ) ++ .then( ( response ) => { ++ product2Id = response.data.id; ++ } ); ++ await api ++ .post( 'orders', { ++ status: 'processing', ++ line_items: [ ++ { ++ product_id: productId, ++ quantity: 1, ++ }, ++ ], ++ billing: customerBilling, ++ } ) ++ .then( ( response ) => { ++ orderId = response.data.id; ++ } ); ++ await api ++ .post( 'orders', { ++ status: 'processing', ++ billing: customerBilling, ++ } ) ++ .then( ( response ) => { ++ noProductOrderId = response.data.id; ++ } ); ++ } ); + +- test( 'can edit downloadable product permissions', async ( { +- page, +- } ) => { +- const expectedDownloadsRemaining = '10'; +- const expectedDownloadsExpirationDate = '2050-01-01'; +- +- // open the order that already has a product assigned +- await page.goto( +- `wp-admin/post.php?post=${ orderId }&action=edit` +- ); +- +- // expand product download permissions +- await page.click( 'h3.fixed' ); +- +- // edit download permissions +- await page.fill( +- 'input[name="downloads_remaining[0]"]', +- expectedDownloadsRemaining +- ); +- await page.fill( +- 'input[name="access_expires[0]"]', +- expectedDownloadsExpirationDate +- ); +- await page.click( 'button.save_order' ); +- +- // verify new downloadable product permissions +- await page.click( 'h3.fixed' ); +- await expect( +- page.locator( 'input[name="downloads_remaining[0]"]' ) +- ).toHaveValue( expectedDownloadsRemaining ); +- await expect( +- page.locator( 'input[name="access_expires[0]"]' ) +- ).toHaveValue( expectedDownloadsExpirationDate ); ++ test.afterEach( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', + } ); ++ await api.delete( `products/${ productId }`, { force: true } ); ++ await api.delete( `products/${ product2Id }`, { force: true } ); ++ await api.delete( `orders/${ orderId }`, { force: true } ); ++ await api.delete( `orders/${ noProductOrderId }`, { force: true } ); ++ } ); ++ ++ test( 'can add downloadable product permissions to order without product', async ( { ++ page, ++ } ) => { ++ // go to the order with no products ++ await page.goto( ++ `wp-admin/post.php?post=${ noProductOrderId }&action=edit` ++ ); ++ ++ // add downloadable product permissions ++ await page.type( 'input.select2-search__field', productName ); ++ await page.click( ++ 'li.select2-results__option.select2-results__option--highlighted' ++ ); ++ await page.click( 'button.grant_access' ); ++ ++ // verify new downloadable product permission details ++ await expect( page.locator( 'h3.fixed' ) ).toContainText( productName ); ++ await expect( ++ page.locator( 'input[name="downloads_remaining[1]"]' ) ++ ).toHaveAttribute( 'placeholder', 'Unlimited' ); ++ await expect( ++ page.locator( 'input[name="access_expires[1]"]' ) ++ ).toHaveAttribute( 'placeholder', 'Never' ); ++ await expect( page.locator( 'button.revoke_access' ) ).toBeVisible(); ++ await expect( page.locator( 'a:has-text("Copy link")' ) ).toBeVisible(); ++ await expect( ++ page.locator( 'a:has-text("View report")' ) ++ ).toBeVisible(); ++ } ); + +- test( 'can revoke downloadable product permissions', async ( { +- page, +- } ) => { +- // open the order that already has a product assigned +- await page.goto( +- `wp-admin/post.php?post=${ orderId }&action=edit` +- ); ++ test( 'can add downloadable product permissions to order with product', async ( { ++ page, ++ } ) => { ++ // open the order that already has a product assigned ++ await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` ); + +- // expand product download permissions +- await page.click( 'h3.fixed' ); ++ // add downloadable product permissions ++ await page.type( 'input.select2-search__field', product2Name ); ++ await page.click( ++ 'li.select2-results__option.select2-results__option--highlighted' ++ ); ++ await page.click( 'button.grant_access' ); + +- // verify prior state before revoking +- await expect( page.locator( 'h3.fixed' ) ).toHaveCount( 1 ); ++ // verify new downloadable product permission details ++ await expect( page.locator( 'h3.fixed >> nth=1' ) ).toContainText( ++ product2Name ++ ); ++ await expect( ++ page.locator( 'input[name="downloads_remaining[2]"]' ) ++ ).toHaveAttribute( 'placeholder', 'Unlimited' ); ++ await expect( ++ page.locator( 'input[name="access_expires[2]"]' ) ++ ).toHaveAttribute( 'placeholder', 'Never' ); ++ } ); + +- // click revoke access +- page.on( 'dialog', ( dialog ) => dialog.accept() ); +- await page.click( 'button.revoke_access' ); +- await page.waitForLoadState( 'networkidle' ); ++ test( 'can edit downloadable product permissions', async ( { page } ) => { ++ const expectedDownloadsRemaining = '10'; ++ const expectedDownloadsExpirationDate = '2050-01-01'; + +- // verify permissions gone +- await expect( page.locator( 'h3.fixed' ) ).toHaveCount( 0 ); +- } ); ++ // open the order that already has a product assigned ++ await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` ); + +- test( 'should not allow downloading a product if download attempts are exceeded', async ( { +- page, +- } ) => { +- const expectedReason = +- 'Sorry, you have reached your download limit for this file'; +- +- // open the order that already has a product assigned +- await page.goto( +- `wp-admin/post.php?post=${ orderId }&action=edit` +- ); +- +- // set the download limit to 0 +- // expand product download permissions +- await page.click( 'h3.fixed' ); +- +- // edit download permissions +- await page.fill( 'input[name="downloads_remaining[0]"]', '0' ); +- await page.click( 'button.save_order' ); +- +- // get the download link +- await page.click( 'h3.fixed' ); +- const downloadPage = await page +- .locator( 'a#copy-download-link' ) +- .getAttribute( 'href' ); +- +- // open download page +- await page.goto( downloadPage ); +- await expect( page.locator( 'div.wp-die-message' ) ).toContainText( +- expectedReason +- ); +- } ); ++ // expand product download permissions ++ await page.click( 'h3.fixed' ); + +- test( 'should not allow downloading a product if expiration date has passed', async ( { +- page, +- } ) => { +- const expectedReason = 'Sorry, this download has expired'; +- +- // open the order that already has a product assigned +- await page.goto( +- `wp-admin/post.php?post=${ orderId }&action=edit` +- ); +- +- // set the download limit to 0 +- // expand product download permissions +- await page.click( 'h3.fixed' ); +- +- // edit download permissions +- await page.fill( 'input[name="access_expires[0]"]', '2018-12-14' ); +- await page.click( 'button.save_order' ); +- +- // get the download link +- await page.click( 'h3.fixed' ); +- const downloadPage = await page +- .locator( 'a#copy-download-link' ) +- .getAttribute( 'href' ); +- +- // open download page +- await page.goto( downloadPage ); +- await expect( page.locator( 'div.wp-die-message' ) ).toContainText( +- expectedReason +- ); +- } ); +- } +-); ++ // edit download permissions ++ await page.fill( ++ 'input[name="downloads_remaining[0]"]', ++ expectedDownloadsRemaining ++ ); ++ await page.fill( ++ 'input[name="access_expires[0]"]', ++ expectedDownloadsExpirationDate ++ ); ++ await page.click( 'button.save_order' ); ++ ++ // verify new downloadable product permissions ++ await page.click( 'h3.fixed' ); ++ await expect( ++ page.locator( 'input[name="downloads_remaining[0]"]' ) ++ ).toHaveValue( expectedDownloadsRemaining ); ++ await expect( ++ page.locator( 'input[name="access_expires[0]"]' ) ++ ).toHaveValue( expectedDownloadsExpirationDate ); ++ } ); ++ ++ test( 'can revoke downloadable product permissions', async ( { page } ) => { ++ // open the order that already has a product assigned ++ await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` ); ++ ++ // expand product download permissions ++ await page.click( 'h3.fixed' ); ++ ++ // verify prior state before revoking ++ await expect( page.locator( 'h3.fixed' ) ).toHaveCount( 1 ); ++ ++ // click revoke access ++ page.on( 'dialog', ( dialog ) => dialog.accept() ); ++ await page.click( 'button.revoke_access' ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ // verify permissions gone ++ await expect( page.locator( 'h3.fixed' ) ).toHaveCount( 0 ); ++ } ); ++ ++ test( 'should not allow downloading a product if download attempts are exceeded', async ( { ++ page, ++ } ) => { ++ const expectedReason = ++ 'Sorry, you have reached your download limit for this file'; ++ ++ // open the order that already has a product assigned ++ await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` ); ++ ++ // set the download limit to 0 ++ // expand product download permissions ++ await page.click( 'h3.fixed' ); ++ ++ // edit download permissions ++ await page.fill( 'input[name="downloads_remaining[0]"]', '0' ); ++ await page.click( 'button.save_order' ); ++ ++ // get the download link ++ await page.click( 'h3.fixed' ); ++ const downloadPage = await page ++ .locator( 'a#copy-download-link' ) ++ .getAttribute( 'href' ); ++ ++ // open download page ++ await page.goto( downloadPage ); ++ await expect( page.locator( 'div.wp-die-message' ) ).toContainText( ++ expectedReason ++ ); ++ } ); ++ ++ test( 'should not allow downloading a product if expiration date has passed', async ( { ++ page, ++ } ) => { ++ const expectedReason = 'Sorry, this download has expired'; ++ ++ // open the order that already has a product assigned ++ await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` ); ++ ++ // set the download limit to 0 ++ // expand product download permissions ++ await page.click( 'h3.fixed' ); ++ ++ // edit download permissions ++ await page.fill( 'input[name="access_expires[0]"]', '2018-12-14' ); ++ await page.click( 'button.save_order' ); ++ ++ // get the download link ++ await page.click( 'h3.fixed' ); ++ const downloadPage = await page ++ .locator( 'a#copy-download-link' ) ++ .getAttribute( 'href' ); ++ ++ // open download page ++ await page.goto( downloadPage ); ++ await expect( page.locator( 'div.wp-die-message' ) ).toContainText( ++ expectedReason ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/order-emails.spec.js b/plugins/woocommerce/e2e/tests/merchant/order-emails.spec.js +index 917f69dbfb..eb4049a0a8 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/order-emails.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/order-emails.spec.js +@@ -5,7 +5,7 @@ test.describe( 'Merchant > Order Action emails received', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + + const customerBilling = { +- email: 'john.doe@example.com', ++ email: 'john.doe.merchant.test@example.com', + }; + const adminEmail = 'admin@woocommercecoree2etestsuite.com'; + const storeName = 'WooCommerce Core E2E Test Suite'; +@@ -29,7 +29,11 @@ test.describe( 'Merchant > Order Action emails received', () => { + } ); + + test.beforeEach( async ( { page } ) => { +- await page.goto( 'wp-admin/tools.php?page=wpml_plugin_log' ); ++ await page.goto( ++ `wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent( ++ customerBilling.email ++ ) }` ++ ); + // clear out the email logs before each test + while ( ( await page.$( '#bulk-action-selector-top' ) ) !== null ) { + await page.click( '#cb-select-all-1' ); +@@ -46,6 +50,7 @@ test.describe( 'Merchant > Order Action emails received', () => { + version: 'wc/v3', + } ); + await api.delete( `orders/${ orderId }`, { force: true } ); ++ await api.delete( `orders/${ newOrderId }`, { force: true } ); + } ); + + test( 'can receive new order email', async ( { page, baseURL } ) => { +@@ -65,16 +70,18 @@ test.describe( 'Merchant > Order Action emails received', () => { + .then( ( response ) => { + newOrderId = response.data.id; + } ); +- +- await page.goto( 'wp-admin/tools.php?page=wpml_plugin_log' ); ++ // search to narrow it down to just the messages we want ++ await page.goto( ++ `wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent( ++ customerBilling.email ++ ) }` ++ ); + await expect( + page.locator( 'td.column-receiver >> nth=1' ) + ).toContainText( adminEmail ); + await expect( + page.locator( 'td.column-subject >> nth=1' ) + ).toContainText( `[${ storeName }]: New order #${ newOrderId }` ); +- +- await api.delete( `orders/${ newOrderId }`, { force: true } ); + } ); + + test( 'can resend new order notification', async ( { page } ) => { +@@ -87,8 +94,12 @@ test.describe( 'Merchant > Order Action emails received', () => { + await page.click( 'button.wc-reload' ); + await page.waitForLoadState( 'networkidle' ); + +- // confirm the message was delivered in the logs +- await page.goto( 'wp-admin/tools.php?page=wpml_plugin_log' ); ++ // search to narrow it down to just the messages we want ++ await page.goto( ++ `wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent( ++ customerBilling.email ++ ) }` ++ ); + await expect( page.locator( 'td.column-receiver' ) ).toContainText( + adminEmail + ); +@@ -105,7 +116,11 @@ test.describe( 'Merchant > Order Action emails received', () => { + await page.waitForLoadState( 'networkidle' ); + + // confirm the message was delivered in the logs +- await page.goto( 'wp-admin/tools.php?page=wpml_plugin_log' ); ++ await page.goto( ++ `wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent( ++ customerBilling.email ++ ) }` ++ ); + await expect( page.locator( 'td.column-receiver' ) ).toContainText( + customerBilling.email + ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/order-refund.spec.js b/plugins/woocommerce/e2e/tests/merchant/order-refund.spec.js +index f3102073a2..bea2ebe6d9 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/order-refund.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/order-refund.spec.js +@@ -81,7 +81,9 @@ test.describe( 'WooCommerce Orders > Refund an order', () => { + + // Do the refund + page.on( 'dialog', ( dialog ) => dialog.accept() ); +- await page.click( '.do-manual-refund' ); ++ await page.click( '.do-manual-refund', { ++ waitForLoadState: 'networkidle', ++ } ); + + // Verify the product line item shows the refunded quantity and amount + await expect( page.locator( 'small.refunded >> nth=0' ) ).toContainText( +@@ -111,7 +113,10 @@ test.describe( 'WooCommerce Orders > Refund an order', () => { + await page.waitForLoadState( 'networkidle' ); + + page.on( 'dialog', ( dialog ) => dialog.accept() ); +- await page.click( 'a.delete_refund', { force: true } ); // have to force it because not visible ++ await page.click( 'a.delete_refund', { ++ force: true, ++ waitForLoadState: 'networkidle', ++ } ); // have to force it because not visible + + // Verify the refunded row item is no longer showing + await expect( page.locator( 'tr.refund' ) ).toHaveCount( 0 ); +@@ -200,7 +205,7 @@ test.describe( 'WooCommerce Orders > Refund and restock an order item', () => { + await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` ); + + // Verify stock reduction system note was added +- await expect( page.locator( '.system-note >> nth=1' ) ).toHaveText( ++ await expect( page.locator( '.system-note >> nth=1' ) ).toContainText( + /Stock levels reduced: Product with stock \(#\d+\) 10→8/ + ); + +@@ -217,16 +222,18 @@ test.describe( 'WooCommerce Orders > Refund and restock an order item', () => { + await page.fill( '.refund_order_item_qty >> nth=1', '2' ); + await page.fill( '#refund_reason', 'No longer wanted' ); + page.on( 'dialog', ( dialog ) => dialog.accept() ); +- await page.click( '.do-manual-refund' ); ++ await page.click( '.do-manual-refund', { ++ waitForLoadState: 'networkidle', ++ } ); + + // Verify restock system note was added +- await expect( page.locator( '.system-note >> nth=0' ) ).toHaveText( ++ await expect( page.locator( '.system-note >> nth=0' ) ).toContainText( + /Item #\d+ stock increased from 8 to 10./ + ); + + // Update the order + await page.click( 'button.save_order' ); +- await expect( page.locator( 'div.notice-success' ) ).toHaveText( ++ await expect( page.locator( 'div.notice-success' ) ).toContainText( + 'Order updated.' + ); + +diff --git a/plugins/woocommerce/e2e/tests/merchant/order-search.spec.js b/plugins/woocommerce/e2e/tests/merchant/order-search.spec.js +index 7621d1baf7..0f767e2138 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/order-search.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/order-search.spec.js +@@ -1,11 +1,11 @@ + const { test, expect } = require( '@playwright/test' ); + const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +-const searchString = 'John Doe'; ++const searchString = 'James Doe'; + const itemName = 'Wanted Product'; + + const customerBilling = { +- first_name: 'John', ++ first_name: 'James', + last_name: 'Doe', + company: 'Automattic', + country: 'US', +@@ -15,7 +15,7 @@ const customerBilling = { + state: 'CA', + postcode: '94107', + phone: '123456789', +- email: 'john.doe@example.com', ++ email: 'john.doe.ordersearch@example.com', + }; + const customerShipping = { + first_name: 'Tim', +@@ -28,7 +28,7 @@ const customerShipping = { + state: 'NY', + postcode: '14201', + phone: '123456789', +- email: 'john.doe@example.com', ++ email: 'john.doe.ordersearch@example.com', + }; + + const queries = [ +@@ -76,9 +76,9 @@ test.describe( 'WooCommerce Orders > Search orders', () => { + // update customer info + await api + .post( 'customers', { +- email: 'john.doe@example.com', +- first_name: 'John', +- last_name: 'Doe', ++ email: customerBilling.email, ++ first_name: customerBilling.first_name, ++ last_name: customerBilling.last_name, + username: 'john.doe', + billing: customerBilling, + shipping: customerShipping, +@@ -134,8 +134,9 @@ test.describe( 'WooCommerce Orders > Search orders', () => { + await page.fill( '#post-search-input', queries[ i ][ 0 ] ); + await page.click( '#search-submit' ); + ++ // always check the last item, in case of multiples + await expect( +- page.locator( '.order_number > a.order-view' ) ++ page.locator( '.order_number > a.order-view >> nth=-1' ) + ).toContainText( `#${ orderId } ${ searchString }` ); + } ); + } +diff --git a/plugins/woocommerce/e2e/tests/merchant/order-status-filter.spec.js b/plugins/woocommerce/e2e/tests/merchant/order-status-filter.spec.js +index 4a998e5fbe..3a1169fc71 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/order-status-filter.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/order-status-filter.spec.js +@@ -56,13 +56,12 @@ test.describe( 'WooCommerce Orders > Filter Order by Status', () => { + await page.goto( 'wp-admin/edit.php?post_type=shop_order' ); + + await page.click( 'li.all > a' ); +- await page.click( 'th#order_number > a' ); // ensure we're sorting in the right order +- let i = 0; +- for ( const [ statusText ] of orderStatus ) { +- await expect( +- page.locator( `${ statusColumnTextSelector } >> nth=${ i }` ) +- ).toContainText( statusText ); +- i++; ++ // because tests are running in parallel, we can't know how many orders there ++ // are beyond the ones we created here. ++ for ( let i = 0; i < orderStatus.length; i++ ) { ++ const statusTag = 'text=' + orderStatus[ i ][ 0 ]; ++ const countElements = await page.locator( statusTag ).count(); ++ await expect( countElements ).toBeGreaterThan( 0 ); + } + } ); + +@@ -73,9 +72,10 @@ test.describe( 'WooCommerce Orders > Filter Order by Status', () => { + await page.goto( 'wp-admin/edit.php?post_type=shop_order' ); + + await page.click( `li.${ orderStatus[ i ][ 1 ] }` ); +- await expect( +- page.locator( statusColumnTextSelector ) +- ).toContainText( orderStatus[ i ][ 0 ] ); ++ const countElements = await page ++ .locator( statusColumnTextSelector ) ++ .count(); ++ await expect( countElements ).toBeGreaterThan( 0 ); + } ); + } + } ); +diff --git a/plugins/woocommerce/e2e/tests/merchant/page-loads.spec.js b/plugins/woocommerce/e2e/tests/merchant/page-loads.spec.js +index d350bbc8b1..a62e80aa21 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/page-loads.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/page-loads.spec.js +@@ -67,7 +67,8 @@ for ( const currentPage of wcPages ) { + await page.goto( 'wp-admin/admin.php?page=wc-admin' ); + } + await page.click( +- `li.wp-menu-open > ul.wp-submenu > li:has-text("${ currentPage.subpages[ i ].name }")` ++ `li.wp-menu-open > ul.wp-submenu > li:has-text("${ currentPage.subpages[ i ].name }")`, ++ { waitForLoadState: 'networkidle' } + ); + + await expect( +diff --git a/plugins/woocommerce/e2e/tests/merchant/product-import-csv.spec.js b/plugins/woocommerce/e2e/tests/merchant/product-import-csv.spec.js +index 9f2ace2653..8dd49d50dc 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/product-import-csv.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/product-import-csv.spec.js +@@ -7,46 +7,47 @@ const filePathOverride = path.resolve( + ); + + const productIds = []; ++const categoryIds = []; + + const productNames = [ +- 'V-Neck T-Shirt', +- 'Hoodie', +- 'Hoodie with Logo', +- 'T-Shirt', +- 'Beanie', +- 'Belt', +- 'Cap', +- 'Sunglasses', +- 'Hoodie with Pocket', +- 'Hoodie with Zipper', +- 'Long Sleeve Tee', +- 'Polo', +- 'Album', +- 'Single', +- 'T-Shirt with Logo', +- 'Beanie with Logo', +- 'Logo Collection', +- 'WordPress Pennant', ++ 'Imported V-Neck T-Shirt', ++ 'Imported Hoodie', ++ 'Imported Hoodie with Logo', ++ 'Imported T-Shirt', ++ 'Imported Beanie', ++ 'Imported Belt', ++ 'Imported Cap', ++ 'Imported Sunglasses', ++ 'Imported Hoodie with Pocket', ++ 'Imported Hoodie with Zipper', ++ 'Imported Long Sleeve Tee', ++ 'Imported Polo', ++ 'Imported Album', ++ 'Imported Single', ++ 'Imported T-Shirt with Logo', ++ 'Imported Beanie with Logo', ++ 'Imported Logo Collection', ++ 'Imported WordPress Pennant', + ]; + const productNamesOverride = [ +- 'V-Neck T-Shirt Override', +- 'Hoodie Override', +- 'Hoodie with Logo Override', +- 'T-Shirt Override', +- 'Beanie Override', +- 'Belt Override', +- 'Cap Override', +- 'Sunglasses Override', +- 'Hoodie with Pocket Override', +- 'Hoodie with Zipper Override', +- 'Long Sleeve Tee Override', +- 'Polo Override', +- 'Album Override', +- 'Single Override', +- 'T-Shirt with Logo Override', +- 'Beanie with Logo Override', +- 'Logo Collection Override', +- 'WordPress Pennant Override', ++ 'Imported V-Neck T-Shirt Override', ++ 'Imported Hoodie Override', ++ 'Imported Hoodie with Logo Override', ++ 'Imported T-Shirt Override', ++ 'Imported Beanie Override', ++ 'Imported Belt Override', ++ 'Imported Cap Override', ++ 'Imported Sunglasses Override', ++ 'Imported Hoodie with Pocket Override', ++ 'Imported Hoodie with Zipper Override', ++ 'Imported Long Sleeve Tee Override', ++ 'Imported Polo Override', ++ 'Imported Album Override', ++ 'Imported Single Override', ++ 'Imported T-Shirt with Logo Override', ++ 'Imported Beanie with Logo Override', ++ 'Imported Logo Collection Override', ++ 'Imported WordPress Pennant Override', + ]; + const productPricesOverride = [ + '$111.05', +@@ -77,6 +78,15 @@ const productPricesOverride = [ + '$115.00', + '$120.00', + ]; ++const productCategories = [ ++ 'Clothing', ++ 'Hoodies', ++ 'Tshirts', ++ 'Accessories', ++ 'Music', ++ 'Decor', ++]; ++ + const errorMessage = + 'Invalid file type. The importer supports CSV and TXT file formats.'; + +@@ -91,7 +101,7 @@ test.describe( 'Import Products from a CSV file', () => { + version: 'wc/v3', + } ); + // get a list of all products +- await api.get( 'products?per_page=20' ).then( ( response ) => { ++ await api.get( 'products?per_page=50' ).then( ( response ) => { + for ( let i = 0; i < response.data.length; i++ ) { + // if the product is one we imported, add it to the array + for ( let j = 0; j < productNamesOverride.length; j++ ) { +@@ -105,6 +115,21 @@ test.describe( 'Import Products from a CSV file', () => { + } ); + // batch delete all products in the array + await api.post( 'products/batch', { delete: [ ...productIds ] } ); ++ // get a list of all product categories ++ await api.get( 'products/categories' ).then( ( response ) => { ++ for ( let i = 0; i < response.data.length; i++ ) { ++ // if the product category is one that was created, add it to the array ++ for ( let j = 0; j < productCategories.length; j++ ) { ++ if ( response.data[ i ].name === productCategories[ j ] ) { ++ categoryIds.push( response.data[ i ].id ); ++ } ++ } ++ } ++ } ); ++ // batch delete all categories in the array ++ await api.post( 'products/categories/batch', { ++ delete: [ ...categoryIds ], ++ } ); + } ); + + test( 'should show error message if you go without providing CSV file', async ( { +@@ -145,8 +170,15 @@ test.describe( 'Import Products from a CSV file', () => { + // View the products + await page.click( 'text=View products' ); + ++ // Search for "import" to narrow the results to just the products we imported ++ await page.fill( '#post-search-input', 'Imported' ); ++ await page.click( '#search-submit' ); ++ + // Compare imported products to what's expected +- await page.waitForSelector( 'a.row-title' ); ++ await page.waitForSelector( 'a.row-title', { ++ state: 'visible', ++ timeout: 120000, // search can take a while ++ } ); + const productTitles = await page.$$eval( 'a.row-title', ( elements ) => + elements.map( ( item ) => item.innerHTML ) + ); +@@ -176,11 +208,15 @@ test.describe( 'Import Products from a CSV file', () => { + // Confirm that the import is done + await expect( + page.locator( '.woocommerce-importer-done' ) +- ).toContainText( 'Import complete!', { timeout: 120000 } ); ++ ).toContainText( 'Import complete!', { timeout: 120000 } ); // import can take a while + + // View the products + await page.click( 'text=View products' ); + ++ // Search for "import" to narrow the results to just the products we imported ++ await page.fill( '#post-search-input', 'Imported' ); ++ await page.click( '#search-submit' ); ++ + // Compare imported products to what's expected + await page.waitForSelector( 'a.row-title' ); + const productTitles = await page.$$eval( 'a.row-title', ( elements ) => +diff --git a/plugins/woocommerce/e2e/tests/merchant/product-search.spec.js b/plugins/woocommerce/e2e/tests/merchant/product-search.spec.js +index bebabdc3e6..1a495b0e05 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/product-search.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/product-search.spec.js +@@ -2,7 +2,7 @@ const { test, expect } = require( '@playwright/test' ); + const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + + let productId; +-const productName = 'Simple product to search'; ++const productName = 'Unique thing that we sell'; + const productPrice = '9.99'; + + test.describe( 'Products > Search and View a product', () => { +@@ -46,6 +46,7 @@ test.describe( 'Products > Search and View a product', () => { + + await page.fill( '#post-search-input', searchString ); + await page.click( '#search-submit' ); ++ await page.waitForLoadState( 'networkidle' ); + + await expect( page.locator( '.row-title' ) ).toContainText( + productName +diff --git a/plugins/woocommerce/e2e/tests/merchant/product-settings.spec.js b/plugins/woocommerce/e2e/tests/merchant/product-settings.spec.js +index 5d9623c300..3ba8b53c4b 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/product-settings.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/product-settings.spec.js +@@ -28,7 +28,7 @@ test.describe( 'WooCommerce Products > Downloadable Product Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +@@ -64,7 +64,7 @@ test.describe( 'WooCommerce Products > Downloadable Product Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +@@ -89,7 +89,7 @@ test.describe( 'WooCommerce Products > Downloadable Product Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +diff --git a/plugins/woocommerce/e2e/tests/merchant/settings-general.spec.js b/plugins/woocommerce/e2e/tests/merchant/settings-general.spec.js +index 2d330b06a4..46dd0d2a93 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/settings-general.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/settings-general.spec.js +@@ -30,7 +30,7 @@ test.describe( 'WooCommerce General Settings', () => { + await page.click( 'text=Save changes' ); + + // confirm setting saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +@@ -45,7 +45,7 @@ test.describe( 'WooCommerce General Settings', () => { + await page.click( 'text=Save changes' ); + + // verify the settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +@@ -68,7 +68,7 @@ test.describe( 'WooCommerce General Settings', () => { + await page.click( 'text=Save changes' ); + + // verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +diff --git a/plugins/woocommerce/e2e/tests/merchant/settings-tax.spec.js b/plugins/woocommerce/e2e/tests/merchant/settings-tax.spec.js +index a4b929b3a1..c9311aea6a 100644 +--- a/plugins/woocommerce/e2e/tests/merchant/settings-tax.spec.js ++++ b/plugins/woocommerce/e2e/tests/merchant/settings-tax.spec.js +@@ -1,6 +1,7 @@ + const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; + +-test.describe( 'WooCommerce Tax Settings', () => { ++test.describe( 'WooCommerce Tax Settings > enable', () => { + test.use( { storageState: 'e2e/storage/adminState.json' } ); + + test( 'can enable tax calculation', async ( { page } ) => { +@@ -16,7 +17,7 @@ test.describe( 'WooCommerce Tax Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( page.locator( '#woocommerce_calc_taxes' ) ).toBeChecked(); +@@ -26,6 +27,33 @@ test.describe( 'WooCommerce Tax Settings', () => { + page.locator( 'a.nav-tab:has-text("Tax")' ) + ).toBeVisible(); + } ); ++} ); ++ ++test.describe( 'WooCommerce Tax Settings', () => { ++ test.use( { storageState: 'e2e/storage/adminState.json' } ); ++ ++ test.beforeEach( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.put( 'settings/general/woocommerce_calc_taxes', { ++ value: 'yes', ++ } ); ++ } ); ++ test.afterEach( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.put( 'settings/general/woocommerce_calc_taxes', { ++ value: 'no', ++ } ); ++ } ); + + test( 'can set tax options', async ( { page } ) => { + await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=tax' ); +@@ -53,7 +81,7 @@ test.describe( 'WooCommerce Tax Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +@@ -88,7 +116,7 @@ test.describe( 'WooCommerce Tax Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that the settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( page.locator( '#woocommerce_tax_classes' ) ).toHaveValue( +@@ -100,7 +128,7 @@ test.describe( 'WooCommerce Tax Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that the settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( +@@ -178,7 +206,7 @@ test.describe( 'WooCommerce Tax Settings', () => { + await page.click( 'text=Save changes' ); + + // Verify that settings have been saved +- await expect( page.locator( 'div.inline' ) ).toContainText( ++ await expect( page.locator( 'div.updated.inline' ) ).toContainText( + 'Your settings have been saved.' + ); + await expect( page.locator( '#woocommerce_tax_classes' ) ).toHaveValue( +diff --git a/plugins/woocommerce/e2e/tests/shopper/calculate-shipping.spec.js b/plugins/woocommerce/e2e/tests/shopper/calculate-shipping.spec.js +new file mode 100644 +index 0000000000..dbbccb748b +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/calculate-shipping.spec.js +@@ -0,0 +1,220 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const firstProductName = 'First Product'; ++const firstProductPrice = '9.99'; ++const secondProductName = 'Second Product'; ++const secondProductPrice = '4.99'; ++const fourProductsTotal = +firstProductPrice * 4; ++const twoProductsTotal = +firstProductPrice + +secondProductPrice; ++const firstProductWithFlatRate = +firstProductPrice + 5; ++const fourProductsWithFlatRate = +fourProductsTotal + 5; ++const twoProductsWithFlatRate = +twoProductsTotal + 5; ++ ++const shippingZoneNameDE = 'Germany Free Shipping'; ++const shippingCountryDE = 'DE'; ++const shippingZoneNameFR = 'France Flat Local'; ++const shippingCountryFR = 'FR'; ++ ++test.describe( 'Cart Calculate Shipping', () => { ++ let firstProductId, secondProductId, shippingZoneDEId, shippingZoneFRId; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add products ++ await api ++ .post( 'products', { ++ name: firstProductName, ++ type: 'simple', ++ regular_price: firstProductPrice, ++ } ) ++ .then( ( response ) => { ++ firstProductId = response.data.id; ++ } ); ++ await api ++ .post( 'products', { ++ name: secondProductName, ++ type: 'simple', ++ regular_price: secondProductPrice, ++ } ) ++ .then( ( response ) => { ++ secondProductId = response.data.id; ++ } ); ++ // create shipping zones ++ await api ++ .post( 'shipping/zones', { ++ name: shippingZoneNameDE, ++ } ) ++ .then( ( response ) => { ++ shippingZoneDEId = response.data.id; ++ } ); ++ await api ++ .post( 'shipping/zones', { ++ name: shippingZoneNameFR, ++ } ) ++ .then( ( response ) => { ++ shippingZoneFRId = response.data.id; ++ } ); ++ // set shipping zone locations ++ await api.put( `shipping/zones/${ shippingZoneDEId }/locations`, [ ++ { ++ code: shippingCountryDE, ++ }, ++ ] ); ++ await api.put( `shipping/zones/${ shippingZoneFRId }/locations`, [ ++ { ++ code: shippingCountryFR, ++ }, ++ ] ); ++ // set shipping zone methods ++ await api.post( `shipping/zones/${ shippingZoneDEId }/methods`, { ++ method_id: 'free_shipping', ++ } ); ++ await api.post( `shipping/zones/${ shippingZoneFRId }/methods`, { ++ method_id: 'flat_rate', ++ settings: { ++ cost: '5.00', ++ }, ++ } ); ++ await api.post( `shipping/zones/${ shippingZoneFRId }/methods`, { ++ method_id: 'local_pickup', ++ } ); ++ // confirm that we allow shipping to any country ++ api.put( 'settings/general/woocommerce_allowed_countries', { ++ value: 'all', ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { page, context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ ++ // all tests use the first product ++ await page.goto( `/shop/?add-to-cart=${ firstProductId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ firstProductId }`, { ++ force: true, ++ } ); ++ await api.delete( `products/${ secondProductId }`, { ++ force: true, ++ } ); ++ await api.delete( `shipping/zones/${ shippingZoneDEId }`, { ++ force: true, ++ } ); ++ await api.delete( `shipping/zones/${ shippingZoneFRId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test( 'allows customer to calculate Free Shipping if in Germany', async ( { ++ page, ++ } ) => { ++ await page.goto( '/cart/' ); ++ // Set shipping country to Germany ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', shippingCountryDE ); ++ await page.click( 'button[name="calc_shipping"]' ); ++ ++ // Verify shipping costs ++ await expect( ++ page.locator( '.shipping ul#shipping_method > li' ) ++ ).toContainText( 'Free shipping' ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ firstProductPrice ++ ); ++ } ); ++ ++ test( 'allows customer to calculate Flat rate and Local pickup if in France', async ( { ++ page, ++ } ) => { ++ await page.goto( '/cart/' ); ++ // Set shipping country to France ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', shippingCountryFR ); ++ await page.click( 'button[name="calc_shipping"]' ); ++ ++ // Verify shipping costs ++ await expect( page.locator( '.shipping .amount' ) ).toContainText( ++ '$5.00' ++ ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ firstProductWithFlatRate }` ++ ); ++ ++ // Set shipping to local pickup instead of flat rate ++ await page.click( 'text=Local pickup' ); ++ ++ // Verify updated shipping costs ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ firstProductPrice }` ++ ); ++ } ); ++ ++ test( 'should show correct total cart price after updating quantity', async ( { ++ page, ++ } ) => { ++ await page.goto( '/cart/' ); ++ await page.fill( 'input.qty', '4' ); ++ await page.click( 'text=Update cart' ); ++ ++ // Set shipping country to France ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', shippingCountryFR ); ++ await page.click( 'button[name="calc_shipping"]' ); ++ ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ fourProductsWithFlatRate }` ++ ); ++ } ); ++ ++ test( 'should show correct total cart price with 2 products and flat rate', async ( { ++ page, ++ } ) => { ++ await page.goto( `/shop/?add-to-cart=${ secondProductId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/cart/' ); ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', shippingCountryFR ); ++ await page.click( 'button[name="calc_shipping"]' ); ++ ++ await expect( page.locator( '.shipping .amount' ) ).toContainText( ++ '$5.00' ++ ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ twoProductsWithFlatRate }` ++ ); ++ } ); ++ ++ test( 'should show correct total cart price with 2 products without flat rate', async ( { ++ page, ++ } ) => { ++ await page.goto( `/shop/?add-to-cart=${ secondProductId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ // Set shipping country to Spain ++ await page.goto( '/cart/' ); ++ await page.click( 'a.shipping-calculator-button' ); ++ await page.selectOption( '#calc_shipping_country', 'ES' ); ++ await page.click( 'button[name="calc_shipping"]' ); ++ ++ // Verify shipping costs ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ twoProductsTotal }` ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/cart-coupons.spec.js b/plugins/woocommerce/e2e/tests/shopper/cart-coupons.spec.js +new file mode 100644 +index 0000000000..e1863afcaa +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/cart-coupons.spec.js +@@ -0,0 +1,177 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const firstProductName = 'Coupon test product'; ++const coupons = [ ++ { ++ code: 'fixed-cart-off', ++ discount_type: 'fixed_cart', ++ amount: '5.00', ++ }, ++ { ++ code: 'percent-off', ++ discount_type: 'percent', ++ amount: '50', ++ }, ++ { ++ code: 'fixed-product-off', ++ discount_type: 'fixed_product', ++ amount: '7.00', ++ }, ++]; ++const discounts = [ '$5.00', '$10.00', '$7.00' ]; ++const totals = [ '$15.00', '$10.00', '$13.00' ]; ++ ++test.describe( 'Cart applying coupons', () => { ++ let firstProductId; ++ const couponBatchId = new Array(); ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: firstProductName, ++ type: 'simple', ++ regular_price: '20.00', ++ } ) ++ .then( ( response ) => { ++ firstProductId = response.data.id; ++ } ); ++ // add coupons ++ await api ++ .post( 'coupons/batch', { ++ create: coupons, ++ } ) ++ .then( ( response ) => { ++ for ( let i = 0; i < response.data.create.length; i++ ) { ++ couponBatchId.push( response.data.create[ i ].id ); ++ } ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { page, context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ ++ // all tests use the first product ++ await page.goto( `/shop/?add-to-cart=${ firstProductId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ firstProductId }`, { ++ force: true, ++ } ); ++ await api.post( 'coupons/batch', { delete: [ ...couponBatchId ] } ); ++ } ); ++ ++ for ( let i = 0; i < coupons.length; i++ ) { ++ test( `allows cart to apply coupon of type ${ coupons[ i ].discount_type }`, async ( { ++ page, ++ } ) => { ++ await page.goto( '/cart/' ); ++ await page.fill( '#coupon_code', coupons[ i ].code ); ++ await page.click( 'text=Apply coupon' ); ++ ++ await expect( ++ page.locator( '.woocommerce-message' ) ++ ).toContainText( 'Coupon code applied successfully.' ); ++ // Checks the coupon amount is credited properly ++ await expect( ++ page.locator( '.cart-discount .amount' ) ++ ).toContainText( discounts[ i ] ); ++ // Checks that the cart total is updated ++ await expect( ++ page.locator( '.order-total .amount' ) ++ ).toContainText( totals[ i ] ); ++ } ); ++ } ++ ++ test( 'prevents cart applying same coupon twice', async ( { page } ) => { ++ await page.goto( '/cart/' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // successful first time ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'Coupon code applied successfully.' ++ ); ++ await page.waitForLoadState( 'networkidle' ); ++ // try to apply the same coupon ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // error received ++ await expect( page.locator( '.woocommerce-error' ) ).toContainText( ++ 'Coupon code already applied!' ++ ); ++ // check cart total ++ await expect( page.locator( '.cart-discount .amount' ) ).toContainText( ++ discounts[ 0 ] ++ ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ totals[ 0 ] ++ ); ++ } ); ++ ++ test( 'allows cart to apply multiple coupons', async ( { page } ) => { ++ await page.goto( '/cart/' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // successful ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'Coupon code applied successfully.' ++ ); ++ ++ await page.waitForLoadState( 'networkidle' ); ++ await page.fill( '#coupon_code', coupons[ 2 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // successful ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'Coupon code applied successfully.' ++ ); ++ // check cart total ++ await expect( ++ page.locator( '.cart-discount .amount >> nth=0' ) ++ ).toContainText( discounts[ 0 ] ); ++ await expect( ++ page.locator( '.cart-discount .amount >> nth=1' ) ++ ).toContainText( discounts[ 2 ] ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ '$8.00' ++ ); ++ } ); ++ ++ test( 'restores cart total when coupons are removed', async ( { ++ page, ++ } ) => { ++ await page.goto( '/cart/' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ ++ // confirm numbers ++ await expect( page.locator( '.cart-discount .amount' ) ).toContainText( ++ discounts[ 0 ] ++ ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ totals[ 0 ] ++ ); ++ ++ await page.click( 'a.woocommerce-remove-coupon' ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ '$20.00' ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/cart-redirection.spec.js b/plugins/woocommerce/e2e/tests/shopper/cart-redirection.spec.js +new file mode 100644 +index 0000000000..668a186431 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/cart-redirection.spec.js +@@ -0,0 +1,79 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++test.describe( 'Cart > Redirect to cart from shop', () => { ++ let productId; ++ const productName = 'A redirect product test'; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add products ++ await api ++ .post( 'products', { ++ name: productName, ++ type: 'simple', ++ regular_price: '17.99', ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ await api.put( ++ 'settings/products/woocommerce_cart_redirect_after_add', ++ { ++ value: 'yes', ++ } ++ ); ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ await api.put( ++ 'settings/products/woocommerce_cart_redirect_after_add', ++ { ++ value: 'no', ++ } ++ ); ++ } ); ++ ++ test( 'can redirect user to cart from shop page', async ( { page } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await expect( page.url() ).toContain( '/cart/' ); ++ await expect( page.locator( 'td.product-name' ) ).toContainText( ++ productName ++ ); ++ } ); ++ ++ test( 'can redirect user to cart from detail page', async ( { page } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `text=${ productName }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.click( 'text=Add to cart' ); ++ ++ await expect( page.url() ).toContain( '/cart/' ); ++ await expect( page.locator( 'td.product-name' ) ).toContainText( ++ productName ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/cart.spec.js b/plugins/woocommerce/e2e/tests/shopper/cart.spec.js +new file mode 100644 +index 0000000000..515b67bf5a +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/cart.spec.js +@@ -0,0 +1,152 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const productName = 'Cart product test'; ++const productPrice = '13.99'; ++const twoProductPrice = +productPrice * 2; ++ ++test.describe( 'Cart page', () => { ++ let productId; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add products ++ await api ++ .post( 'products', { ++ name: productName, ++ type: 'simple', ++ regular_price: productPrice, ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test( 'should display no item in the cart', async ( { page } ) => { ++ await page.goto( '/cart/' ); ++ await expect( page.locator( '.cart-empty' ) ).toContainText( ++ 'Your cart is currently empty.' ++ ); ++ } ); ++ ++ test( 'should add the product to the cart from the shop page', async ( { ++ page, ++ } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/cart/' ); ++ await expect( page.locator( 'td.product-name' ) ).toContainText( ++ productName ++ ); ++ } ); ++ ++ test( 'should increase item quantity when "Add to cart" of the same product is clicked', async ( { ++ page, ++ } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ // Once the view cart link is visible, item has been added ++ await page.waitForLoadState( 'networkidle' ); ++ // Click add to cart a second time (load the shop in case redirection enabled) ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/cart/' ); ++ await expect( page.locator( 'input.qty' ) ).toHaveValue( '2' ); ++ } ); ++ ++ test( 'should update quantity when updated via quantity input', async ( { ++ page, ++ } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/cart/' ); ++ await page.fill( 'input.qty', '2' ); ++ await page.click( 'text=Update cart' ); ++ ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ twoProductPrice }` ++ ); ++ } ); ++ ++ test( 'should remove the item from the cart when remove is clicked', async ( { ++ page, ++ } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ await page.goto( '/cart/' ); ++ ++ // make sure that the product is in the cart ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ productPrice }` ++ ); ++ ++ await page.click( 'a.remove' ); ++ ++ await expect( page.locator( 'p.woocommerce-info' ) ).toContainText( ++ 'Your cart is currently empty.' ++ ); ++ } ); ++ ++ test( 'should update subtotal in cart totals when adding product to the cart', async ( { ++ page, ++ } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/cart/' ); ++ await expect( page.locator( '.cart-subtotal .amount' ) ).toContainText( ++ `$${ productPrice }` ++ ); ++ ++ await page.fill( 'input.qty', '2' ); ++ await page.click( 'text=Update cart' ); ++ ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ `$${ twoProductPrice }` ++ ); ++ } ); ++ ++ test( 'should go to the checkout page when "Proceed to Checkout" is clicked', async ( { ++ page, ++ } ) => { ++ await page.goto( '/shop/' ); ++ await page.click( `a:below(:text("${ productName }"))` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/cart/' ); ++ ++ await page.click( '.checkout-button' ); ++ ++ await expect( page.locator( '#order_review' ) ).toBeVisible(); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/checkout-coupons.spec.js b/plugins/woocommerce/e2e/tests/shopper/checkout-coupons.spec.js +new file mode 100644 +index 0000000000..9455d6ee32 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/checkout-coupons.spec.js +@@ -0,0 +1,179 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const firstProductName = 'Coupon checkout test product'; ++const coupons = [ ++ { ++ code: 'fixed-cart-off-checkout', ++ discount_type: 'fixed_cart', ++ amount: '5.00', ++ }, ++ { ++ code: 'percent-off-checkout', ++ discount_type: 'percent', ++ amount: '50', ++ }, ++ { ++ code: 'fixed-product-off-checkout', ++ discount_type: 'fixed_product', ++ amount: '7.00', ++ }, ++]; ++const discounts = [ '$5.00', '$10.00', '$7.00' ]; ++const totals = [ '$15.00', '$10.00', '$13.00' ]; ++ ++test.describe( 'Checkout coupons', () => { ++ let firstProductId; ++ const couponBatchId = new Array(); ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: firstProductName, ++ type: 'simple', ++ regular_price: '20.00', ++ } ) ++ .then( ( response ) => { ++ firstProductId = response.data.id; ++ } ); ++ // add coupons ++ await api ++ .post( 'coupons/batch', { ++ create: coupons, ++ } ) ++ .then( ( response ) => { ++ for ( let i = 0; i < response.data.create.length; i++ ) { ++ couponBatchId.push( response.data.create[ i ].id ); ++ } ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { page, context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ ++ // all tests use the first product ++ await page.goto( `/shop/?add-to-cart=${ firstProductId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ firstProductId }`, { ++ force: true, ++ } ); ++ await api.post( 'coupons/batch', { delete: [ ...couponBatchId ] } ); ++ } ); ++ ++ for ( let i = 0; i < coupons.length; i++ ) { ++ test( `allows checkout to apply coupon of type ${ coupons[ i ].discount_type }`, async ( { ++ page, ++ } ) => { ++ await page.goto( '/checkout/' ); ++ await page.click( 'text=Click here to enter your code' ); ++ await page.fill( '#coupon_code', coupons[ i ].code ); ++ await page.click( 'text=Apply coupon' ); ++ ++ await expect( ++ page.locator( '.woocommerce-message' ) ++ ).toContainText( 'Coupon code applied successfully.' ); ++ await expect( ++ page.locator( '.cart-discount .amount' ) ++ ).toContainText( discounts[ i ] ); ++ await expect( ++ page.locator( '.order-total .amount' ) ++ ).toContainText( totals[ i ] ); ++ } ); ++ } ++ ++ test( 'prevents checkout applying same coupon twice', async ( { ++ page, ++ } ) => { ++ await page.goto( '/checkout/' ); ++ await page.click( 'text=Click here to enter your code' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // successful first time ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'Coupon code applied successfully.' ++ ); ++ // try to apply the same coupon ++ await page.click( 'text=Click here to enter your code' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // error received ++ await expect( page.locator( '.woocommerce-error' ) ).toContainText( ++ 'Coupon code already applied!' ++ ); ++ // check cart total ++ await expect( page.locator( '.cart-discount .amount' ) ).toContainText( ++ discounts[ 0 ] ++ ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ totals[ 0 ] ++ ); ++ } ); ++ ++ test( 'allows checkout to apply multiple coupons', async ( { page } ) => { ++ await page.goto( '/checkout/' ); ++ await page.click( 'text=Click here to enter your code' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // successful ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'Coupon code applied successfully.' ++ ); ++ await page.click( 'text=Click here to enter your code' ); ++ await page.fill( '#coupon_code', coupons[ 2 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ // successful ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'Coupon code applied successfully.' ++ ); ++ // check cart total ++ await expect( ++ page.locator( '.cart-discount .amount >> nth=0' ) ++ ).toContainText( discounts[ 0 ] ); ++ await expect( ++ page.locator( '.cart-discount .amount >> nth=1' ) ++ ).toContainText( discounts[ 2 ] ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ '$8.00' ++ ); ++ } ); ++ ++ test( 'restores checkout total when coupons are removed', async ( { ++ page, ++ } ) => { ++ await page.goto( '/checkout/' ); ++ await page.click( 'text=Click here to enter your code' ); ++ await page.fill( '#coupon_code', coupons[ 0 ].code ); ++ await page.click( 'text=Apply coupon' ); ++ ++ // confirm numbers ++ await expect( page.locator( '.cart-discount .amount' ) ).toContainText( ++ discounts[ 0 ] ++ ); ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ totals[ 0 ] ++ ); ++ ++ await page.click( 'a.woocommerce-remove-coupon' ); ++ ++ await expect( page.locator( '.order-total .amount' ) ).toContainText( ++ '$20.00' ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/checkout-create-account.spec.js b/plugins/woocommerce/e2e/tests/shopper/checkout-create-account.spec.js +new file mode 100644 +index 0000000000..a82dac8d0c +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/checkout-create-account.spec.js +@@ -0,0 +1,146 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const billingEmail = 'marge-test-account@example.com'; ++ ++test.describe( 'Shopper Checkout Create Account', () => { ++ let productId, orderId, shippingZoneId; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: 'Checkout Create Account', ++ type: 'simple', ++ regular_price: '19.99', ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ await api.put( ++ 'settings/account/woocommerce_enable_signup_and_login_from_checkout', ++ { ++ value: 'yes', ++ } ++ ); ++ await api ++ .post( 'shipping/zones', { ++ name: 'Free Shipping CA', ++ } ) ++ .then( ( response ) => { ++ shippingZoneId = response.data.id; ++ } ); ++ await api.put( `shipping/zones/${ shippingZoneId }/locations`, [ ++ { ++ code: 'US:CA', ++ type: 'state', ++ }, ++ ] ); ++ await api.post( `shipping/zones/${ shippingZoneId }/methods`, { ++ method_id: 'free_shipping', ++ } ); ++ await api.put( 'payment_gateways/cod', { ++ enabled: true, ++ } ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ if ( orderId ) { ++ await api.delete( `orders/${ orderId }`, { ++ force: true, ++ } ); ++ } ++ await api.put( ++ 'settings/account/woocommerce_enable_signup_and_login_from_checkout', ++ { ++ value: 'no', ++ } ++ ); ++ await api.delete( `shipping/zones/${ shippingZoneId }`, { ++ force: true, ++ } ); ++ await api.put( 'payment_gateways/cod', { ++ enabled: false, ++ } ); ++ // clear out the customer we create during the test ++ await api.get( 'customers' ).then( ( response ) => { ++ for ( let i = 0; i < response.data.length; i++ ) { ++ if ( response.data[ i ].billing.email === billingEmail ) { ++ api.delete( `customers/${ response.data[ i ].id }`, { ++ force: true, ++ } ); ++ } ++ } ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { page, context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ ++ // all tests use the first product ++ await page.goto( `shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ); ++ ++ test( 'can create an account during checkout', async ( { page } ) => { ++ await page.goto( 'checkout/' ); ++ await page.fill( '#billing_first_name', 'Marge' ); ++ await page.fill( '#billing_last_name', 'Simpson' ); ++ await page.fill( '#billing_address_1', '742 Evergreen Terrace' ); ++ await page.fill( '#billing_address_2', 'c/o Maggie Simpson' ); ++ await page.fill( '#billing_city', 'Springfield' ); ++ await page.fill( '#billing_postcode', '97403' ); ++ await page.fill( '#billing_phone', '123456789' ); ++ await page.fill( '#billing_email', billingEmail ); ++ ++ await page.check( '#createaccount' ); ++ ++ await page.click( '#place_order' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'Order received' ++ ); ++ ++ // get order ID from the page ++ const orderReceivedHtmlElement = await page.$( ++ '.woocommerce-order-overview__order.order' ++ ); ++ const orderReceivedText = await page.evaluate( ++ ( element ) => element.textContent, ++ orderReceivedHtmlElement ++ ); ++ orderId = orderReceivedText.split( /(\s+)/ )[ 6 ].toString(); ++ ++ await page.goto( '/my-account/' ); ++ // confirms that an account was created ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'My account' ++ ); ++ await page.click( 'text=Logout' ); ++ // sign in as admin to confirm account creation ++ await page.fill( '#username', 'admin' ); ++ await page.fill( '#password', 'password' ); ++ await page.click( 'text=Log in' ); ++ ++ await page.goto( 'wp-admin/users.php' ); ++ await expect( page.locator( 'tbody#the-list' ) ).toContainText( ++ billingEmail ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/checkout-login.spec.js b/plugins/woocommerce/e2e/tests/shopper/checkout-login.spec.js +new file mode 100644 +index 0000000000..1bcadeedac +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/checkout-login.spec.js +@@ -0,0 +1,186 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const first_name = 'Jane'; ++const last_name = 'Smith'; ++const address_1 = '123 Anywhere St.'; ++const address_2 = 'Apartment 42'; ++const city = 'New York'; ++const state = 'NY'; ++const postcode = '10010'; ++const country = 'US'; ++const phone = '(555) 777-7777'; ++ ++test.describe( 'Shopper Checkout Login Account', () => { ++ let productId, orderId, shippingZoneId; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: 'Checkout Login Account', ++ type: 'simple', ++ regular_price: '19.99', ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ await api.put( ++ 'settings/account/woocommerce_enable_checkout_login_reminder', ++ { ++ value: 'yes', ++ } ++ ); ++ // add a shipping zone and method ++ await api ++ .post( 'shipping/zones', { ++ name: 'Free Shipping New York', ++ } ) ++ .then( ( response ) => { ++ shippingZoneId = response.data.id; ++ } ); ++ await api.put( `shipping/zones/${ shippingZoneId }/locations`, [ ++ { ++ code: 'US:NY', ++ type: 'state', ++ }, ++ ] ); ++ await api.post( `shipping/zones/${ shippingZoneId }/methods`, { ++ method_id: 'free_shipping', ++ } ); ++ // update customer billing details. ++ await api.put( 'customers/2', { ++ billing: { ++ first_name, ++ last_name, ++ address_1, ++ address_2, ++ city, ++ state, ++ postcode, ++ country, ++ phone, ++ }, ++ } ); ++ // enable a payment method ++ await api.put( 'payment_gateways/cod', { ++ enabled: true, ++ } ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ if ( orderId ) { ++ await api.delete( `orders/${ orderId }`, { force: true } ); ++ } ++ await api.put( ++ 'settings/account/woocommerce_enable_checkout_login_reminder', ++ { ++ value: 'no', ++ } ++ ); ++ // reset the customer account to how it was at the start ++ await api.put( 'customers/2', { ++ billing: { ++ address_1: '', ++ address_2: '', ++ city: '', ++ state: '', ++ postcode: '', ++ country: '', ++ phone: '', ++ }, ++ } ); ++ // disable payment method ++ await api.put( 'payment_gateways/cod', { ++ enabled: false, ++ } ); ++ // delete shipping ++ await api.delete( `shipping/zones/${ shippingZoneId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { page, context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ ++ // all tests use the first product ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ); ++ ++ test( 'can login to an existing account during checkout', async ( { ++ page, ++ } ) => { ++ await page.goto( '/checkout/' ); ++ await page.click( 'text=Click here to login' ); ++ ++ // fill in the customer account info ++ await page.fill( '#username', 'customer' ); ++ await page.fill( '#password', 'password' ); ++ await page.click( 'button[name="login"]' ); ++ ++ // billing form should pre-populate ++ await expect( page.locator( '#billing_first_name' ) ).toHaveValue( ++ first_name ++ ); ++ await expect( page.locator( '#billing_last_name' ) ).toHaveValue( ++ last_name ++ ); ++ await expect( page.locator( '#billing_address_1' ) ).toHaveValue( ++ address_1 ++ ); ++ await expect( page.locator( '#billing_address_2' ) ).toHaveValue( ++ address_2 ++ ); ++ await expect( page.locator( '#billing_city' ) ).toHaveValue( city ); ++ await expect( page.locator( '#billing_state' ) ).toHaveValue( state ); ++ await expect( page.locator( '#billing_postcode' ) ).toHaveValue( ++ postcode ++ ); ++ await expect( page.locator( '#billing_phone' ) ).toHaveValue( phone ); ++ ++ // place an order ++ await page.click( 'text=Place order' ); ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'Order received' ++ ); ++ ++ await page.waitForLoadState( 'networkidle' ); ++ // get order ID from the page ++ const orderReceivedHtmlElement = await page.$( ++ '.woocommerce-order-overview__order.order' ++ ); ++ const orderReceivedText = await page.evaluate( ++ ( element ) => element.textContent, ++ orderReceivedHtmlElement ++ ); ++ orderId = orderReceivedText.split( /(\s+)/ )[ 6 ].toString(); ++ ++ await expect( page.locator( 'ul > li.email' ) ).toContainText( ++ 'customer@woocommercecoree2etestsuite.com' ++ ); ++ ++ // check my account page ++ await page.goto( '/my-account/' ); ++ await expect( page.url() ).toContain( 'my-account/' ); ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'My account' ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/checkout.spec.js b/plugins/woocommerce/e2e/tests/shopper/checkout.spec.js +new file mode 100644 +index 0000000000..feb9a4bcf6 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/checkout.spec.js +@@ -0,0 +1,332 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const guestEmail = 'checkout-guest@example.com'; ++const customerEmail = 'checkout-customer@example.com'; ++ ++test.describe( 'Checkout page', () => { ++ const singleProductPrice = '9.99'; ++ const simpleProductName = 'Checkout Page Product'; ++ const twoProductPrice = ( singleProductPrice * 2 ).toString(); ++ const threeProductPrice = ( singleProductPrice * 3 ).toString(); ++ ++ let guestOrderId, customerOrderId, productId, shippingZoneId; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: simpleProductName, ++ type: 'simple', ++ regular_price: singleProductPrice, ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ // add a shipping zone and method ++ await api ++ .post( 'shipping/zones', { ++ name: 'Free Shipping Oregon', ++ } ) ++ .then( ( response ) => { ++ shippingZoneId = response.data.id; ++ } ); ++ await api.put( `shipping/zones/${ shippingZoneId }/locations`, [ ++ { ++ code: 'US:OR', ++ type: 'state', ++ }, ++ ] ); ++ await api.post( `shipping/zones/${ shippingZoneId }/methods`, { ++ method_id: 'free_shipping', ++ } ); ++ // enable bank transfers and COD for payment ++ await api.put( 'payment_gateways/bacs', { ++ enabled: true, ++ } ); ++ await api.put( 'payment_gateways/cod', { ++ enabled: true, ++ } ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ await api.delete( `shipping/zones/${ shippingZoneId }`, { ++ force: true, ++ } ); ++ await api.put( 'payment_gateways/bacs', { ++ enabled: false, ++ } ); ++ await api.put( 'payment_gateways/cod', { ++ enabled: false, ++ } ); ++ // delete the orders we created ++ if ( guestOrderId ) { ++ await api.delete( `orders/${ guestOrderId }`, { force: true } ); ++ } ++ if ( customerOrderId ) { ++ await api.delete( `orders/${ customerOrderId }`, { force: true } ); ++ } ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test( 'should display cart items in order review', async ( { page } ) => { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/checkout/' ); ++ ++ await expect( page.locator( 'td.product-name' ) ).toContainText( ++ simpleProductName ++ ); ++ await expect( page.locator( 'strong.product-quantity' ) ).toContainText( ++ '1' ++ ); ++ await expect( page.locator( 'td.product-total' ) ).toContainText( ++ singleProductPrice ++ ); ++ } ); ++ ++ test( 'allows customer to choose available payment methods', async ( { ++ page, ++ } ) => { ++ // this time we're going to add two products to the cart ++ for ( let i = 1; i < 3; i++ ) { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ++ ++ await page.goto( '/checkout/' ); ++ await expect( page.locator( 'strong.product-quantity' ) ).toContainText( ++ '2' ++ ); ++ await expect( page.locator( 'td.product-total' ) ).toContainText( ++ twoProductPrice ++ ); ++ ++ // check the payment methods ++ await expect( page.locator( '#payment_method_bacs' ) ).toBeEnabled(); ++ await expect( page.locator( '#payment_method_cod' ) ).toBeEnabled(); ++ } ); ++ ++ test( 'allows customer to fill billing details', async ( { page } ) => { ++ // this time we're going to add three products to the cart ++ for ( let i = 1; i < 4; i++ ) { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ++ ++ await page.goto( '/checkout/' ); ++ await expect( page.locator( 'strong.product-quantity' ) ).toContainText( ++ '3' ++ ); ++ await expect( page.locator( 'td.product-total' ) ).toContainText( ++ threeProductPrice ++ ); ++ ++ // asserting that you can fill in the billing details ++ await expect( page.locator( '#billing_first_name' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_last_name' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_company' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_country' ) ).toBeEnabled(); ++ await expect( page.locator( '#billing_address_1' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_address_2' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_city' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_state' ) ).toBeEnabled(); ++ await expect( page.locator( '#billing_postcode' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_phone' ) ).toBeEditable(); ++ await expect( page.locator( '#billing_email' ) ).toBeEditable(); ++ } ); ++ ++ test( 'allows customer to fill shipping details', async ( { page } ) => { ++ for ( let i = 1; i < 3; i++ ) { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ++ ++ await page.goto( '/checkout/' ); ++ await expect( page.locator( 'strong.product-quantity' ) ).toContainText( ++ '2' ++ ); ++ await expect( page.locator( 'td.product-total' ) ).toContainText( ++ twoProductPrice ++ ); ++ ++ await page.click( '#ship-to-different-address' ); ++ ++ // asserting that you can fill in the shipping details ++ await expect( page.locator( '#shipping_first_name' ) ).toBeEditable(); ++ await expect( page.locator( '#shipping_last_name' ) ).toBeEditable(); ++ await expect( page.locator( '#shipping_company' ) ).toBeEditable(); ++ await expect( page.locator( '#shipping_country' ) ).toBeEnabled(); ++ await expect( page.locator( '#shipping_address_1' ) ).toBeEditable(); ++ await expect( page.locator( '#shipping_address_2' ) ).toBeEditable(); ++ await expect( page.locator( '#shipping_city' ) ).toBeEditable(); ++ await expect( page.locator( '#shipping_state' ) ).toBeEnabled(); ++ await expect( page.locator( '#shipping_postcode' ) ).toBeEditable(); ++ } ); ++ ++ test( 'allows guest customer to place an order', async ( { page } ) => { ++ for ( let i = 1; i < 3; i++ ) { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ++ ++ await page.goto( '/checkout/' ); ++ await expect( page.locator( 'strong.product-quantity' ) ).toContainText( ++ '2' ++ ); ++ await expect( page.locator( 'td.product-total' ) ).toContainText( ++ twoProductPrice ++ ); ++ ++ await page.fill( '#billing_first_name', 'Lisa' ); ++ await page.fill( '#billing_last_name', 'Simpson' ); ++ await page.fill( '#billing_address_1', '123 Evergreen Terrace' ); ++ await page.fill( '#billing_city', 'Springfield' ); ++ await page.selectOption( '#billing_state', 'OR' ); ++ await page.fill( '#billing_postcode', '97403' ); ++ await page.fill( '#billing_phone', '555 555-5555' ); ++ await page.fill( '#billing_email', guestEmail ); ++ ++ await page.click( 'text=Cash on delivery' ); ++ await expect( page.locator( 'div.payment_method_cod' ) ).toBeVisible(); ++ ++ await page.click( 'text=Place order' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'Order received' ++ ); ++ ++ // get order ID from the page ++ const orderReceivedHtmlElement = await page.$( ++ '.woocommerce-order-overview__order.order' ++ ); ++ const orderReceivedText = await page.evaluate( ++ ( element ) => element.textContent, ++ orderReceivedHtmlElement ++ ); ++ guestOrderId = await orderReceivedText.split( /(\s+)/ )[ 6 ].toString(); ++ ++ await page.goto( 'wp-login.php' ); ++ await page.fill( 'input[name="log"]', 'admin' ); ++ await page.fill( 'input[name="pwd"]', 'password' ); ++ await page.click( 'text=Log In' ); ++ ++ // load the order placed as a guest ++ await page.goto( ++ `wp-admin/post.php?post=${ guestOrderId }&action=edit` ++ ); ++ ++ await expect( ++ page.locator( 'h2.woocommerce-order-data__heading' ) ++ ).toContainText( `Order #${ guestOrderId } details` ); ++ await expect( page.locator( '.wc-order-item-name' ) ).toContainText( ++ simpleProductName ++ ); ++ await expect( page.locator( 'td.quantity >> nth=0' ) ).toContainText( ++ '2' ++ ); ++ await expect( page.locator( 'td.item_cost >> nth=0' ) ).toContainText( ++ singleProductPrice ++ ); ++ await expect( page.locator( 'td.line_cost >> nth=0' ) ).toContainText( ++ twoProductPrice ++ ); ++ } ); ++ ++ test( 'allows existing customer to place order', async ( { page } ) => { ++ await page.goto( 'wp-admin/' ); ++ await page.fill( 'input[name="log"]', 'customer' ); ++ await page.fill( 'input[name="pwd"]', 'password' ); ++ await page.click( 'text=Log In' ); ++ await page.waitForLoadState( 'networkidle' ); ++ for ( let i = 1; i < 3; i++ ) { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ } ++ ++ await page.goto( '/checkout/' ); ++ await expect( page.locator( 'strong.product-quantity' ) ).toContainText( ++ '2' ++ ); ++ await expect( page.locator( 'td.product-total' ) ).toContainText( ++ twoProductPrice ++ ); ++ ++ await page.fill( '#billing_first_name', 'Homer' ); ++ await page.fill( '#billing_last_name', 'Simpson' ); ++ await page.fill( '#billing_address_1', '123 Evergreen Terrace' ); ++ await page.fill( '#billing_city', 'Springfield' ); ++ await page.selectOption( '#billing_state', 'OR' ); ++ await page.fill( '#billing_postcode', '97403' ); ++ await page.fill( '#billing_phone', '555 555-5555' ); ++ await page.fill( '#billing_email', customerEmail ); ++ ++ await page.click( 'text=Cash on delivery' ); ++ await expect( page.locator( 'div.payment_method_cod' ) ).toBeVisible(); ++ ++ await page.click( 'text=Place order' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'Order received' ++ ); ++ ++ // get order ID from the page ++ const orderReceivedHtmlElement = await page.$( ++ '.woocommerce-order-overview__order.order' ++ ); ++ const orderReceivedText = await page.evaluate( ++ ( element ) => element.textContent, ++ orderReceivedHtmlElement ++ ); ++ customerOrderId = await orderReceivedText ++ .split( /(\s+)/ )[ 6 ] ++ .toString(); ++ ++ await page.goto( 'wp-login.php?loggedout=true' ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.fill( 'input[name="log"]', 'admin' ); ++ await page.fill( 'input[name="pwd"]', 'password' ); ++ await page.click( 'text=Log In' ); ++ ++ // load the order placed as a customer ++ await page.goto( ++ `wp-admin/post.php?post=${ customerOrderId }&action=edit` ++ ); ++ await expect( ++ page.locator( 'h2.woocommerce-order-data__heading' ) ++ ).toContainText( `Order #${ customerOrderId } details` ); ++ await expect( page.locator( '.wc-order-item-name' ) ).toContainText( ++ simpleProductName ++ ); ++ await expect( page.locator( 'td.quantity >> nth=0' ) ).toContainText( ++ '2' ++ ); ++ await expect( page.locator( 'td.item_cost >> nth=0' ) ).toContainText( ++ singleProductPrice ++ ); ++ await expect( page.locator( 'td.line_cost >> nth=0' ) ).toContainText( ++ twoProductPrice ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/my-account-create-account.spec.js b/plugins/woocommerce/e2e/tests/shopper/my-account-create-account.spec.js +new file mode 100644 +index 0000000000..1e7309979b +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/my-account-create-account.spec.js +@@ -0,0 +1,67 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const customerEmailAddress = 'john.doe.test@example.com'; ++ ++test.describe( 'Shopper My Account Create Account', () => { ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.put( ++ 'settings/account/woocommerce_enable_myaccount_registration', ++ { ++ value: 'yes', ++ } ++ ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // get a list of all customers ++ await api.get( 'customers' ).then( ( response ) => { ++ for ( let i = 0; i < response.data.length; i++ ) { ++ if ( response.data[ i ].email === customerEmailAddress ) { ++ api.delete( `customers/${ response.data[ i ].id }`, { ++ force: true, ++ } ); ++ } ++ } ++ } ); ++ await api.put( ++ 'settings/account/woocommerce_enable_myaccount_registration', ++ { ++ value: 'no', ++ } ++ ); ++ } ); ++ ++ test( 'can create a new account via my account', async ( { page } ) => { ++ await page.goto( 'my-account/' ); ++ ++ await expect( ++ page.locator( '.woocommerce-form-register' ) ++ ).toBeVisible(); ++ ++ await page.fill( 'input#reg_email', customerEmailAddress ); ++ await page.click( 'button[name="register"]' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'My account' ++ ); ++ await expect( page.locator( 'text=Logout' ) ).toBeVisible(); ++ ++ await page.goto( 'my-account/edit-account/' ); ++ await expect( page.locator( '#account_email' ) ).toHaveValue( ++ customerEmailAddress ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/my-account-pay-order.spec.js b/plugins/woocommerce/e2e/tests/shopper/my-account-pay-order.spec.js +new file mode 100644 +index 0000000000..84a11eea46 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/my-account-pay-order.spec.js +@@ -0,0 +1,89 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++test.describe( 'Customer can pay for their order through My Account', () => { ++ let productId, orderId; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: 'Pay Order My Account', ++ type: 'simple', ++ regular_price: '15.77', ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ // create an order ++ await api ++ .post( 'orders', { ++ set_paid: false, ++ billing: { ++ first_name: 'Jane', ++ last_name: 'Smith', ++ email: 'customer@woocommercecoree2etestsuite.com', ++ }, ++ line_items: [ ++ { ++ product_id: productId, ++ quantity: 1, ++ }, ++ ], ++ } ) ++ .then( ( response ) => { ++ orderId = response.data.id; ++ } ); ++ // once the order is created, assign it to our existing customer user ++ await api.put( `orders/${ orderId }`, { ++ customer_id: 2, // should be safe to use this ID. Saves an API call to retrieve. ++ } ); ++ // enable COD payment ++ await api.put( 'payment_gateways/cod', { ++ enabled: true, ++ } ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ await api.delete( `orders/${ orderId }`, { force: true } ); ++ await api.put( 'payment_gateways/cod', { ++ enabled: false, ++ } ); ++ } ); ++ ++ test( 'allows customer to pay for their order in My Account', async ( { ++ page, ++ } ) => { ++ await page.goto( 'my-account/orders/' ); ++ // sign in as the "customer" user ++ await page.fill( '#username', 'customer' ); ++ await page.fill( '#password', 'password' ); ++ await page.click( 'text=Log in' ); ++ ++ await page.click( 'a.pay' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'Pay for order' ++ ); ++ await page.click( '#place_order' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'Order received' ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/my-account.spec.js b/plugins/woocommerce/e2e/tests/shopper/my-account.spec.js +new file mode 100644 +index 0000000000..b04b0fb617 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/my-account.spec.js +@@ -0,0 +1,54 @@ ++const { test, expect } = require( '@playwright/test' ); ++ ++const pages = [ ++ [ 'Orders', 'my-account/orders' ], ++ [ 'Downloads', 'my-account/downloads' ], ++ [ 'Addresses', 'my-account/edit-address' ], ++ [ 'Account details', 'my-account/edit-account' ], ++]; ++ ++test.describe( 'My account page', () => { ++ test.use( { storageState: 'e2e/storage/customerState.json' } ); ++ ++ test( 'allows customer to login', async ( { page } ) => { ++ await page.goto( 'my-account/' ); ++ ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ 'My account' ++ ); ++ await expect( ++ page.locator( 'div.woocommerce-MyAccount-content > p >> nth=0' ) ++ ).toContainText( 'Jane Smith' ); ++ ++ // assert that navigation is visible ++ await expect( ++ page.locator( '.woocommerce-MyAccount-navigation-link >> nth=0' ) ++ ).toContainText( 'Dashboard' ); ++ await expect( ++ page.locator( '.woocommerce-MyAccount-navigation-link >> nth=1' ) ++ ).toContainText( 'Orders' ); ++ await expect( ++ page.locator( '.woocommerce-MyAccount-navigation-link >> nth=2' ) ++ ).toContainText( 'Downloads' ); ++ await expect( ++ page.locator( '.woocommerce-MyAccount-navigation-link >> nth=3' ) ++ ).toContainText( 'Addresses' ); ++ await expect( ++ page.locator( '.woocommerce-MyAccount-navigation-link >> nth=4' ) ++ ).toContainText( 'Account details' ); ++ await expect( ++ page.locator( '.woocommerce-MyAccount-navigation-link >> nth=5' ) ++ ).toContainText( 'Logout' ); ++ } ); ++ ++ for ( let i = 0; i < pages.length; i++ ) { ++ test( `allows customer to see ${ pages[ i ][ 0 ] } page`, async ( { ++ page, ++ } ) => { ++ await page.goto( pages[ i ][ 1 ] ); ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ pages[ i ][ 0 ] ++ ); ++ } ); ++ } ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/order-email-receiving.spec.js b/plugins/woocommerce/e2e/tests/shopper/order-email-receiving.spec.js +new file mode 100644 +index 0000000000..0739eb7267 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/order-email-receiving.spec.js +@@ -0,0 +1,109 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++let productId, orderId; ++const productName = 'Order email product'; ++const customerEmail = 'order-email-test@example.com'; ++const storeName = 'WooCommerce Core E2E Test Suite'; ++ ++test.describe( 'Shopper Order Email Receiving', () => { ++ test.use( { storageState: 'e2e/storage/adminState.json' } ); ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: productName, ++ type: 'simple', ++ regular_price: '42.77', ++ } ) ++ .then( ( response ) => { ++ productId = response.data.id; ++ } ); ++ // enable COD payment ++ await api.put( 'payment_gateways/cod', { ++ enabled: true, ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { page } ) => { ++ await page.goto( ++ `wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent( ++ customerEmail ++ ) }` ++ ); ++ // clear out the email logs before each test ++ while ( ( await page.$( '#bulk-action-selector-top' ) ) !== null ) { ++ await page.click( '#cb-select-all-1' ); ++ await page.selectOption( '#bulk-action-selector-top', 'delete' ); ++ await page.click( '#doaction' ); ++ } ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ productId }`, { ++ force: true, ++ } ); ++ if ( orderId ) { ++ await api.delete( `orders/${ orderId }`, { ++ force: true, ++ } ); ++ } ++ await api.put( 'payment_gateways/cod', { ++ enabled: false, ++ } ); ++ } ); ++ ++ test( 'should receive order email after purchasing an item', async ( { ++ page, ++ } ) => { ++ await page.goto( `/shop/?add-to-cart=${ productId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( '/checkout/' ); ++ ++ await page.fill( '#billing_first_name', 'Maggie' ); ++ await page.fill( '#billing_last_name', 'Simpson' ); ++ await page.fill( '#billing_address_1', '123 Evergreen Terrace' ); ++ await page.fill( '#billing_city', 'Springfield' ); ++ await page.selectOption( '#billing_state', 'OR' ); ++ await page.fill( '#billing_postcode', '97403' ); ++ await page.fill( '#billing_phone', '555 555-5555' ); ++ await page.fill( '#billing_email', customerEmail ); ++ ++ await page.click( 'text=Place order' ); ++ ++ await page.waitForSelector( ++ 'li.woocommerce-order-overview__order > strong' ++ ); ++ orderId = await page.textContent( ++ 'li.woocommerce-order-overview__order > strong' ++ ); ++ ++ // search to narrow it down to just the messages we want ++ await page.goto( ++ `wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent( ++ customerEmail ++ ) }` ++ ); ++ await page.waitForLoadState( 'networkidle' ); ++ await expect( ++ page.locator( 'td.column-receiver >> nth=0' ) ++ ).toContainText( customerEmail ); ++ await expect( ++ page.locator( 'td.column-subject >> nth=1' ) ++ ).toContainText( `[${ storeName }]: New order #${ orderId }` ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/product-browse-search-sort.spec.js b/plugins/woocommerce/e2e/tests/shopper/product-browse-search-sort.spec.js +new file mode 100644 +index 0000000000..715f631e6a +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/product-browse-search-sort.spec.js +@@ -0,0 +1,161 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const singleProductPrice1 = '979.99'; ++const singleProductPrice2 = '989.99'; ++const singleProductPrice3 = '999.99'; ++ ++const simpleProductName = 'AAA Search and Browse Product'; ++ ++const categoryA = 'Dogs'; ++const categoryB = 'Cats'; ++const categoryC = 'Fish'; ++ ++let categoryAId, categoryBId, categoryCId, product1Id, product2Id, product3Id; ++ ++test.describe( ++ 'Search, browse by categories and sort items in the shop', ++ () => { ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product categories ++ await api ++ .post( 'products/categories', { ++ name: categoryA, ++ } ) ++ .then( ( response ) => { ++ categoryAId = response.data.id; ++ } ); ++ await api ++ .post( 'products/categories', { ++ name: categoryB, ++ } ) ++ .then( ( response ) => { ++ categoryBId = response.data.id; ++ } ); ++ await api ++ .post( 'products/categories', { ++ name: categoryC, ++ } ) ++ .then( ( response ) => { ++ categoryCId = response.data.id; ++ } ); ++ ++ // add products ++ await api ++ .post( 'products', { ++ name: simpleProductName + ' 1', ++ type: 'simple', ++ regular_price: singleProductPrice1, ++ categories: [ { id: categoryAId } ], ++ } ) ++ .then( ( response ) => { ++ product1Id = response.data.id; ++ } ); ++ await api ++ .post( 'products', { ++ name: simpleProductName + ' 2', ++ type: 'simple', ++ regular_price: singleProductPrice2, ++ categories: [ { id: categoryBId } ], ++ } ) ++ .then( ( response ) => { ++ product2Id = response.data.id; ++ } ); ++ await api ++ .post( 'products', { ++ name: simpleProductName + ' 3', ++ type: 'simple', ++ regular_price: singleProductPrice3, ++ categories: [ { id: categoryCId } ], ++ } ) ++ .then( ( response ) => { ++ product3Id = response.data.id; ++ } ); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.post( 'products/batch', { ++ delete: [ product1Id, product2Id, product3Id ], ++ } ); ++ await api.post( 'products/categories/batch', { ++ delete: [ categoryAId, categoryBId, categoryCId ], ++ } ); ++ } ); ++ ++ test( 'should let user search the store', async ( { page } ) => { ++ await page.goto( 'shop/' ); ++ ++ await page.fill( ++ '#wp-block-search__input-1', ++ simpleProductName + ' 1' ++ ); ++ await page.click( 'button.wp-block-search__button' ); ++ ++ await expect( page.locator( 'h1.page-title' ) ).toContainText( ++ `${ simpleProductName } 1` ++ ); ++ await expect( page.locator( 'h2.entry-title' ) ).toContainText( ++ simpleProductName + ' 1' ++ ); ++ } ); ++ ++ test( 'should let user browse products by categories', async ( { ++ page, ++ } ) => { ++ // browse the Audio category ++ await page.goto( 'shop/' ); ++ await page.click( `text=${ simpleProductName } 2` ); ++ await page.click( 'span.posted_in > a', { hasText: categoryB } ); ++ ++ // verify the Audio category page ++ await expect( page.locator( 'h1.page-title' ) ).toContainText( ++ categoryB ++ ); ++ await expect( ++ page.locator( 'h2.woocommerce-loop-product__title' ) ++ ).toContainText( simpleProductName + ' 2' ); ++ await page.click( `text=${ simpleProductName } 2` ); ++ await expect( page.locator( 'h1.entry-title' ) ).toContainText( ++ simpleProductName + ' 2' ++ ); ++ } ); ++ ++ test( 'should let user sort the products in the shop', async ( { ++ page, ++ } ) => { ++ await page.goto( 'shop/' ); ++ ++ // sort by price high to low ++ await page.selectOption( '.orderby', 'price-desc' ); ++ // last product is most expensive ++ await expect( ++ page.locator( 'ul.products > li:nth-child(1)' ) ++ ).toContainText( `${ simpleProductName } 3` ); ++ await expect( ++ page.locator( 'ul.products > li:nth-child(3)' ) ++ ).toContainText( `${ simpleProductName } 1` ); ++ ++ // sort by price low to high ++ await page.selectOption( '.orderby', 'price' ); ++ // last product is most expensive ++ await expect( ++ page.locator( 'ul.products > li:nth-last-child(3)' ) ++ ).toContainText( `${ simpleProductName } 1` ); ++ await expect( ++ page.locator( 'ul.products > li:nth-last-child(1)' ) ++ ).toContainText( `${ simpleProductName } 3` ); ++ } ); ++ } ++); +diff --git a/plugins/woocommerce/e2e/tests/shopper/single-product.spec.js b/plugins/woocommerce/e2e/tests/shopper/single-product.spec.js +new file mode 100644 +index 0000000000..0d3182333e +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/single-product.spec.js +@@ -0,0 +1,324 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const productPrice = '18.16'; ++const simpleProductName = 'Simple single product'; ++const variableProductName = 'Variable single product'; ++const variations = [ ++ { ++ regular_price: productPrice, ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'Small', ++ }, ++ ], ++ }, ++ { ++ regular_price: ( +productPrice * 2 ).toString(), ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'Medium', ++ }, ++ ], ++ }, ++ { ++ regular_price: ( +productPrice * 3 ).toString(), ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'Large', ++ }, ++ ], ++ }, ++ { ++ regular_price: ( +productPrice * 4 ).toString(), ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'XLarge', ++ }, ++ ], ++ }, ++]; ++const groupedProductName = 'Grouped single product'; ++ ++let simpleProductId, simpleProduct2Id, variableProductId, groupedProductId; ++ ++test.describe( 'Single Product Page', () => { ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: simpleProductName, ++ type: 'simple', ++ regular_price: productPrice, ++ } ) ++ .then( ( response ) => { ++ simpleProductId = response.data.id; ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ simpleProductId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test( 'should be able to add simple products to the cart', async ( { ++ page, ++ } ) => { ++ const slug = simpleProductName.replace( / /gi, '-' ).toLowerCase(); ++ await page.goto( `product/${ slug }` ); ++ ++ await page.fill( 'input.qty', '5' ); ++ await page.click( 'text=Add to cart' ); ++ ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ 'have been added to your cart.' ++ ); ++ ++ await page.goto( 'cart/' ); ++ await expect( page.locator( 'td.product-name' ) ).toContainText( ++ simpleProductName ++ ); ++ await expect( page.locator( 'input.qty' ) ).toHaveValue( '5' ); ++ await expect( page.locator( 'td.product-subtotal' ) ).toContainText( ++ ( 5 * +productPrice ).toString() ++ ); ++ } ); ++ ++ test( 'should be able to remove simple products from the cart', async ( { ++ page, ++ } ) => { ++ await page.goto( `/shop/?add-to-cart=${ simpleProductId }` ); ++ await page.waitForLoadState( 'networkidle' ); ++ ++ await page.goto( 'cart/' ); ++ await page.click( 'a.remove' ); ++ ++ await expect( page.locator( 'p.cart-empty' ) ).toContainText( ++ 'Your cart is currently empty.' ++ ); ++ } ); ++} ); ++ ++test.describe( 'Variable Product Page', () => { ++ const slug = variableProductName.replace( / /gi, '-' ).toLowerCase(); ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: variableProductName, ++ type: 'variable', ++ attributes: [ ++ { ++ name: 'Size', ++ options: [ 'Small', 'Medium', 'Large', 'XLarge' ], ++ visible: true, ++ variation: true, ++ }, ++ ], ++ } ) ++ .then( ( response ) => { ++ variableProductId = response.data.id; ++ for ( const key in variations ) { ++ api.post( ++ `products/${ variableProductId }/variations`, ++ variations[ key ] ++ ); ++ } ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ variableProductId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test( 'should be able to add variation products to the cart', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ ++ for ( const attr of variations ) { ++ await page.selectOption( '#size', attr.attributes[ 0 ].option ); ++ await page.click( 'text=Add to cart' ); ++ await expect( ++ page.locator( '.woocommerce-message' ) ++ ).toContainText( 'has been added to your cart.' ); ++ } ++ ++ await page.goto( 'cart/' ); ++ await expect( ++ page.locator( 'td.product-name >> nth=0' ) ++ ).toContainText( variableProductName ); ++ await expect( page.locator( 'tr.order-total > td' ) ).toContainText( ++ ( +productPrice * 10 ).toString() ++ ); ++ } ); ++ ++ test( 'should be able to remove variation products from the cart', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ await page.selectOption( '#size', 'Large' ); ++ await page.click( 'text=Add to cart' ); ++ ++ await page.goto( 'cart/' ); ++ await page.click( 'a.remove' ); ++ ++ await expect( page.locator( 'p.cart-empty' ) ).toContainText( ++ 'Your cart is currently empty.' ++ ); ++ } ); ++} ); ++ ++test.describe( 'Grouped Product Page', () => { ++ const slug = groupedProductName.replace( / /gi, '-' ).toLowerCase(); ++ const simpleProduct1 = simpleProductName + ' 1'; ++ const simpleProduct2 = simpleProductName + ' 2'; ++ ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add products ++ await api ++ .post( 'products', { ++ name: simpleProduct1, ++ type: 'simple', ++ regular_price: productPrice, ++ } ) ++ .then( ( response ) => { ++ simpleProductId = response.data.id; ++ } ); ++ await api ++ .post( 'products', { ++ name: simpleProduct2, ++ type: 'simple', ++ regular_price: productPrice, ++ } ) ++ .then( ( response ) => { ++ simpleProduct2Id = response.data.id; ++ } ); ++ await api ++ .post( 'products', { ++ name: groupedProductName, ++ type: 'grouped', ++ grouped_products: [ simpleProductId, simpleProduct2Id ], ++ } ) ++ .then( ( response ) => { ++ groupedProductId = response.data.id; ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ simpleProductId }`, { ++ force: true, ++ } ); ++ await api.delete( `products/${ simpleProduct2Id }`, { ++ force: true, ++ } ); ++ await api.delete( `products/${ groupedProductId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test( 'should be able to add grouped products to the cart', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ ++ await page.click( 'text=Add to cart' ); ++ await expect( page.locator( '.woocommerce-error' ) ).toContainText( ++ 'Please choose the quantity of items you wish to add to your cart…' ++ ); ++ ++ await page.fill( 'div.quantity input.qty >> nth=0', '5' ); ++ await page.fill( 'div.quantity input.qty >> nth=1', '5' ); ++ await page.click( 'text=Add to cart' ); ++ await expect( page.locator( '.woocommerce-message' ) ).toContainText( ++ `“${ simpleProduct1 }” and “${ simpleProduct2 }” have been added to your cart.` ++ ); ++ ++ await page.goto( 'cart/' ); ++ await expect( ++ page.locator( 'td.product-name >> nth=0' ) ++ ).toContainText( simpleProduct1 ); ++ await expect( ++ page.locator( 'td.product-name >> nth=1' ) ++ ).toContainText( simpleProduct2 ); ++ await expect( page.locator( 'tr.order-total > td' ) ).toContainText( ++ ( +productPrice * 10 ).toString() ++ ); ++ } ); ++ ++ test( 'should be able to remove grouped products from the cart', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ await page.fill( 'div.quantity input.qty >> nth=0', '1' ); ++ await page.fill( 'div.quantity input.qty >> nth=1', '1' ); ++ await page.click( 'text=Add to cart' ); ++ ++ await page.goto( 'cart/' ); ++ await page.click( 'a.remove >> nth=1' ); ++ await page.click( 'a.remove >> nth=0' ); ++ ++ await expect( page.locator( 'p.cart-empty' ) ).toContainText( ++ 'Your cart is currently empty.' ++ ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/e2e/tests/shopper/variable-product-updates.spec.js b/plugins/woocommerce/e2e/tests/shopper/variable-product-updates.spec.js +new file mode 100644 +index 0000000000..9f31e198b8 +--- /dev/null ++++ b/plugins/woocommerce/e2e/tests/shopper/variable-product-updates.spec.js +@@ -0,0 +1,254 @@ ++const { test, expect } = require( '@playwright/test' ); ++const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; ++ ++const variableProductName = 'Variable Product Updates'; ++const productPrice = '11.16'; ++const variations = [ ++ { ++ attributes: [ ++ { ++ name: 'Colour', ++ option: 'Red', ++ }, ++ ], ++ }, ++ { ++ attributes: [ ++ { ++ name: 'Colour', ++ option: 'Blue', ++ }, ++ ], ++ }, ++ { ++ attributes: [ ++ { ++ name: 'Colour', ++ option: 'Green', ++ }, ++ ], ++ }, ++ { ++ regular_price: productPrice, ++ weight: '100', ++ dimensions: { ++ length: '5', ++ width: '10', ++ height: '10', ++ }, ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'Small', ++ }, ++ ], ++ }, ++ { ++ regular_price: productPrice, ++ weight: '100', ++ dimensions: { ++ length: '5', ++ width: '10', ++ height: '10', ++ }, ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'Medium', ++ }, ++ ], ++ }, ++ { ++ regular_price: ( +productPrice * 2 ).toString(), ++ weight: '200', ++ dimensions: { ++ length: '10', ++ width: '20', ++ height: '15', ++ }, ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'Large', ++ }, ++ ], ++ }, ++ { ++ regular_price: ( +productPrice * 2 ).toString(), ++ weight: '400', ++ dimensions: { ++ length: '20', ++ width: '40', ++ height: '30', ++ }, ++ attributes: [ ++ { ++ name: 'Size', ++ option: 'XLarge', ++ }, ++ ], ++ }, ++]; ++ ++const cartDialogMessage = ++ 'Please select some product options before adding this product to your cart.'; ++ ++test.describe( 'Shopper > Update variable product', () => { ++ let variableProductId; ++ const slug = variableProductName.replace( / /gi, '-' ).toLowerCase(); ++ test.beforeAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ // add product ++ await api ++ .post( 'products', { ++ name: variableProductName, ++ type: 'variable', ++ attributes: [ ++ { ++ name: 'Size', ++ options: [ 'Small', 'Medium', 'Large', 'XLarge' ], ++ visible: true, ++ variation: true, ++ }, ++ { ++ name: 'Colour', ++ options: [ 'Red', 'Green', 'Blue' ], ++ visible: true, ++ variation: true, ++ }, ++ ], ++ } ) ++ .then( ( response ) => { ++ variableProductId = response.data.id; ++ for ( const key in variations ) { ++ api.post( ++ `products/${ variableProductId }/variations`, ++ variations[ key ] ++ ); ++ } ++ } ); ++ } ); ++ ++ test.beforeEach( async ( { context } ) => { ++ // Shopping cart is very sensitive to cookies, so be explicit ++ context.clearCookies(); ++ } ); ++ ++ test.afterAll( async ( { baseURL } ) => { ++ const api = new wcApi( { ++ url: baseURL, ++ consumerKey: process.env.CONSUMER_KEY, ++ consumerSecret: process.env.CONSUMER_SECRET, ++ version: 'wc/v3', ++ } ); ++ await api.delete( `products/${ variableProductId }`, { ++ force: true, ++ } ); ++ } ); ++ ++ test( 'Shopper can change variable attributes to the same value', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ ++ await page.selectOption( '#size', 'Small' ); ++ ++ await page.selectOption( '#colour', 'Red' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ ++ await page.selectOption( '#colour', 'Green' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ ++ await page.selectOption( '#colour', 'Blue' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ } ); ++ ++ test( 'Shopper can change attributes to combination with dimentions and weight', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ ++ await page.selectOption( '#colour', 'Red' ); ++ ++ await page.selectOption( '#size', 'Small' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ await expect( ++ page.locator( '.woocommerce-product-attributes-item--weight' ) ++ ).toContainText( '100 kg' ); ++ await expect( ++ page.locator( '.woocommerce-product-attributes-item--dimensions' ) ++ ).toContainText( '5 × 10 × 10 cm' ); ++ ++ await page.selectOption( '#size', 'XLarge' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( ( +productPrice * 2 ).toString() ); ++ await expect( ++ page.locator( '.woocommerce-product-attributes-item--weight' ) ++ ).toContainText( '400 kg' ); ++ await expect( ++ page.locator( '.woocommerce-product-attributes-item--dimensions' ) ++ ).toContainText( '20 × 40 × 30 cm' ); ++ } ); ++ ++ test( 'Shopper can change variable product attributes to variation with a different price', async ( { ++ page, ++ } ) => { ++ await page.goto( `product/${ slug }` ); ++ ++ await page.selectOption( '#colour', 'Red' ); ++ ++ await page.selectOption( '#size', 'Small' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ ++ await page.selectOption( '#size', 'Medium' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ ++ await page.selectOption( '#size', 'Large' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( ( +productPrice * 2 ).toString() ); ++ ++ await page.selectOption( '#size', 'XLarge' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( ( +productPrice * 2 ).toString() ); ++ } ); ++ ++ test( 'Shopper can reset variations', async ( { page } ) => { ++ await page.goto( `product/${ slug }` ); ++ ++ await page.selectOption( '#colour', 'Red' ); ++ ++ await page.selectOption( '#size', 'Small' ); ++ await expect( ++ page.locator( '.woocommerce-variation-price' ) ++ ).toContainText( productPrice ); ++ ++ await page.click( 'a.reset_variations' ); ++ ++ // Verify the reset by attempting to add the product to the cart ++ page.on( 'dialog', async ( dialog ) => { ++ expect( dialog.message() ).toContain( cartDialogMessage ); ++ await dialog.dismiss(); ++ } ); ++ await page.click( '.single_add_to_cart_button' ); ++ } ); ++} ); +diff --git a/plugins/woocommerce/i18n/locale-info.php b/plugins/woocommerce/i18n/locale-info.php +index 59d0ea4892..49ff4a3e3f 100644 +--- a/plugins/woocommerce/i18n/locale-info.php ++++ b/plugins/woocommerce/i18n/locale-info.php +@@ -936,7 +936,7 @@ return array( + 'name' => 'Danish krone', + 'singular' => 'Danish krone', + 'plural' => 'Danish kroner', +- 'short_symbol' => 'kr', ++ 'short_symbol' => 'kr.', + 'locales' => $locales['DKK'], + ), + 'DM' => array( +diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +index 52f9087be6..a52049d8be 100644 +--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php ++++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +@@ -1550,6 +1550,29 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { + ) + ); + ++ /** ++ * Filters whether apply base tax for local pickup shipping method or not. ++ * ++ * @since 6.8.0 ++ * @param boolean apply_base_tax Whether apply base tax for local pickup. Default true. ++ */ ++ $apply_base_tax = true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ); ++ ++ /** ++ * Filters local pickup shipping methods. ++ * ++ * @since 6.8.0 ++ * @param string[] $local_pickup_methods Local pickup shipping method IDs. ++ */ ++ $local_pickup_methods = apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ); ++ ++ $shipping_method_ids = ArrayUtil::select( $this->get_shipping_methods(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD ); ++ ++ // Set shop base address as a tax location if order has local pickup shipping method. ++ if ( $apply_base_tax && count( array_intersect( $shipping_method_ids, $local_pickup_methods ) ) > 0 ) { ++ $tax_based_on = 'base'; ++ } ++ + // Default to base. + if ( 'base' === $tax_based_on || empty( $args['country'] ) ) { + $args['country'] = WC()->countries->get_base_country(); +diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php +index 6357b46e8b..d87525f252 100644 +--- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php ++++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php +@@ -7,6 +7,7 @@ + */ + + use Automattic\Jetpack\Constants; ++use Automattic\WooCommerce\Admin\Features\Features; + + if ( ! defined( 'ABSPATH' ) ) { + exit; // Exit if accessed directly. +@@ -127,7 +128,8 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) : + + include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php'; + +- $is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false ); ++ //phpcs:ignore ++ $is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false ) || ! Features::is_enabled( 'analytics' ); + + $reports = new WC_Admin_Report(); + +diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +index 99671c3e22..3205076eff 100644 +--- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php ++++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +@@ -7,6 +7,7 @@ + */ + + use Automattic\WooCommerce\Internal\Admin\Orders\ListTable as Custom_Orders_List_Table; ++use Automattic\WooCommerce\Internal\Admin\Orders\PageController as Custom_Orders_PageController; + use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; + + defined( 'ABSPATH' ) || exit; +@@ -316,40 +317,11 @@ class WC_Admin_Menus { + */ + public function orders_menu(): void { + if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { +- add_submenu_page( 'woocommerce', __( 'Orders', 'woocommerce' ), __( 'Orders', 'woocommerce' ), 'edit_others_shop_orders', 'wc-orders', array( $this, 'orders_page' ) ); +- add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'orders_table' ) ); +- +- // In some cases (such as if the authoritative order store was changed earlier in the current request) we +- // need an extra step to remove the menu entry for the menu post type. +- add_action( +- 'admin_init', +- function () { +- remove_submenu_page( 'woocommerce', 'edit.php?post_type=shop_order' ); +- } +- ); ++ $this->orders_page_controller = new Custom_Orders_PageController(); ++ $this->orders_page_controller->setup(); + } + } + +- /** +- * Set-up the orders admin list table. +- * +- * @return void +- */ +- public function orders_table(): void { +- $this->orders_list_table = new Custom_Orders_List_Table(); +- $this->orders_list_table->setup(); +- } +- +- /** +- * Render the orders admin list table. +- * +- * @return void +- */ +- public function orders_page(): void { +- $this->orders_list_table->prepare_items(); +- $this->orders_list_table->display(); +- } +- + /** + * Add custom nav meta box. + * +diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php +index 4b61acb09c..df630e5b6a 100644 +--- a/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php ++++ b/plugins/woocommerce/includes/admin/class-wc-admin-meta-boxes.php +@@ -43,6 +43,7 @@ class WC_Admin_Meta_Boxes { + add_action( 'add_meta_boxes', array( $this, 'remove_meta_boxes' ), 10 ); + add_action( 'add_meta_boxes', array( $this, 'rename_meta_boxes' ), 20 ); + add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 30 ); ++ add_action( 'add_meta_boxes', array( $this, 'add_product_boxes_sort_order' ), 40 ); + add_action( 'save_post', array( $this, 'save_meta_boxes' ), 1, 2 ); + + /** +@@ -166,6 +167,28 @@ class WC_Admin_Meta_Boxes { + } + } + ++ /** ++ * Add default sort order for meta boxes on product page. ++ */ ++ public function add_product_boxes_sort_order() { ++ $current_value = get_user_meta( get_current_user_id(), 'meta-box-order_product', true ); ++ ++ if ( $current_value ) { ++ return; ++ } ++ ++ update_user_meta( ++ get_current_user_id(), ++ 'meta-box-order_product', ++ array( ++ 'side' => 'submitdiv,postimagediv,woocommerce-product-images,product_catdiv,tagsdiv-product_tag', ++ 'normal' => 'woocommerce-product-data,postcustom,slugdiv,postexcerpt', ++ 'advanced' => '', ++ ) ++ ); ++ ++ } ++ + /** + * Remove bloat. + */ +diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-webhooks.php b/plugins/woocommerce/includes/admin/class-wc-admin-webhooks.php +index 9614c9dc1d..007c9299e1 100644 +--- a/plugins/woocommerce/includes/admin/class-wc-admin-webhooks.php ++++ b/plugins/woocommerce/includes/admin/class-wc-admin-webhooks.php +@@ -30,7 +30,8 @@ class WC_Admin_Webhooks { + * @return bool + */ + public function allow_save_settings( $allow ) { +- if ( ! isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( ! isset( $_GET['edit-webhook'] ) ) { + return false; + } + +@@ -43,7 +44,8 @@ class WC_Admin_Webhooks { + * @return bool + */ + private function is_webhook_settings_page() { +- return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'advanced' === $_GET['tab'] && 'webhooks' === $_GET['section']; // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ return isset( $_GET['page'], $_GET['tab'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && 'advanced' === $_GET['tab'] && 'webhooks' === $_GET['section']; + } + + /** +@@ -56,19 +58,22 @@ class WC_Admin_Webhooks { + wp_die( esc_html__( 'You do not have permission to update Webhooks', 'woocommerce' ) ); + } + +- $errors = array(); +- $webhook_id = isset( $_POST['webhook_id'] ) ? absint( $_POST['webhook_id'] ) : 0; // WPCS: input var okay, CSRF ok. ++ $errors = array(); ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $webhook_id = isset( $_POST['webhook_id'] ) ? absint( $_POST['webhook_id'] ) : 0; + $webhook = new WC_Webhook( $webhook_id ); + + // Name. +- if ( ! empty( $_POST['webhook_name'] ) ) { // WPCS: input var okay, CSRF ok. +- $name = sanitize_text_field( wp_unslash( $_POST['webhook_name'] ) ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( ! empty( $_POST['webhook_name'] ) ) { ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $name = sanitize_text_field( wp_unslash( $_POST['webhook_name'] ) ); + } else { + $name = sprintf( + /* translators: %s: date */ + __( 'Webhook created on %s', 'woocommerce' ), + // @codingStandardsIgnoreStart +- strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ++ (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) + // @codingStandardsIgnoreEnd + ); + } +@@ -80,32 +85,39 @@ class WC_Admin_Webhooks { + } + + // Status. +- $webhook->set_status( ! empty( $_POST['webhook_status'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_status'] ) ) : 'disabled' ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $webhook->set_status( ! empty( $_POST['webhook_status'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_status'] ) ) : 'disabled' ); + + // Delivery URL. +- $delivery_url = ! empty( $_POST['webhook_delivery_url'] ) ? esc_url_raw( wp_unslash( $_POST['webhook_delivery_url'] ) ) : ''; // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $delivery_url = ! empty( $_POST['webhook_delivery_url'] ) ? esc_url_raw( wp_unslash( $_POST['webhook_delivery_url'] ) ) : ''; + + if ( wc_is_valid_url( $delivery_url ) ) { + $webhook->set_delivery_url( $delivery_url ); + } + + // Secret. +- $secret = ! empty( $_POST['webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_secret'] ) ) : wp_generate_password( 50, true, true ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $secret = ! empty( $_POST['webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_secret'] ) ) : wp_generate_password( 50, true, true ); + $webhook->set_secret( $secret ); + + // Topic. +- if ( ! empty( $_POST['webhook_topic'] ) ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( ! empty( $_POST['webhook_topic'] ) ) { + $resource = ''; + $event = ''; + +- switch ( $_POST['webhook_topic'] ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ switch ( $_POST['webhook_topic'] ) { + case 'action': + $resource = 'action'; +- $event = ! empty( $_POST['webhook_action_event'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_action_event'] ) ) : ''; // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $event = ! empty( $_POST['webhook_action_event'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_action_event'] ) ) : ''; + break; + + default: +- list( $resource, $event ) = explode( '.', sanitize_text_field( wp_unslash( $_POST['webhook_topic'] ) ) ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ list( $resource, $event ) = explode( '.', sanitize_text_field( wp_unslash( $_POST['webhook_topic'] ) ) ); + break; + } + +@@ -120,7 +132,8 @@ class WC_Admin_Webhooks { + + // API version. + $rest_api_versions = wc_get_webhook_rest_api_versions(); +- $webhook->set_api_version( ! empty( $_POST['webhook_api_version'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_api_version'] ) ) : end( $rest_api_versions ) ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $webhook->set_api_version( ! empty( $_POST['webhook_api_version'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_api_version'] ) ) : end( $rest_api_versions ) ); + + $webhook->save(); + +@@ -130,7 +143,8 @@ class WC_Admin_Webhooks { + // Redirect to webhook edit page to avoid settings save actions. + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( implode( '|', $errors ) ) ) ); + exit(); +- } elseif ( isset( $_POST['webhook_status'] ) && 'active' === $_POST['webhook_status'] && $webhook->get_pending_delivery() ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ } elseif ( isset( $_POST['webhook_status'] ) && 'active' === $_POST['webhook_status'] && $webhook->get_pending_delivery() ) { + // Ping the webhook at the first time that is activated. + $result = $webhook->deliver_ping(); + +@@ -157,8 +171,9 @@ class WC_Admin_Webhooks { + $webhook->delete( true ); + } + +- $qty = count( $webhooks ); +- $status = isset( $_GET['status'] ) ? '&status=' . sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; // WPCS: input var okay, CSRF ok. ++ $qty = count( $webhooks ); ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $status = isset( $_GET['status'] ) ? '&status=' . sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; + + // Redirect to webhooks page. + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' . $status . '&deleted=' . $qty ) ); +@@ -171,8 +186,10 @@ class WC_Admin_Webhooks { + private function delete() { + check_admin_referer( 'delete-webhook' ); + +- if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok. +- $webhook_id = absint( $_GET['delete'] ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['delete'] ) ) { ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $webhook_id = absint( $_GET['delete'] ); + + if ( $webhook_id ) { + $this->bulk_delete( array( $webhook_id ) ); +@@ -186,12 +203,14 @@ class WC_Admin_Webhooks { + public function actions() { + if ( $this->is_webhook_settings_page() ) { + // Save. +- if ( isset( $_POST['save'] ) && isset( $_POST['webhook_id'] ) ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Missing ++ if ( isset( $_POST['save'] ) && isset( $_POST['webhook_id'] ) ) { + $this->save(); + } + + // Delete webhook. +- if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['delete'] ) ) { + $this->delete(); + } + } +@@ -204,11 +223,13 @@ class WC_Admin_Webhooks { + // Hide the save button. + $GLOBALS['hide_save_button'] = true; + +- if ( isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok. +- $webhook_id = absint( $_GET['edit-webhook'] ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['edit-webhook'] ) ) { ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $webhook_id = absint( $_GET['edit-webhook'] ); + $webhook = new WC_Webhook( $webhook_id ); + +- include __DIR__ . '/settings/views/html-webhooks-edit.php'; ++ include __DIR__ . '/settings/views/html-webhooks-edit.php'; + return; + } + +@@ -219,23 +240,29 @@ class WC_Admin_Webhooks { + * Notices. + */ + public static function notices() { +- if ( isset( $_GET['deleted'] ) ) { // WPCS: input var okay, CSRF ok. +- $deleted = absint( $_GET['deleted'] ); // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['deleted'] ) ) { ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ $deleted = absint( $_GET['deleted'] ); + + /* translators: %d: count */ + WC_Admin_Settings::add_message( sprintf( _n( '%d webhook permanently deleted.', '%d webhooks permanently deleted.', $deleted, 'woocommerce' ), $deleted ) ); + } + +- if ( isset( $_GET['updated'] ) ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['updated'] ) ) { + WC_Admin_Settings::add_message( __( 'Webhook updated successfully.', 'woocommerce' ) ); + } + +- if ( isset( $_GET['created'] ) ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['created'] ) ) { + WC_Admin_Settings::add_message( __( 'Webhook created successfully.', 'woocommerce' ) ); + } + +- if ( isset( $_GET['error'] ) ) { // WPCS: input var okay, CSRF ok. +- foreach ( explode( '|', sanitize_text_field( wp_unslash( $_GET['error'] ) ) ) as $message ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( isset( $_GET['error'] ) ) { ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ foreach ( explode( '|', sanitize_text_field( wp_unslash( $_GET['error'] ) ) ) as $message ) { + WC_Admin_Settings::add_error( trim( $message ) ); + } + } +@@ -247,7 +274,8 @@ class WC_Admin_Webhooks { + public function screen_option() { + global $webhooks_table_list; + +- if ( ! isset( $_GET['edit-webhook'] ) && $this->is_webhook_settings_page() ) { // WPCS: input var okay, CSRF ok. ++ // phpcs:ignore WordPress.Security.NonceVerification.Recommended ++ if ( ! isset( $_GET['edit-webhook'] ) && $this->is_webhook_settings_page() ) { + $webhooks_table_list = new WC_Admin_Webhooks_Table_List(); + + // Add screen option. +diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php +index 25ec134f63..65e55285bd 100644 +--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php ++++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php +@@ -124,6 +124,37 @@ class WC_Helper_Updater { + return $transient; + } + ++ /** ++ * Get update data for all plugins. ++ * ++ * @return array Update data {product_id => data} ++ * @see get_update_data ++ */ ++ public static function get_available_extensions_downloads_data() { ++ $payload = array(); ++ ++ // Scan subscriptions. ++ foreach ( WC_Helper::get_subscriptions() as $subscription ) { ++ $payload[ $subscription['product_id'] ] = array( ++ 'product_id' => $subscription['product_id'], ++ 'file_id' => '', ++ ); ++ } ++ ++ // Scan local plugins which may or may not have a subscription. ++ foreach ( WC_Helper::get_local_woo_plugins() as $data ) { ++ if ( ! isset( $payload[ $data['_product_id'] ] ) ) { ++ $payload[ $data['_product_id'] ] = array( ++ 'product_id' => $data['_product_id'], ++ ); ++ } ++ ++ $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; ++ } ++ ++ return self::_update_check( $payload ); ++ } ++ + /** + * Get update data for all extensions. + * +diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php +index 4bab3e0bd0..9d85dfa451 100644 +--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php ++++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php +@@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) { + 'value' => $product_object->get_manage_stock( 'edit' ) ? 'yes' : 'no', + 'wrapper_class' => 'show_if_simple show_if_variable', + 'label' => __( 'Manage stock?', 'woocommerce' ), +- 'description' => __( 'Enable stock management at product level', 'woocommerce' ), ++ 'description' => __( 'Manage stock level (quantity)', 'woocommerce' ), + ) + ); + +@@ -111,7 +111,7 @@ if ( ! defined( 'ABSPATH' ) ) { + ?> + + +-
++
+ $product_object->get_sold_individually( 'edit' ) ? 'yes' : 'no', + 'wrapper_class' => 'show_if_simple show_if_variable', + 'label' => __( 'Sold individually', 'woocommerce' ), +- 'description' => __( 'Enable this to only allow one of this item to be bought in a single order', 'woocommerce' ), ++ 'description' => __( 'Limit purchases to 1 item per order', 'woocommerce' ), + ) + ); + ++ echo wc_help_tip( __( 'Check to let customers to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, for example art or handmade goods.', 'woocommerce' ) ); ++ + do_action( 'woocommerce_product_options_sold_individually' ); + ?> +
+diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php +index bc35103b2e..5bff4c42b5 100644 +--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php ++++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-variations.php +@@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) { + +
+
+- : ++ : + get_name() ) ] ) ? $default_attributes[ sanitize_title( $attribute->get_name() ) ] : ''; +diff --git a/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php b/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php +index d6dceb681f..4998d53ca5 100644 +--- a/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php ++++ b/plugins/woocommerce/includes/admin/reports/class-wc-admin-report.php +@@ -1,7 +1,14 @@ + 'total_sales' + * ) + * +- * @param array $args ++ * @param array $args arguments for the report. + * @return mixed depending on query_type + */ + public function get_order_report_data( $args = array() ) { +@@ -103,6 +112,7 @@ class WC_Admin_Report { + $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args ); + $args = wp_parse_args( $args, $default_args ); + ++ // phpcs:ignore WordPress.PHP.DontExtract.extract_extract + extract( $args ); + + if ( empty( $data ) ) { +@@ -157,7 +167,7 @@ class WC_Admin_Report { + $query['select'] = 'SELECT ' . implode( ',', $select ); + $query['from'] = "FROM {$wpdb->posts} AS posts"; + +- // Joins ++ // Joins. + $joins = array(); + + foreach ( ( $data + $where ) as $raw_key => $value ) { +@@ -204,7 +214,7 @@ class WC_Admin_Report { + $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id"; + + } else { +- // If we have a where clause for meta, join the postmeta table ++ // If we have a where clause for meta, join the postmeta table. + $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id"; + } + } +@@ -234,12 +244,14 @@ class WC_Admin_Report { + } + } + ++ // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date + if ( $filter_range ) { + $query['where'] .= " + AND posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "' + AND posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "' + "; + } ++ // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date + + if ( ! empty( $where_meta ) ) { + +@@ -255,9 +267,10 @@ class WC_Admin_Report { + + $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] ); + +- if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) { ++ if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) { + + if ( is_array( $value['meta_value'] ) ) { ++ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + $value['meta_value'] = implode( "','", $value['meta_value'] ); + } + +@@ -302,7 +315,7 @@ class WC_Admin_Report { + + foreach ( $where as $value ) { + +- if ( strtolower( $value['operator'] ) == 'in' || strtolower( $value['operator'] ) == 'not in' ) { ++ if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) { + + if ( is_array( $value['value'] ) ) { + $value['value'] = implode( "','", $value['value'] ); +@@ -349,7 +362,7 @@ class WC_Admin_Report { + } else { + $query_hash = md5( $query_type . $query ); + $result = $this->get_cached_query( $query_hash ); +- if ( $result === null ) { ++ if ( null === $result ) { + self::enable_big_selects(); + + $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data ); +@@ -414,7 +427,11 @@ class WC_Admin_Report { + $class = strtolower( get_class( $this ) ); + + if ( ! isset( self::$cached_results[ $class ] ) ) { +- self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) ); ++ self::$cached_results[ $class ] = get_transient( $class ); ++ } ++ ++ if ( false === self::$cached_results[ $class ] ) { ++ self::$cached_results[ $class ] = array(); + } + + self::add_update_transients_hook(); +@@ -437,15 +454,17 @@ class WC_Admin_Report { + /** + * Put data with post_date's into an array of times. + * +- * @param array $data array of your data +- * @param string $date_key key for the 'date' field. e.g. 'post_date' +- * @param string $data_key key for the data you are charting +- * @param int $interval +- * @param string $start_date +- * @param string $group_by ++ * @param array $data array of your data. ++ * @param string $date_key key for the 'date' field. e.g. 'post_date'. ++ * @param string $data_key key for the data you are charting. ++ * @param int $interval interval to use. ++ * @param string $start_date start date. ++ * @param string $group_by group by. + * @return array + */ + public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) { ++ // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date ++ + $prepared_data = array(); + + // Ensure all days (or months) have values in this range. +@@ -500,6 +519,8 @@ class WC_Admin_Report { + } + + return $prepared_data; ++ ++ // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date + } + + /** +@@ -512,6 +533,8 @@ class WC_Admin_Report { + */ + public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) { + ++ // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested ++ + if ( $id ) { + $meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty'; + +@@ -590,7 +613,7 @@ class WC_Admin_Report { + + if ( 'sales' === $type ) { + /* translators: 1: total income 2: days */ +- $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); ++ $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), wp_strip_all_tags( wc_price( $total ) ), $days ); + } else { + /* translators: 1: total items sold 2: days */ + $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); +@@ -599,29 +622,36 @@ class WC_Admin_Report { + $sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) ); + + return ''; ++ ++ // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested + } + + /** + * Get the current range and calculate the start and end dates. + * +- * @param string $current_range ++ * @param string $current_range Type of range. + */ + public function calculate_current_range( $current_range ) { + ++ // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested ++ // phpcs:disable WordPress.Security.NonceVerification.Recommended ++ + switch ( $current_range ) { + + case 'custom': +- $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( $_GET['start_date'] ) ) ); ++ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated ++ $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) ) ); + + if ( empty( $_GET['end_date'] ) ) { + $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); + } else { +- $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( $_GET['end_date'] ) ) ); ++ $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) ) ); + } + + $interval = 0; + $min_date = $this->start_date; + ++ // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition + while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) { + $interval ++; + } +@@ -660,7 +690,7 @@ class WC_Admin_Report { + break; + } + +- // Group by ++ // Group by. + switch ( $this->chart_groupby ) { + + case 'day': +@@ -674,6 +704,7 @@ class WC_Admin_Report { + $this->chart_interval = 0; + $min_date = strtotime( date( 'Y-m-01', $this->start_date ) ); + ++ // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition + while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) { + $this->chart_interval ++; + } +@@ -681,6 +712,9 @@ class WC_Admin_Report { + $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000; + break; + } ++ ++ // phpcs:enable WordPress.Security.NonceVerification.Recommended ++ // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested + } + + /** +@@ -752,12 +786,14 @@ class WC_Admin_Report { + return; + } + +- if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) { // WPCS: input var ok, CSRF ok. ++ if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) { ++ // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated + wp_die( + /* translators: %1$s: open link, %2$s: close link */ +- sprintf( esc_html__( 'This report link has expired. %1$sClick here to view the filtered report%2$s.', 'woocommerce' ), '', '' ), // @codingStandardsIgnoreLine. ++ sprintf( esc_html__( 'This report link has expired. %1$sClick here to view the filtered report%2$s.', 'woocommerce' ), '', '' ), + esc_attr__( 'Confirm navigation', 'woocommerce' ) + ); ++ // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated + exit; + } + } +diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php +index b9b8bf46f7..a519ce2614 100644 +--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php ++++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php +@@ -6,6 +6,8 @@ + */ + + use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; ++use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init; ++use Automattic\WooCommerce\Admin\PluginsHelper; + + defined( 'ABSPATH' ) || exit; + +@@ -143,7 +145,8 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { + + + payment_gateways->payment_gateways() as $gateway ) { ++ $payment_gateways = WC()->payment_gateways->payment_gateways(); ++ foreach ( $payment_gateways as $gateway ) { + + echo ''; + +@@ -198,7 +201,7 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { + $setup_url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . strtolower( $gateway->id ) ); + } + /* Translators: %s Payment gateway name. */ +- echo '' . esc_html__( 'Set up', 'woocommerce' ) . ''; ++ echo '' . esc_html__( 'Finish set up', 'woocommerce' ) . ''; + } + break; + case 'status': +@@ -225,18 +228,49 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { + * See https://github.com/woocommerce/woocommerce/issues/32130 for more details. + */ + if ( WooCommercePayments::is_supported() ) { +- $columns_count = count( $columns ); +- $link_text = __( 'Other payment methods', 'woocommerce' ); ++ $wcpay_setup = isset( $payment_gateways['woocommerce_payments'] ) && ! $payment_gateways['woocommerce_payments']->needs_setup(); ++ $country = wc_get_base_location()['country']; ++ $plugin_suggestions = Init::get_suggestions(); ++ $active_plugins = PluginsHelper::get_active_plugin_slugs(); ++ ++ if ( $wcpay_setup ) { ++ $link_text = __( 'Discover additional payment providers', 'woocommerce' ); ++ $filter_by = 'category_additional'; ++ } else { ++ $link_text = __( 'Discover other payment providers', 'woocommerce' ); ++ $filter_by = 'category_other'; ++ } ++ ++ $plugin_suggestions = array_filter( ++ $plugin_suggestions, ++ function( $plugin ) use ( $country, $filter_by, $active_plugins ) { ++ if ( ! isset( $plugin->{$filter_by} ) || ! isset( $plugin->image_72x72 ) || ! isset( $plugin->plugins[0] ) || in_array( $plugin->plugins[0], $active_plugins, true ) ) { ++ return false; ++ } ++ return in_array( $country, $plugin->{$filter_by}, true ); ++ } ++ ); ++ ++ $columns_count = count( $columns ); ++ + $external_link_icon = ''; + echo ''; + // phpcs:ignore -- ignoring the error since the value is harded. + echo ""; +- echo ""; ++ echo ""; + // phpcs:ignore + echo $link_text; + // phpcs:ignore + echo $external_link_icon; + echo ''; ++ if ( count( $plugin_suggestions ) ) { ++ foreach ( $plugin_suggestions as $plugin_suggestion ) { ++ $alt = str_replace( '.png', '', basename( $plugin_suggestion->image_72x72 ) ); ++ // phpcs:ignore ++ echo "${alt}"; ++ } ++ echo '& more.'; ++ } + echo ''; + echo ''; + } +diff --git a/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php b/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php +index 89bfebb217..c5d192a0b6 100644 +--- a/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php ++++ b/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php +@@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) { + + format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ) ); // @codingStandardsIgnoreLine + ?> + + +diff --git a/plugins/woocommerce/includes/class-wc-cli.php b/plugins/woocommerce/includes/class-wc-cli.php +index 52ebb622b8..303f1b44b5 100644 +--- a/plugins/woocommerce/includes/class-wc-cli.php ++++ b/plugins/woocommerce/includes/class-wc-cli.php +@@ -31,6 +31,8 @@ class WC_CLI { + require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tool-command.php'; + require_once dirname( __FILE__ ) . '/cli/class-wc-cli-update-command.php'; + require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tracker-command.php'; ++ require_once dirname( __FILE__ ) . '/cli/class-wc-cli-com-command.php'; ++ require_once dirname( __FILE__ ) . '/cli/class-wc-cli-com-extension-command.php'; + } + + /** +@@ -41,6 +43,8 @@ class WC_CLI { + WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tool_Command::register_commands' ); + WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Update_Command::register_commands' ); + WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tracker_Command::register_commands' ); ++ WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_COM_Command::register_commands' ); ++ WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_COM_Extension_Command::register_commands' ); + $cli_runner = wc_get_container()->get( CLIRunner::class ); + WP_CLI::add_hook( 'after_wp_load', array( $cli_runner, 'register_commands' ) ); + } +diff --git a/plugins/woocommerce/includes/class-wc-geolocation.php b/plugins/woocommerce/includes/class-wc-geolocation.php +index 2e02d85589..1bd5fa0f3c 100644 +--- a/plugins/woocommerce/includes/class-wc-geolocation.php ++++ b/plugins/woocommerce/includes/class-wc-geolocation.php +@@ -113,7 +113,13 @@ class WC_Geolocation { + + foreach ( $ip_lookup_services_keys as $service_name ) { + $service_endpoint = $ip_lookup_services[ $service_name ]; +- $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); ++ $response = wp_safe_remote_get( ++ $service_endpoint, ++ array( ++ 'timeout' => 2, ++ 'user-agent' => 'WooCommerce/' . wc()->version, ++ ) ++ ); + + if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) { + $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); +@@ -276,7 +282,13 @@ class WC_Geolocation { + + foreach ( $geoip_services_keys as $service_name ) { + $service_endpoint = $geoip_services[ $service_name ]; +- $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); ++ $response = wp_safe_remote_get( ++ sprintf( $service_endpoint, $ip_address ), ++ array( ++ 'timeout' => 2, ++ 'user-agent' => 'WooCommerce/' . wc()->version, ++ ) ++ ); + + if ( ! is_wp_error( $response ) && $response['body'] ) { + switch ( $service_name ) { +diff --git a/plugins/woocommerce/includes/class-wc-order-refund.php b/plugins/woocommerce/includes/class-wc-order-refund.php +index b24ccc6061..73144ee689 100644 +--- a/plugins/woocommerce/includes/class-wc-order-refund.php ++++ b/plugins/woocommerce/includes/class-wc-order-refund.php +@@ -64,7 +64,7 @@ class WC_Order_Refund extends WC_Abstract_Order { + */ + public function get_post_title() { + // @codingStandardsIgnoreStart +- return sprintf( __( 'Refund – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); ++ return sprintf( __( 'Refund – %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + } + +diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php +index 1962215143..97e10351cb 100644 +--- a/plugins/woocommerce/includes/class-wc-order.php ++++ b/plugins/woocommerce/includes/class-wc-order.php +@@ -103,7 +103,7 @@ class WC_Order extends WC_Abstract_Order { + } + + try { +- do_action( 'woocommerce_pre_payment_complete', $this->get_id() ); ++ do_action( 'woocommerce_pre_payment_complete', $this->get_id(), $transaction_id ); + + if ( WC()->session ) { + WC()->session->set( 'order_awaiting_payment', false ); +@@ -119,9 +119,9 @@ class WC_Order extends WC_Abstract_Order { + $this->set_status( apply_filters( 'woocommerce_payment_complete_order_status', $this->needs_processing() ? 'processing' : 'completed', $this->get_id(), $this ) ); + $this->save(); + +- do_action( 'woocommerce_payment_complete', $this->get_id() ); ++ do_action( 'woocommerce_payment_complete', $this->get_id(), $transaction_id ); + } else { +- do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id() ); ++ do_action( 'woocommerce_payment_complete_order_status_' . $this->get_status(), $this->get_id(), $transaction_id ); + } + } catch ( Exception $e ) { + /** +@@ -948,8 +948,8 @@ class WC_Order extends WC_Abstract_Order { + * Filter orders formatted shipping address. + * + * @since 3.8.0 +- * @param string $address Formatted billing address string. +- * @param array $raw_address Raw billing address. ++ * @param string $address Formatted shipping address string. ++ * @param array $raw_address Raw shipping address. + * @param WC_Order $order Order data. @since 3.9.0 + */ + return apply_filters( 'woocommerce_order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this ); +diff --git a/plugins/woocommerce/includes/class-wc-post-data.php b/plugins/woocommerce/includes/class-wc-post-data.php +index 8473de148c..3a3888e722 100644 +--- a/plugins/woocommerce/includes/class-wc-post-data.php ++++ b/plugins/woocommerce/includes/class-wc-post-data.php +@@ -8,6 +8,7 @@ + * @version 2.2.0 + */ + ++use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer; + use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore; + use Automattic\WooCommerce\Proxies\LegacyProxy; + +@@ -326,6 +327,7 @@ class WC_Post_Data { + + break; + case 'shop_order': ++ case DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE: + global $wpdb; + + $refunds = $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type = 'shop_order_refund' AND post_parent = %d", $id ) ); +diff --git a/plugins/woocommerce/includes/class-wc-regenerate-images-request.php b/plugins/woocommerce/includes/class-wc-regenerate-images-request.php +index a6c7996fc2..047a9e2f2a 100644 +--- a/plugins/woocommerce/includes/class-wc-regenerate-images-request.php ++++ b/plugins/woocommerce/includes/class-wc-regenerate-images-request.php +@@ -145,16 +145,6 @@ class WC_Regenerate_Images_Request extends WC_Background_Process { + $new_metadata['sizes'][ $old_size ] = $old_metadata['sizes'][ $old_size ]; + } + } +- // Handle legacy sizes. +- if ( isset( $new_metadata['sizes']['shop_thumbnail'], $new_metadata['sizes']['woocommerce_gallery_thumbnail'] ) ) { +- $new_metadata['sizes']['shop_thumbnail'] = $new_metadata['sizes']['woocommerce_gallery_thumbnail']; +- } +- if ( isset( $new_metadata['sizes']['shop_catalog'], $new_metadata['sizes']['woocommerce_thumbnail'] ) ) { +- $new_metadata['sizes']['shop_catalog'] = $new_metadata['sizes']['woocommerce_thumbnail']; +- } +- if ( isset( $new_metadata['sizes']['shop_single'], $new_metadata['sizes']['woocommerce_single'] ) ) { +- $new_metadata['sizes']['shop_single'] = $new_metadata['sizes']['woocommerce_single']; +- } + } + + // Update the meta data with the new size values. +diff --git a/plugins/woocommerce/includes/class-wc-regenerate-images.php b/plugins/woocommerce/includes/class-wc-regenerate-images.php +index 6e6ed20b02..ca59231e7f 100644 +--- a/plugins/woocommerce/includes/class-wc-regenerate-images.php ++++ b/plugins/woocommerce/includes/class-wc-regenerate-images.php +@@ -68,7 +68,7 @@ class WC_Regenerate_Images { + * @return array + */ + public static function filter_image_get_intermediate_size( $data, $attachment_id, $size ) { +- if ( ! is_string( $size ) || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) { ++ if ( ! is_string( $size ) || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single' ) ), true ) ) { + return $data; + } + +@@ -204,7 +204,7 @@ class WC_Regenerate_Images { + } + + // List of sizes we want to resize. Ignore others. +- if ( ! $image || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) { ++ if ( ! $image || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single' ) ), true ) ) { + return $image; + } + +diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php +index 19783b0f73..f67646e768 100644 +--- a/plugins/woocommerce/includes/class-woocommerce.php ++++ b/plugins/woocommerce/includes/class-woocommerce.php +@@ -30,7 +30,7 @@ final class WooCommerce { + * + * @var string + */ +- public $version = '6.7.0'; ++ public $version = '6.8.2'; + + /** + * WooCommerce Schema version. +@@ -725,15 +725,6 @@ final class WooCommerce { + add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); + add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] ); + add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); +- +- /** +- * Legacy image sizes. +- * +- * @deprecated 3.3.0 These sizes will be removed in 4.6.0. +- */ +- add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); +- add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] ); +- add_image_size( 'shop_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); + } + + /** +diff --git a/plugins/woocommerce/includes/cli/class-wc-cli-com-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-com-command.php +new file mode 100644 +index 0000000000..1237fc2da4 +--- /dev/null ++++ b/plugins/woocommerce/includes/cli/class-wc-cli-com-command.php +@@ -0,0 +1,88 @@ ++ 2 ) { ++ $product_slug = $product_url_parts[ count( $product_url_parts ) - 2 ]; ++ } ++ return array( ++ 'product_slug' => $product_slug, ++ 'product_name' => htmlspecialchars_decode( $item['product_name'] ), ++ 'auto_renew' => $item['autorenew'] ? 'On' : 'Off', ++ 'expires_on' => gmdate( 'Y-m-d', $item['expires'] ), ++ 'expired' => $item['expired'] ? 'Yes' : 'No', ++ 'sites_max' => $item['sites_max'], ++ 'sites_active' => $item['sites_active'], ++ 'maxed' => $item['maxed'] ? 'Yes' : 'No', ++ ); ++ }, ++ $data ++ ); ++ ++ $formatter->display_items( $data ); ++ } ++} +diff --git a/plugins/woocommerce/includes/cli/class-wc-cli-com-extension-command.php b/plugins/woocommerce/includes/cli/class-wc-cli-com-extension-command.php +new file mode 100644 +index 0000000000..ee57fbfa48 +--- /dev/null ++++ b/plugins/woocommerce/includes/cli/class-wc-cli-com-extension-command.php +@@ -0,0 +1,103 @@ ++... ++ * : One or more plugins to install. Accepts a plugin slug. ++ * ++ * [--force] ++ * : If set, the command will overwrite any installed version of the plugin, without prompting ++ * for confirmation. ++ * ++ * [--activate] ++ * : If set, the plugin will be activated immediately after install. ++ * ++ * [--activate-network] ++ * : If set, the plugin will be network activated immediately after install ++ * ++ * [--insecure] ++ * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. ++ * ++ * ## EXAMPLES ++ * ++ * # Install the latest version from woocommerce.com and activate ++ * $ wp wc com extension install automatewoo --activate ++ * Downloading install package from http://s3.amazonaws.com/bucketname/automatewoo.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef...... ++ * Using cached file '/home/vagrant/.wp-cli/cache/plugin/automatewoo.zip'... ++ * Unpacking the package... ++ * Installing the plugin... ++ * Plugin installed successfully. ++ * Activating 'automatewoo'... ++ * Plugin 'automatewoo' activated. ++ * Success: Installed 1 of 1 plugins. ++ * ++ * # Forcefully re-install an installed plugin ++ * $ wp wc com extension install automatewoo --force ++ * Downloading install package from http://s3.amazonaws.com/bucketname/automatewoo.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef... ++ * Unpacking the package... ++ * Installing the plugin... ++ * Removing the old version of the plugin... ++ * Plugin updated successfully ++ * Success: Installed 1 of 1 plugins. ++ * ++ * @param array $args WP-CLI positional arguments. ++ * @param array $assoc_args WP-CLI associative arguments. ++ */ ++ public function install( $args, $assoc_args ) { ++ $subscriptions = WC_Helper_Updater::get_available_extensions_downloads_data(); ++ $extension = reset( $args ); ++ $extension_package_url = null; ++ ++ // Remove `--version` as we don't support it. ++ unset( $assoc_args['version'] ); ++ ++ // Filter by slug. ++ foreach ( $subscriptions as $subscription ) { ++ if ( $subscription['slug'] === $extension && ! is_null( $subscription['package'] ) ) { ++ ++ $extension_package_url = $subscription['package']; ++ break; ++ } ++ } ++ ++ // No package found. ++ if ( is_null( $extension_package_url ) ) { ++ WP_CLI::warning( sprintf( 'We couldn\'t find a Subscription for \'%s\'', $extension ) ); ++ WP_CLI\Utils\report_batch_operation_results( $this->item_type, 'install', count( $args ), 0, 1 ); ++ ++ return; ++ } ++ ++ parent::install( array( $extension_package_url ), $assoc_args ); ++ } ++} +diff --git a/plugins/woocommerce/includes/cli/class-wc-cli-runner.php b/plugins/woocommerce/includes/cli/class-wc-cli-runner.php +index 0804fb9ac2..1e5e00fe6e 100644 +--- a/plugins/woocommerce/includes/cli/class-wc-cli-runner.php ++++ b/plugins/woocommerce/includes/cli/class-wc-cli-runner.php +@@ -127,7 +127,7 @@ class WC_CLI_Runner { + preg_match_all( '#\([^\)]+\)#', $route, $matches ); + $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; + $trimmed_route = rtrim( $route ); +- $is_singular = substr( $trimmed_route, - strlen( $resource_id ) ) === $resource_id; ++ $is_singular = substr( $trimmed_route, - strlen( $resource_id ?? '' ) ) === $resource_id; + + // List a collection. + if ( array( 'GET' ) === $endpoint['methods'] && ! $is_singular ) { +diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php +index ce37852322..748ccfe5f0 100644 +--- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php ++++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php +@@ -16,7 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) { + * + * @version 3.0.0 + */ +-abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Abstract_Order_Data_Store_Interface { ++abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP implements WC_Abstract_Order_Data_Store_Interface, WC_Object_Data_Store_Interface { + + /** + * Internal meta type used to store order data. +@@ -261,7 +261,7 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme + protected function get_post_title() { + // @codingStandardsIgnoreStart + /* translators: %s: Order date */ +- return sprintf( __( 'Order – %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) ); ++ return sprintf( __( 'Order – %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) ); + // @codingStandardsIgnoreEnd + } + +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php +index 4cc09195e9..88b82521f2 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php +@@ -306,7 +306,7 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement + } + + // If customer changed, update any downloadable permissions. +- if ( in_array( 'customer_id', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { ++ if ( in_array( 'customer_id', $updated_props, true ) || in_array( 'billing_email', $updated_props, true ) ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() ); + } +@@ -437,7 +437,7 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement + AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')", + $order->get_id() + ) +- ); ++ ) ?? 0; + + return abs( $total ); + } +@@ -461,7 +461,7 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement + AND order_itemmeta.meta_key IN ('cost')", + $order->get_id() + ) +- ); ++ ) ?? 0; + + return abs( $total ); + } +@@ -889,6 +889,7 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement + $wp_query_args['date_query'] = array(); + } + if ( ! isset( $wp_query_args['meta_query'] ) ) { ++ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $wp_query_args['meta_query'] = array(); + } + +@@ -1029,7 +1030,7 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement + return; + } + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { +- if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) { ++ if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'], true ) ) { + return; + } + } +@@ -1155,7 +1156,7 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement + global $wpdb; + + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { +- if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) { ++ if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'], true ) ) { + return; + } + } +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-item-product-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-product-data-store.php +index 2f78691b57..0fc6a16066 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-order-item-product-data-store.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-order-item-product-data-store.php +@@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { + * + * @version 3.0.0 + */ +-class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface { ++class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface { + + /** + * Data stored in meta keys. +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-refund-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-refund-data-store-cpt.php +index 090d3a5cb6..dc11dd42db 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-order-refund-data-store-cpt.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-order-refund-data-store-cpt.php +@@ -46,8 +46,8 @@ class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT im + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$order, $args = array() ) { +- $id = $order->get_id(); +- $parent_order_id = $order->get_parent_id(); ++ $id = $order->get_id(); ++ $parent_order_id = $order->get_parent_id(); + $refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id; + + if ( ! $id ) { +@@ -116,7 +116,7 @@ class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT im + return sprintf( + /* translators: %s: Order date */ + __( 'Refund – %s', 'woocommerce' ), +- strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText ++ ( new DateTime( 'now' ) )->format( _x( 'M d, Y @ h:i A', 'Order date parsed by DateTime::format', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText + ); + } + } +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php +index 11e510a872..c6d6e5012b 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php +@@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { + * + * @version 3.0.0 + */ +-class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Payment_Token_Data_Store_Interface, WC_Object_Data_Store_Interface { ++class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Payment_Token_Data_Store_Interface { + + /** + * Meta type. Payment tokens are a new object type. +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php +index 190749d7db..8864155371 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php +@@ -368,7 +368,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da + $set_props['category_ids'] = $this->get_term_ids( $product, 'product_cat' ); + $set_props['tag_ids'] = $this->get_term_ids( $product, 'product_tag' ); + $set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) ); +- $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) ); ++ $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ?? '' ) ); + + $product->set_props( $set_props ); + } +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php +index 5b91e54825..d39c9b069c 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php +@@ -119,6 +119,9 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple + public function read_children( &$product, $force_read = false ) { + $children_transient_name = 'wc_product_children_' . $product->get_id(); + $children = get_transient( $children_transient_name ); ++ if ( false === $children ) { ++ $children = array(); ++ } + + if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) { + $all_args = array( +diff --git a/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php +index 6d79561165..b9be9505e0 100644 +--- a/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php ++++ b/plugins/woocommerce/includes/data-stores/class-wc-shipping-zone-data-store.php +@@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { + * + * @version 3.0.0 + */ +-class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Shipping_Zone_Data_Store_Interface, WC_Object_Data_Store_Interface { ++class WC_Shipping_Zone_Data_Store extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Shipping_Zone_Data_Store_Interface { + + /** + * Method to create a new shipping zone. +diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php +index 18bd88c02c..b143fa34c9 100644 +--- a/plugins/woocommerce/includes/emails/class-wc-email.php ++++ b/plugins/woocommerce/includes/emails/class-wc-email.php +@@ -318,7 +318,18 @@ class WC_Email extends WC_Settings_API { + * Set the locale to the store locale for customer emails to make sure emails are in the store language. + */ + public function setup_locale() { +- if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) { ++ ++ /** ++ * Filter the ability to switch email locale. ++ * ++ * @since 6.8.0 ++ * ++ * @param bool $default_value The default returned value. ++ * @param WC_Email $this The WC_Email object. ++ */ ++ $switch_email_locale = apply_filters( 'woocommerce_allow_switching_email_locale', true, $this ); ++ ++ if ( $switch_email_locale && $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) { + wc_switch_to_site_locale(); + } + } +@@ -327,7 +338,18 @@ class WC_Email extends WC_Settings_API { + * Restore the locale to the default locale. Use after finished with setup_locale. + */ + public function restore_locale() { +- if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) { ++ ++ /** ++ * Filter the ability to restore email locale. ++ * ++ * @since 6.8.0 ++ * ++ * @param bool $default_value The default returned value. ++ * @param WC_Email $this The WC_Email object. ++ */ ++ $restore_email_locale = apply_filters( 'woocommerce_allow_restoring_email_locale', true, $this ); ++ ++ if ( $restore_email_locale && $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) { + wc_restore_locale(); + } + } +diff --git a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php +index ba73503493..d65f36bed1 100644 +--- a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php ++++ b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php +@@ -130,7 +130,18 @@ abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter { + return false; + } + +- $fp = fopen( $this->get_file_path(), 'a+' ); ++ /** ++ * Filters the mode parameter which specifies the type of access you require to the stream (used during file ++ * writing for CSV exports). Defaults to 'a+' (which supports both reading and writing, and places the file ++ * pointer at the end of the file). ++ * ++ * @see https://www.php.net/manual/en/function.fopen.php ++ * @since 6.8.0 ++ * ++ * @param string $fopen_mode, either (r, r+, w, w+, a, a+, x, x+, c, c+, e) ++ */ ++ $fopen_mode = apply_filters( 'woocommerce_csv_exporter_fopen_mode', 'a+' ); ++ $fp = fopen( $this->get_file_path(), $fopen_mode ); + + if ( $fp ) { + fwrite( $fp, $data ); +diff --git a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php +index 0be4abb9c4..30f6b096ff 100644 +--- a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php ++++ b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php +@@ -250,7 +250,17 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter { + $this->prepare_downloads_for_export( $product, $row ); + $this->prepare_attributes_for_export( $product, $row ); + $this->prepare_meta_for_export( $product, $row ); +- return apply_filters( 'woocommerce_product_export_row_data', $row, $product ); ++ ++ /** ++ * Allow third-party plugins to filter the data in a single row of the exported CSV file. ++ * ++ * @since 3.1.0 ++ * ++ * @param array $row An associative array with the data of a single row in the CSV file. ++ * @param WC_Product $product The product object correspnding to the current row. ++ * @param WC_Product_CSV_Exporter $exporter The instance of the CSV exporter. ++ */ ++ return apply_filters( 'woocommerce_product_export_row_data', $row, $product, $this ); + } + + /** +diff --git a/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-webhooks.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-webhooks.php +index 98e14b5418..1c32598058 100644 +--- a/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-webhooks.php ++++ b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-webhooks.php +@@ -196,7 +196,7 @@ class WC_API_Webhooks extends WC_API_Resource { + 'ping_status' => 'closed', + 'post_author' => get_current_user_id(), + 'post_password' => 'webhook_' . wp_generate_password(), +- 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), ++ 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ), + ), $data, $this ); + + $webhook = new WC_Webhook(); +diff --git a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-webhooks.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-webhooks.php +index 98e14b5418..1c32598058 100644 +--- a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-webhooks.php ++++ b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-webhooks.php +@@ -196,7 +196,7 @@ class WC_API_Webhooks extends WC_API_Resource { + 'ping_status' => 'closed', + 'post_author' => get_current_user_id(), + 'post_password' => 'webhook_' . wp_generate_password(), +- 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ), ++ 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ), + ), $data, $this ); + + $webhook = new WC_Webhook(); +diff --git a/plugins/woocommerce/includes/libraries/wp-background-process.php b/plugins/woocommerce/includes/libraries/wp-background-process.php +index 7e3f1013c2..67bceb89b2 100644 +--- a/plugins/woocommerce/includes/libraries/wp-background-process.php ++++ b/plugins/woocommerce/includes/libraries/wp-background-process.php +@@ -370,7 +370,7 @@ abstract class WP_Background_Process extends WP_Async_Request { + $memory_limit = '32000M'; + } + +- return intval( $memory_limit ) * 1024 * 1024; ++ return wp_convert_hr_to_bytes( $memory_limit ); + } + + /** +diff --git a/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php b/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php +index 4ea6da68c4..cbc427bf96 100644 +--- a/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php ++++ b/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php +@@ -35,6 +35,11 @@ use Automattic\WooCommerce\Admin\WCAdminHelper; + * $allow_tracking + * ); + * ++ * OR use the helper function: ++ * ++ * WooCommerce\Admin\Experimental_Abtest::in_treatment('experiment_name'); ++ * ++ * + * $isTreatment = $abtest->get_variation('your-experiment-name') === 'treatment'; + * + * @internal This class is experimental and should not be used externally due to planned breaking changes. +@@ -91,6 +96,26 @@ final class Experimental_Abtest { + $this->as_auth_wpcom_user = $as_auth_wpcom_user; + } + ++ /** ++ * Returns true if the current user is in the treatment group of the given experiment. ++ * ++ * @param string $experiment_name Name of the experiment. ++ * @param bool $as_auth_wpcom_user Request variation as a auth wp user or not. ++ * @return bool ++ */ ++ public static function in_treatment( string $experiment_name, bool $as_auth_wpcom_user = false ) { ++ $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : ''; ++ $allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' ); ++ $abtest = new self( ++ $anon_id, ++ 'woocommerce', ++ $allow_tracking, ++ $as_auth_wpcom_user ++ ); ++ ++ return $abtest->get_variation( $experiment_name ) === 'treatment'; ++ } ++ + /** + * Retrieve the test variation for a provided A/B test. + * +diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php +index 4a2463aca3..4ef6e74fd5 100644 +--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php ++++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php +@@ -45,78 +45,93 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + * Register the routes for webhooks. + */ + public function register_routes() { +- register_rest_route( $this->namespace, '/' . $this->rest_base, array( ++ register_rest_route( ++ $this->namespace, ++ '/' . $this->rest_base, + array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_items' ), +- 'permission_callback' => array( $this, 'get_items_permissions_check' ), +- 'args' => $this->get_collection_params(), +- ), ++ array( ++ 'methods' => WP_REST_Server::READABLE, ++ 'callback' => array( $this, 'get_items' ), ++ 'permission_callback' => array( $this, 'get_items_permissions_check' ), ++ 'args' => $this->get_collection_params(), ++ ), ++ array( ++ 'methods' => WP_REST_Server::CREATABLE, ++ 'callback' => array( $this, 'create_item' ), ++ 'permission_callback' => array( $this, 'create_item_permissions_check' ), ++ 'args' => array_merge( ++ $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ++ array( ++ 'topic' => array( ++ 'required' => true, ++ 'type' => 'string', ++ 'description' => __( 'Webhook topic.', 'woocommerce' ), ++ ), ++ 'delivery_url' => array( ++ 'required' => true, ++ 'type' => 'string', ++ 'description' => __( 'Webhook delivery URL.', 'woocommerce' ), ++ ), ++ ) ++ ), ++ ), ++ 'schema' => array( $this, 'get_public_item_schema' ), ++ ) ++ ); ++ ++ register_rest_route( ++ $this->namespace, ++ '/' . $this->rest_base . '/(?P[\d]+)', + array( +- 'methods' => WP_REST_Server::CREATABLE, +- 'callback' => array( $this, 'create_item' ), +- 'permission_callback' => array( $this, 'create_item_permissions_check' ), +- 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( +- 'topic' => array( +- 'required' => true, +- 'type' => 'string', +- 'description' => __( 'Webhook topic.', 'woocommerce' ), ++ 'args' => array( ++ 'id' => array( ++ 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), ++ 'type' => 'integer', + ), +- 'delivery_url' => array( +- 'required' => true, +- 'type' => 'string', +- 'description' => __( 'Webhook delivery URL.', 'woocommerce' ), ++ ), ++ array( ++ 'methods' => WP_REST_Server::READABLE, ++ 'callback' => array( $this, 'get_item' ), ++ 'permission_callback' => array( $this, 'get_item_permissions_check' ), ++ 'args' => array( ++ 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), +- ) ), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) ); +- +- register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( +- 'args' => array( +- 'id' => array( +- 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), +- 'type' => 'integer', + ), +- ), +- array( +- 'methods' => WP_REST_Server::READABLE, +- 'callback' => array( $this, 'get_item' ), +- 'permission_callback' => array( $this, 'get_item_permissions_check' ), +- 'args' => array( +- 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ++ array( ++ 'methods' => WP_REST_Server::EDITABLE, ++ 'callback' => array( $this, 'update_item' ), ++ 'permission_callback' => array( $this, 'update_item_permissions_check' ), ++ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), +- ), +- array( +- 'methods' => WP_REST_Server::EDITABLE, +- 'callback' => array( $this, 'update_item' ), +- 'permission_callback' => array( $this, 'update_item_permissions_check' ), +- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), +- ), +- array( +- 'methods' => WP_REST_Server::DELETABLE, +- 'callback' => array( $this, 'delete_item' ), +- 'permission_callback' => array( $this, 'delete_item_permissions_check' ), +- 'args' => array( +- 'force' => array( +- 'default' => false, +- 'type' => 'boolean', +- 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ++ array( ++ 'methods' => WP_REST_Server::DELETABLE, ++ 'callback' => array( $this, 'delete_item' ), ++ 'permission_callback' => array( $this, 'delete_item_permissions_check' ), ++ 'args' => array( ++ 'force' => array( ++ 'default' => false, ++ 'type' => 'boolean', ++ 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), ++ ), + ), + ), +- ), +- 'schema' => array( $this, 'get_public_item_schema' ), +- ) ); ++ 'schema' => array( $this, 'get_public_item_schema' ), ++ ) ++ ); + +- register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( ++ register_rest_route( ++ $this->namespace, ++ '/' . $this->rest_base . '/batch', + array( +- 'methods' => WP_REST_Server::EDITABLE, +- 'callback' => array( $this, 'batch_items' ), +- 'permission_callback' => array( $this, 'batch_items_permissions_check' ), +- 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), +- ), +- 'schema' => array( $this, 'get_public_batch_schema' ), +- ) ); ++ array( ++ 'methods' => WP_REST_Server::EDITABLE, ++ 'callback' => array( $this, 'batch_items' ), ++ 'permission_callback' => array( $this, 'batch_items_permissions_check' ), ++ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ++ ), ++ 'schema' => array( $this, 'get_public_batch_schema' ), ++ ) ++ ); + } + + /** +@@ -250,13 +265,13 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + $prepared_args['paginate'] = true; + + // Get the webhooks. +- $webhooks = array(); +- $data_store = WC_Data_Store::load( 'webhook' ); +- $results = $data_store->search_webhooks( $prepared_args ); +- $webhook_ids = $results->webhooks; ++ $webhooks = array(); ++ $data_store = WC_Data_Store::load( 'webhook' ); ++ $results = $data_store->search_webhooks( $prepared_args ); ++ $webhook_ids = $results->webhooks; + + foreach ( $webhook_ids as $webhook_id ) { +- $data = $this->prepare_item_for_response( $webhook_id, $request ); ++ $data = $this->prepare_item_for_response( $webhook_id, $request ); + $webhooks[] = $this->prepare_response_for_collection( $data ); + } + +@@ -352,7 +367,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating item, false when updating. + */ +- do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, true ); ++ do_action( 'woocommerce_rest_insert_webhook_object', $webhook, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); +@@ -432,7 +447,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating item, false when updating. + */ +- do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, false ); ++ do_action( 'woocommerce_rest_insert_webhook_object', $webhook, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); +@@ -477,7 +492,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ +- do_action( "woocommerce_rest_delete_webhook_object", $webhook, $response, $request ); ++ do_action( 'woocommerce_rest_delete_webhook_object', $webhook, $response, $request ); + + return $response; + } +@@ -489,7 +504,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { +- $data = new stdClass; ++ $data = new stdClass(); + + // Post ID. + if ( isset( $request['id'] ) ) { +@@ -498,7 +513,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { +- $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); // @codingStandardsIgnoreLine ++ $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ); // @codingStandardsIgnoreLine + + // Post author. + $data->post_author = get_current_user_id(); +@@ -538,8 +553,8 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + /** + * Prepare a single webhook output for response. + * +- * @param int $id Webhook ID or object. +- * @param WP_REST_Request $request Request object. ++ * @param int $id Webhook ID or object. ++ * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $id, $request ) { +@@ -549,7 +564,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + +- $data = array( ++ $data = array( + 'id' => $webhook->get_id(), + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), +@@ -589,7 +604,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + */ + protected function prepare_links( $id ) { + $links = array( +- 'self' => array( ++ 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ), + ), + 'collection' => array( +@@ -611,63 +626,63 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + 'title' => 'webhook', + 'type' => 'object', + 'properties' => array( +- 'id' => array( ++ 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), +- 'name' => array( ++ 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), +- 'status' => array( ++ 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array_keys( wc_get_webhook_statuses() ), + 'context' => array( 'view', 'edit' ), + ), +- 'topic' => array( ++ 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), +- 'resource' => array( ++ 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), +- 'event' => array( ++ 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), +- 'hooks' => array( ++ 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( +- 'type' => 'string', ++ 'type' => 'string', + ), + ), +- 'delivery_url' => array( ++ 'delivery_url' => array( + 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), +- 'secret' => array( ++ 'secret' => array( + 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), +- 'date_created' => array( ++ 'date_created' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), +@@ -695,23 +710,23 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + + $params['context']['default'] = 'view'; + +- $params['after'] = array( +- 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), +- 'type' => 'string', +- 'format' => 'date-time', +- 'validate_callback' => 'rest_validate_request_arg', ++ $params['after'] = array( ++ 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), ++ 'type' => 'string', ++ 'format' => 'date-time', ++ 'validate_callback' => 'rest_validate_request_arg', + ); +- $params['before'] = array( +- 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), +- 'type' => 'string', +- 'format' => 'date-time', +- 'validate_callback' => 'rest_validate_request_arg', ++ $params['before'] = array( ++ 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), ++ 'type' => 'string', ++ 'format' => 'date-time', ++ 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( +- 'type' => 'integer', ++ 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', +@@ -720,36 +735,36 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller { + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( +- 'type' => 'integer', ++ 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); +- $params['offset'] = array( +- 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), +- 'type' => 'integer', +- 'sanitize_callback' => 'absint', +- 'validate_callback' => 'rest_validate_request_arg', ++ $params['offset'] = array( ++ 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), ++ 'type' => 'integer', ++ 'sanitize_callback' => 'absint', ++ 'validate_callback' => 'rest_validate_request_arg', + ); +- $params['order'] = array( +- 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), +- 'type' => 'string', +- 'default' => 'desc', +- 'enum' => array( 'asc', 'desc' ), +- 'validate_callback' => 'rest_validate_request_arg', ++ $params['order'] = array( ++ 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), ++ 'type' => 'string', ++ 'default' => 'desc', ++ 'enum' => array( 'asc', 'desc' ), ++ 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( +- 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), +- 'type' => 'string', +- 'default' => 'date', +- 'enum' => array( ++ 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), ++ 'type' => 'string', ++ 'default' => 'date', ++ 'enum' => array( + 'date', + 'id', + 'title', + ), +- 'validate_callback' => 'rest_validate_request_arg', ++ 'validate_callback' => 'rest_validate_request_arg', + ); +- $params['status'] = array( ++ $params['status'] = array( + 'default' => 'all', + 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), + 'type' => 'string', +diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php +index 3295d62480..0ed36db888 100644 +--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php ++++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php +@@ -39,7 +39,9 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + */ + public function register_routes() { + register_rest_route( +- $this->namespace, '/' . $this->rest_base, array( ++ $this->namespace, ++ '/' . $this->rest_base, ++ array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), +@@ -56,7 +58,9 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + ); + + register_rest_route( +- $this->namespace, '/' . $this->rest_base . '/batch', array( ++ $this->namespace, ++ '/' . $this->rest_base . '/batch', ++ array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), +@@ -74,7 +78,9 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + ); + + register_rest_route( +- $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( ++ $this->namespace, ++ '/' . $this->rest_base . '/(?P[\w-]+)', ++ array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), +@@ -159,6 +165,7 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); + } + ++ // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); + + if ( empty( $settings ) ) { +@@ -240,7 +247,7 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + return $settings; + } + +- $array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id ); ++ $array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id, true ); + + if ( empty( $array_key ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); +@@ -309,7 +316,7 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + if ( is_array( $setting['option_key'] ) ) { + $setting['value'] = $value; + $option_key = $setting['option_key']; +- $prev = get_option( $option_key[0] ); ++ $prev = get_option( $option_key[0], null ) ?? array(); + $prev[ $option_key[1] ] = $request['value']; + update_option( $option_key[0], $prev ); + } else { +@@ -448,7 +455,8 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + */ + public function allowed_setting_keys( $key ) { + return in_array( +- $key, array( ++ $key, ++ array( + 'id', + 'label', + 'description', +@@ -459,7 +467,8 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + 'options', + 'value', + 'option_key', +- ) ++ ), ++ true + ); + } + +@@ -472,7 +481,8 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + */ + public function is_setting_type_valid( $type ) { + return in_array( +- $type, array( ++ $type, ++ array( + 'text', // Validates with validate_setting_text_field. + 'email', // Validates with validate_setting_text_field. + 'number', // Validates with validate_setting_text_field. +@@ -485,7 +495,8 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller { + 'checkbox', // Validates with validate_setting_checkbox_field. + 'image_width', // Validates with validate_setting_image_width_field. + 'thumbnail_cropping', // Validates with validate_setting_text_field. +- ) ++ ), ++ true + ); + } + +diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php +index 40423decb8..befbac1296 100644 +--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php ++++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php +@@ -360,7 +360,7 @@ abstract class WC_REST_Terms_Controller extends WC_REST_Controller { + $max_pages = ceil( $total_terms / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + +- $base = str_replace( '(?P[\d]+)', $request['attribute_id'], $this->rest_base ); ++ $base = str_replace( '(?P[\d]+)', $request['attribute_id'] ?? '', $this->rest_base ); + $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; +diff --git a/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php b/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php +index 7190829183..258cb3097b 100644 +--- a/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php ++++ b/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php +@@ -64,12 +64,17 @@ class WC_Site_Tracking { + * Adds the tracking function to the admin footer. + */ + public static function add_tracking_function() { ++ $user = wp_get_current_user(); ++ $server_details = WC_Tracks::get_server_details(); ++ $blog_details = WC_Tracks::get_blog_details( $user->ID ); ++ ++ $client_tracking_properties = array_merge( $server_details, $blog_details ); + /** + * Add global tracks event properties. + * + * @since 6.5.0 + */ +- $filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', array(), false ); ++ $filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', $client_tracking_properties, false ); + ?> + +