From 422d4281d1c508a5a9b6ff73e25a6662d128aa6a Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sat, 31 Oct 2026 22:56:29 +0100
Subject: [PATCH 01/50] Update packages
Refresh the root's, Linked Orders' and Internal Comments' Composer
locks, which move only roave/security-advisories, and their npm locks,
which take Babel 7.29.11, ignore 7.0.14, http-proxy-middleware 4.2.1
and their patch-level neighbours, every change upward. No code change
was needed.
Assisted-by: Claude Code:claude-opus-5-5
---
composer.lock | 121 +++++---
package-lock.json | 90 +++---
plugins/internal-comments/composer.lock | 255 +++++++++------
plugins/internal-comments/package-lock.json | 118 +++----
.../composer.lock | 291 ++++++++++++------
.../package-lock.json | 66 ++--
6 files changed, 581 insertions(+), 360 deletions(-)
diff --git a/composer.lock b/composer.lock
index a353343..0d6bf14 100644
--- a/composer.lock
+++ b/composer.lock
@@ -206,7 +206,8 @@
"support": {
"source": "https://github.com/Automattic/jetpack-changelogger/tree/v7.0.5"
},
- "time": "2026-10-20T16:01:37+00:00"
+ "time": "2026-10-20T16:01:37+00:00",
+ "published-time": "2026-10-20T16:01:50+00:00"
},
{
"name": "composer/class-map-generator",
@@ -275,7 +276,8 @@
"type": "github"
}
],
- "time": "2026-09-25T15:24:19+00:00"
+ "time": "2026-09-25T15:24:19+00:00",
+ "published-time": "2026-09-25T15:27:30+00:00"
},
{
"name": "composer/installers",
@@ -421,7 +423,8 @@
"type": "tidelift"
}
],
- "time": "2024-06-24T20:46:46+00:00"
+ "time": "2024-06-24T20:46:46+00:00",
+ "published-time": "2024-06-24T21:01:36+00:00"
},
{
"name": "composer/pcre",
@@ -497,7 +500,8 @@
"type": "github"
}
],
- "time": "2026-06-07T11:47:49+00:00"
+ "time": "2026-06-07T11:47:49+00:00",
+ "published-time": "2026-06-11T12:12:37+00:00"
},
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
@@ -593,7 +597,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-05-06T08:26:05+00:00"
+ "time": "2026-05-06T08:26:05+00:00",
+ "published-time": "2026-05-06T12:56:03+00:00"
},
{
"name": "johnbillion/wp-compat",
@@ -668,7 +673,8 @@
"type": "github"
}
],
- "time": "2026-10-21T20:54:48+00:00"
+ "time": "2026-10-21T20:54:48+00:00",
+ "published-time": "2026-10-21T20:54:55+00:00"
},
{
"name": "nikic/php-parser",
@@ -725,7 +731,8 @@
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0"
},
- "time": "2026-09-13T18:51:52+00:00"
+ "time": "2026-09-13T18:51:52+00:00",
+ "published-time": "2026-09-13T18:53:16+00:00"
},
{
"name": "php-stubs/wordpress-stubs",
@@ -777,7 +784,8 @@
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.2"
},
- "time": "2026-10-06T06:59:21+00:00"
+ "time": "2026-10-06T06:59:21+00:00",
+ "published-time": "2026-10-06T07:32:21+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@@ -867,7 +875,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-28T11:36:33+00:00"
+ "time": "2025-11-28T11:36:33+00:00",
+ "published-time": "2025-11-28T11:41:33+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-paragonie",
@@ -938,7 +947,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-29T13:09:49+00:00"
+ "time": "2025-11-29T13:09:49+00:00",
+ "published-time": "2025-11-29T13:22:17+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-wp",
@@ -1005,7 +1015,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-12-16T13:35:20+00:00"
+ "time": "2025-12-16T13:35:20+00:00",
+ "published-time": "2025-12-16T13:47:29+00:00"
},
{
"name": "phpcsstandards/phpcsextra",
@@ -1087,7 +1098,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-07-27T11:13:17+00:00"
+ "time": "2026-07-27T11:13:17+00:00",
+ "published-time": "2026-07-27T11:19:59+00:00"
},
{
"name": "phpcsstandards/phpcsutils",
@@ -1180,7 +1192,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-07-27T10:28:41+00:00"
+ "time": "2026-07-27T10:28:41+00:00",
+ "published-time": "2026-07-27T10:36:31+00:00"
},
{
"name": "phpstan/extension-installer",
@@ -1228,7 +1241,8 @@
"issues": "https://github.com/phpstan/extension-installer/issues",
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
},
- "time": "2024-09-04T20:21:43+00:00"
+ "time": "2024-09-04T20:21:43+00:00",
+ "published-time": "2024-09-04T20:22:27+00:00"
},
{
"name": "phpstan/phpstan",
@@ -1292,7 +1306,8 @@
"type": "github"
}
],
- "time": "2026-10-28T22:38:53+00:00"
+ "time": "2026-10-28T22:38:53+00:00",
+ "published-time": "2026-10-28T22:39:06+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -1343,7 +1358,8 @@
"issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues",
"source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5"
},
- "time": "2026-07-22T06:50:43+00:00"
+ "time": "2026-07-22T06:50:43+00:00",
+ "published-time": "2026-07-27T15:18:00+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
@@ -1395,7 +1411,8 @@
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0"
},
- "time": "2026-10-01T21:30:37+00:00"
+ "time": "2026-10-01T21:30:37+00:00",
+ "published-time": "2026-10-06T07:32:29+00:00"
},
{
"name": "psr/container",
@@ -1448,7 +1465,8 @@
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
- "time": "2021-11-05T16:47:00+00:00"
+ "time": "2021-11-05T16:47:00+00:00",
+ "published-time": "2021-11-05T16:53:27+00:00"
},
{
"name": "roave/security-advisories",
@@ -1456,12 +1474,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "8e26a62e09d971af73b5455efcc52f6d142741c9"
+ "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/8e26a62e09d971af73b5455efcc52f6d142741c9",
- "reference": "8e26a62e09d971af73b5455efcc52f6d142741c9",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7c964bc969c1b16050a4b85b7a55614777a3f597",
+ "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597",
"shasum": ""
},
"conflict": {
@@ -1469,7 +1487,7 @@
"adaptcms/adaptcms": "<=1.3",
"adawolfa/isdoc": "<1.4.3|>=1.5,<1.5.1|>=1.6,<1.6.1",
"admidio/admidio": "<=5.0.11",
- "adminneo-org/adminneo": ">=4.6.2,<5.5",
+ "adminneo-org/adminneo": "<5.5.1",
"adodb/adodb-php": "<=5.22.9",
"aheinze/cockpit": "<2.2",
"aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2",
@@ -2618,7 +2636,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-30T20:26:18+00:00"
+ "time": "2026-10-31T00:24:47+00:00",
+ "published-time": "2026-10-31T00:24:51+00:00"
},
{
"name": "sniccowp/php-scoper-wordpress-excludes",
@@ -2665,7 +2684,8 @@
"issues": "https://github.com/snicco/php-scoper-wordpress-excludes/issues",
"source": "https://github.com/snicco/php-scoper-wordpress-excludes/tree/7.1.0"
},
- "time": "2026-08-28T02:50:05+00:00"
+ "time": "2026-08-28T02:50:05+00:00",
+ "published-time": "2026-09-23T19:33:26+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@@ -2744,7 +2764,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-08-06T00:17:32+00:00"
+ "time": "2026-08-06T00:17:32+00:00",
+ "published-time": "2026-08-06T01:05:02+00:00"
},
{
"name": "swissspidy/phpstan-no-private",
@@ -2796,7 +2817,8 @@
"issues": "https://github.com/swissspidy/phpstan-no-private/issues",
"source": "https://github.com/swissspidy/phpstan-no-private/tree/v1.0.0"
},
- "time": "2024-11-11T11:04:45+00:00"
+ "time": "2024-11-11T11:04:45+00:00",
+ "published-time": "2024-11-11T11:08:03+00:00"
},
{
"name": "symfony/console",
@@ -2894,7 +2916,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-28T11:08:27+00:00"
+ "time": "2026-10-28T11:08:27+00:00",
+ "published-time": "2026-10-28T18:21:33+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -2965,7 +2988,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-05T06:23:12+00:00"
+ "time": "2026-06-05T06:23:12+00:00",
+ "published-time": "2026-06-27T21:46:34+00:00"
},
{
"name": "symfony/filesystem",
@@ -3035,7 +3059,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-06T20:05:19+00:00"
+ "time": "2026-10-06T20:05:19+00:00",
+ "published-time": "2026-10-14T06:45:33+00:00"
},
{
"name": "symfony/finder",
@@ -3103,7 +3128,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-19T16:56:54+00:00"
+ "time": "2026-10-19T16:56:54+00:00",
+ "published-time": "2026-10-28T18:21:14+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -3186,7 +3212,8 @@
"type": "tidelift"
}
],
- "time": "2026-04-10T16:19:22+00:00"
+ "time": "2026-04-10T16:19:22+00:00",
+ "published-time": "2026-04-26T14:44:50+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@@ -3268,7 +3295,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-15T09:18:53+00:00"
+ "time": "2026-10-15T09:18:53+00:00",
+ "published-time": "2026-10-28T10:07:44+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
@@ -3353,7 +3381,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-23T22:02:09+00:00"
+ "time": "2026-09-23T22:02:09+00:00",
+ "published-time": "2026-09-30T10:23:47+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@@ -3438,7 +3467,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-28T09:35:49+00:00"
+ "time": "2026-10-28T09:35:49+00:00",
+ "published-time": "2026-10-28T10:07:45+00:00"
},
{
"name": "symfony/process",
@@ -3503,7 +3533,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-25T14:40:16+00:00"
+ "time": "2026-10-25T14:40:16+00:00",
+ "published-time": "2026-10-28T18:21:18+00:00"
},
{
"name": "symfony/service-contracts",
@@ -3590,7 +3621,8 @@
"type": "tidelift"
}
],
- "time": "2026-07-27T15:39:01+00:00"
+ "time": "2026-07-27T15:39:01+00:00",
+ "published-time": "2026-08-30T19:56:37+00:00"
},
{
"name": "symfony/string",
@@ -3680,7 +3712,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-23T17:06:53+00:00"
+ "time": "2026-10-23T17:06:53+00:00",
+ "published-time": "2026-10-28T18:24:27+00:00"
},
{
"name": "szepeviktor/phpstan-wordpress",
@@ -3743,7 +3776,8 @@
"issues": "https://github.com/szepeviktor/phpstan-wordpress/issues",
"source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4"
},
- "time": "2026-05-22T16:22:09+00:00"
+ "time": "2026-05-22T16:22:09+00:00",
+ "published-time": "2026-08-28T06:48:49+00:00"
},
{
"name": "wp-coding-standards/wpcs",
@@ -3809,7 +3843,8 @@
"type": "custom"
}
],
- "time": "2026-07-27T11:53:23+00:00"
+ "time": "2026-07-27T11:53:23+00:00",
+ "published-time": "2026-07-27T11:55:52+00:00"
},
{
"name": "wp-hooks/wordpress-core",
@@ -3892,7 +3927,8 @@
"type": "github"
}
],
- "time": "2026-08-20T19:54:29+00:00"
+ "time": "2026-08-20T19:54:29+00:00",
+ "published-time": "2026-08-20T19:54:42+00:00"
},
{
"name": "wp-plugin/woocommerce",
@@ -3923,7 +3959,8 @@
"issues": "https://wordpress.org/support/plugin/woocommerce",
"source": "https://plugins.svn.wordpress.org/woocommerce"
},
- "time": "2026-10-09T15:35:21+00:00"
+ "time": "2026-10-09T15:35:21+00:00",
+ "published-time": "2026-10-09T15:35:21+00:00"
}
],
"aliases": [],
@@ -3937,5 +3974,5 @@
"php": ">=8.5"
},
"platform-dev": {},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.11.0"
}
diff --git a/package-lock.json b/package-lock.json
index 72de75f..8f43839 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -370,9 +370,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -429,13 +429,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
- "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz",
+ "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
+ "@babel/types": "^7.29.11"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -708,9 +708,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
- "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.11.tgz",
+ "integrity": "sha512-iARF4jjHSM0v8DImCACPXrwEltBP3DgqDpXTvjAMBAVpyEY0LiGss78wvI+KtmW+IJNEvZ5sOhPDCsOsXsIybA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -758,9 +758,9 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
- "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.11.tgz",
+ "integrity": "sha512-f8kTDM/nTvpLeafb+TJ4jboFr/Y3BbC+QzgPpPGzSKJqWLnj9LW0WLqv4XKA11LX5sr+EgvLE6Ix3YsCEusodw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -769,7 +769,7 @@
"@babel/helper-globals": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7",
"@babel/helper-replace-supers": "^7.29.7",
- "@babel/traverse": "^7.29.7"
+ "@babel/traverse": "^7.29.10"
},
"engines": {
"node": ">=6.9.0"
@@ -1494,9 +1494,9 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz",
- "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.11.tgz",
+ "integrity": "sha512-5xWGhfuMgbOfIjsk/I2c4uMnoqRwQdKYi4QjvybQrcEVE0G9/p3qmIkp2Sxe8YF8DGNNVs5whfwNqIM5684XoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1832,13 +1832,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-string-parser": "^7.29.11",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
@@ -6435,9 +6435,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8866,9 +8866,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.11.31",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.31.tgz",
- "integrity": "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q==",
+ "version": "2.11.32",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz",
+ "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -10883,9 +10883,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.463",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz",
- "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==",
+ "version": "1.5.464",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz",
+ "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==",
"dev": true,
"license": "ISC"
},
@@ -13296,9 +13296,9 @@
}
},
"node_modules/http-proxy-middleware": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.0.tgz",
- "integrity": "sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.1.tgz",
+ "integrity": "sha512-60SmYZpPD4dPbC2GZVkVV4uP/V/cbsXSwzGQXeHWbbE3Q1XdccJpqHTg6NJ6iMpFGVA1OEry8c8kHhx4ahiU1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14781,9 +14781,9 @@
}
},
"node_modules/legacy-javascript": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.3.tgz",
- "integrity": "sha512-1r6MT4dVaJyXt4G07Cm2cQ0AUaLJdefiEp4SmIAylA++gnEQ82YvmzE58RkM5EP0fk5hROLCslUPGnwIYdZQVA==",
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.4.tgz",
+ "integrity": "sha512-UTmeKBVxnfJcpmpOvzzNnLsQT99BXHpPlc/ZKddIRmWmrR0fw6335I3lq4+sJoP60UTbBNaYsGYVPc65YB1x3A==",
"dev": true,
"license": "Apache-2.0"
},
@@ -15257,9 +15257,9 @@
"license": "Python-2.0"
},
"node_modules/markdownlint-cli/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -20863,9 +20863,9 @@
}
},
"node_modules/stylelint/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -22119,9 +22119,9 @@
"license": "Apache-2.0"
},
"node_modules/webpack": {
- "version": "5.112.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.1.tgz",
- "integrity": "sha512-DsEgOfaY5XrvTTEU7QvktlOpYEipgNUyXQY4rQO4KtFmr71IwjMoRWyv9rJNo6qwqctRzh9sO9VIwMgnWmiBVg==",
+ "version": "5.112.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.2.tgz",
+ "integrity": "sha512-B2tp1LI9D8GVpPDqprdt12ukTkXXahj9MzqSWJq4faSPgOUM7CdZaGAB6HrL/8E6UwbBpdj4vzWDbKoFnVoU6g==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/plugins/internal-comments/composer.lock b/plugins/internal-comments/composer.lock
index 549309e..cd939fa 100644
--- a/plugins/internal-comments/composer.lock
+++ b/plugins/internal-comments/composer.lock
@@ -370,7 +370,8 @@
"support": {
"source": "https://github.com/Automattic/jetpack-changelogger/tree/v7.0.5"
},
- "time": "2026-10-20T16:01:37+00:00"
+ "time": "2026-10-20T16:01:37+00:00",
+ "published-time": "2026-10-20T16:01:50+00:00"
},
{
"name": "composer/class-map-generator",
@@ -439,7 +440,8 @@
"type": "github"
}
],
- "time": "2026-09-25T15:24:19+00:00"
+ "time": "2026-09-25T15:24:19+00:00",
+ "published-time": "2026-09-25T15:27:30+00:00"
},
{
"name": "composer/installers",
@@ -585,7 +587,8 @@
"type": "tidelift"
}
],
- "time": "2024-06-24T20:46:46+00:00"
+ "time": "2024-06-24T20:46:46+00:00",
+ "published-time": "2024-06-24T21:01:36+00:00"
},
{
"name": "composer/pcre",
@@ -661,7 +664,8 @@
"type": "github"
}
],
- "time": "2026-06-07T11:47:49+00:00"
+ "time": "2026-06-07T11:47:49+00:00",
+ "published-time": "2026-06-11T12:12:37+00:00"
},
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
@@ -757,7 +761,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-05-06T08:26:05+00:00"
+ "time": "2026-05-06T08:26:05+00:00",
+ "published-time": "2026-05-06T12:56:03+00:00"
},
{
"name": "fidry/console",
@@ -843,7 +848,8 @@
"type": "github"
}
],
- "time": "2025-02-14T11:06:15+00:00"
+ "time": "2025-02-14T11:06:15+00:00",
+ "published-time": "2025-02-14T11:09:06+00:00"
},
{
"name": "fidry/filesystem",
@@ -909,7 +915,8 @@
"type": "github"
}
],
- "time": "2025-02-13T23:05:19+00:00"
+ "time": "2025-02-13T23:05:19+00:00",
+ "published-time": "2025-02-13T23:09:29+00:00"
},
{
"name": "humbug/php-scoper",
@@ -995,7 +1002,8 @@
"issues": "https://github.com/humbug/php-scoper/issues",
"source": "https://github.com/humbug/php-scoper/tree/0.18.20"
},
- "time": "2026-10-08T08:21:05+00:00"
+ "time": "2026-10-08T08:21:05+00:00",
+ "published-time": "2026-10-08T08:29:48+00:00"
},
{
"name": "jetbrains/phpstorm-stubs",
@@ -1039,7 +1047,8 @@
"stubs",
"type"
],
- "time": "2026-06-12T13:19:10+00:00"
+ "time": "2026-06-12T13:19:10+00:00",
+ "published-time": "2026-07-22T11:41:13+00:00"
},
{
"name": "johnbillion/wp-compat",
@@ -1114,7 +1123,8 @@
"type": "github"
}
],
- "time": "2026-10-21T20:54:48+00:00"
+ "time": "2026-10-21T20:54:48+00:00",
+ "published-time": "2026-10-21T20:54:55+00:00"
},
{
"name": "johnpbloch/wordpress-core",
@@ -1162,7 +1172,8 @@
"source": "https://core.trac.wordpress.org/browser",
"wiki": "https://codex.wordpress.org/"
},
- "time": "2026-10-28T19:49:26+00:00"
+ "time": "2026-10-28T19:49:26+00:00",
+ "published-time": "2026-10-28T19:49:51+00:00"
},
{
"name": "laravel/serializable-closure",
@@ -1223,7 +1234,8 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2026-09-22T14:32:34+00:00"
+ "time": "2026-09-22T14:32:34+00:00",
+ "published-time": "2026-09-22T14:33:13+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -1283,7 +1295,8 @@
"type": "github"
}
],
- "time": "2026-08-11T10:17:44+00:00"
+ "time": "2026-08-11T10:17:44+00:00",
+ "published-time": "2026-08-11T10:29:20+00:00"
},
{
"name": "nikic/php-parser",
@@ -1340,7 +1353,8 @@
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0"
},
- "time": "2026-09-13T18:51:52+00:00"
+ "time": "2026-09-13T18:51:52+00:00",
+ "published-time": "2026-09-13T18:53:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -1407,7 +1421,8 @@
"type": "github"
}
],
- "time": "2024-03-03T12:33:53+00:00"
+ "time": "2024-03-03T12:33:53+00:00",
+ "published-time": "2024-03-03T12:39:44+00:00"
},
{
"name": "phar-io/version",
@@ -1458,7 +1473,8 @@
"issues": "https://github.com/phar-io/version/issues",
"source": "https://github.com/phar-io/version/tree/3.2.1"
},
- "time": "2022-02-21T01:04:05+00:00"
+ "time": "2022-02-21T01:04:05+00:00",
+ "published-time": "2022-02-21T01:04:30+00:00"
},
{
"name": "php-di/invoker",
@@ -1513,7 +1529,8 @@
"type": "github"
}
],
- "time": "2025-08-30T10:22:22+00:00"
+ "time": "2025-08-30T10:22:22+00:00",
+ "published-time": "2025-08-30T10:22:43+00:00"
},
{
"name": "php-di/php-di",
@@ -1586,7 +1603,8 @@
"type": "tidelift"
}
],
- "time": "2025-08-16T11:10:48+00:00"
+ "time": "2025-08-16T11:10:48+00:00",
+ "published-time": "2025-08-16T11:11:08+00:00"
},
{
"name": "php-stubs/woocommerce-stubs",
@@ -1630,7 +1648,8 @@
"issues": "https://github.com/php-stubs/woocommerce-stubs/issues",
"source": "https://github.com/php-stubs/woocommerce-stubs/tree/v11.2.0"
},
- "time": "2026-10-07T19:02:59+00:00"
+ "time": "2026-10-07T19:02:59+00:00",
+ "published-time": "2026-10-07T19:04:59+00:00"
},
{
"name": "php-stubs/wordpress-stubs",
@@ -1682,7 +1701,8 @@
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.2"
},
- "time": "2026-10-06T06:59:21+00:00"
+ "time": "2026-10-06T06:59:21+00:00",
+ "published-time": "2026-10-06T07:32:21+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@@ -1772,7 +1792,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-28T11:36:33+00:00"
+ "time": "2025-11-28T11:36:33+00:00",
+ "published-time": "2025-11-28T11:41:33+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-paragonie",
@@ -1843,7 +1864,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-29T13:09:49+00:00"
+ "time": "2025-11-29T13:09:49+00:00",
+ "published-time": "2025-11-29T13:22:17+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-wp",
@@ -1910,7 +1932,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-12-16T13:35:20+00:00"
+ "time": "2025-12-16T13:35:20+00:00",
+ "published-time": "2025-12-16T13:47:29+00:00"
},
{
"name": "phpcsstandards/phpcsextra",
@@ -1992,7 +2015,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-07-27T11:13:17+00:00"
+ "time": "2026-07-27T11:13:17+00:00",
+ "published-time": "2026-07-27T11:19:59+00:00"
},
{
"name": "phpcsstandards/phpcsutils",
@@ -2085,7 +2109,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-07-27T10:28:41+00:00"
+ "time": "2026-07-27T10:28:41+00:00",
+ "published-time": "2026-07-27T10:36:31+00:00"
},
{
"name": "phpstan/extension-installer",
@@ -2133,7 +2158,8 @@
"issues": "https://github.com/phpstan/extension-installer/issues",
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
},
- "time": "2024-09-04T20:21:43+00:00"
+ "time": "2024-09-04T20:21:43+00:00",
+ "published-time": "2024-09-04T20:22:27+00:00"
},
{
"name": "phpstan/phpstan",
@@ -2197,7 +2223,8 @@
"type": "github"
}
],
- "time": "2026-10-28T22:38:53+00:00"
+ "time": "2026-10-28T22:38:53+00:00",
+ "published-time": "2026-10-28T22:39:06+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -2248,7 +2275,8 @@
"issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues",
"source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5"
},
- "time": "2026-07-22T06:50:43+00:00"
+ "time": "2026-07-22T06:50:43+00:00",
+ "published-time": "2026-07-27T15:18:00+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
@@ -2300,7 +2328,8 @@
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0"
},
- "time": "2026-10-01T21:30:37+00:00"
+ "time": "2026-10-01T21:30:37+00:00",
+ "published-time": "2026-10-06T07:32:29+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -2389,7 +2418,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-30T08:11:15+00:00"
+ "time": "2026-10-30T08:11:15+00:00",
+ "published-time": "2026-10-30T08:11:34+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -2462,7 +2492,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-25T14:47:43+00:00"
+ "time": "2026-08-25T14:47:43+00:00",
+ "published-time": "2026-08-25T14:48:49+00:00"
},
{
"name": "phpunit/php-invoker",
@@ -2538,7 +2569,8 @@
"type": "tidelift"
}
],
- "time": "2026-02-06T04:34:47+00:00"
+ "time": "2026-02-06T04:34:47+00:00",
+ "published-time": "2026-02-06T04:35:29+00:00"
},
{
"name": "phpunit/php-text-template",
@@ -2610,7 +2642,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-03T04:54:24+00:00"
+ "time": "2026-10-03T04:54:24+00:00",
+ "published-time": "2026-10-03T04:54:39+00:00"
},
{
"name": "phpunit/php-timer",
@@ -2682,7 +2715,8 @@
"type": "tidelift"
}
],
- "time": "2026-02-06T04:37:53+00:00"
+ "time": "2026-02-06T04:37:53+00:00",
+ "published-time": "2026-02-06T04:38:39+00:00"
},
{
"name": "phpunit/phpunit",
@@ -2774,7 +2808,8 @@
"type": "other"
}
],
- "time": "2026-10-27T04:16:22+00:00"
+ "time": "2026-10-27T04:16:22+00:00",
+ "published-time": "2026-10-27T04:18:40+00:00"
},
{
"name": "pronamic/changelog-md-to-wordpress-plugin-readme-txt",
@@ -2814,7 +2849,8 @@
"issues": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/issues",
"source": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/tree/v1.0.0"
},
- "time": "2024-10-01T13:41:34+00:00"
+ "time": "2024-10-01T13:41:34+00:00",
+ "published-time": "2024-10-02T09:31:51+00:00"
},
{
"name": "psr/clock",
@@ -2862,7 +2898,8 @@
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
- "time": "2022-11-25T14:36:26+00:00"
+ "time": "2022-11-25T14:36:26+00:00",
+ "published-time": "2022-11-25T14:59:22+00:00"
},
{
"name": "psr/container",
@@ -2915,7 +2952,8 @@
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
- "time": "2021-11-05T16:47:00+00:00"
+ "time": "2021-11-05T16:47:00+00:00",
+ "published-time": "2021-11-05T16:53:27+00:00"
},
{
"name": "psr/event-dispatcher",
@@ -2965,7 +3003,8 @@
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
- "time": "2019-01-08T18:20:26+00:00"
+ "time": "2019-01-08T18:20:26+00:00",
+ "published-time": "2019-03-25T15:31:44+00:00"
},
{
"name": "psr/log",
@@ -3015,7 +3054,8 @@
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
- "time": "2024-09-11T13:17:53+00:00"
+ "time": "2024-09-11T13:17:53+00:00",
+ "published-time": "2024-09-11T13:21:14+00:00"
},
{
"name": "psr/simple-cache",
@@ -3066,7 +3106,8 @@
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
- "time": "2021-10-29T13:26:27+00:00"
+ "time": "2021-10-29T13:26:27+00:00",
+ "published-time": "2021-10-29T13:27:19+00:00"
},
{
"name": "roave/security-advisories",
@@ -3074,12 +3115,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66"
+ "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66",
- "reference": "7fd2aa4f9a2f0db3e04724273d5121c1d16a6c66",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7c964bc969c1b16050a4b85b7a55614777a3f597",
+ "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597",
"shasum": ""
},
"conflict": {
@@ -3087,6 +3128,7 @@
"adaptcms/adaptcms": "<=1.3",
"adawolfa/isdoc": "<1.4.3|>=1.5,<1.5.1|>=1.6,<1.6.1",
"admidio/admidio": "<=5.0.11",
+ "adminneo-org/adminneo": "<5.5.1",
"adodb/adodb-php": "<=5.22.9",
"aheinze/cockpit": "<2.2",
"aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2",
@@ -4235,7 +4277,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-29T16:37:57+00:00"
+ "time": "2026-10-31T00:24:47+00:00",
+ "published-time": "2026-10-31T00:24:51+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -4304,7 +4347,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-01T04:27:14+00:00"
+ "time": "2026-08-01T04:27:14+00:00",
+ "published-time": "2026-08-01T04:29:01+00:00"
},
{
"name": "sebastian/comparator",
@@ -4396,7 +4440,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-07T07:23:13+00:00"
+ "time": "2026-08-07T07:23:13+00:00",
+ "published-time": "2026-08-07T07:23:29+00:00"
},
{
"name": "sebastian/complexity",
@@ -4466,7 +4511,8 @@
"type": "tidelift"
}
],
- "time": "2026-02-06T04:41:32+00:00"
+ "time": "2026-02-06T04:41:32+00:00",
+ "published-time": "2026-02-06T04:41:49+00:00"
},
{
"name": "sebastian/diff",
@@ -4545,7 +4591,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-25T15:38:55+00:00"
+ "time": "2026-08-25T15:38:55+00:00",
+ "published-time": "2026-08-25T15:39:44+00:00"
},
{
"name": "sebastian/environment",
@@ -4621,7 +4668,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-01T12:37:56+00:00"
+ "time": "2026-10-01T12:37:56+00:00",
+ "published-time": "2026-10-01T12:38:48+00:00"
},
{
"name": "sebastian/exporter",
@@ -4711,7 +4759,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-07T07:22:06+00:00"
+ "time": "2026-08-07T07:22:06+00:00",
+ "published-time": "2026-08-07T07:23:47+00:00"
},
{
"name": "sebastian/file-filter",
@@ -4780,7 +4829,8 @@
"type": "tidelift"
}
],
- "time": "2026-04-22T07:20:04+00:00"
+ "time": "2026-04-22T07:20:04+00:00",
+ "published-time": "2026-04-22T07:20:25+00:00"
},
{
"name": "sebastian/git-state",
@@ -4849,7 +4899,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-21T05:59:52+00:00"
+ "time": "2026-10-21T05:59:52+00:00",
+ "published-time": "2026-10-21T06:01:28+00:00"
},
{
"name": "sebastian/global-state",
@@ -4923,7 +4974,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-01T15:11:33+00:00"
+ "time": "2026-06-01T15:11:33+00:00",
+ "published-time": "2026-06-01T15:20:45+00:00"
},
{
"name": "sebastian/lines-of-code",
@@ -4993,7 +5045,8 @@
"type": "tidelift"
}
],
- "time": "2026-07-09T08:42:34+00:00"
+ "time": "2026-07-09T08:42:34+00:00",
+ "published-time": "2026-07-09T08:44:06+00:00"
},
{
"name": "sebastian/object-enumerator",
@@ -5062,7 +5115,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-13T07:05:05+00:00"
+ "time": "2026-08-13T07:05:05+00:00",
+ "published-time": "2026-08-13T07:07:58+00:00"
},
{
"name": "sebastian/object-reflector",
@@ -5130,7 +5184,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-13T06:34:36+00:00"
+ "time": "2026-08-13T06:34:36+00:00",
+ "published-time": "2026-08-13T06:39:22+00:00"
},
{
"name": "sebastian/recursion-context",
@@ -5206,7 +5261,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-03T05:58:12+00:00"
+ "time": "2026-08-03T05:58:12+00:00",
+ "published-time": "2026-08-03T05:59:59+00:00"
},
{
"name": "sebastian/type",
@@ -5275,7 +5331,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-10T08:00:57+00:00"
+ "time": "2026-08-10T08:00:57+00:00",
+ "published-time": "2026-08-10T08:01:43+00:00"
},
{
"name": "sebastian/version",
@@ -5341,7 +5398,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-04T05:51:14+00:00"
+ "time": "2026-10-04T05:51:14+00:00",
+ "published-time": "2026-10-04T05:52:22+00:00"
},
{
"name": "sniccowp/php-scoper-wordpress-excludes",
@@ -5388,7 +5446,8 @@
"issues": "https://github.com/snicco/php-scoper-wordpress-excludes/issues",
"source": "https://github.com/snicco/php-scoper-wordpress-excludes/tree/7.1.0"
},
- "time": "2026-08-28T02:50:05+00:00"
+ "time": "2026-08-28T02:50:05+00:00",
+ "published-time": "2026-09-23T19:33:26+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@@ -5467,7 +5526,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-08-06T00:17:32+00:00"
+ "time": "2026-08-06T00:17:32+00:00",
+ "published-time": "2026-08-06T01:05:02+00:00"
},
{
"name": "staabm/side-effects-detector",
@@ -5519,7 +5579,8 @@
"type": "github"
}
],
- "time": "2024-10-20T05:08:20+00:00"
+ "time": "2024-10-20T05:08:20+00:00",
+ "published-time": "2024-10-20T05:09:39+00:00"
},
{
"name": "swissspidy/phpstan-no-private",
@@ -5571,7 +5632,8 @@
"issues": "https://github.com/swissspidy/phpstan-no-private/issues",
"source": "https://github.com/swissspidy/phpstan-no-private/tree/v1.0.0"
},
- "time": "2024-11-11T11:04:45+00:00"
+ "time": "2024-11-11T11:04:45+00:00",
+ "published-time": "2024-11-11T11:08:03+00:00"
},
{
"name": "symfony/console",
@@ -5669,7 +5731,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-28T11:08:27+00:00"
+ "time": "2026-10-28T11:08:27+00:00",
+ "published-time": "2026-10-28T18:21:33+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -5740,7 +5803,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-05T06:23:12+00:00"
+ "time": "2026-06-05T06:23:12+00:00",
+ "published-time": "2026-06-27T21:46:34+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -5820,7 +5884,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-05T06:23:12+00:00"
+ "time": "2026-06-05T06:23:12+00:00",
+ "published-time": "2026-06-27T21:46:35+00:00"
},
{
"name": "symfony/filesystem",
@@ -5890,7 +5955,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-06T20:05:19+00:00"
+ "time": "2026-10-06T20:05:19+00:00",
+ "published-time": "2026-10-14T06:45:33+00:00"
},
{
"name": "symfony/finder",
@@ -5958,7 +6024,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-19T16:56:54+00:00"
+ "time": "2026-10-19T16:56:54+00:00",
+ "published-time": "2026-10-28T18:21:14+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -6041,7 +6108,8 @@
"type": "tidelift"
}
],
- "time": "2026-04-10T16:19:22+00:00"
+ "time": "2026-04-10T16:19:22+00:00",
+ "published-time": "2026-04-26T14:44:50+00:00"
},
{
"name": "symfony/polyfill-iconv",
@@ -6125,7 +6193,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-29T06:25:14+00:00"
+ "time": "2026-09-29T06:25:14+00:00",
+ "published-time": "2026-09-30T10:23:39+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@@ -6207,7 +6276,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-15T09:18:53+00:00"
+ "time": "2026-10-15T09:18:53+00:00",
+ "published-time": "2026-10-28T10:07:44+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
@@ -6292,7 +6362,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-23T22:02:09+00:00"
+ "time": "2026-09-23T22:02:09+00:00",
+ "published-time": "2026-09-30T10:23:47+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@@ -6377,7 +6448,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-28T09:35:49+00:00"
+ "time": "2026-10-28T09:35:49+00:00",
+ "published-time": "2026-10-28T10:07:45+00:00"
},
{
"name": "symfony/polyfill-php84",
@@ -6457,7 +6529,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-23T22:01:09+00:00"
+ "time": "2026-09-23T22:01:09+00:00",
+ "published-time": "2026-09-30T10:23:33+00:00"
},
{
"name": "symfony/process",
@@ -6522,7 +6595,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-25T14:40:16+00:00"
+ "time": "2026-10-25T14:40:16+00:00",
+ "published-time": "2026-10-28T18:21:18+00:00"
},
{
"name": "symfony/service-contracts",
@@ -6609,7 +6683,8 @@
"type": "tidelift"
}
],
- "time": "2026-07-27T15:39:01+00:00"
+ "time": "2026-07-27T15:39:01+00:00",
+ "published-time": "2026-08-30T19:56:37+00:00"
},
{
"name": "symfony/string",
@@ -6699,7 +6774,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-23T17:06:53+00:00"
+ "time": "2026-10-23T17:06:53+00:00",
+ "published-time": "2026-10-28T18:24:27+00:00"
},
{
"name": "symfony/var-dumper",
@@ -6786,7 +6862,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-25T21:04:04+00:00"
+ "time": "2026-10-25T21:04:04+00:00",
+ "published-time": "2026-10-28T18:20:00+00:00"
},
{
"name": "szepeviktor/phpstan-wordpress",
@@ -6849,7 +6926,8 @@
"issues": "https://github.com/szepeviktor/phpstan-wordpress/issues",
"source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4"
},
- "time": "2026-05-22T16:22:09+00:00"
+ "time": "2026-05-22T16:22:09+00:00",
+ "published-time": "2026-08-28T06:48:49+00:00"
},
{
"name": "thecodingmachine/safe",
@@ -6992,7 +7070,8 @@
"type": "github"
}
],
- "time": "2026-02-04T18:08:13+00:00"
+ "time": "2026-02-04T18:08:13+00:00",
+ "published-time": "2026-02-15T15:48:29+00:00"
},
{
"name": "webmozart/assert",
@@ -7050,7 +7129,8 @@
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
- "time": "2025-10-29T15:56:20+00:00"
+ "time": "2025-10-29T15:56:20+00:00",
+ "published-time": "2025-10-29T15:56:42+00:00"
},
{
"name": "wp-coding-standards/wpcs",
@@ -7116,7 +7196,8 @@
"type": "custom"
}
],
- "time": "2026-07-27T11:53:23+00:00"
+ "time": "2026-07-27T11:53:23+00:00",
+ "published-time": "2026-07-27T11:55:52+00:00"
},
{
"name": "wp-hooks/wordpress-core",
@@ -7199,7 +7280,8 @@
"type": "github"
}
],
- "time": "2026-08-20T19:54:29+00:00"
+ "time": "2026-08-20T19:54:29+00:00",
+ "published-time": "2026-08-20T19:54:42+00:00"
},
{
"name": "wp-plugin/woocommerce",
@@ -7230,7 +7312,8 @@
"issues": "https://wordpress.org/support/plugin/woocommerce",
"source": "https://plugins.svn.wordpress.org/woocommerce"
},
- "time": "2026-10-09T15:35:21+00:00"
+ "time": "2026-10-09T15:35:21+00:00",
+ "published-time": "2026-10-09T15:35:21+00:00"
}
],
"aliases": [],
@@ -7244,5 +7327,5 @@
"php": ">=8.5"
},
"platform-dev": {},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.11.0"
}
diff --git a/plugins/internal-comments/package-lock.json b/plugins/internal-comments/package-lock.json
index 5ce1977..99a817c 100644
--- a/plugins/internal-comments/package-lock.json
+++ b/plugins/internal-comments/package-lock.json
@@ -391,9 +391,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -450,13 +450,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
- "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz",
+ "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
+ "@babel/types": "^7.29.11"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -729,9 +729,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
- "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.11.tgz",
+ "integrity": "sha512-iARF4jjHSM0v8DImCACPXrwEltBP3DgqDpXTvjAMBAVpyEY0LiGss78wvI+KtmW+IJNEvZ5sOhPDCsOsXsIybA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -779,9 +779,9 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
- "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.11.tgz",
+ "integrity": "sha512-f8kTDM/nTvpLeafb+TJ4jboFr/Y3BbC+QzgPpPGzSKJqWLnj9LW0WLqv4XKA11LX5sr+EgvLE6Ix3YsCEusodw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -790,7 +790,7 @@
"@babel/helper-globals": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7",
"@babel/helper-replace-supers": "^7.29.7",
- "@babel/traverse": "^7.29.7"
+ "@babel/traverse": "^7.29.10"
},
"engines": {
"node": ">=6.9.0"
@@ -1515,9 +1515,9 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz",
- "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.11.tgz",
+ "integrity": "sha512-5xWGhfuMgbOfIjsk/I2c4uMnoqRwQdKYi4QjvybQrcEVE0G9/p3qmIkp2Sxe8YF8DGNNVs5whfwNqIM5684XoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1853,13 +1853,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-string-parser": "^7.29.11",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
@@ -6456,9 +6456,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8912,9 +8912,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.11.31",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.31.tgz",
- "integrity": "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q==",
+ "version": "2.11.32",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz",
+ "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -10929,9 +10929,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.463",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz",
- "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==",
+ "version": "1.5.464",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz",
+ "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==",
"dev": true,
"license": "ISC"
},
@@ -12257,13 +12257,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/express/node_modules/path-to-regexp": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
- "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -13349,9 +13342,9 @@
}
},
"node_modules/http-proxy-middleware": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.0.tgz",
- "integrity": "sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.1.tgz",
+ "integrity": "sha512-60SmYZpPD4dPbC2GZVkVV4uP/V/cbsXSwzGQXeHWbbE3Q1XdccJpqHTg6NJ6iMpFGVA1OEry8c8kHhx4ahiU1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14834,9 +14827,9 @@
}
},
"node_modules/legacy-javascript": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.3.tgz",
- "integrity": "sha512-1r6MT4dVaJyXt4G07Cm2cQ0AUaLJdefiEp4SmIAylA++gnEQ82YvmzE58RkM5EP0fk5hROLCslUPGnwIYdZQVA==",
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.4.tgz",
+ "integrity": "sha512-UTmeKBVxnfJcpmpOvzzNnLsQT99BXHpPlc/ZKddIRmWmrR0fw6335I3lq4+sJoP60UTbBNaYsGYVPc65YB1x3A==",
"dev": true,
"license": "Apache-2.0"
},
@@ -15310,9 +15303,9 @@
"license": "Python-2.0"
},
"node_modules/markdownlint-cli/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -17433,6 +17426,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -18440,13 +18440,6 @@
"react-is": "^16.13.1"
}
},
- "node_modules/prop-types/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/proxy-addr": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
@@ -18707,6 +18700,13 @@
"react": "^19.3.1"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -20909,9 +20909,9 @@
}
},
"node_modules/stylelint/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -22164,9 +22164,9 @@
"license": "Apache-2.0"
},
"node_modules/webpack": {
- "version": "5.112.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.1.tgz",
- "integrity": "sha512-DsEgOfaY5XrvTTEU7QvktlOpYEipgNUyXQY4rQO4KtFmr71IwjMoRWyv9rJNo6qwqctRzh9sO9VIwMgnWmiBVg==",
+ "version": "5.112.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.2.tgz",
+ "integrity": "sha512-B2tp1LI9D8GVpPDqprdt12ukTkXXahj9MzqSWJq4faSPgOUM7CdZaGAB6HrL/8E6UwbBpdj4vzWDbKoFnVoU6g==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/plugins/linked-orders-for-woocommerce/composer.lock b/plugins/linked-orders-for-woocommerce/composer.lock
index f6057e6..01c7822 100644
--- a/plugins/linked-orders-for-woocommerce/composer.lock
+++ b/plugins/linked-orders-for-woocommerce/composer.lock
@@ -12,7 +12,7 @@
"version": "dev-trunk",
"source": {
"type": "git",
- "url": "git@github.com:ahegyes/wordpress-configs.git",
+ "url": "https://github.com/ahegyes/wordpress-configs.git",
"reference": "8d53efd292e4abee4a9c57b0e379434f54bedbe9"
},
"dist": {
@@ -135,6 +135,10 @@
}
],
"description": "Shared PHPCS, PHPStan and php-scoper configurations and reusable GitHub Actions workflows for WordPress projects.",
+ "support": {
+ "source": "https://github.com/ahegyes/wordpress-configs/tree/trunk",
+ "issues": "https://github.com/ahegyes/wordpress-configs/issues"
+ },
"time": "2026-10-30T11:34:20+00:00"
},
{
@@ -142,7 +146,7 @@
"version": "dev-trunk",
"source": {
"type": "git",
- "url": "git@github.com:ahegyes/wp-framework-bootstrap.git",
+ "url": "https://github.com/ahegyes/wp-framework-bootstrap.git",
"reference": "6f48ed4ed852ba35a06ef46dcfc104170732a189"
},
"dist": {
@@ -167,14 +171,17 @@
],
"description": "The code a WordPress plugin runs before its autoloader. It parses on PHP 7.4, so a site below the plugin's floors gets a notice instead of a fatal error and still receives the release that fixes it.",
"homepage": "https://github.com/ahegyes/wordpress-framework",
- "time": "2026-10-30T10:27:50+00:00"
+ "support": {
+ "source": "https://github.com/ahegyes/wp-framework-bootstrap/tree/trunk"
+ },
+ "time": "2026-10-30T10:32:53+00:00"
},
{
"name": "ahegyes/wp-framework-core",
"version": "dev-trunk",
"source": {
"type": "git",
- "url": "git@github.com:ahegyes/wp-framework-core.git",
+ "url": "https://github.com/ahegyes/wp-framework-core.git",
"reference": "6d087554f21ccc65ac52583d8d9e7de542c84cfd"
},
"dist": {
@@ -212,14 +219,17 @@
],
"description": "The plugin kernel. A plugin is composed of small components that load only where they apply, on WordPress's own APIs, with the infrastructure WordPress leaves every plugin to rebuild.",
"homepage": "https://github.com/ahegyes/wordpress-framework",
- "time": "2026-10-30T10:27:50+00:00"
+ "support": {
+ "source": "https://github.com/ahegyes/wp-framework-core/tree/trunk"
+ },
+ "time": "2026-10-30T10:32:53+00:00"
},
{
"name": "ahegyes/wp-framework-settings",
"version": "dev-trunk",
"source": {
"type": "git",
- "url": "git@github.com:ahegyes/wp-framework-settings.git",
+ "url": "https://github.com/ahegyes/wp-framework-settings.git",
"reference": "5942e878af9f8536698c724116ceef5b50948e24"
},
"dist": {
@@ -251,6 +261,9 @@
],
"description": "Settings screens and edit-screen fields declared as data, so WordPress's own APIs render, validate and save them and a plugin never hand-writes form handling.",
"homepage": "https://github.com/ahegyes/wordpress-framework",
+ "support": {
+ "source": "https://github.com/ahegyes/wp-framework-settings/tree/trunk"
+ },
"time": "2026-10-30T10:12:55+00:00"
},
{
@@ -258,7 +271,7 @@
"version": "dev-trunk",
"source": {
"type": "git",
- "url": "git@github.com:ahegyes/wp-framework-shared.git",
+ "url": "https://github.com/ahegyes/wp-framework-shared.git",
"reference": "72568a6fd790b49926f6f74e4dd2478e31d91b8a"
},
"dist": {
@@ -287,6 +300,9 @@
],
"description": "The WordPress-free building blocks every framework package shares, kept portable enough to work unchanged in any PHP project.",
"homepage": "https://github.com/ahegyes/wordpress-framework",
+ "support": {
+ "source": "https://github.com/ahegyes/wp-framework-shared/tree/trunk"
+ },
"time": "2026-10-30T10:12:55+00:00"
},
{
@@ -294,7 +310,7 @@
"version": "dev-trunk",
"source": {
"type": "git",
- "url": "git@github.com:ahegyes/wp-framework-woocommerce.git",
+ "url": "https://github.com/ahegyes/wp-framework-woocommerce.git",
"reference": "9dd04e7c2c0e99160a231ce3a0f17a443a492631"
},
"dist": {
@@ -332,6 +348,9 @@
],
"description": "The framework's settings, edit-screen and logging helpers carried onto WooCommerce's own screens and APIs, for plugins that extend WooCommerce.",
"homepage": "https://github.com/ahegyes/wordpress-framework",
+ "support": {
+ "source": "https://github.com/ahegyes/wp-framework-woocommerce/tree/trunk"
+ },
"time": "2026-10-30T10:12:55+00:00"
},
{
@@ -399,7 +418,8 @@
"support": {
"source": "https://github.com/Automattic/jetpack-changelogger/tree/v7.0.5"
},
- "time": "2026-10-20T16:01:37+00:00"
+ "time": "2026-10-20T16:01:37+00:00",
+ "published-time": "2026-10-20T16:01:50+00:00"
},
{
"name": "composer/class-map-generator",
@@ -468,7 +488,8 @@
"type": "github"
}
],
- "time": "2026-09-25T15:24:19+00:00"
+ "time": "2026-09-25T15:24:19+00:00",
+ "published-time": "2026-09-25T15:27:30+00:00"
},
{
"name": "composer/installers",
@@ -614,7 +635,8 @@
"type": "tidelift"
}
],
- "time": "2024-06-24T20:46:46+00:00"
+ "time": "2024-06-24T20:46:46+00:00",
+ "published-time": "2024-06-24T21:01:36+00:00"
},
{
"name": "composer/pcre",
@@ -690,7 +712,8 @@
"type": "github"
}
],
- "time": "2026-06-07T11:47:49+00:00"
+ "time": "2026-06-07T11:47:49+00:00",
+ "published-time": "2026-06-11T12:12:37+00:00"
},
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
@@ -786,7 +809,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-05-06T08:26:05+00:00"
+ "time": "2026-05-06T08:26:05+00:00",
+ "published-time": "2026-05-06T12:56:03+00:00"
},
{
"name": "fidry/console",
@@ -872,7 +896,8 @@
"type": "github"
}
],
- "time": "2025-02-14T11:06:15+00:00"
+ "time": "2025-02-14T11:06:15+00:00",
+ "published-time": "2025-02-14T11:09:06+00:00"
},
{
"name": "fidry/filesystem",
@@ -938,7 +963,8 @@
"type": "github"
}
],
- "time": "2025-02-13T23:05:19+00:00"
+ "time": "2025-02-13T23:05:19+00:00",
+ "published-time": "2025-02-13T23:09:29+00:00"
},
{
"name": "humbug/php-scoper",
@@ -1024,7 +1050,8 @@
"issues": "https://github.com/humbug/php-scoper/issues",
"source": "https://github.com/humbug/php-scoper/tree/0.18.20"
},
- "time": "2026-10-08T08:21:05+00:00"
+ "time": "2026-10-08T08:21:05+00:00",
+ "published-time": "2026-10-08T08:29:48+00:00"
},
{
"name": "jetbrains/phpstorm-stubs",
@@ -1068,7 +1095,8 @@
"stubs",
"type"
],
- "time": "2026-06-12T13:19:10+00:00"
+ "time": "2026-06-12T13:19:10+00:00",
+ "published-time": "2026-07-22T11:41:13+00:00"
},
{
"name": "johnbillion/wp-compat",
@@ -1143,7 +1171,8 @@
"type": "github"
}
],
- "time": "2026-10-21T20:54:48+00:00"
+ "time": "2026-10-21T20:54:48+00:00",
+ "published-time": "2026-10-21T20:54:55+00:00"
},
{
"name": "johnpbloch/wordpress-core",
@@ -1191,7 +1220,8 @@
"source": "https://core.trac.wordpress.org/browser",
"wiki": "https://codex.wordpress.org/"
},
- "time": "2026-10-28T19:49:26+00:00"
+ "time": "2026-10-28T19:49:26+00:00",
+ "published-time": "2026-10-28T19:49:51+00:00"
},
{
"name": "laravel/serializable-closure",
@@ -1252,7 +1282,8 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2026-09-22T14:32:34+00:00"
+ "time": "2026-09-22T14:32:34+00:00",
+ "published-time": "2026-09-22T14:33:13+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -1312,7 +1343,8 @@
"type": "github"
}
],
- "time": "2026-08-11T10:17:44+00:00"
+ "time": "2026-08-11T10:17:44+00:00",
+ "published-time": "2026-08-11T10:29:20+00:00"
},
{
"name": "nikic/php-parser",
@@ -1369,7 +1401,8 @@
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0"
},
- "time": "2026-09-13T18:51:52+00:00"
+ "time": "2026-09-13T18:51:52+00:00",
+ "published-time": "2026-09-13T18:53:16+00:00"
},
{
"name": "phar-io/manifest",
@@ -1436,7 +1469,8 @@
"type": "github"
}
],
- "time": "2024-03-03T12:33:53+00:00"
+ "time": "2024-03-03T12:33:53+00:00",
+ "published-time": "2024-03-03T12:39:44+00:00"
},
{
"name": "phar-io/version",
@@ -1487,7 +1521,8 @@
"issues": "https://github.com/phar-io/version/issues",
"source": "https://github.com/phar-io/version/tree/3.2.1"
},
- "time": "2022-02-21T01:04:05+00:00"
+ "time": "2022-02-21T01:04:05+00:00",
+ "published-time": "2022-02-21T01:04:30+00:00"
},
{
"name": "php-di/invoker",
@@ -1542,7 +1577,8 @@
"type": "github"
}
],
- "time": "2025-08-30T10:22:22+00:00"
+ "time": "2025-08-30T10:22:22+00:00",
+ "published-time": "2025-08-30T10:22:43+00:00"
},
{
"name": "php-di/php-di",
@@ -1615,7 +1651,8 @@
"type": "tidelift"
}
],
- "time": "2025-08-16T11:10:48+00:00"
+ "time": "2025-08-16T11:10:48+00:00",
+ "published-time": "2025-08-16T11:11:08+00:00"
},
{
"name": "php-stubs/woocommerce-stubs",
@@ -1659,7 +1696,8 @@
"issues": "https://github.com/php-stubs/woocommerce-stubs/issues",
"source": "https://github.com/php-stubs/woocommerce-stubs/tree/v11.2.0"
},
- "time": "2026-10-07T19:02:59+00:00"
+ "time": "2026-10-07T19:02:59+00:00",
+ "published-time": "2026-10-07T19:04:59+00:00"
},
{
"name": "php-stubs/wordpress-stubs",
@@ -1711,7 +1749,8 @@
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.2"
},
- "time": "2026-10-06T06:59:21+00:00"
+ "time": "2026-10-06T06:59:21+00:00",
+ "published-time": "2026-10-06T07:32:21+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@@ -1801,7 +1840,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-28T11:36:33+00:00"
+ "time": "2025-11-28T11:36:33+00:00",
+ "published-time": "2025-11-28T11:41:33+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-paragonie",
@@ -1872,7 +1912,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-11-29T13:09:49+00:00"
+ "time": "2025-11-29T13:09:49+00:00",
+ "published-time": "2025-11-29T13:22:17+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-wp",
@@ -1939,7 +1980,8 @@
"type": "thanks_dev"
}
],
- "time": "2025-12-16T13:35:20+00:00"
+ "time": "2025-12-16T13:35:20+00:00",
+ "published-time": "2025-12-16T13:47:29+00:00"
},
{
"name": "phpcsstandards/phpcsextra",
@@ -2021,7 +2063,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-07-27T11:13:17+00:00"
+ "time": "2026-07-27T11:13:17+00:00",
+ "published-time": "2026-07-27T11:19:59+00:00"
},
{
"name": "phpcsstandards/phpcsutils",
@@ -2114,7 +2157,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-07-27T10:28:41+00:00"
+ "time": "2026-07-27T10:28:41+00:00",
+ "published-time": "2026-07-27T10:36:31+00:00"
},
{
"name": "phpstan/extension-installer",
@@ -2162,7 +2206,8 @@
"issues": "https://github.com/phpstan/extension-installer/issues",
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
},
- "time": "2024-09-04T20:21:43+00:00"
+ "time": "2024-09-04T20:21:43+00:00",
+ "published-time": "2024-09-04T20:22:27+00:00"
},
{
"name": "phpstan/phpstan",
@@ -2226,7 +2271,8 @@
"type": "github"
}
],
- "time": "2026-10-28T22:38:53+00:00"
+ "time": "2026-10-28T22:38:53+00:00",
+ "published-time": "2026-10-28T22:39:06+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -2277,7 +2323,8 @@
"issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues",
"source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5"
},
- "time": "2026-07-22T06:50:43+00:00"
+ "time": "2026-07-22T06:50:43+00:00",
+ "published-time": "2026-07-27T15:18:00+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
@@ -2329,7 +2376,8 @@
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0"
},
- "time": "2026-10-01T21:30:37+00:00"
+ "time": "2026-10-01T21:30:37+00:00",
+ "published-time": "2026-10-06T07:32:29+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -2418,7 +2466,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-30T08:11:15+00:00"
+ "time": "2026-10-30T08:11:15+00:00",
+ "published-time": "2026-10-30T08:11:34+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -2491,7 +2540,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-25T14:47:43+00:00"
+ "time": "2026-08-25T14:47:43+00:00",
+ "published-time": "2026-08-25T14:48:49+00:00"
},
{
"name": "phpunit/php-invoker",
@@ -2567,7 +2617,8 @@
"type": "tidelift"
}
],
- "time": "2026-02-06T04:34:47+00:00"
+ "time": "2026-02-06T04:34:47+00:00",
+ "published-time": "2026-02-06T04:35:29+00:00"
},
{
"name": "phpunit/php-text-template",
@@ -2639,7 +2690,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-03T04:54:24+00:00"
+ "time": "2026-10-03T04:54:24+00:00",
+ "published-time": "2026-10-03T04:54:39+00:00"
},
{
"name": "phpunit/php-timer",
@@ -2711,7 +2763,8 @@
"type": "tidelift"
}
],
- "time": "2026-02-06T04:37:53+00:00"
+ "time": "2026-02-06T04:37:53+00:00",
+ "published-time": "2026-02-06T04:38:39+00:00"
},
{
"name": "phpunit/phpunit",
@@ -2803,7 +2856,8 @@
"type": "other"
}
],
- "time": "2026-10-27T04:16:22+00:00"
+ "time": "2026-10-27T04:16:22+00:00",
+ "published-time": "2026-10-27T04:18:40+00:00"
},
{
"name": "pronamic/changelog-md-to-wordpress-plugin-readme-txt",
@@ -2843,7 +2897,8 @@
"issues": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/issues",
"source": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/tree/v1.0.0"
},
- "time": "2024-10-01T13:41:34+00:00"
+ "time": "2024-10-01T13:41:34+00:00",
+ "published-time": "2024-10-02T09:31:51+00:00"
},
{
"name": "psr/clock",
@@ -2891,7 +2946,8 @@
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
- "time": "2022-11-25T14:36:26+00:00"
+ "time": "2022-11-25T14:36:26+00:00",
+ "published-time": "2022-11-25T14:59:22+00:00"
},
{
"name": "psr/container",
@@ -2944,7 +3000,8 @@
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
- "time": "2021-11-05T16:47:00+00:00"
+ "time": "2021-11-05T16:47:00+00:00",
+ "published-time": "2021-11-05T16:53:27+00:00"
},
{
"name": "psr/event-dispatcher",
@@ -2994,7 +3051,8 @@
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
- "time": "2019-01-08T18:20:26+00:00"
+ "time": "2019-01-08T18:20:26+00:00",
+ "published-time": "2019-03-25T15:31:44+00:00"
},
{
"name": "psr/log",
@@ -3044,7 +3102,8 @@
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
- "time": "2024-09-11T13:17:53+00:00"
+ "time": "2024-09-11T13:17:53+00:00",
+ "published-time": "2024-09-11T13:21:14+00:00"
},
{
"name": "psr/simple-cache",
@@ -3095,7 +3154,8 @@
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
- "time": "2021-10-29T13:26:27+00:00"
+ "time": "2021-10-29T13:26:27+00:00",
+ "published-time": "2021-10-29T13:27:19+00:00"
},
{
"name": "roave/security-advisories",
@@ -3103,12 +3163,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "8e26a62e09d971af73b5455efcc52f6d142741c9"
+ "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/8e26a62e09d971af73b5455efcc52f6d142741c9",
- "reference": "8e26a62e09d971af73b5455efcc52f6d142741c9",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7c964bc969c1b16050a4b85b7a55614777a3f597",
+ "reference": "7c964bc969c1b16050a4b85b7a55614777a3f597",
"shasum": ""
},
"conflict": {
@@ -3116,7 +3176,7 @@
"adaptcms/adaptcms": "<=1.3",
"adawolfa/isdoc": "<1.4.3|>=1.5,<1.5.1|>=1.6,<1.6.1",
"admidio/admidio": "<=5.0.11",
- "adminneo-org/adminneo": ">=4.6.2,<5.5",
+ "adminneo-org/adminneo": "<5.5.1",
"adodb/adodb-php": "<=5.22.9",
"aheinze/cockpit": "<2.2",
"aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2",
@@ -4265,7 +4325,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-30T20:26:18+00:00"
+ "time": "2026-10-31T00:24:47+00:00",
+ "published-time": "2026-10-31T00:24:51+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -4334,7 +4395,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-01T04:27:14+00:00"
+ "time": "2026-08-01T04:27:14+00:00",
+ "published-time": "2026-08-01T04:29:01+00:00"
},
{
"name": "sebastian/comparator",
@@ -4426,7 +4488,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-07T07:23:13+00:00"
+ "time": "2026-08-07T07:23:13+00:00",
+ "published-time": "2026-08-07T07:23:29+00:00"
},
{
"name": "sebastian/complexity",
@@ -4496,7 +4559,8 @@
"type": "tidelift"
}
],
- "time": "2026-02-06T04:41:32+00:00"
+ "time": "2026-02-06T04:41:32+00:00",
+ "published-time": "2026-02-06T04:41:49+00:00"
},
{
"name": "sebastian/diff",
@@ -4575,7 +4639,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-25T15:38:55+00:00"
+ "time": "2026-08-25T15:38:55+00:00",
+ "published-time": "2026-08-25T15:39:44+00:00"
},
{
"name": "sebastian/environment",
@@ -4651,7 +4716,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-01T12:37:56+00:00"
+ "time": "2026-10-01T12:37:56+00:00",
+ "published-time": "2026-10-01T12:38:48+00:00"
},
{
"name": "sebastian/exporter",
@@ -4741,7 +4807,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-07T07:22:06+00:00"
+ "time": "2026-08-07T07:22:06+00:00",
+ "published-time": "2026-08-07T07:23:47+00:00"
},
{
"name": "sebastian/file-filter",
@@ -4810,7 +4877,8 @@
"type": "tidelift"
}
],
- "time": "2026-04-22T07:20:04+00:00"
+ "time": "2026-04-22T07:20:04+00:00",
+ "published-time": "2026-04-22T07:20:25+00:00"
},
{
"name": "sebastian/git-state",
@@ -4879,7 +4947,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-21T05:59:52+00:00"
+ "time": "2026-10-21T05:59:52+00:00",
+ "published-time": "2026-10-21T06:01:28+00:00"
},
{
"name": "sebastian/global-state",
@@ -4953,7 +5022,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-01T15:11:33+00:00"
+ "time": "2026-06-01T15:11:33+00:00",
+ "published-time": "2026-06-01T15:20:45+00:00"
},
{
"name": "sebastian/lines-of-code",
@@ -5023,7 +5093,8 @@
"type": "tidelift"
}
],
- "time": "2026-07-09T08:42:34+00:00"
+ "time": "2026-07-09T08:42:34+00:00",
+ "published-time": "2026-07-09T08:44:06+00:00"
},
{
"name": "sebastian/object-enumerator",
@@ -5092,7 +5163,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-13T07:05:05+00:00"
+ "time": "2026-08-13T07:05:05+00:00",
+ "published-time": "2026-08-13T07:07:58+00:00"
},
{
"name": "sebastian/object-reflector",
@@ -5160,7 +5232,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-13T06:34:36+00:00"
+ "time": "2026-08-13T06:34:36+00:00",
+ "published-time": "2026-08-13T06:39:22+00:00"
},
{
"name": "sebastian/recursion-context",
@@ -5236,7 +5309,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-03T05:58:12+00:00"
+ "time": "2026-08-03T05:58:12+00:00",
+ "published-time": "2026-08-03T05:59:59+00:00"
},
{
"name": "sebastian/type",
@@ -5305,7 +5379,8 @@
"type": "tidelift"
}
],
- "time": "2026-08-10T08:00:57+00:00"
+ "time": "2026-08-10T08:00:57+00:00",
+ "published-time": "2026-08-10T08:01:43+00:00"
},
{
"name": "sebastian/version",
@@ -5371,7 +5446,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-04T05:51:14+00:00"
+ "time": "2026-10-04T05:51:14+00:00",
+ "published-time": "2026-10-04T05:52:22+00:00"
},
{
"name": "sniccowp/php-scoper-wordpress-excludes",
@@ -5418,7 +5494,8 @@
"issues": "https://github.com/snicco/php-scoper-wordpress-excludes/issues",
"source": "https://github.com/snicco/php-scoper-wordpress-excludes/tree/7.1.0"
},
- "time": "2026-08-28T02:50:05+00:00"
+ "time": "2026-08-28T02:50:05+00:00",
+ "published-time": "2026-09-23T19:33:26+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@@ -5497,7 +5574,8 @@
"type": "thanks_dev"
}
],
- "time": "2026-08-06T00:17:32+00:00"
+ "time": "2026-08-06T00:17:32+00:00",
+ "published-time": "2026-08-06T01:05:02+00:00"
},
{
"name": "staabm/side-effects-detector",
@@ -5549,7 +5627,8 @@
"type": "github"
}
],
- "time": "2024-10-20T05:08:20+00:00"
+ "time": "2024-10-20T05:08:20+00:00",
+ "published-time": "2024-10-20T05:09:39+00:00"
},
{
"name": "swissspidy/phpstan-no-private",
@@ -5601,7 +5680,8 @@
"issues": "https://github.com/swissspidy/phpstan-no-private/issues",
"source": "https://github.com/swissspidy/phpstan-no-private/tree/v1.0.0"
},
- "time": "2024-11-11T11:04:45+00:00"
+ "time": "2024-11-11T11:04:45+00:00",
+ "published-time": "2024-11-11T11:08:03+00:00"
},
{
"name": "symfony/console",
@@ -5699,7 +5779,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-28T11:08:27+00:00"
+ "time": "2026-10-28T11:08:27+00:00",
+ "published-time": "2026-10-28T18:21:33+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -5770,7 +5851,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-05T06:23:12+00:00"
+ "time": "2026-06-05T06:23:12+00:00",
+ "published-time": "2026-06-27T21:46:34+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -5850,7 +5932,8 @@
"type": "tidelift"
}
],
- "time": "2026-06-05T06:23:12+00:00"
+ "time": "2026-06-05T06:23:12+00:00",
+ "published-time": "2026-06-27T21:46:35+00:00"
},
{
"name": "symfony/filesystem",
@@ -5920,7 +6003,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-06T20:05:19+00:00"
+ "time": "2026-10-06T20:05:19+00:00",
+ "published-time": "2026-10-14T06:45:33+00:00"
},
{
"name": "symfony/finder",
@@ -5988,7 +6072,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-19T16:56:54+00:00"
+ "time": "2026-10-19T16:56:54+00:00",
+ "published-time": "2026-10-28T18:21:14+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -6071,7 +6156,8 @@
"type": "tidelift"
}
],
- "time": "2026-04-10T16:19:22+00:00"
+ "time": "2026-04-10T16:19:22+00:00",
+ "published-time": "2026-04-26T14:44:50+00:00"
},
{
"name": "symfony/polyfill-iconv",
@@ -6155,7 +6241,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-29T06:25:14+00:00"
+ "time": "2026-09-29T06:25:14+00:00",
+ "published-time": "2026-09-30T10:23:39+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@@ -6237,7 +6324,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-15T09:18:53+00:00"
+ "time": "2026-10-15T09:18:53+00:00",
+ "published-time": "2026-10-28T10:07:44+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
@@ -6322,7 +6410,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-23T22:02:09+00:00"
+ "time": "2026-09-23T22:02:09+00:00",
+ "published-time": "2026-09-30T10:23:47+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@@ -6407,7 +6496,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-28T09:35:49+00:00"
+ "time": "2026-10-28T09:35:49+00:00",
+ "published-time": "2026-10-28T10:07:45+00:00"
},
{
"name": "symfony/polyfill-php84",
@@ -6487,7 +6577,8 @@
"type": "tidelift"
}
],
- "time": "2026-09-23T22:01:09+00:00"
+ "time": "2026-09-23T22:01:09+00:00",
+ "published-time": "2026-09-30T10:23:33+00:00"
},
{
"name": "symfony/process",
@@ -6552,7 +6643,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-25T14:40:16+00:00"
+ "time": "2026-10-25T14:40:16+00:00",
+ "published-time": "2026-10-28T18:21:18+00:00"
},
{
"name": "symfony/service-contracts",
@@ -6639,7 +6731,8 @@
"type": "tidelift"
}
],
- "time": "2026-07-27T15:39:01+00:00"
+ "time": "2026-07-27T15:39:01+00:00",
+ "published-time": "2026-08-30T19:56:37+00:00"
},
{
"name": "symfony/string",
@@ -6729,7 +6822,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-23T17:06:53+00:00"
+ "time": "2026-10-23T17:06:53+00:00",
+ "published-time": "2026-10-28T18:24:27+00:00"
},
{
"name": "symfony/var-dumper",
@@ -6816,7 +6910,8 @@
"type": "tidelift"
}
],
- "time": "2026-10-25T21:04:04+00:00"
+ "time": "2026-10-25T21:04:04+00:00",
+ "published-time": "2026-10-28T18:20:00+00:00"
},
{
"name": "szepeviktor/phpstan-wordpress",
@@ -6879,7 +6974,8 @@
"issues": "https://github.com/szepeviktor/phpstan-wordpress/issues",
"source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4"
},
- "time": "2026-05-22T16:22:09+00:00"
+ "time": "2026-05-22T16:22:09+00:00",
+ "published-time": "2026-08-28T06:48:49+00:00"
},
{
"name": "thecodingmachine/safe",
@@ -7022,7 +7118,8 @@
"type": "github"
}
],
- "time": "2026-02-04T18:08:13+00:00"
+ "time": "2026-02-04T18:08:13+00:00",
+ "published-time": "2026-02-15T15:48:29+00:00"
},
{
"name": "webmozart/assert",
@@ -7080,7 +7177,8 @@
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
- "time": "2025-10-29T15:56:20+00:00"
+ "time": "2025-10-29T15:56:20+00:00",
+ "published-time": "2025-10-29T15:56:42+00:00"
},
{
"name": "wp-coding-standards/wpcs",
@@ -7146,7 +7244,8 @@
"type": "custom"
}
],
- "time": "2026-07-27T11:53:23+00:00"
+ "time": "2026-07-27T11:53:23+00:00",
+ "published-time": "2026-07-27T11:55:52+00:00"
},
{
"name": "wp-hooks/wordpress-core",
@@ -7229,7 +7328,8 @@
"type": "github"
}
],
- "time": "2026-08-20T19:54:29+00:00"
+ "time": "2026-08-20T19:54:29+00:00",
+ "published-time": "2026-08-20T19:54:42+00:00"
},
{
"name": "wp-plugin/woocommerce",
@@ -7260,7 +7360,8 @@
"issues": "https://wordpress.org/support/plugin/woocommerce",
"source": "https://plugins.svn.wordpress.org/woocommerce"
},
- "time": "2026-10-09T15:35:21+00:00"
+ "time": "2026-10-09T15:35:21+00:00",
+ "published-time": "2026-10-09T15:35:21+00:00"
}
],
"aliases": [],
@@ -7274,5 +7375,5 @@
"php": ">=8.5"
},
"platform-dev": {},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.11.0"
}
diff --git a/plugins/linked-orders-for-woocommerce/package-lock.json b/plugins/linked-orders-for-woocommerce/package-lock.json
index 40e0b98..ce304a7 100644
--- a/plugins/linked-orders-for-woocommerce/package-lock.json
+++ b/plugins/linked-orders-for-woocommerce/package-lock.json
@@ -389,9 +389,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
+ "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -448,13 +448,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
- "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz",
+ "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.8"
+ "@babel/types": "^7.29.11"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -727,9 +727,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
- "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.11.tgz",
+ "integrity": "sha512-iARF4jjHSM0v8DImCACPXrwEltBP3DgqDpXTvjAMBAVpyEY0LiGss78wvI+KtmW+IJNEvZ5sOhPDCsOsXsIybA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -777,9 +777,9 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
- "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.11.tgz",
+ "integrity": "sha512-f8kTDM/nTvpLeafb+TJ4jboFr/Y3BbC+QzgPpPGzSKJqWLnj9LW0WLqv4XKA11LX5sr+EgvLE6Ix3YsCEusodw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -788,7 +788,7 @@
"@babel/helper-globals": "^7.29.7",
"@babel/helper-plugin-utils": "^7.29.7",
"@babel/helper-replace-supers": "^7.29.7",
- "@babel/traverse": "^7.29.7"
+ "@babel/traverse": "^7.29.10"
},
"engines": {
"node": ">=6.9.0"
@@ -1513,9 +1513,9 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz",
- "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.11.tgz",
+ "integrity": "sha512-5xWGhfuMgbOfIjsk/I2c4uMnoqRwQdKYi4QjvybQrcEVE0G9/p3qmIkp2Sxe8YF8DGNNVs5whfwNqIM5684XoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1851,13 +1851,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
- "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "version": "7.29.11",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
+ "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-string-parser": "^7.29.11",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
@@ -6454,9 +6454,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13315,9 +13315,9 @@
}
},
"node_modules/http-proxy-middleware": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.0.tgz",
- "integrity": "sha512-ZA+oNOoM+GLoFTIzhkJptVQov73Srep2LBqhF8hG8CIPKO3nam1jonXVQ/QUH8RbwsmaaVz2SOJdzBNBHNtKbw==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-4.2.1.tgz",
+ "integrity": "sha512-60SmYZpPD4dPbC2GZVkVV4uP/V/cbsXSwzGQXeHWbbE3Q1XdccJpqHTg6NJ6iMpFGVA1OEry8c8kHhx4ahiU1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15276,9 +15276,9 @@
"license": "Python-2.0"
},
"node_modules/markdownlint-cli/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -20882,9 +20882,9 @@
}
},
"node_modules/stylelint/node_modules/ignore": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz",
- "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
From ae66d2946edac0649cae8a86cd431d424faa6c42 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sat, 31 Oct 2026 22:57:05 +0100
Subject: [PATCH 02/50] Test that LO and IC load their safeguards before the
floor gate
Linked Orders and Internal Comments load includes/safeguards.php after
the bootstrap package's floor gate, as the template did, so on a
runtime below a floor their restrictions would never load while a
failed boot keeps them. The template now loads the safeguards first.
Each plugin's PluginBootTest gains the template's row, which expects
includes/safeguards.php among the included files before the bootstrap
package's load.php, and the include-loader row drops its safeguards.php
assertion, which the order row covers. Both fail until the entries move.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/PluginBootTest.php | 11 ++++++++++-
.../tests/Integration/PluginBootTest.php | 11 ++++++++++-
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/plugins/internal-comments/tests/Integration/PluginBootTest.php b/plugins/internal-comments/tests/Integration/PluginBootTest.php
index 624c5ee..d84da1a 100644
--- a/plugins/internal-comments/tests/Integration/PluginBootTest.php
+++ b/plugins/internal-comments/tests/Integration/PluginBootTest.php
@@ -32,7 +32,6 @@ public function test_the_plugin_boots_every_component(): void {
public function test_the_entry_loads_every_include_but_those_whose_names_start_with_an_underscore(): void {
$includes = \dirname( DWS_IC_FILE ) . '/includes/';
- self::assertContains( $includes . 'safeguards.php', \get_included_files() );
self::assertContains( $includes . 'index.php', \get_included_files() );
self::assertIsInt( \file_put_contents( $includes . '_ic-skipped.php', "migrations );
self::assertSame( \count( dws_ic_get_migration_steps() ), MigrationRunner::get_revision( Plugin::PREFIX . '_revision' ) );
diff --git a/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php
index 4171d66..30d9086 100644
--- a/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php
+++ b/plugins/linked-orders-for-woocommerce/tests/Integration/PluginBootTest.php
@@ -32,7 +32,6 @@ public function test_the_plugin_boots_every_component_but_the_integration_with_a
public function test_the_entry_loads_every_include_but_those_whose_names_start_with_an_underscore(): void {
$includes = \dirname( DWS_LOWC_FILE ) . '/includes/';
- self::assertContains( $includes . 'safeguards.php', \get_included_files() );
self::assertContains( $includes . 'index.php', \get_included_files() );
self::assertIsInt( \file_put_contents( $includes . '_lowc-skipped.php', "migrations );
self::assertSame( \count( dws_lowc_get_migration_steps() ), MigrationRunner::get_revision( Plugin::PREFIX . '_revision' ) );
From c16dcf21c40f07c097e99bd429b03e746d56f51b Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sat, 31 Oct 2026 23:01:07 +0100
Subject: [PATCH 03/50] Load LO's and IC's safeguards before the floor gate
Linked Orders and Internal Comments loaded includes/safeguards.php
after Requirements::check_floors(), as the template did, so a site
below a floor would lose a safeguard's restriction while a failed boot
keeps it. Both safeguards files are empty today, but a restriction
either plugin adds must hold whenever the plugin cannot boot.
Each entry now requires the safeguards first, as the template's does.
The template's accompanying changes come along, since each plugin
carries its own copy: the AGENTS.md layout lines and safeguards
convention, docs/ARCHITECTURE.md's load order, the safeguards file's
docblock, and phpstan.safeguards.neon, which lint:php:phpstan runs at
PHP 7.4 and WordPress 5.2 and the release zip leaves out. Linked
Orders' convention names the template's recipes, as Internal
Comments' does, since it ships no docs/recipes.md.
X ruling 89.
Assisted-by: Claude Code:claude-opus-5-5
---
plugins/internal-comments/.distignore | 1 +
plugins/internal-comments/AGENTS.md | 8 ++++----
plugins/internal-comments/composer.json | 5 ++++-
plugins/internal-comments/docs/ARCHITECTURE.md | 6 +++---
plugins/internal-comments/includes/safeguards.php | 2 +-
plugins/internal-comments/internal-comments.php | 4 ++--
plugins/internal-comments/phpstan.safeguards.neon | 13 +++++++++++++
plugins/linked-orders-for-woocommerce/.distignore | 1 +
plugins/linked-orders-for-woocommerce/AGENTS.md | 8 ++++----
plugins/linked-orders-for-woocommerce/composer.json | 5 ++++-
.../docs/ARCHITECTURE.md | 6 +++---
.../includes/safeguards.php | 2 +-
.../linked-orders-for-woocommerce.php | 4 ++--
.../phpstan.safeguards.neon | 13 +++++++++++++
14 files changed, 56 insertions(+), 22 deletions(-)
create mode 100644 plugins/internal-comments/phpstan.safeguards.neon
create mode 100644 plugins/linked-orders-for-woocommerce/phpstan.safeguards.neon
diff --git a/plugins/internal-comments/.distignore b/plugins/internal-comments/.distignore
index ee1bef4..5faf091 100644
--- a/plugins/internal-comments/.distignore
+++ b/plugins/internal-comments/.distignore
@@ -34,6 +34,7 @@ stylelint.config.js
phpcs.dist.xml
phpcs.tests.dist.xml
phpstan.dist.neon
+phpstan.safeguards.neon
phpunit.dist.xml
playwright.config.js
scoper.inc.php
diff --git a/plugins/internal-comments/AGENTS.md b/plugins/internal-comments/AGENTS.md
index 96df771..c14ff40 100644
--- a/plugins/internal-comments/AGENTS.md
+++ b/plugins/internal-comments/AGENTS.md
@@ -7,7 +7,7 @@ v1 sites upgrade in place, so the port keeps v1's comment type, settings rows, c
## Layout
```
-internal-comments.php the entry: bootstrap load.php, the inert updater, the floor check, safeguards, the build gate, the autoloader
+internal-comments.php the entry: safeguards, bootstrap load.php, the inert updater, the floor check, the build gate, the autoloader
functions.php dws_ic_plugin(), the boot on plugins_loaded 15, deactivation, the order-tables declaration, v1's boot hooks, the includes/ loader
uninstall.php the framework's uninstall engine over the footprint, with v1's remove-data-uninstall consent
src/ the composition root and its components only
@@ -20,7 +20,7 @@ src/ the composition root and its components only
└── AdminScreens.php the posts and orders list column with its quick view, the Comments screen view, the comment edit screen
models/DWS_Internal_Comment.php v1's global model class on the classmap, wrapping an internal comment with the user's permissions on it
includes/ hook-free code as dws_ic_ functions and DWS_IC_ constants, loaded in name order, _-prefixed files skipped
-├── safeguards.php restrictions that hold when the build fails; loaded by the entry before the autoloader
+├── safeguards.php restrictions that hold whenever the plugin cannot boot; loaded by the entry before the floor check
├── capabilities.php v1's twelve capabilities and the roles CapabilityGrants gives them
├── options.php v1's two settings rows, their fields and their defaults
├── settings.php v1's settings API: the raw and validated reads, by v1 field id and group
@@ -41,7 +41,7 @@ changelog/ the pending entries changelog:write turns into CHAN
composer packages-install # dependencies, then php-scoper into vendor-prefixed/
composer test:unit
composer test:integration # starts .wp-env.tests.json, then runs test:integration:run, which builds the scripts first
-composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan, Unit and Integration
+composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan (both profiles), Unit and Integration
composer changelog:add # stages an entry in changelog/; composer changelog:write, at release, also syncs readme.txt
npm run setup # both installs, the scripts' build, then the development site
npm run build # the meta box's and quick view's assets through the local wp-scripts; npm run start watches them in a POSIX shell
@@ -69,7 +69,7 @@ Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI run
7. **Internal status.** An internal comment is stored under the `dws_internal` status, which keeps it out of WordPress's queries, counts, feeds and REST API without the plugin's help, so no code stores one under another status, and a query that lists them names the status. `Comments` puts back any that WordPress or another plugin moves, announcing the move as a status change after WordPress announces its own, so caches that follow status changes, such as WooCommerce's comment counts, end where the comment is; the migration's bulk move resets WooCommerce's counts instead. Until the migration has moved the comments v1 stored approved, it leaves the type out of every comment query and feed that names neither it nor the status. Every check of what a user may do with one goes through the `see_`, `edit_` and `trash_dws_internal_comment` meta capabilities.
8. **Order storage.** Order code takes ids WooCommerce resolves in either storage, through `dws_ic_get_post_type()` and `edit_post`, which WooCommerce maps for orders, and WooCommerce's screen ids and list hooks, never the posts table; every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorage`.
9. **v1 hooks.** A hook v1 fired keeps its v1 literal, fired first through `apply_filters_deprecated()` or `do_action_deprecated()` with `'2.0.0'` and the v2 name, then the v2 name.
-10. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_ic_is_relaxed()` (the template's recipes).
+10. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_ic_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks.
11. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails; bulk work is a batched step, which runs only from the runner's scheduled hook.
## Style
diff --git a/plugins/internal-comments/composer.json b/plugins/internal-comments/composer.json
index bdb915e..3a38d76 100644
--- a/plugins/internal-comments/composer.json
+++ b/plugins/internal-comments/composer.json
@@ -100,7 +100,10 @@
],
"lint:php:phpcs": "phpcs --standard=./phpcs.dist.xml --basepath=. ./ -v",
"lint:php:phpcs:tests": "phpcs --standard=./phpcs.tests.dist.xml --basepath=. ./tests -v",
- "lint:php:phpstan": "phpstan analyse -v --memory-limit=1G",
+ "lint:php:phpstan": [
+ "phpstan analyse -v --memory-limit=1G",
+ "phpstan analyse -c phpstan.safeguards.neon -v --memory-limit=1G"
+ ],
"test": [
"@test:unit",
diff --git a/plugins/internal-comments/docs/ARCHITECTURE.md b/plugins/internal-comments/docs/ARCHITECTURE.md
index cbda4d3..d857f7c 100644
--- a/plugins/internal-comments/docs/ARCHITECTURE.md
+++ b/plugins/internal-comments/docs/ARCHITECTURE.md
@@ -7,8 +7,8 @@ This document describes how the plugin loads, boots, stores its data and leaves
`internal-comments.php` is the only file WordPress includes, and it parses on PHP 7.4 so that a site below the floors gets a notice instead of a fatal error:
1. It defines `DWS_IC_FILE`; translations come from wordpress.org's language packs, which WordPress loads just in time.
-2. It requires `vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php`, registers the GitHub updater, which stays inert because the entry file has no `Update URI` header, then checks the `Requires PHP` and `Requires at least` headers. On a site below a floor, WordPress's own notice shows and nothing else loads.
-3. It loads `includes/safeguards.php`, which holds even when the rest of the build fails.
+2. It loads `includes/safeguards.php`, whose restrictions hold on any runtime that parses the plugin: below the floors, on a partial deploy and when the boot fails.
+3. It requires `vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php`, registers the GitHub updater, which stays inert because the entry file has no `Update URI` header, then checks the `Requires PHP` and `Requires at least` headers. On a site below a floor, WordPress's own notice shows and nothing past the safeguards loads.
4. It stops with a notice when the prefixed framework or the meta box's built script is missing. It then requires the autoloader and `functions.php`; a missing file throws, which local and development environments rethrow and every other environment logs, showing the same notice.
`functions.php` defines the `dws_ic_plugin()` accessor, boots the plugin on `plugins_loaded` at priority 15, after WooCommerce and Action Scheduler have loaded where they are active, registers the deactivation callback, declares the plugin compatible with WooCommerce's order tables, relays the framework's boot hooks to v1's `dws_ic_initialized` and `dws_ic_initialization_failure`, and requires every `includes/*.php` in name order except those whose names start with `_`.
@@ -29,7 +29,7 @@ WooCommerce is optional: without it, every feature but the order screens and the
- `src/` holds the composition root and its components: final classes that implement the framework's `ComponentInterface` and add their hooks in `register_hooks()`. `Settings` declares v1's settings page to the framework's `OptionsPage`; `Permissions` maps the meta capabilities and holds core's REST API and XML-RPC to them, which otherwise let moderators and a comment's author past; `Comments` keeps internal comments under their status and records their revisions; `MetaBox` adds the meta box to posts and orders of the selected types; `RestRoutes` registers the routes the meta box and quick view call; `AdminScreens` adds the list column and quick view, the Comments screen view and the edit screen's revisions and privacy. Each checks the user's capabilities in its callbacks.
- `models/DWS_Internal_Comment.php` keeps v1's global class on Composer's classmap: it wraps an internal comment, passes reads and calls to it, and answers whether a user may see, edit or trash it.
-- `includes/` holds code that registers no hooks and is not part of the tree, as prefixed functions and constants: v1's capabilities (`capabilities.php`), the settings rows, fields and defaults (`options.php`), v1's settings and comment APIs (`settings.php`, `comments.php`), the markup the meta box prints and the routes answer with (`markup.php`), v1's deprecated root functions, constants and boot-hook relays (`deprecated.php`), the footprint (`footprint.php`), the migration steps (`migrations.php`), the asset registrars (`assets.php`) and the slug, name and version getters a peer plugin reads (`metadata.php`). Quality's syntax job parses the folder on PHP 7.4, because `safeguards.php` and what it requires run before the floor check passes on a site without the built files.
+- `includes/` holds code that registers no hooks and is not part of the tree, as prefixed functions and constants: v1's capabilities (`capabilities.php`), the settings rows, fields and defaults (`options.php`), v1's settings and comment APIs (`settings.php`, `comments.php`), the markup the meta box prints and the routes answer with (`markup.php`), v1's deprecated root functions, constants and boot-hook relays (`deprecated.php`), the footprint (`footprint.php`), the migration steps (`migrations.php`), the asset registrars (`assets.php`) and the slug, name and version getters a peer plugin reads (`metadata.php`). Quality's syntax job parses the folder on PHP 7.4, because `safeguards.php` runs before the floor check, and `phpstan.safeguards.neon` analyzes it at PHP 7.4 and the oldest WordPress the bootstrap package runs on.
- `assets/src/` holds the meta box's and quick view's TypeScript and SCSS, which `npm run build` compiles into `assets/build/`; `includes/assets.php` reads the `*.asset.php` file wp-scripts writes beside each build.
## Data
diff --git a/plugins/internal-comments/includes/safeguards.php b/plugins/internal-comments/includes/safeguards.php
index 280a797..c60efc1 100644
--- a/plugins/internal-comments/includes/safeguards.php
+++ b/plugins/internal-comments/includes/safeguards.php
@@ -1,6 +1,6 @@
Date: Sat, 31 Oct 2026 23:02:46 +0100
Subject: [PATCH 04/50] Add Locked Payment Methods from the plugin template
Locked Payment Methods for WooCommerce is the third plugin ported to
v2. It starts from wordpress-plugin-template's trunk (321d243) through
the template's own fill-in, run in a clone of the template: name
Locked Payment Methods for WooCommerce, slug
locked-payment-methods-for-woocommerce, namespace
DeepWebSolutions\LockedPaymentMethods, prefix dws_lpmwc, ports 8826 and
8827, repository ahegyes/wc-locked-payment-methods.
As the v1 successor it stays at 2.0.0, which the fill-in sets to 0.1.0
for a new plugin, and its Composer name is its mirror,
ahegyes/wc-locked-payment-methods. The directory keeps the template's
files but for CONTRIBUTING.md, the pull-request and issue templates,
Dependabot's configuration and the fill-in's workflow, script and
Quality job, which belong to the monorepo. The template's demo stays
until the port replaces it, and every suite passes as filled in.
X rulings 87 and 88.
Assisted-by: Claude Code:claude-opus-5-5
---
.../.distignore | 40 +
.../.editorconfig | 24 +
.../.github/workflows/audit.yml | 21 +
.../.github/workflows/codeql.yml | 23 +
.../.github/workflows/quality.yml | 71 +
.../.github/workflows/release.yml | 53 +
.../.github/workflows/tests-nightly.yml | 29 +
.../.github/workflows/tests.yml | 74 +
.../.github/workflows/workflow-checks.yml | 27 +
.../.gitignore | 354 +
.../.wp-env.json | 19 +
.../.wp-env.tests.json | 19 +
.../AGENTS.md | 74 +
.../CHANGELOG.md | 10 +
.../CLAUDE.md | 1 +
.../LICENSE | 674 +
.../README.md | 73 +
.../SECURITY.md | 14 +
.../assets/index.php | 1 +
.../assets/src/editor.ts | 7 +
.../assets/src/greeting.scss | 3 +
.../assets/src/index.php | 1 +
.../blocks/index.php | 1 +
.../blocks/src/declarations.d.ts | 17 +
.../blocks/src/example-notice/block.json | 18 +
.../blocks/src/example-notice/edit.tsx | 15 +
.../blocks/src/example-notice/editor.scss | 3 +
.../blocks/src/example-notice/index.php | 1 +
.../blocks/src/example-notice/index.ts | 11 +
.../blocks/src/example-notice/save.tsx | 10 +
.../blocks/src/example-notice/style.scss | 8 +
.../blocks/src/index.php | 1 +
.../changelog/.gitkeep | 0
.../composer.json | 183 +
.../composer.lock | 7338 +++++
.../docs/ARCHITECTURE.md | 45 +
.../docs/recipes.md | 1293 +
.../eslint.config.mjs | 3 +
.../functions.php | 92 +
.../includes/assets.php | 77 +
.../includes/footprint.php | 32 +
.../includes/index.php | 1 +
.../includes/metadata.php | 51 +
.../includes/migrations.php | 49 +
.../includes/options.php | 187 +
.../includes/safeguards.php | 11 +
.../index.php | 1 +
.../languages/index.php | 1 +
...locked-payment-methods-for-woocommerce.php | 90 +
.../models/index.php | 1 +
.../package-lock.json | 25485 ++++++++++++++++
.../package.json | 60 +
.../phpcs.dist.xml | 18 +
.../phpcs.tests.dist.xml | 4 +
.../phpstan.dist.neon | 20 +
.../phpstan.safeguards.neon | 13 +
.../phpunit.dist.xml | 32 +
.../playwright.config.js | 11 +
.../scoper.inc.php | 8 +
.../src/Blocks.php | 60 +
.../src/Feature/Feature.php | 51 +
.../src/Feature/FeatureDetail.php | 70 +
.../src/Feature/index.php | 1 +
.../src/Plugin.php | 219 +
.../src/Settings.php | 123 +
.../src/index.php | 1 +
.../stylelint.config.js | 3 +
.../templates/index.php | 1 +
.../tests/Fixtures/built.asset.php | 12 +
.../tests/Integration/AssetsTest.php | 45 +
.../tests/Integration/BlocksTest.php | 42 +
.../tests/Integration/DeactivationTest.php | 32 +
.../Integration/Feature/FeatureDetailTest.php | 130 +
.../tests/Integration/Feature/FeatureTest.php | 63 +
.../tests/Integration/MetadataTest.php | 57 +
.../tests/Integration/MigrationsTest.php | 80 +
.../tests/Integration/OptionsTest.php | 149 +
.../tests/Integration/PartialDeployTest.php | 100 +
.../tests/Integration/PluginBootTest.php | 46 +
.../tests/Integration/SettingsTest.php | 127 +
.../tests/Integration/UninstallTest.php | 75 +
.../tests/Integration/bootstrap.php | 19 +
.../tests/README.md | 58 +
.../tests/Unit/ComponentTreeTest.php | 25 +
.../tests/Unit/FootprintTest.php | 68 +
.../tests/Unit/bootstrap.php | 8 +
.../tests/bootstrap.php | 3 +
.../tests/e2e/settings.spec.ts | 74 +
.../tests/e2e/without-woocommerce.spec.ts | 51 +
.../tsconfig.json | 4 +
.../uninstall.php | 55 +
91 files changed, 38655 insertions(+)
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.distignore
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.editorconfig
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.gitignore
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.wp-env.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/.wp-env.tests.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/AGENTS.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md
create mode 120000 plugins/locked-payment-methods-for-woocommerce/CLAUDE.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/LICENSE
create mode 100644 plugins/locked-payment-methods-for-woocommerce/README.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/SECURITY.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/src/editor.ts
create mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/src/greeting.scss
create mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/src/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/declarations.d.ts
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/block.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/edit.tsx
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/editor.scss
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/index.ts
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/save.tsx
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/style.scss
create mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/src/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/.gitkeep
create mode 100644 plugins/locked-payment-methods-for-woocommerce/composer.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/composer.lock
create mode 100644 plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/docs/recipes.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/eslint.config.mjs
create mode 100644 plugins/locked-payment-methods-for-woocommerce/functions.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/assets.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/metadata.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/options.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/languages/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/models/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/package-lock.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/package.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/phpcs.tests.dist.xml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/phpstan.dist.neon
create mode 100644 plugins/locked-payment-methods-for-woocommerce/phpstan.safeguards.neon
create mode 100644 plugins/locked-payment-methods-for-woocommerce/phpunit.dist.xml
create mode 100644 plugins/locked-payment-methods-for-woocommerce/playwright.config.js
create mode 100644 plugins/locked-payment-methods-for-woocommerce/scoper.inc.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/Blocks.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/Feature/Feature.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/Feature/FeatureDetail.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/Feature/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/Plugin.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/Settings.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/src/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/stylelint.config.js
create mode 100644 plugins/locked-payment-methods-for-woocommerce/templates/index.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Fixtures/built.asset.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/AssetsTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/BlocksTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeactivationTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/Feature/FeatureDetailTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/Feature/FeatureTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/MetadataTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/MigrationsTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/OptionsTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/PartialDeployTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/SettingsTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/bootstrap.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/README.md
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Unit/ComponentTreeTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Unit/bootstrap.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/bootstrap.php
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/e2e/settings.spec.ts
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/e2e/without-woocommerce.spec.ts
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tsconfig.json
create mode 100644 plugins/locked-payment-methods-for-woocommerce/uninstall.php
diff --git a/plugins/locked-payment-methods-for-woocommerce/.distignore b/plugins/locked-payment-methods-for-woocommerce/.distignore
new file mode 100644
index 0000000..5faf091
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.distignore
@@ -0,0 +1,40 @@
+.cache
+.distignore
+.editorconfig
+.git
+.github
+.gitignore
+.idea
+.vscode
+.wp-env.json
+.wp-env.override.json
+.wp-env.tests.json
+
+AGENTS.md
+CHANGELOG.md
+CLAUDE.md
+CONTRIBUTING.md
+README.md
+SECURITY.md
+
+changelog/
+docs/
+
+node_modules/
+tests/
+vendor/bin/
+
+composer.json
+composer.lock
+package.json
+package-lock.json
+tsconfig.json
+eslint.config.mjs
+stylelint.config.js
+phpcs.dist.xml
+phpcs.tests.dist.xml
+phpstan.dist.neon
+phpstan.safeguards.neon
+phpunit.dist.xml
+playwright.config.js
+scoper.inc.php
diff --git a/plugins/locked-payment-methods-for-woocommerce/.editorconfig b/plugins/locked-payment-methods-for-woocommerce/.editorconfig
new file mode 100644
index 0000000..05c9aa0
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.editorconfig
@@ -0,0 +1,24 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# WordPress Coding Standards
+# https://make.wordpress.org/core/handbook/coding-standards/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+
+[*.{yml,yaml,json}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.txt]
+end_of_line = crlf
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml
new file mode 100644
index 0000000..36537c6
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/audit.yml
@@ -0,0 +1,21 @@
+name: Supply-Chain Audit
+
+on:
+ push:
+ branches: [trunk]
+ pull_request:
+ schedule:
+ # Weekly Monday at 07:00 UTC — staggered after dependabot (06:00) so PRs from
+ # dependency bumps surface audit results in the same morning batch.
+ - cron: '0 7 * * 1'
+
+permissions:
+ contents: read
+
+jobs:
+ audit:
+ name: Audit
+ # npm gate audits production deps only — the dev-tooling graph (wp-scripts/playwright trees) is advisory via dependabot alerts.
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ npm-audit-flags: '--omit=dev --audit-level=high'
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml
new file mode 100644
index 0000000..faef0d2
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/codeql.yml
@@ -0,0 +1,23 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [trunk]
+ pull_request:
+ branches: [trunk]
+ schedule:
+ - cron: '0 6 * * 1'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions: {}
+
+jobs:
+ analyze:
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml
new file mode 100644
index 0000000..27586d6
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml
@@ -0,0 +1,71 @@
+name: Quality
+
+on:
+ push:
+ branches: [trunk]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ lint-php:
+ name: PHP Lint
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ php-version: '8.5'
+ scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]'
+
+ lint-scripts-styles:
+ name: Scripts and styles lint
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ scripts: '["lint:scripts", "lint:styles", "lint:types"]'
+
+ block-json:
+ name: block.json schema
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-block-json-check.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+
+ slug:
+ name: Plugin slug
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ slug: ${{ steps.slug.outputs.slug }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ # GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares.
+ - name: Read the slug from composer.json
+ id: slug
+ run: |
+ slug=$(jq -r '.extra["text-domain"] // empty' composer.json)
+ if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
+ echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'."
+ exit 1
+ fi
+ echo "slug=$slug" >> "$GITHUB_OUTPUT"
+
+ php-syntax:
+ name: PHP syntax
+ needs: slug
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-syntax-check.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ php-versions: '["7.4"]'
+ paths: ${{ format('["{0}.php", "uninstall.php", "includes"]', needs.slug.outputs.slug) }}
+
+ changelog:
+ name: Changelog fragments
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
+ with:
+ php-version: '8.5'
+ coverage: none
+ tools: composer:v2
+ - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0
+ - run: composer changelog:validate
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml
new file mode 100644
index 0000000..da95cb5
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml
@@ -0,0 +1,53 @@
+name: Release
+
+on:
+ push:
+ tags: ['v*']
+ pull_request:
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+permissions:
+ contents: read
+
+jobs:
+ slug:
+ name: Plugin slug
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ slug: ${{ steps.slug.outputs.slug }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ # GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares.
+ - name: Read the slug from composer.json
+ id: slug
+ run: |
+ slug=$(jq -r '.extra["text-domain"] // empty' composer.json)
+ if ! [[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
+ echo "::error::composer.json's extra.text-domain must be the plugin's kebab-case slug — got '$slug'."
+ exit 1
+ fi
+ echo "slug=$slug" >> "$GITHUB_OUTPUT"
+
+ release:
+ name: Release
+ needs: slug
+ # A reusable workflow can't elevate above the caller's token; grant the provenance check's and the GitHub Release's scopes here.
+ permissions:
+ actions: read
+ contents: write
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-release.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ project-path: '.'
+ plugin-slug: ${{ needs.slug.outputs.slug }}
+ wp-env-config-file: '.wp-env.tests.json'
+ # Both parse every PHP file with php-parser 4, which stops at PHP 8.4 syntax such as protected(set).
+ exclude-checks: 'prefixing,direct_file_access'
+ # A pull request or a dispatch on a branch rehearses the release without publishing it.
+ publish: ${{ github.ref_type == 'tag' }}
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml
new file mode 100644
index 0000000..ed0d800
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests-nightly.yml
@@ -0,0 +1,29 @@
+name: Tests (nightly)
+
+# WordPress trunk changes daily without a commit here, so this runs on a schedule instead of blocking pull requests.
+on:
+ schedule:
+ - cron: '0 5 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ integration:
+ name: Integration
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ php-version: '8.5'
+ wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip'
+ composer-script: 'test:integration:run'
+ wp-env-config-file: '.wp-env.tests.json'
+
+ e2e:
+ name: E2E
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ artifact-slug: nightly
+ php-version: '8.5'
+ wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip'
+ wp-env-config-file: '.wp-env.tests.json'
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml
new file mode 100644
index 0000000..eff35af
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/tests.yml
@@ -0,0 +1,74 @@
+name: Tests
+
+on:
+ push:
+ branches: [trunk]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ unit:
+ name: Unit
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ php-version: '8.5'
+ composer-script: 'test:unit'
+ needs-wp-env: false
+
+ wordpress:
+ name: WordPress versions
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ outputs:
+ versions: ${{ steps.versions.outputs.versions }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ # The floor is the Requires at least header of the entry file composer.json's text domain names, and WordPress's version API offers the newest release of every branch, so new releases, floors and a filled-in slug join without an edit here.
+ - name: List the newest release of every branch from the floor up
+ id: versions
+ run: |
+ entry="$(jq -r '.extra["text-domain"] // empty' composer.json).php"
+ floor=$(sed -nE 's|^[[:space:]/*#@]*Requires at least:[[:space:]]*([0-9]+(\.[0-9]+)*).*$|\1|p' "$entry" | head -n 1)
+ if [ -z "$floor" ]; then
+ echo "::error::$entry declares no Requires at least header."
+ exit 1
+ fi
+ offers=$(curl -fsS https://api.wordpress.org/core/version-check/1.7/)
+ versions=$(printf '%s' "$offers" | jq -c --arg floor "$floor" '[.offers[].version | select((split(".")[0:2] | map(tonumber)) >= ($floor | split(".")[0:2] | map(tonumber)))] | unique | if length == 0 then error("No WordPress release at or above \($floor).") else . end')
+ echo "versions=$versions" >> "$GITHUB_OUTPUT"
+
+ integration:
+ name: Integration
+ needs: wordpress
+ strategy:
+ fail-fast: false
+ matrix:
+ wp: ${{ fromJSON(needs.wordpress.outputs.versions) }}
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ php-version: '8.5'
+ wp-version: ${{ matrix.wp }}
+ composer-script: 'test:integration:run'
+ wp-env-config-file: '.wp-env.tests.json'
+
+ e2e:
+ name: E2E
+ needs: wordpress
+ strategy:
+ fail-fast: false
+ matrix:
+ wp: ${{ fromJSON(needs.wordpress.outputs.versions) }}
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
+ with:
+ artifact-slug: wp-${{ matrix.wp }}
+ php-version: '8.5'
+ wp-version: ${{ matrix.wp }}
+ wp-env-config-file: '.wp-env.tests.json'
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml
new file mode 100644
index 0000000..8ea457c
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/workflow-checks.yml
@@ -0,0 +1,27 @@
+name: Workflow Checks
+
+on:
+ push:
+ branches: [trunk]
+ paths: ['.github/workflows/**']
+ pull_request:
+ paths: ['.github/workflows/**']
+ schedule:
+ # Weekly Monday 06:00 UTC. Keeps zizmor's online audits (e.g.
+ # known-vulnerable-actions) fresh between workflow changes.
+ - cron: '0 6 * * 1'
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions: {}
+
+jobs:
+ checks:
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+ uses: ahegyes/wordpress-configs/.github/workflows/reusable-workflow-checks.yml@8d53efd292e4abee4a9c57b0e379434f54bedbe9
diff --git a/plugins/locked-payment-methods-for-woocommerce/.gitignore b/plugins/locked-payment-methods-for-woocommerce/.gitignore
new file mode 100644
index 0000000..9a44d1b
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.gitignore
@@ -0,0 +1,354 @@
+vendor-prefixed
+blocks/build
+assets/build
+tests/.cache
+.wp-env.override.json
+
+# Created by https://www.toptal.com/developers/gitignore/api/node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode
+# Edit at https://www.toptal.com/developers/gitignore?templates=node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode
+
+### Composer ###
+composer.phar
+/vendor/
+
+# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+# composer.lock
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### Node Patch ###
+# Serverless Webpack directories
+.webpack/
+
+# Optional stylelint cache
+
+# SvelteKit build / generate output
+.svelte-kit
+
+### PhpStorm+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### PhpStorm+all Patch ###
+# Ignore everything but code style settings and run configurations
+# that are supposed to be shared within teams.
+
+.idea/*
+
+!.idea/codeStyles
+!.idea/runConfigurations
+
+### PHPUnit ###
+# Covers PHPUnit
+# Reference: https://phpunit.de/
+
+# Generated files
+.phpunit.result.cache
+.phpunit.cache
+
+# PHPUnit
+/app/phpunit.xml
+/phpunit.xml
+
+# Build data
+/build/
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/node,linux,macos,phpunit,windows,composer,phpstorm+all,visualstudiocode
diff --git a/plugins/locked-payment-methods-for-woocommerce/.wp-env.json b/plugins/locked-payment-methods-for-woocommerce/.wp-env.json
new file mode 100644
index 0000000..129d6be
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.wp-env.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/wp-env.schema.json",
+ "core": "WordPress/WordPress#tags/7.1.3",
+ "phpVersion": "8.5",
+ "port": 8826,
+ "testsEnvironment": false,
+ "mappings": {
+ "wp-content/plugins/locked-payment-methods-for-woocommerce": ".",
+ "wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce"
+ },
+ "config": {
+ "WP_DEBUG": true,
+ "SCRIPT_DEBUG": true,
+ "WP_ENVIRONMENT_TYPE": "local"
+ },
+ "lifecycleScripts": {
+ "afterStart": "node_modules/.bin/wp-env run cli wp plugin activate woocommerce && node_modules/.bin/wp-env run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env run cli wp plugin activate locked-payment-methods-for-woocommerce"
+ }
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/.wp-env.tests.json b/plugins/locked-payment-methods-for-woocommerce/.wp-env.tests.json
new file mode 100644
index 0000000..b3f5b0b
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/.wp-env.tests.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/wp-env.schema.json",
+ "core": "WordPress/WordPress#tags/7.1.3",
+ "phpVersion": "8.5",
+ "port": 8827,
+ "testsEnvironment": false,
+ "mappings": {
+ "wp-content/plugins/locked-payment-methods-for-woocommerce": ".",
+ "wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce"
+ },
+ "config": {
+ "WP_DEBUG": true,
+ "SCRIPT_DEBUG": true,
+ "WP_ENVIRONMENT_TYPE": false
+ },
+ "lifecycleScripts": {
+ "afterStart": "node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate woocommerce && node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate locked-payment-methods-for-woocommerce"
+ }
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md
new file mode 100644
index 0000000..c701853
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md
@@ -0,0 +1,74 @@
+# wc-locked-payment-methods
+
+A WordPress plugin built on the five wp-framework packages (`ahegyes/wp-framework-{bootstrap,shared,core,settings,woocommerce}`), from the template that starts every such plugin. It ships its own copy of the framework, PHP-DI and their dependencies, prefixed under `DeepWebSolutions\LockedPaymentMethods\Scoped` by php-scoper through wordpress-configs into `vendor-prefixed/` on every development install. Its architecture follows the framework's conventions 1–10 in [wordpress-framework's AGENTS.md](https://github.com/ahegyes/wordpress-framework/blob/trunk/AGENTS.md); this file covers what is particular to the plugin. `docs/ARCHITECTURE.md` describes the boot, `docs/recipes.md` the common changes, `tests/README.md` the suites.
+
+## Layout
+
+```
+locked-payment-methods-for-woocommerce.php the entry: safeguards, bootstrap load.php, the updater, the floor check, the build gate, the autoloader
+functions.php dws_lpmwc_plugin(), the boot on plugins_loaded 15, deactivation, the HPOS declaration (false), the includes/ loader
+uninstall.php the framework's uninstall engine over the footprint, with DWS_LPMWC_OPTION_DELETE_DATA's consent
+src/ the composition root and its components only
+├── Plugin.php SLUG, PREFIX (dws_lpmwc), COMPONENTS, the logger, the scheduler, the migration runner and the kernel
+├── Settings.php the WooCommerce settings tab dws_lpmwc: the greeting's fields and the uninstall consent
+├── Blocks.php the blocks from blocks/build/blocks-manifest.php and the block-editor script
+└── Feature/ the greeting: a composite gated on DWS_LPMWC_OPTION_FEATURE and its account-dashboard child
+includes/ hook-free code as dws_lpmwc_ functions and DWS_LPMWC_ constants, loaded in name order, _-prefixed files skipped
+├── safeguards.php restrictions that hold whenever the plugin cannot boot; loaded by the entry before the floor check
+├── options.php the option keys, their defaults, the typed readers, the translated text and the validator
+├── footprint.php dws_lpmwc_get_footprint(), every kind of data the plugin stores
+├── migrations.php dws_lpmwc_get_migration_steps() and one function per step, append-only
+├── assets.php the *.asset.php reader and the script and style registrars
+└── metadata.php the slug, name and version getters, the plugin's surface for a peer plugin
+blocks/src/, assets/src/ TypeScript and SCSS, built into blocks/build/ and assets/build/ (untracked)
+models/, templates/ empty extension points
+.github/scripts/fill-in-scaffold.sh the fill-in's sweep, which fill-in-scaffold.yml runs once in a new plugin and Quality rehearses here
+```
+
+## Commands
+
+```sh
+composer packages-install # dependencies, then php-scoper into vendor-prefixed/
+composer test:unit
+composer test:integration # starts .wp-env.tests.json, then runs test:integration:run, which builds the scripts first
+composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan (both profiles), Unit and Integration
+npm run setup # both installs, the scripts' build, then the development site
+npm run build # blocks and assets through the local wp-scripts; npm run start watches them in a POSIX shell
+npm run lint:scripts # ESLint, beside lint:styles (Stylelint) and lint:types (tsc)
+npm run test:e2e # Playwright on .wp-env.tests.json, starting it if needed; the scripts must be built
+```
+
+Run wp-scripts only through the npm scripts or `node_modules/.bin/wp-scripts`: `npx wp-scripts` can install an unrelated package of that name.
+
+## Environments
+
+- `.wp-env.json`: development, as `local`, on port 8826, behind `npm run setup` and `wp-env:*`. The framework rethrows boot and migration failures there.
+- `.wp-env.tests.json`: Integration and end to end, as `production`, on port 8827, behind `wp-env:tests:*`, `composer test:integration` and `npm run test:e2e`. The end-to-end suite deactivates and reactivates WooCommerce there.
+
+Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk.
+
+## Conventions
+
+1. **Placement.** `src/` holds only the composition root and its components. Code that registers no hooks, such as keys, readers, the footprint, migration steps and asset registrars, is `dws_lpmwc_` functions and `DWS_LPMWC_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match` or first-class callables there. Container definitions are composition and stay in `Plugin::get_definitions()`.
+2. **Variants.** A component that exists only in one build variant is a child in a branch of its parent's `get_child_component_classes()`, never a second entry in `COMPONENTS`.
+3. **Request context.** `is_admin()` is false on REST requests and true in admin-ajax ones, so an admin-only feature decides the request context in its callbacks, never in `should_load()`.
+4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. It reads a template-built base plugin's slug, name and version only through that plugin's `{prefix}_get_plugin_*()` getters, from `plugins_loaded` on, since the add-on may load first, and behind `function_exists()`, since a base plugin below its floors or with a partial build may stay active without defining them.
+5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input.
+6. **Options and data.** Options are read only through `includes/options.php`'s typed readers, safeguards excepted. Everything stored lives under `dws_lpmwc_`, or `_dws_lpmwc_` for hidden meta. The footprint covers the prefix's options, transients and user meta; object meta needs a `meta_prefixes` entry per type, and anything else its own entry.
+7. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_lpmwc_is_relaxed()` (`docs/recipes.md`). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks.
+8. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails.
+
+## Style
+
+- Every PHP file opens with `:`, never `Co-Authored-By`.
+
+## Limitations
+
+- The fill-in pushes with the workflow's own token, which GitHub refuses for workflow changes, so it never edits `.github/workflows/`; the workflows read the slug from `composer.json`'s `extra.text-domain`.
+- Integration and end to end share the tests site, so a row that changes shared data in a separate process flushes the parent's object cache.
+- The release zip carries the TypeScript and SCSS sources beside the built files, since a plugin's repository may be private and the zip is then the only place its users find them.
diff --git a/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md b/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md
new file mode 100644
index 0000000..cc205c3
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this plugin are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+Pending entries live in [`changelog/`](./changelog) — add via `composer changelog:add`. Aggregate into a release with `composer changelog:write`.
+
+## 2.0.0 - unreleased
+
+### Added
+- Initial release.
diff --git a/plugins/locked-payment-methods-for-woocommerce/CLAUDE.md b/plugins/locked-payment-methods-for-woocommerce/CLAUDE.md
new file mode 120000
index 0000000..47dc3e3
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/plugins/locked-payment-methods-for-woocommerce/LICENSE b/plugins/locked-payment-methods-for-woocommerce/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/plugins/locked-payment-methods-for-woocommerce/README.md b/plugins/locked-payment-methods-for-woocommerce/README.md
new file mode 100644
index 0000000..1574192
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/README.md
@@ -0,0 +1,73 @@
+# Locked Payment Methods for WooCommerce
+
+TODO: one sentence saying why this plugin exists, its philosophy rather than a list of what it holds. The same sentence is `composer.json`'s and `package.json`'s `description`, the entry file's `Description` header and the repository's GitHub description.
+
+## Requirements
+
+The entry file's headers state the floors: `Requires PHP`, `Requires at least` for WordPress, and `Requires Plugins` with `WC requires at least` for WooCommerce. On a site below them, WordPress shows its own notice and none of the plugin's features load.
+
+## Installation
+
+Download `locked-payment-methods-for-woocommerce.zip` from the repository's latest release, then upload it under Plugins → Add New Plugin → Upload Plugin. Later releases arrive through WordPress's own update check, from the repository the entry file's `Update URI` names.
+
+## Development
+
+Development needs PHP and Composer at the `Requires PHP` floor or later, Node.js and npm at `package.json`'s `engines`, Docker for `@wordpress/env`, and a POSIX shell for `npm run start`.
+
+Install the dependencies, prefix the framework into `vendor-prefixed/`, build the scripts and start the development site on :
+
+```sh
+npm run setup
+```
+
+Rebuild the blocks and assets on every change:
+
+```sh
+npm run start
+```
+
+Run the manifest and changelog checks, PHPCS, PHPStan, and the Unit and Integration suites, starting the tests site on as needed:
+
+```sh
+composer quality-check
+```
+
+Install the browser the end-to-end suite runs in, once per machine, then run the suite against the tests site:
+
+```sh
+npx playwright install chromium
+npm run test:e2e
+```
+
+Lint the scripts, styles and types:
+
+```sh
+npm run lint:scripts && npm run lint:styles && npm run lint:types
+```
+
+`docs/ARCHITECTURE.md` describes how the plugin loads and boots, `docs/recipes.md` holds the step-by-step changes most plugins need, `tests/README.md` describes the test suites, and `AGENTS.md` holds the conventions for people and agents working on the code.
+
+## After the fill-in
+
+The fill-in renamed the plugin and kept the template's demo, which boots and passes every suite. Replace or delete each part as the plugin grows its own:
+
+| Part | Where it lives | Removing it |
+| --- | --- | --- |
+| The greeting | `src/Feature/`, the Greeting section of `src/Settings.php`, and the `DWS_LPMWC_OPTION_FEATURE`, `DWS_LPMWC_OPTION_GREETING` and `DWS_LPMWC_OPTION_GREETING_ORDERS` keys in `includes/options.php` | Delete `src/Feature/` and its row in `Plugin::COMPONENTS`, the Greeting section of `src/Settings.php`, `assets/src/greeting.scss` and its `greeting=` entry in `package.json`'s `build:assets` and `start:assets`, the three keys with their defaults, text and validator, their lines in `dws_lpmwc_add_settings()`, and `tests/Integration/Feature/`, then the rows naming them in `OptionsTest`, `MigrationsTest` and `SettingsTest`, and `PluginBootTest`'s expectation that the greeting is skipped. `tests/e2e/settings.spec.ts` drives the greeting's validated field, so point it at a validated field of the plugin's own. |
+| The example block | `blocks/src/example-notice/`, registered by `src/Blocks.php` from `blocks/build/blocks-manifest.php` | Delete `blocks/`, `build:blocks` and `start:blocks` with their calls in `build` and `start`, `register_blocks()` and its `init` hook in `src/Blocks.php`, and `BlocksTest`'s block rows. |
+| The editor script | `assets/src/editor.ts`, enqueued in the block editor by `src/Blocks.php` | Delete it, its `editor=` entry in `build:assets` and `start:assets`, `enqueue_editor_script()` and its hook in `src/Blocks.php`, and `BlocksTest`'s editor row. Once `assets/src/` holds nothing, delete `build:assets` and `start:assets` with their calls in `build` and `start`. Once the plugin registers no blocks either, delete `src/Blocks.php`, its row in `Plugin::COMPONENTS`, `BlocksTest`, and `Blocks` in `PluginBootTest`'s resolved components. |
+| `models/` and `templates/` | Empty extension points, for classes on Composer's classmap and for template partials | Delete the folders the plugin never fills, and `models/` from `composer.json`'s `autoload.classmap`, since Composer fails to dump the autoloader for a missing folder. |
+| The fill-in | `.github/workflows/fill-in-scaffold.yml`, `.github/scripts/fill-in-scaffold.sh` and Quality's `scaffold` job | Delete all three: GitHub refuses the fill-in's own token for workflow changes, so the fill-in cannot remove them, and the job skips itself outside the template. |
+
+Whichever parts go, four settings follow what is left, since a plugin removes them in any order:
+
+- The entry's build gate checks `blocks/build/blocks-manifest.php`, and the plugin stays off until that file exists, so point it at a file the remaining build writes, such as `assets/build/editor.asset.php`, or drop its built-file check once nothing is built.
+- `lint:scripts` names only folders that still hold a script, since ESLint fails on a path that holds none.
+- `lint:styles` names only folders that still hold SCSS. Once none does, it keeps one pattern, such as `'assets/src/**/*.scss'`, and takes `--allow-empty-input`, since Stylelint fails when nothing matches, Quality runs it, and without a pattern wp-scripts lints every stylesheet in the project.
+- `build` stays, even once it runs nothing, since `npm run setup` and the Integration run call it.
+
+Still to decide by hand:
+
+- The Composer package name: the fill-in set `ahegyes/locked-payment-methods-for-woocommerce`, while plugins published to Composer use `ahegyes/wc-` for WooCommerce and `ahegyes/wp-` otherwise.
+- WooCommerce's order tables: `dws_lpmwc_declare_compatibility()` in `functions.php` declares the plugin incompatible until its order code is verified on both storages.
+- `SECURITY.md`'s scope and the issue forms in `.github/ISSUE_TEMPLATE/`, whose wording describes this plugin's demo.
diff --git a/plugins/locked-payment-methods-for-woocommerce/SECURITY.md b/plugins/locked-payment-methods-for-woocommerce/SECURITY.md
new file mode 100644
index 0000000..bd45895
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/SECURITY.md
@@ -0,0 +1,14 @@
+# Security policy
+
+## Reporting a vulnerability
+
+Report it privately through [GitHub's security advisory form](https://github.com/ahegyes/wc-locked-payment-methods/security/advisories/new), never in a public issue. The first response comes within 7 days.
+
+## Scope
+
+In scope: the plugin's PHP, JavaScript and CSS, its prefixing configuration (`scoper.inc.php`) and its CI workflows.
+
+Out of scope, reported upstream instead:
+- plugins generated from this repository, to their maintainers;
+- the wp-framework packages, to `ahegyes/wordpress-framework`;
+- WordPress, WooCommerce and the other Composer and npm dependencies, to their projects. Composer refuses to resolve a dependency version with a known advisory through `roave/security-advisories`. The Supply-Chain Audit workflow fails on an advisory anywhere in the Composer lock, and on a high-severity one among the npm runtime dependencies, of which the template has none; advisories in the npm development tools reach Dependabot alerts instead.
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/index.php b/plugins/locked-payment-methods-for-woocommerce/assets/index.php
new file mode 100644
index 0000000..f767346
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/assets/index.php
@@ -0,0 +1 @@
+
+ ): HTMLAttributes< HTMLElement >;
+
+ export namespace useBlockProps {
+ function save(
+ props?: HTMLAttributes< HTMLElement >
+ ): HTMLAttributes< HTMLElement >;
+ }
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/block.json b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/block.json
new file mode 100644
index 0000000..0b4f6df
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/block.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "locked-payment-methods-for-woocommerce/example-notice",
+ "title": "Example Notice",
+ "category": "widgets",
+ "icon": "info",
+ "description": "Displays a static example notice.",
+ "attributes": {},
+ "example": {},
+ "supports": {
+ "html": false
+ },
+ "textdomain": "locked-payment-methods-for-woocommerce",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css"
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/edit.tsx b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/edit.tsx
new file mode 100644
index 0000000..90b0382
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/edit.tsx
@@ -0,0 +1,15 @@
+import { useBlockProps } from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+
+import './editor.scss';
+
+export default function Edit() {
+ return (
+
+ { __(
+ 'An example notice from Locked Payment Methods for WooCommerce.',
+ 'locked-payment-methods-for-woocommerce'
+ ) }
+
+ );
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/editor.scss b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/editor.scss
new file mode 100644
index 0000000..dd5ac10
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/editor.scss
@@ -0,0 +1,3 @@
+.wp-block-locked-payment-methods-for-woocommerce-example-notice {
+ outline: 1px dashed currentcolor;
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/index.php b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/index.php
new file mode 100644
index 0000000..f767346
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/blocks/src/example-notice/index.php
@@ -0,0 +1 @@
+
+ An example notice from Locked Payment Methods for WooCommerce.
+
';
+ }
+
+ /**
+ * Saves the form with the plan's data box.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param mixed $post_data The data Memberships saves.
+ * @param mixed $meta_box_id The id of the box Memberships saves.
+ * @param int $plan_id The plan's id.
+ *
+ * @return void
+ */
+ public function save_panel( mixed $post_data, mixed $meta_box_id, int $plan_id ): void {
+ $this->form->save( $plan_id );
+ }
+
+ // endregion
+
+ // region HELPERS
+
+ /**
+ * Returns the id of the form's panel.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @return string
+ */
+ protected function get_panel_id(): string {
+ return \str_replace( '_', '-', $this->form->get_id() ) . '-data';
+ }
+
+ // endregion
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php b/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php
index 98cc01d..9445a6a 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/Plugin.php
@@ -17,6 +17,7 @@
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\OrderStrategy;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserProfileStrategy;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserRoleStrategy;
+use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\WooCommerceMembershipsStrategy;
\defined( 'ABSPATH' ) || exit;
@@ -66,6 +67,7 @@ final class Plugin {
UserProfileStrategy::class,
OrderStrategy::class,
GroupsStrategy::class,
+ WooCommerceMembershipsStrategy::class,
);
/**
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/Settings.php b/plugins/locked-payment-methods-for-woocommerce/src/Settings.php
index f1f3238..4d2833b 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/Settings.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/Settings.php
@@ -8,6 +8,7 @@
use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\WooCommerce\Setting\SectionFieldsDefinitionInterface;
use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\WooCommerce\Setting\SettingsSection;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\GroupsStrategy;
+use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\WooCommerceMembershipsStrategy;
\defined( 'ABSPATH' ) || exit;
@@ -217,12 +218,18 @@ protected function get_group_fields( string $group ): array {
),
'integrations' => \array_filter(
array(
- 'unlock-via-groups' => GroupsStrategy::is_available() ? array(
+ 'unlock-via-groups' => GroupsStrategy::is_available() ? array(
'title' => __( 'Unlock via Groups', 'locked-payment-methods-for-woocommerce' ),
'type' => 'checkbox',
'desc' => __( 'Unlock payment methods for the members of a Groups group from the group\'s settings', 'locked-payment-methods-for-woocommerce' ),
'default' => wc_bool_to_string( DWS_LPMWC_OPTION_DEFAULTS[ DWS_LPMWC_OPTION_UNLOCK_VIA_GROUPS ] ),
) : array(),
+ 'unlock-via-wc-memberships' => WooCommerceMembershipsStrategy::is_available() ? array(
+ 'title' => __( 'Unlock via WooCommerce Memberships', 'locked-payment-methods-for-woocommerce' ),
+ 'type' => 'checkbox',
+ 'desc' => __( 'Unlock payment methods for the active members of a membership plan from the plan\'s data', 'locked-payment-methods-for-woocommerce' ),
+ 'default' => wc_bool_to_string( DWS_LPMWC_OPTION_DEFAULTS[ DWS_LPMWC_OPTION_UNLOCK_VIA_WC_MEMBERSHIPS ] ),
+ ) : array(),
)
),
'plugin' => array(
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
new file mode 100644
index 0000000..6c1c350
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
@@ -0,0 +1,153 @@
+notices )->register_hooks();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ */
+ #[\Override]
+ public function get_object_form( string $form_id, int $object_id ): array {
+ $options = dws_lpmwc_get_grant_choices( dws_lpmwc_get_plan_unlocked_payment_method_ids( $object_id ), 0 );
+
+ return array(
+ 'title' => __( 'Unlocked Payment Methods', 'locked-payment-methods-for-woocommerce' ),
+ 'capability' => DWS_LPMWC_CAP_UNLOCK_VIA_WC_MEMBERSHIPS,
+ 'fields' => array() === $options ? array() : array(
+ array(
+ 'type' => 'checkboxes',
+ 'id' => DWS_LPMWC_META_UNLOCKED_METHODS,
+ 'title' => __( 'Unlocked payment methods', 'locked-payment-methods-for-woocommerce' ),
+ 'desc' => __( 'The plan\'s active members may pay with these methods.', 'locked-payment-methods-for-woocommerce' ),
+ 'options' => $options,
+ ),
+ ),
+ );
+ }
+
+ // endregion
+
+ // region METHODS
+
+ /**
+ * Returns whether WooCommerce Memberships is active at the oldest version v1 supported.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @return bool
+ */
+ public static function is_available(): bool {
+ $version = get_option( 'wc_memberships_version', '0.0.0' );
+
+ return \function_exists( 'wc_memberships_get_user_active_memberships' ) && \is_string( $version ) && \version_compare( $version, '1.18.0', '>=' );
+ }
+
+ // endregion
+
+ // region HOOKS
+
+ /**
+ * Returns false for a payment method a plan the user is an active member of unlocks, and the lock as given otherwise.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param bool $lock Whether the payment method stays locked.
+ * @param string $gateway_id The payment method's id.
+ * @param int $user_id The paying user, or 0 for a guest.
+ *
+ * @return bool
+ */
+ public function unlock_member_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ if ( 0 === $user_id ) {
+ return $lock;
+ }
+
+ foreach ( (array) wc_memberships_get_user_active_memberships( $user_id ) as $membership ) {
+ // Memberships sometimes returns cancelled memberships among the active ones.
+ $plan_id = \is_object( $membership ) && \method_exists( $membership, 'is_active' ) && \method_exists( $membership, 'get_plan_id' ) && true === $membership->is_active() ? $membership->get_plan_id() : null;
+ if ( \is_numeric( $plan_id ) && \in_array( $gateway_id, dws_lpmwc_get_plan_unlocked_payment_method_ids( (int) $plan_id ), true ) ) {
+ return false;
+ }
+ }
+
+ return $lock;
+ }
+
+ // endregion
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
index e4f893a..f8694df 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
@@ -12,6 +12,7 @@
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\OrderStrategy;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserProfileStrategy;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserRoleStrategy;
+use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\WooCommerceMembershipsStrategy;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;
@@ -22,7 +23,7 @@ public function test_the_plugin_boots_every_component_whose_switch_is_on(): void
self::assertSame( 'booted', $plugin->boot->status );
self::assertSame( array( Scheduler::class, AdminNotices::class, CapabilityGrants::class, Settings::class, UserRoleStrategy::class, UserProfileStrategy::class ), $plugin->kernel?->resolved );
- self::assertSame( array( OrderStrategy::class, GroupsStrategy::class ), $plugin->kernel->skipped ); // Orders and Groups are off by default, as v1's were.
+ self::assertSame( array( OrderStrategy::class, GroupsStrategy::class, WooCommerceMembershipsStrategy::class ), $plugin->kernel->skipped ); // Orders and the integrations are off by default, as v1's were.
}
public function test_the_entry_loads_every_include_but_those_whose_names_start_with_an_underscore(): void {
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php b/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php
new file mode 100644
index 0000000..5cd8268
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/PHPStan/woocommerce-memberships.php
@@ -0,0 +1,16 @@
+ $args The query's arguments.
+ *
+ * @return array
+ */
+function wc_memberships_get_user_active_memberships( $user_id = null, $args = array() ) {} // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- A declaration only.
From 794ca0075d1fdfdb10e7640e5d971b8ee198dd07 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:12:00 +0100
Subject: [PATCH 24/50] Test Locked Payment Methods' v1 API as deprecated shims
v1 exposed root getters, hook-tag builders, constants, boot hooks,
access checks and setting readers that returned true for "locked"
under names that read the other way and took v1's field ids, so code
written against v1 calls them.
DeprecatedApiTest expects v1's getters and constants to answer from
the plugin header, v1's hook-tag builders to build v1's names for the
plugin and for v1's twelve components, v1's boot hooks to fire after
the v2 ones, dws_lpmwc_is_payment_method_locked() to answer for a
user, v1's access checks to fire v1's filters and then answer from the
strategies, v1's per-strategy checks to answer from their strategy
alone and true while it is off, and v1's setting readers to map v1's
ids to the plugin's options through v1's value filters, each shim
deprecated with its replacement.
X rulings 94 and 95.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/DeprecatedApiTest.php | 257 ++++++++++++++++++
1 file changed, 257 insertions(+)
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php
new file mode 100644
index 0000000..cff59ff
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php
@@ -0,0 +1,257 @@
+ array( 'dws_lpmwc_get_hook_tag', array( 'check_payment_method_access_for_user' ), 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_user' ),
+ 'plugin-unsafe' => array( 'dws_lpmwc_get_hook_tag', array( 'Some Name', array( 'A\\B', '' ) ), 'locked_payment_methods_for_woocommerce/some_name/a_b' ),
+ 'component' => array( 'dws_lpmwc_get_component_hook_tag', array( 'lock-manager', 'is_locked_payment_method', array( 'cod' ) ), 'locked_payment_methods_for_woocommerce/lock_manager/is_locked_payment_method/cod' ),
+ 'component-group' => array( 'dws_lpmwc_get_component_hook_tag', array( 'general-settings', 'get_group_fields' ), 'locked_payment_methods_for_woocommerce/general_settings/get_group_fields' ),
+ 'component-strategy' => array( 'dws_lpmwc_get_component_hook_tag', array( 'user-meta-strategy', 'name' ), 'locked_payment_methods_for_woocommerce/user_meta_unlock_strategy/name' ),
+ 'component-premium' => array( 'dws_lpmwc_get_component_hook_tag', array( 'wc-memberships-strategy', 'name' ), 'locked_payment_methods_for_woocommerce/wc_memberships_unlock_strategy/name' ),
+ 'component-integrate' => array( 'dws_lpmwc_get_component_hook_tag', array( 'integrations-settings', 'get_group_fields' ), 'locked_payment_methods_for_woocommerce/integrations_settings/get_group_fields' ),
+ 'component-absent' => array( 'dws_lpmwc_get_component_hook_tag', array( 'freemius', 'name' ), null ),
+ );
+ }
+
+ #[\Override]
+ public static function setUpBeforeClass(): void {
+ require_once ABSPATH . 'wp-admin/includes/user.php';
+ foreach ( array( 'shop_manager', 'customer' ) as $role ) {
+ self::$users[ $role ] = (int) wp_insert_user(
+ array(
+ 'user_login' => "lpm-deprecated-$role",
+ 'user_pass' => 'password',
+ 'user_email' => "lpm-deprecated-$role@example.org",
+ 'role' => $role,
+ )
+ );
+ }
+ }
+
+ #[\Override]
+ public static function tearDownAfterClass(): void {
+ foreach ( self::$users as $user ) {
+ wp_delete_user( $user );
+ }
+ }
+
+ #[Before]
+ protected function record_deprecations(): void {
+ foreach ( self::KEYS as $key ) {
+ $this->stored[ $key ] = get_option( $key );
+ }
+ update_option( DWS_LPMWC_OPTION_LOCKED_METHODS, array( 'cod' ) );
+ update_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES, array( 'shop_manager' ) );
+ add_filter( 'deprecated_function_trigger_error', '__return_false' );
+ add_filter( 'deprecated_hook_trigger_error', '__return_false' );
+ add_action( 'deprecated_function_run', array( $this, 'record' ), 10, 2 );
+ add_action( 'deprecated_hook_run', array( $this, 'record' ), 10, 2 );
+ }
+
+ #[After]
+ protected function stop_recording(): void {
+ foreach ( $this->stored as $key => $value ) {
+ false === $value ? delete_option( $key ) : update_option( $key, $value );
+ }
+ remove_filter( 'deprecated_function_trigger_error', '__return_false' );
+ remove_filter( 'deprecated_hook_trigger_error', '__return_false' );
+ remove_action( 'deprecated_function_run', array( $this, 'record' ) );
+ remove_action( 'deprecated_hook_run', array( $this, 'record' ) );
+ }
+
+ public function record( string $name, string $replacement ): void {
+ $this->deprecated[] = array( $name, $replacement );
+ }
+
+ public function test_the_v1_getters_answer_from_the_plugin_header(): void {
+ $header = get_file_data(
+ DWS_LPMWC_FILE,
+ array(
+ 'Version' => 'Version',
+ 'RequiresWP' => 'Requires at least',
+ 'RequiresPHP' => 'Requires PHP',
+ )
+ );
+
+ self::assertSame( dws_lpmwc_get_plugin_name(), dws_lpmwc_name() );
+ self::assertSame( $header['Version'], dws_lpmwc_version() );
+ self::assertSame( DWS_LPMWC_FILE, dws_lpmwc_path() );
+ self::assertSame( $header['RequiresPHP'], dws_lpmwc_min_php() );
+ self::assertSame( $header['RequiresWP'], dws_lpmwc_min_wp() );
+ self::assertSame(
+ array(
+ array( 'dws_lpmwc_name', 'dws_lpmwc_get_plugin_name()' ),
+ array( 'dws_lpmwc_version', 'dws_lpmwc_get_plugin_version()' ),
+ array( 'dws_lpmwc_path', '' ),
+ array( 'dws_lpmwc_min_php', '' ),
+ array( 'dws_lpmwc_min_wp', '' ),
+ ),
+ $this->deprecated
+ );
+ }
+
+ public function test_the_v1_constants_hold_the_plugin_header_without_v1s_whitelabel_prefix(): void {
+ $header = get_file_data(
+ DWS_LPMWC_FILE,
+ array(
+ 'Version' => 'Version',
+ 'RequiresWP' => 'Requires at least',
+ 'RequiresPHP' => 'Requires PHP',
+ )
+ );
+
+ self::assertSame( 'Locked Payment Methods for WooCommerce', DWS_LPMWC_NAME );
+ self::assertSame( $header['Version'], DWS_LPMWC_VERSION );
+ self::assertSame( DWS_LPMWC_FILE, DWS_LPMWC_PATH );
+ self::assertSame( $header['RequiresPHP'], DWS_LPMWC_MIN_PHP );
+ self::assertSame( $header['RequiresWP'], DWS_LPMWC_MIN_WP );
+ }
+
+ #[DataProvider( 'hook_tags' )]
+ public function test_a_v1_hook_tag_builds_as_v1_built_it( string $shim, array $arguments, ?string $expected ): void {
+ self::assertSame( $expected, $shim( ...$arguments ) );
+ self::assertSame( array( array( $shim, '' ) ), $this->deprecated );
+ }
+
+ public function test_the_v1_boot_hooks_fire_after_the_v2_ones_with_the_failure(): void {
+ $fired = array();
+ $spy = static function ( mixed $error = null ) use ( &$fired ): void {
+ $fired[] = array( current_action(), $error );
+ };
+ add_action( 'dws_lpmwc_initialized', $spy );
+ add_action( 'dws_lpmwc_initialization_failure', $spy );
+ $error = new \RuntimeException( 'Failed.' );
+
+ try {
+ do_action( 'dws_lpmwc/boot/succeeded' );
+ do_action( 'dws_lpmwc/boot/failed', $error );
+ } finally {
+ remove_action( 'dws_lpmwc_initialized', $spy );
+ remove_action( 'dws_lpmwc_initialization_failure', $spy );
+ }
+
+ self::assertSame(
+ array(
+ array( 'dws_lpmwc_initialized', null ),
+ array( 'dws_lpmwc_initialization_failure', $error ),
+ ),
+ $fired
+ );
+ self::assertSame(
+ array(
+ array( 'dws_lpmwc_initialized', 'dws_lpmwc/boot/succeeded' ),
+ array( 'dws_lpmwc_initialization_failure', 'dws_lpmwc/boot/failed' ),
+ ),
+ $this->deprecated
+ );
+ }
+
+ public function test_a_payment_method_is_locked_for_a_user_when_it_is_listed_and_no_strategy_unlocks_it(): void {
+ self::assertTrue( dws_lpmwc_is_payment_method_locked( 'cod', self::$users['customer'] ) );
+ self::assertFalse( dws_lpmwc_is_payment_method_locked( 'cod', self::$users['shop_manager'] ) );
+ self::assertFalse( dws_lpmwc_is_payment_method_locked( 'bacs', self::$users['customer'] ) );
+ self::assertSame( array(), $this->deprecated );
+ }
+
+ public function test_v1s_access_checks_answer_true_for_locked_from_the_strategies_alone_after_v1s_filter(): void {
+ $v1 = 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_user';
+ $seen = array();
+ $spy = static function ( mixed ...$args ) use ( &$seen ): mixed {
+ $seen[] = $args;
+
+ return $args[0];
+ };
+ add_filter( $v1, $spy, 10, 3 );
+
+ try {
+ self::assertTrue( dws_lpmwc_check_payment_method_access_for_user( 'bacs', self::$users['customer'] ) );
+ self::assertFalse( dws_lpmwc_check_payment_method_access_for_user( 'bacs', self::$users['shop_manager'] ) );
+ } finally {
+ remove_filter( $v1, $spy );
+ }
+
+ self::assertSame( array( array( true, 'bacs', self::$users['customer'] ), array( true, 'bacs', self::$users['shop_manager'] ) ), $seen );
+ self::assertContains( array( 'dws_lpmwc_check_payment_method_access_for_user', 'dws_lpmwc_is_payment_method_locked()' ), $this->deprecated );
+ self::assertContains( array( $v1, 'dws_lpmwc/payment_method/should_lock' ), $this->deprecated );
+ }
+
+ public function test_v1s_order_access_check_judges_the_order_and_its_customer_after_v1s_filter(): void {
+ $order = wc_create_order( array( 'customer_id' => self::$users['shop_manager'] ) );
+ $v1 = 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_wc_order';
+ $seen = array();
+ $spy = static function ( mixed ...$args ) use ( &$seen ): mixed {
+ $seen[] = $args;
+
+ return $args[0];
+ };
+ add_filter( $v1, $spy, 10, 3 );
+
+ try {
+ self::assertFalse( dws_lpmwc_check_payment_method_access_for_wc_order( 'cod', $order->get_id() ) );
+ } finally {
+ remove_filter( $v1, $spy );
+ $order->delete( true );
+ }
+
+ self::assertSame( array( array( true, 'cod', $order->get_id() ) ), $seen );
+ self::assertContains( array( $v1, 'dws_lpmwc/payment_method/should_lock' ), $this->deprecated );
+ }
+
+ public function test_v1s_per_strategy_checks_answer_from_their_strategy_alone_and_true_while_it_is_off(): void {
+ $order = wc_create_order();
+ $order->update_meta_data( DWS_LPMWC_META_UNLOCKED_METHODS, array( 'cod' ) );
+ $order->save();
+
+ try {
+ self::assertFalse( dws_lpmwc_check_payment_method_access_via_user_roles( 'cod', self::$users['shop_manager'] ) );
+ self::assertTrue( dws_lpmwc_check_payment_method_access_via_user_roles( 'cod', self::$users['customer'] ) );
+ self::assertTrue( dws_lpmwc_check_payment_method_access_via_user_meta( 'cod', self::$users['shop_manager'] ) );
+ self::assertTrue( dws_lpmwc_check_payment_method_access_via_wc_order_meta( 'cod', $order->get_id() ) );
+
+ update_option( DWS_LPMWC_OPTION_UNLOCK_VIA_ORDERS, 'yes' );
+ self::assertFalse( dws_lpmwc_check_payment_method_access_via_wc_order_meta( 'cod', $order->get_id() ) );
+
+ update_option( DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, 'no' );
+ self::assertTrue( dws_lpmwc_check_payment_method_access_via_user_roles( 'cod', self::$users['shop_manager'] ) );
+ } finally {
+ $order->delete( true );
+ }
+
+ self::assertContains( array( 'dws_lpmwc_check_payment_method_access_via_user_roles', 'dws_lpmwc_is_payment_method_locked()' ), $this->deprecated );
+ }
+
+ public function test_v1s_setting_readers_map_v1s_ids_to_the_plugins_options_through_v1s_filters(): void {
+ update_option( DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, 'yes' );
+ $page = 'locked_payment_methods_for_woocommerce/settings/get_validated_option_value';
+
+ self::assertSame( array( 'cod' ), dws_lpmwc_get_validated_general_setting( 'locked-payment-methods' ) );
+ self::assertTrue( dws_lpmwc_get_validated_setting( 'unlock-via-user-role', 'general' ) );
+ self::assertSame( array( 'shop_manager' ), dws_lpmwc_get_validated_setting( 'general/full-access-user-roles' ) );
+ self::assertSame( 'yes', dws_lpmwc_get_raw_setting( 'unlock-via-user-role', 'general' ) );
+ self::assertFalse( dws_lpmwc_validate_setting( 'maybe-not', 'remove-data-uninstall', 'plugin' ) );
+ self::assertSame( array( 'bacs' ), dws_lpmwc_validate_setting( array( 'bacs', 7 ), 'locked-payment-methods', 'general' ) );
+ self::assertNull( dws_lpmwc_get_validated_setting( 'unknown', 'general' ) );
+
+ self::assertContains( array( 'locked_payment_methods_for_woocommerce/general_settings/get_validated_option_value', 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), $this->deprecated );
+ self::assertNotContains( array( $page, 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), $this->deprecated ); // No callback hooks the page's filter, so it reports no deprecation.
+ self::assertContains( array( 'dws_lpmwc_get_validated_general_setting', 'get_option()' ), $this->deprecated );
+ }
+}
From c50138281b38b7d2f3518c18b49248599541dce3 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:15:28 +0100
Subject: [PATCH 25/50] Keep Locked Payment Methods' v1 API as deprecated shims
v1 exposed root getters, hook-tag builders, constants, boot hooks,
access checks and setting readers that returned true for "locked"
under names that read the other way, ignored the locked list and took
v1's field ids, which no longer name options.
includes/deprecated.php keeps them through 2.x, each deprecated with
its replacement: the getters and constants answer from the plugin
header, without v1's whitelabel prefix; the hook-tag builders build
v1's names for the plugin and its twelve v1 components; v1's boot
hooks fire after the v2 ones; the access checks fire v1's per-user and
per-order filters, then answer from the strategies; the per-strategy
checks answer from their strategy alone, through an internal static
check each strategy's hook now wraps, and true while it is off; and
the setting readers map v1's ids to the plugin's options through v1's
page and group value filters. dws_lpmwc_is_payment_method_locked()
answers whether a method is on the list and no strategy unlocks it
for a user and an order, and the profile and Users screens ask it.
X rulings 94 and 95.
Assisted-by: Claude Code:claude-opus-5-5
---
.../functions.php | 2 +
.../includes/deprecated.php | 584 ++++++++++++++++++
.../includes/grants.php | 20 +-
.../src/UnlockStrategies/GroupsStrategy.php | 39 +-
.../src/UnlockStrategies/OrderStrategy.php | 23 +-
.../UnlockStrategies/UserProfileStrategy.php | 29 +-
.../src/UnlockStrategies/UserRoleStrategy.php | 35 +-
.../WooCommerceMembershipsStrategy.php | 39 +-
.../tests/Integration/DeprecatedApiTest.php | 38 +-
9 files changed, 758 insertions(+), 51 deletions(-)
create mode 100644 plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php
diff --git a/plugins/locked-payment-methods-for-woocommerce/functions.php b/plugins/locked-payment-methods-for-woocommerce/functions.php
index 53e92c5..b320d21 100644
--- a/plugins/locked-payment-methods-for-woocommerce/functions.php
+++ b/plugins/locked-payment-methods-for-woocommerce/functions.php
@@ -28,6 +28,8 @@
// The hooks come last, so an include that throws leaves nothing of the plugin hooked.
add_action( 'plugins_loaded', 'dws_lpmwc_boot', 15 );
add_action( 'before_woocommerce_init', 'dws_lpmwc_declare_compatibility' );
+add_action( 'dws_lpmwc/boot/succeeded', 'dws_lpmwc_fire_v1_initialized' );
+add_action( 'dws_lpmwc/boot/failed', 'dws_lpmwc_fire_v1_initialization_failure' );
register_deactivation_hook( DWS_LPMWC_FILE, 'dws_lpmwc_deactivate' );
/**
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php
new file mode 100644
index 0000000..e0604d2
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/deprecated.php
@@ -0,0 +1,584 @@
+
+ */
+$dws_lpmwc_header = get_file_data(
+ DWS_LPMWC_FILE,
+ array(
+ 'Name' => 'Plugin Name',
+ 'Version' => 'Version',
+ 'RequiresWP' => 'Requires at least',
+ 'RequiresPHP' => 'Requires PHP',
+ )
+);
+
+/**
+ * The plugin's untranslated name.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_name().
+ */
+define( 'DWS_LPMWC_NAME', $dws_lpmwc_header['Name'] );
+
+/**
+ * The plugin's version.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_version().
+ */
+define( 'DWS_LPMWC_VERSION', $dws_lpmwc_header['Version'] );
+
+/**
+ * The path of the plugin's main file.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use DWS_LPMWC_FILE.
+ */
+define( 'DWS_LPMWC_PATH', DWS_LPMWC_FILE );
+
+/**
+ * The oldest PHP version the plugin runs on.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ */
+define( 'DWS_LPMWC_MIN_PHP', $dws_lpmwc_header['RequiresPHP'] );
+
+/**
+ * The oldest WordPress version the plugin runs on.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ */
+define( 'DWS_LPMWC_MIN_WP', $dws_lpmwc_header['RequiresWP'] );
+
+unset( $dws_lpmwc_header );
+
+/**
+ * The root of each v1 component's hook tags, keyed by the component's id, which v1 built from the component's name.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @var array
+ */
+const DWS_LPMWC_V1_COMPONENTS = array(
+ 'settings' => 'settings',
+ 'general-settings' => 'general_settings',
+ 'plugin-settings' => 'plugin_settings',
+ 'permissions' => 'permissions',
+ 'lock-manager' => 'lock_manager',
+ 'user-meta-strategy' => 'user_meta_unlock_strategy',
+ 'user-role-strategy' => 'user_role_unlock_strategy',
+ 'integrations-settings' => 'integrations_settings',
+ 'integrations-permissions' => 'integrations_permissions',
+ 'order-meta-strategy' => 'order_meta_unlock_strategy',
+ 'groups-strategy' => 'groups_unlock_strategy',
+ 'wc-memberships-strategy' => 'wc_memberships_unlock_strategy',
+);
+
+/**
+ * Returns the plugin's name, translated once init has fired.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_name().
+ *
+ * @return string
+ */
+function dws_lpmwc_name(): string {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_get_plugin_name()' );
+
+ return dws_lpmwc_get_plugin_name();
+}
+
+/**
+ * Returns the plugin's version.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_get_plugin_version().
+ *
+ * @return string
+ */
+function dws_lpmwc_version(): string {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_get_plugin_version()' );
+
+ return dws_lpmwc_get_plugin_version();
+}
+
+/**
+ * Returns the path of the plugin's main file.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ *
+ * @return string
+ */
+function dws_lpmwc_path(): string {
+ _deprecated_function( __FUNCTION__, '2.0.0' );
+
+ return DWS_LPMWC_PATH;
+}
+
+/**
+ * Returns the oldest PHP version the plugin runs on.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ *
+ * @return string
+ */
+function dws_lpmwc_min_php(): string {
+ _deprecated_function( __FUNCTION__, '2.0.0' );
+
+ return DWS_LPMWC_MIN_PHP;
+}
+
+/**
+ * Returns the oldest WordPress version the plugin runs on.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ *
+ * @return string
+ */
+function dws_lpmwc_min_wp(): string {
+ _deprecated_function( __FUNCTION__, '2.0.0' );
+
+ return DWS_LPMWC_MIN_WP;
+}
+
+/**
+ * Returns a hook name as v1 built it for the plugin.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ *
+ * @param string $name The hook's name.
+ * @param string|array $extra The name's further parts.
+ *
+ * @return string
+ */
+function dws_lpmwc_get_hook_tag( string $name, $extra = array() ): string {
+ _deprecated_function( __FUNCTION__, '2.0.0' );
+
+ return dws_lpmwc_join_v1_hook_tag( 'locked_payment_methods_for_woocommerce', $name, $extra );
+}
+
+/**
+ * Returns a hook name as v1 built it for one of its components, or null for an id v1 had no component for.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ *
+ * @param string $component_id The v1 component's id.
+ * @param string $name The hook's name.
+ * @param string|array $extra The name's further parts.
+ *
+ * @return string|null
+ */
+function dws_lpmwc_get_component_hook_tag( string $component_id, string $name, $extra = array() ): ?string {
+ _deprecated_function( __FUNCTION__, '2.0.0' );
+
+ return isset( DWS_LPMWC_V1_COMPONENTS[ $component_id ] ) ? dws_lpmwc_join_v1_hook_tag( 'locked_payment_methods_for_woocommerce/' . DWS_LPMWC_V1_COMPONENTS[ $component_id ], $name, $extra ) : null;
+}
+
+/**
+ * Returns a hook name joined and sanitized as v1 did: the non-empty parts joined with slashes, spaces and backslashes as underscores, lowercased.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param string $root The name's root.
+ * @param string $name The hook's name.
+ * @param string|array $extra The name's further parts.
+ *
+ * @return string
+ */
+function dws_lpmwc_join_v1_hook_tag( string $root, string $name, $extra ): string {
+ return strtolower( str_replace( array( ' ', '\\' ), '_', implode( '/', array_filter( array_merge( array( $root, $name ), is_array( $extra ) ? $extra : array( $extra ) ), 'boolval' ) ) ) );
+}
+
+/**
+ * Fires v1's hook for a plugin that booted.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @return void
+ */
+function dws_lpmwc_fire_v1_initialized(): void {
+ /**
+ * Fires once the plugin has booted.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc/boot/succeeded.
+ */
+ do_action_deprecated( 'dws_lpmwc_initialized', array(), '2.0.0', 'dws_lpmwc/boot/succeeded' );
+}
+
+/**
+ * Fires v1's hook for a plugin whose boot failed, with the failure.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param Throwable $error The failure.
+ *
+ * @return void
+ */
+function dws_lpmwc_fire_v1_initialization_failure( Throwable $error ): void {
+ /**
+ * Fires when the plugin's boot failed.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc/boot/failed.
+ *
+ * @param Throwable $error The failure, where v1 passed its own exception.
+ */
+ do_action_deprecated( 'dws_lpmwc_initialization_failure', array( $error ), '2.0.0', 'dws_lpmwc/boot/failed' );
+}
+
+/**
+ * Returns whether a payment method stays locked for a user, as the plugin's strategies judge it, firing v1's filter first.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $user_id The user's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_for_user( string $locked_method_id, int $user_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+
+ /**
+ * Filters whether a payment method stays locked for a user.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc/payment_method/should_lock.
+ *
+ * @param bool $is_locked Whether the payment method stays locked.
+ * @param string $locked_method_id The payment method's id.
+ * @param int $user_id The user's id.
+ */
+ $lock = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_user', array( true, $locked_method_id, $user_id ), '2.0.0', 'dws_lpmwc/payment_method/should_lock' );
+
+ return dws_lpmwc_should_lock( false !== $lock, $locked_method_id, $user_id, 0 );
+}
+
+/**
+ * Returns whether a payment method stays locked for paying an order, as the plugin's strategies judge it for the order and its customer, firing v1's filter first.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $order_id The order's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_for_wc_order( string $locked_method_id, int $order_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+ $order = wc_get_order( $order_id );
+
+ /**
+ * Filters whether a payment method stays locked for paying an order.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc/payment_method/should_lock.
+ *
+ * @param bool $is_locked Whether the payment method stays locked.
+ * @param string $locked_method_id The payment method's id.
+ * @param int $order_id The order's id.
+ */
+ $lock = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_wc_order', array( true, $locked_method_id, $order_id ), '2.0.0', 'dws_lpmwc/payment_method/should_lock' );
+
+ return dws_lpmwc_should_lock( false !== $lock, $locked_method_id, $order instanceof WC_Order ? $order->get_customer_id() : 0, $order_id );
+}
+
+/**
+ * Returns whether the user-profile strategy leaves a payment method locked for a user, true while the strategy is off.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $user_id The user's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_via_user_meta( string $locked_method_id, int $user_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+
+ return ! ( UserProfileStrategy::should_load() && UserProfileStrategy::unlocks( $locked_method_id, $user_id ) );
+}
+
+/**
+ * Returns whether the role strategy leaves a payment method locked for a user, true while the strategy is off.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $user_id The user's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_via_user_roles( string $locked_method_id, int $user_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+
+ return ! ( UserRoleStrategy::should_load() && UserRoleStrategy::unlocks( $locked_method_id, $user_id ) );
+}
+
+/**
+ * Returns whether the Groups strategy leaves a payment method locked for a user, true while the strategy is off.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $user_id The user's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_via_groups( string $locked_method_id, int $user_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+
+ return ! ( GroupsStrategy::should_load() && GroupsStrategy::unlocks( $locked_method_id, $user_id ) );
+}
+
+/**
+ * Returns whether the WooCommerce Memberships strategy leaves a payment method locked for a user, true while the strategy is off.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $user_id The user's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_via_wc_memberships( string $locked_method_id, int $user_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+
+ return ! ( WooCommerceMembershipsStrategy::should_load() && WooCommerceMembershipsStrategy::unlocks( $locked_method_id, $user_id ) );
+}
+
+/**
+ * Returns whether the order strategy leaves a payment method locked for paying an order, true while the strategy is off.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use dws_lpmwc_is_payment_method_locked().
+ *
+ * @param string $locked_method_id The payment method's id.
+ * @param int $order_id The order's id.
+ *
+ * @return bool
+ */
+function dws_lpmwc_check_payment_method_access_via_wc_order_meta( string $locked_method_id, int $order_id ): bool {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'dws_lpmwc_is_payment_method_locked()' );
+
+ return ! ( OrderStrategy::should_load() && OrderStrategy::unlocks( $locked_method_id, $order_id ) );
+}
+
+/**
+ * Returns a setting's stored value, by v1's field id and group, or null for an id v1 had no setting for.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use get_option().
+ *
+ * @param string $field_id v1's id of the setting, prefixed with its group and a slash when the group is null.
+ * @param string|null $group Optional. v1's id of the setting's group. Default null.
+ *
+ * @return mixed
+ */
+function dws_lpmwc_get_raw_setting( string $field_id, ?string $group = null ) {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' );
+ $setting = dws_lpmwc_find_v1_setting( $field_id, $group );
+
+ return null === $setting ? null : get_option( $setting['key'], null );
+}
+
+/**
+ * Returns a setting's value, of the setting's kind, by v1's field id and group, through v1's value filters, or null for an id v1 had no setting for.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use get_option().
+ *
+ * @param string $field_id v1's id of the setting, prefixed with its group and a slash when the group is null.
+ * @param string|null $group Optional. v1's id of the setting's group. Default null.
+ *
+ * @return mixed
+ */
+function dws_lpmwc_get_validated_setting( string $field_id, ?string $group = null ) {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' );
+
+ return dws_lpmwc_read_v1_setting( $field_id, $group );
+}
+
+/**
+ * Returns a value as the setting's kind, by v1's field id and group: a boolean for a switch and a list of strings for a list, falling back to the setting's default.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0
+ *
+ * @param mixed $value The value.
+ * @param string $field_id v1's id of the setting, prefixed with its group and a slash when the group is null.
+ * @param string|null $group Optional. v1's id of the setting's group. Default null.
+ *
+ * @return mixed
+ */
+function dws_lpmwc_validate_setting( $value, string $field_id, ?string $group = null ) {
+ _deprecated_function( __FUNCTION__, '2.0.0' );
+ $setting = dws_lpmwc_find_v1_setting( $field_id, $group );
+
+ return null === $setting ? null : dws_lpmwc_validate_v1_setting( $value, $setting['key'] );
+}
+
+/**
+ * Returns a General setting's value, of the setting's kind, by v1's field id, through v1's value filter.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use get_option().
+ *
+ * @param string $field_id v1's id of the setting.
+ *
+ * @return mixed
+ */
+function dws_lpmwc_get_validated_general_setting( string $field_id ) {
+ _deprecated_function( __FUNCTION__, '2.0.0', 'get_option()' );
+
+ return dws_lpmwc_read_v1_setting( $field_id, 'general' );
+}
+
+/**
+ * Returns v1's group and field ids and the plugin's option for one of v1's settings, or null for an id v1 had no setting for.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param string $field_id v1's id of the setting, prefixed with its group and a slash when the group is null.
+ * @param string|null $group v1's id of the setting's group.
+ *
+ * @return array{group: string, field: string, key: string}|null
+ */
+function dws_lpmwc_find_v1_setting( string $field_id, ?string $group ): ?array {
+ if ( null === $group ) {
+ $parts = explode( '/', $field_id, 2 );
+ if ( 2 !== count( $parts ) ) {
+ return null;
+ }
+ list( $group, $field_id ) = $parts;
+ }
+
+ $key = DWS_LPMWC_V1_SETTINGS[ $group ][ $field_id ] ?? null;
+
+ return null === $key ? null : array(
+ 'group' => $group,
+ 'field' => $field_id,
+ 'key' => $key,
+ );
+}
+
+/**
+ * Returns a setting's value as v1's settings read it: through v1's page filter when the group is null, then through its group's filter.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param string $field_id v1's id of the setting, prefixed with its group and a slash when the group is null.
+ * @param string|null $group v1's id of the setting's group.
+ *
+ * @return mixed
+ */
+function dws_lpmwc_read_v1_setting( string $field_id, ?string $group ) {
+ $setting = dws_lpmwc_find_v1_setting( $field_id, $group );
+ if ( null === $group ) {
+ /**
+ * Filters a setting's value, by its group-prefixed v1 id, before its group reads it.
+ *
+ * @since 1.0.0
+ * @version 2.0.0
+ * @deprecated 2.0.0 Use the hook the deprecation notice names.
+ *
+ * @param mixed $value Null, or the value a callback answers.
+ * @param string $field_id v1's id of the setting, prefixed with its group and a slash.
+ */
+ $value = apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/settings/get_validated_option_value', array( null, $field_id ), '2.0.0', null === $setting ? 'get_option' : 'option_' . $setting['key'] );
+ if ( null !== $value ) {
+ return $value;
+ }
+ }
+
+ return null === $setting ? null : dws_lpmwc_apply_v1_setting_filter( dws_lpmwc_validate_v1_setting( get_option( $setting['key'], null ), $setting['key'] ), $setting['group'], $setting['field'], 'option_' . $setting['key'] );
+}
+
+/**
+ * Returns a value as a setting's kind, a boolean for a switch and a list of strings for a list, or the setting's default.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param mixed $value The value.
+ * @param string $key The setting's option.
+ *
+ * @return mixed
+ */
+function dws_lpmwc_validate_v1_setting( $value, string $key ) {
+ $default = DWS_LPMWC_OPTION_DEFAULTS[ $key ] ?? null;
+ if ( is_bool( $default ) ) {
+ return filter_var( $value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ?? $default;
+ }
+
+ return is_array( $value ) ? array_values( array_filter( $value, 'is_string' ) ) : $default;
+}
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
index fa5c5ab..97611b9 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
@@ -1,6 +1,6 @@
=' );
}
- // endregion
-
- // region HOOKS
-
/**
- * Returns false for a payment method a group of the user, or a group above one, unlocks, and the lock as given otherwise.
+ * Returns whether a group of the user, or a group above one, unlocks the payment method.
+ *
+ * @internal
*
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
* @param string $gateway_id The payment method's id.
- * @param int $user_id The paying user, or 0 for a guest.
+ * @param int $user_id The user's id, or 0 for a guest.
*
* @return bool
*/
- public function unlock_group_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ public static function unlocks( string $gateway_id, int $user_id ): bool {
if ( 0 === $user_id ) {
- return $lock;
+ return false;
}
$groups = dws_lpmwc_get_groups_unlocked_payment_method_ids();
foreach ( (array) ( new \Groups_User( $user_id ) )->group_ids_deep as $group_id ) { // @phpstan-ignore property.notFound (Groups_User resolves its group ids through __get, as at v1's oldest Groups.)
if ( \is_numeric( $group_id ) && \in_array( $gateway_id, $groups[ (int) $group_id ] ?? array(), true ) ) {
- return false;
+ return true;
}
}
- return $lock;
+ return false;
+ }
+
+ // endregion
+
+ // region HOOKS
+
+ /**
+ * Returns false for a payment method a group of the user, or a group above one, unlocks, and the lock as given otherwise.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param bool $lock Whether the payment method stays locked.
+ * @param string $gateway_id The payment method's id.
+ * @param int $user_id The paying user, or 0 for a guest.
+ *
+ * @return bool
+ */
+ public function unlock_group_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
/**
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php
index df4abc8..f4ccf2c 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php
@@ -104,6 +104,27 @@ public function get_object_form( string $form_id, int $object_id ): array {
// endregion
+ // region METHODS
+
+ /**
+ * Returns whether the order's grants list holds the payment method.
+ *
+ * @internal
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param string $gateway_id The payment method's id.
+ * @param int $order_id The order's id, or 0 for none.
+ *
+ * @return bool
+ */
+ public static function unlocks( string $gateway_id, int $order_id ): bool {
+ return \in_array( $gateway_id, dws_lpmwc_get_order_unlocked_payment_method_ids( $order_id ), true );
+ }
+
+ // endregion
+
// region HOOKS
/**
@@ -120,7 +141,7 @@ public function get_object_form( string $form_id, int $object_id ): array {
* @return bool
*/
public function unlock_granted_methods( bool $lock, string $gateway_id, int $user_id, int $order_id ): bool {
- return \in_array( $gateway_id, dws_lpmwc_get_order_unlocked_payment_method_ids( $order_id ), true ) ? false : $lock;
+ return self::unlocks( $gateway_id, $order_id ) ? false : $lock;
}
// endregion
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
index 9fb0c18..b288fb6 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
@@ -115,6 +115,27 @@ public function get_object_form( string $form_id, int $object_id ): array {
// endregion
+ // region METHODS
+
+ /**
+ * Returns whether the user's grants list on the current site holds the payment method.
+ *
+ * @internal
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param string $gateway_id The payment method's id.
+ * @param int $user_id The user's id, or 0 for a guest.
+ *
+ * @return bool
+ */
+ public static function unlocks( string $gateway_id, int $user_id ): bool {
+ return \in_array( $gateway_id, dws_lpmwc_get_user_unlocked_payment_method_ids( $user_id ), true );
+ }
+
+ // endregion
+
// region HOOKS
/**
@@ -130,7 +151,7 @@ public function get_object_form( string $form_id, int $object_id ): array {
* @return bool
*/
public function unlock_granted_methods( bool $lock, string $gateway_id, int $user_id ): bool {
- return \in_array( $gateway_id, dws_lpmwc_get_user_unlocked_payment_method_ids( $user_id ), true ) ? false : $lock;
+ return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
/**
@@ -171,9 +192,9 @@ public function render_users_column( string $output, string $column, int $user_i
$lines = array();
foreach ( dws_lpmwc_get_locked_payment_method_ids() as $id ) {
$state = match ( true ) {
- \in_array( $id, $granted, true ) => __( 'unlocked on the profile', 'locked-payment-methods-for-woocommerce' ),
- ! dws_lpmwc_should_lock( true, $id, $user_id, 0 ) => __( 'unlocked by another rule', 'locked-payment-methods-for-woocommerce' ),
- default => __( 'locked', 'locked-payment-methods-for-woocommerce' ),
+ \in_array( $id, $granted, true ) => __( 'unlocked on the profile', 'locked-payment-methods-for-woocommerce' ),
+ ! dws_lpmwc_is_payment_method_locked( $id, $user_id ) => __( 'unlocked by another rule', 'locked-payment-methods-for-woocommerce' ),
+ default => __( 'locked', 'locked-payment-methods-for-woocommerce' ),
};
/* translators: 1: payment method title, 2: the payment method's state for the user. */
$lines[] = esc_html( \sprintf( __( '%1$s: %2$s', 'locked-payment-methods-for-woocommerce' ), dws_lpmwc_get_payment_method_title( $id ), $state ) );
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php
index 91c4a32..b078b29 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php
@@ -40,29 +40,50 @@ public function register_hooks(): void {
// endregion
- // region HOOKS
+ // region METHODS
/**
- * Returns false for a user holding one of the full-access roles, and the lock as given otherwise.
+ * Returns whether the user holds one of the full-access roles, which unlocks every locked payment method.
+ *
+ * @internal
*
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
* @param string $gateway_id The payment method's id.
- * @param int $user_id The paying user, or 0 for a guest.
+ * @param int $user_id The user's id, or 0 for a guest.
*
* @return bool
*/
- public function unlock_for_full_access_roles( bool $lock, string $gateway_id, int $user_id ): bool {
+ public static function unlocks( string $gateway_id, int $user_id ): bool {
$user = get_userdata( $user_id );
if ( ! $user instanceof \WP_User ) {
- return $lock;
+ return false;
}
$roles = dws_lpmwc_apply_v1_setting_filter( dws_lpmwc_get_list_option( DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), 'general', 'full-access-user-roles', 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES );
- return \is_array( $roles ) && array() !== \array_intersect( $user->roles, \array_filter( $roles, 'is_string' ) ) ? false : $lock;
+ return \is_array( $roles ) && array() !== \array_intersect( $user->roles, \array_filter( $roles, 'is_string' ) );
+ }
+
+ // endregion
+
+ // region HOOKS
+
+ /**
+ * Returns false for a user holding one of the full-access roles, and the lock as given otherwise.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param bool $lock Whether the payment method stays locked.
+ * @param string $gateway_id The payment method's id.
+ * @param int $user_id The paying user, or 0 for a guest.
+ *
+ * @return bool
+ */
+ public function unlock_for_full_access_roles( bool $lock, string $gateway_id, int $user_id ): bool {
+ return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
// endregion
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
index 6c1c350..51076c9 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
@@ -117,36 +117,53 @@ public static function is_available(): bool {
return \function_exists( 'wc_memberships_get_user_active_memberships' ) && \is_string( $version ) && \version_compare( $version, '1.18.0', '>=' );
}
- // endregion
-
- // region HOOKS
-
/**
- * Returns false for a payment method a plan the user is an active member of unlocks, and the lock as given otherwise.
+ * Returns whether a plan the user is an active member of unlocks the payment method.
+ *
+ * @internal
*
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
* @param string $gateway_id The payment method's id.
- * @param int $user_id The paying user, or 0 for a guest.
+ * @param int $user_id The user's id, or 0 for a guest.
*
* @return bool
*/
- public function unlock_member_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ public static function unlocks( string $gateway_id, int $user_id ): bool {
if ( 0 === $user_id ) {
- return $lock;
+ return false;
}
foreach ( (array) wc_memberships_get_user_active_memberships( $user_id ) as $membership ) {
// Memberships sometimes returns cancelled memberships among the active ones.
$plan_id = \is_object( $membership ) && \method_exists( $membership, 'is_active' ) && \method_exists( $membership, 'get_plan_id' ) && true === $membership->is_active() ? $membership->get_plan_id() : null;
if ( \is_numeric( $plan_id ) && \in_array( $gateway_id, dws_lpmwc_get_plan_unlocked_payment_method_ids( (int) $plan_id ), true ) ) {
- return false;
+ return true;
}
}
- return $lock;
+ return false;
+ }
+
+ // endregion
+
+ // region HOOKS
+
+ /**
+ * Returns false for a payment method a plan the user is an active member of unlocks, and the lock as given otherwise.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param bool $lock Whether the payment method stays locked.
+ * @param string $gateway_id The payment method's id.
+ * @param int $user_id The paying user, or 0 for a guest.
+ *
+ * @return bool
+ */
+ public function unlock_member_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
// endregion
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php
index cff59ff..7cddae8 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/DeprecatedApiTest.php
@@ -194,10 +194,11 @@ public function test_v1s_access_checks_answer_true_for_locked_from_the_strategie
}
public function test_v1s_order_access_check_judges_the_order_and_its_customer_after_v1s_filter(): void {
- $order = wc_create_order( array( 'customer_id' => self::$users['shop_manager'] ) );
- $v1 = 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_wc_order';
- $seen = array();
- $spy = static function ( mixed ...$args ) use ( &$seen ): mixed {
+ $order = wc_create_order( array( 'customer_id' => self::$users['shop_manager'] ) );
+ $order_id = $order->get_id();
+ $v1 = 'locked_payment_methods_for_woocommerce/check_payment_method_access_for_wc_order';
+ $seen = array();
+ $spy = static function ( mixed ...$args ) use ( &$seen ): mixed {
$seen[] = $args;
return $args[0];
@@ -205,13 +206,13 @@ public function test_v1s_order_access_check_judges_the_order_and_its_customer_af
add_filter( $v1, $spy, 10, 3 );
try {
- self::assertFalse( dws_lpmwc_check_payment_method_access_for_wc_order( 'cod', $order->get_id() ) );
+ self::assertFalse( dws_lpmwc_check_payment_method_access_for_wc_order( 'cod', $order_id ) );
} finally {
remove_filter( $v1, $spy );
$order->delete( true );
}
- self::assertSame( array( array( true, 'cod', $order->get_id() ) ), $seen );
+ self::assertSame( array( array( true, 'cod', $order_id ) ), $seen );
self::assertContains( array( $v1, 'dws_lpmwc/payment_method/should_lock' ), $this->deprecated );
}
@@ -240,17 +241,24 @@ public function test_v1s_per_strategy_checks_answer_from_their_strategy_alone_an
public function test_v1s_setting_readers_map_v1s_ids_to_the_plugins_options_through_v1s_filters(): void {
update_option( DWS_LPMWC_OPTION_UNLOCK_VIA_USER_ROLES, 'yes' );
- $page = 'locked_payment_methods_for_woocommerce/settings/get_validated_option_value';
+ $page = 'locked_payment_methods_for_woocommerce/settings/get_validated_option_value';
+ $group = 'locked_payment_methods_for_woocommerce/general_settings/get_validated_option_value';
+ $through = static fn ( mixed $value ): mixed => $value;
+ add_filter( $group, $through );
- self::assertSame( array( 'cod' ), dws_lpmwc_get_validated_general_setting( 'locked-payment-methods' ) );
- self::assertTrue( dws_lpmwc_get_validated_setting( 'unlock-via-user-role', 'general' ) );
- self::assertSame( array( 'shop_manager' ), dws_lpmwc_get_validated_setting( 'general/full-access-user-roles' ) );
- self::assertSame( 'yes', dws_lpmwc_get_raw_setting( 'unlock-via-user-role', 'general' ) );
- self::assertFalse( dws_lpmwc_validate_setting( 'maybe-not', 'remove-data-uninstall', 'plugin' ) );
- self::assertSame( array( 'bacs' ), dws_lpmwc_validate_setting( array( 'bacs', 7 ), 'locked-payment-methods', 'general' ) );
- self::assertNull( dws_lpmwc_get_validated_setting( 'unknown', 'general' ) );
+ try {
+ self::assertSame( array( 'cod' ), dws_lpmwc_get_validated_general_setting( 'locked-payment-methods' ) );
+ self::assertTrue( dws_lpmwc_get_validated_setting( 'unlock-via-user-role', 'general' ) );
+ self::assertSame( array( 'shop_manager' ), dws_lpmwc_get_validated_setting( 'general/full-access-user-roles' ) );
+ self::assertSame( 'yes', dws_lpmwc_get_raw_setting( 'unlock-via-user-role', 'general' ) );
+ self::assertFalse( dws_lpmwc_validate_setting( 'maybe-not', 'remove-data-uninstall', 'plugin' ) );
+ self::assertSame( array( 'bacs' ), dws_lpmwc_validate_setting( array( 'bacs', 7 ), 'locked-payment-methods', 'general' ) );
+ self::assertNull( dws_lpmwc_get_validated_setting( 'unknown', 'general' ) );
+ } finally {
+ remove_filter( $group, $through );
+ }
- self::assertContains( array( 'locked_payment_methods_for_woocommerce/general_settings/get_validated_option_value', 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), $this->deprecated );
+ self::assertContains( array( $group, 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), $this->deprecated );
self::assertNotContains( array( $page, 'option_' . DWS_LPMWC_OPTION_FULL_ACCESS_ROLES ), $this->deprecated ); // No callback hooks the page's filter, so it reports no deprecation.
self::assertContains( array( 'dws_lpmwc_get_validated_general_setting', 'get_option()' ), $this->deprecated );
}
From 5e6ab43987fcbda7ad8ad61aae4e2bb3576c9961 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:16:57 +0100
Subject: [PATCH 26/50] Test that Locked Payment Methods' uninstall removes
v2's and v1's data
v1 removed data only from a Freemius hook, deleting its options under
an unescaped LIKE and its capabilities, and left user, order and plan
grants behind.
FootprintTest expects the footprint to name v1's option prefix,
version rows and notices, order meta under both prefixes, the plan
meta under both keys, v1's global user grants and the four
capabilities. UninstallTest expects a consenting uninstall to remove
the Groups map, v1's options, a user's site grants, v1's global
grants, v1's notices, an order's and a plan's grants under both keys,
and the capabilities.
X ruling 96.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/UninstallTest.php | 53 +++++++++++++++++++
.../tests/Unit/FootprintTest.php | 14 ++++-
2 files changed, 66 insertions(+), 1 deletion(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
index 9b48f4e..e0ee6a2 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
@@ -72,4 +72,57 @@ public function test_a_consenting_uninstall_removes_a_row_of_every_kind_the_fram
dws_lpmwc_plugin()->migrations?->run();
self::assertSame( 'no', get_option( DWS_LPMWC_OPTION_DELETE_DATA ) );
}
+
+ #[RunInSeparateProcess]
+ public function test_a_consenting_uninstall_removes_the_grants_v1s_rows_and_the_capabilities(): void {
+ global $wpdb;
+
+ update_option( DWS_LPMWC_OPTION_DELETE_DATA, 'yes' );
+ update_option( DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS, array( 7 => array( 'cod' ) ) );
+ foreach ( array( 'dws-wc-lpm_general_locked-payment-methods', 'locked_payment_methods_for_woocommerce_version', 'locked_payment_methods_for_woocommerce_original_version', '_locked_payment_methods_for_woocommerce_admin_notices' ) as $v1_option ) {
+ update_option( $v1_option, 'seeded' );
+ }
+ update_user_meta( self::USER, $wpdb->get_blog_prefix() . DWS_LPMWC_USER_OPTION_UNLOCKED_METHODS, array( 'cod' ) );
+ update_user_meta( self::USER, 'dws_wc_lpm_grant_access_cod', 'yes' );
+ update_user_meta( self::USER, '_locked_payment_methods_for_woocommerce_admin_notices', array( 'seeded' ) );
+ $order = wc_create_order();
+ $order->update_meta_data( DWS_LPMWC_META_UNLOCKED_METHODS, array( 'cod' ) );
+ $order->update_meta_data( '_dws_wc_lpm_grant_access_cod', 'yes' );
+ $order->save();
+ $plan = (int) wp_insert_post(
+ array(
+ 'post_type' => 'post',
+ 'post_title' => 'LPM plan',
+ 'post_status' => 'draft',
+ )
+ );
+ update_post_meta( $plan, DWS_LPMWC_META_UNLOCKED_METHODS, array( 'cod' ) );
+ update_post_meta( $plan, '_dws_wc_lpm_unlocked_payment_methods', array( 'cod' ) );
+ self::assertTrue( get_role( 'administrator' )?->has_cap( DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS ) );
+
+ try {
+ self::assertTrue( uninstall_plugin( plugin_basename( DWS_LPMWC_FILE ) ) );
+ wp_cache_flush();
+ wp_roles()->for_site();
+
+ self::assertFalse( get_option( DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS ) );
+ foreach ( array( 'dws-wc-lpm_general_locked-payment-methods', 'locked_payment_methods_for_woocommerce_version', 'locked_payment_methods_for_woocommerce_original_version', '_locked_payment_methods_for_woocommerce_admin_notices' ) as $v1_option ) {
+ self::assertFalse( get_option( $v1_option ), $v1_option );
+ }
+ self::assertSame( '', get_user_meta( self::USER, $wpdb->get_blog_prefix() . DWS_LPMWC_USER_OPTION_UNLOCKED_METHODS, true ) );
+ self::assertSame( '', get_user_meta( self::USER, 'dws_wc_lpm_grant_access_cod', true ) );
+ self::assertSame( '', get_user_meta( self::USER, '_locked_payment_methods_for_woocommerce_admin_notices', true ) );
+ self::assertSame( '', wc_get_order( $order->get_id() )?->get_meta( DWS_LPMWC_META_UNLOCKED_METHODS ) );
+ self::assertSame( '', wc_get_order( $order->get_id() )?->get_meta( '_dws_wc_lpm_grant_access_cod' ) );
+ self::assertSame( '', get_post_meta( $plan, DWS_LPMWC_META_UNLOCKED_METHODS, true ) );
+ self::assertSame( '', get_post_meta( $plan, '_dws_wc_lpm_unlocked_payment_methods', true ) );
+ self::assertFalse( get_role( 'administrator' )?->has_cap( DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS ) );
+ } finally {
+ $order->delete( true );
+ wp_delete_post( $plan, true );
+ // The other tests share the database, so the runner stores again what a new site starts with, and the grants act again.
+ dws_lpmwc_plugin()->migrations?->run();
+ do_action( 'wp_loaded' );
+ }
+ }
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
index dc918c5..8514628 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
@@ -10,7 +10,7 @@
#[CoversNothing]
final class FootprintTest extends TestCase {
// Prefix families a port keeps from its v1 names, which uninstall must still remove.
- protected const array LEGACY_FAMILIES = array();
+ protected const array LEGACY_FAMILIES = array( 'dws-wc-lpm_', '_dws_wc_lpm_', 'dws_wc_lpm_', 'locked_payment_methods_for_woocommerce_', '_locked_payment_methods_for_woocommerce_', 'unlock_dws_lpm_via_' );
public function test_the_prefix_has_one_segment_after_dws_so_no_prefix_in_its_family_starts_with_another(): void {
self::assertMatchesRegularExpression( '/^dws_[a-z0-9]+$/', Plugin::PREFIX );
@@ -65,4 +65,16 @@ public function test_it_covers_every_option_key(): void {
self::assertTrue( $covered, "The option $key is outside the footprint." );
}
}
+
+ public function test_it_covers_what_v1_stored_and_the_grants_and_capabilities(): void {
+ $footprint = dws_lpmwc_get_footprint();
+
+ self::assertContains( 'dws-wc-lpm_', $footprint['option_prefixes'] );
+ self::assertSame( array( 'locked_payment_methods_for_woocommerce_version', 'locked_payment_methods_for_woocommerce_original_version', '_locked_payment_methods_for_woocommerce_admin_notices' ), $footprint['options'] );
+ self::assertSame( array( '_' . Plugin::PREFIX . '_', '_dws_wc_lpm_' ), $footprint['meta_prefixes']['order'] );
+ self::assertSame( array( DWS_LPMWC_META_UNLOCKED_METHODS, '_dws_wc_lpm_unlocked_payment_methods' ), $footprint['meta']['post'] );
+ self::assertContains( 'dws_wc_lpm_', $footprint['user_meta_global_prefixes'] );
+ self::assertSame( array( '_locked_payment_methods_for_woocommerce_admin_notices' ), $footprint['user_meta_global'] );
+ self::assertSame( array( DWS_LPMWC_CAP_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_CAP_UNLOCK_VIA_ORDERS, DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS, DWS_LPMWC_CAP_UNLOCK_VIA_WC_MEMBERSHIPS ), $footprint['capabilities'] );
+ }
}
From a828378ab00143080fb268fa20146c53615304a7 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:18:14 +0100
Subject: [PATCH 27/50] Remove Locked Payment Methods' v2 and v1 data on a
consenting uninstall
v1 removed data only from a Freemius hook, deleting its options under
an unescaped LIKE and its capabilities, and left user, order and plan
grants behind.
The footprint, which the framework's uninstall engine removes where
the site consents through dws_lpmwc_delete_data, adds v1's option
prefix, version rows and notices; order meta under the plugin's and
v1's prefixes, on either storage; the plan meta under both keys; v1's
global user grants that a migration has not moved yet; and the four
capabilities. The Unit bootstrap loads the files whose constants the
footprint reads.
X ruling 96.
Assisted-by: Claude Code:claude-opus-5-5
---
.../includes/footprint.php | 13 +++++++++----
.../tests/Unit/bootstrap.php | 2 ++
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php b/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
index fdb32bc..ae53bad 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
@@ -13,19 +13,24 @@
defined( 'ABSPATH' ) || exit;
/**
- * Returns the plugin's stored data by kind, which covers every option, transient, user meta key and scheduled job under the plugin's prefix.
+ * Returns the plugin's stored data by kind, which covers every option, transient, user meta key and scheduled job under the plugin's prefix, the grants on users, orders, Groups groups and WooCommerce Memberships plans, and what v1 stored under its own names: its settings, version rows, notices, grants and capabilities.
*
* @since 2.0.0
* @version 2.0.0
*
- * @return array{option_prefixes: list, user_meta_site_prefixes: list, user_meta_global_prefixes: list, network_options: list, cron_hooks: list, as_groups: list}
+ * @return array{option_prefixes: list, options: list, meta: array>, meta_prefixes: array>, user_meta_site_prefixes: list, user_meta_global_prefixes: list, user_meta_global: list, network_options: list, capabilities: list, cron_hooks: list, as_groups: list}
*/
function dws_lpmwc_get_footprint(): array {
return array(
- 'option_prefixes' => array( Plugin::PREFIX . '_', '_transient_' . Plugin::PREFIX . '_', '_transient_timeout_' . Plugin::PREFIX . '_' ),
+ 'option_prefixes' => array( Plugin::PREFIX . '_', '_transient_' . Plugin::PREFIX . '_', '_transient_timeout_' . Plugin::PREFIX . '_', 'dws-wc-lpm_' ),
+ 'options' => array( 'locked_payment_methods_for_woocommerce_version', 'locked_payment_methods_for_woocommerce_original_version', '_locked_payment_methods_for_woocommerce_admin_notices' ),
+ 'meta' => array( 'post' => array( DWS_LPMWC_META_UNLOCKED_METHODS, '_dws_wc_lpm_unlocked_payment_methods' ) ),
+ 'meta_prefixes' => array( 'order' => array( '_' . Plugin::PREFIX . '_', '_dws_wc_lpm_' ) ),
'user_meta_site_prefixes' => array( Plugin::PREFIX . '_' ),
- 'user_meta_global_prefixes' => array( Plugin::PREFIX . '_' ),
+ 'user_meta_global_prefixes' => array( Plugin::PREFIX . '_', 'dws_wc_lpm_' ),
+ 'user_meta_global' => array( '_locked_payment_methods_for_woocommerce_admin_notices' ),
'network_options' => array( Plugin::PREFIX . '_network_notices' ),
+ 'capabilities' => array( DWS_LPMWC_CAP_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_CAP_UNLOCK_VIA_ORDERS, DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS, DWS_LPMWC_CAP_UNLOCK_VIA_WC_MEMBERSHIPS ),
'cron_hooks' => array( Plugin::PREFIX . '_revision/migration/run' ),
'as_groups' => array( Plugin::PREFIX ),
);
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/bootstrap.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/bootstrap.php
index af0ab55..b371842 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/bootstrap.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/bootstrap.php
@@ -4,5 +4,7 @@
define( 'ABSPATH', __DIR__ . '/' );
// WordPress's include loader is absent here, so the suite requires the declarations it reads.
+require_once dirname( __DIR__, 2 ) . '/includes/capabilities.php';
+require_once dirname( __DIR__, 2 ) . '/includes/grants.php';
require_once dirname( __DIR__, 2 ) . '/includes/options.php';
require_once dirname( __DIR__, 2 ) . '/includes/footprint.php';
From f5e2a323317cacf66a8d353edfd689072396c95f Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:19:20 +0100
Subject: [PATCH 28/50] Remove the demo's build output from Locked Payment
Methods
Removing the template's demo also dropped the ignore rules for
blocks/build and assets/build, so the same commit added the build
output the scaffold had left in place: the example block's and the
greeting's compiled scripts and styles, which nothing loads.
Assisted-by: Claude Code:claude-opus-5-5
---
.../assets/build/editor.asset.php | 7 -----
.../assets/build/editor.js | 1 -
.../assets/build/greeting-rtl.css | 1 -
.../assets/build/greeting.asset.php | 6 -----
.../assets/build/greeting.css | 1 -
.../assets/build/greeting.js | 0
.../blocks/build/blocks-manifest.php | 26 -------------------
.../blocks/build/example-notice/block.json | 18 -------------
.../blocks/build/example-notice/index-rtl.css | 1 -
.../build/example-notice/index.asset.php | 9 -------
.../blocks/build/example-notice/index.css | 1 -
.../blocks/build/example-notice/index.js | 1 -
.../build/example-notice/style-index-rtl.css | 1 -
.../build/example-notice/style-index.css | 1 -
14 files changed, 74 deletions(-)
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/build/editor.asset.php
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/build/editor.js
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/build/greeting-rtl.css
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.asset.php
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.css
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.js
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/blocks-manifest.php
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/block.json
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index-rtl.css
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.asset.php
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.css
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.js
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index-rtl.css
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index.css
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/build/editor.asset.php b/plugins/locked-payment-methods-for-woocommerce/assets/build/editor.asset.php
deleted file mode 100644
index 23e6096..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/assets/build/editor.asset.php
+++ /dev/null
@@ -1,7 +0,0 @@
- array(
- 'wp-blocks',
- 'wp-i18n'
- ),
- 'version' => 'bf1425f40472313c45de'
-);
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/build/editor.js b/plugins/locked-payment-methods-for-woocommerce/assets/build/editor.js
deleted file mode 100644
index 675511f..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/assets/build/editor.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{"use strict";const e=window.wp.blocks,o=window.wp.i18n;(0,e.registerBlockStyle)("locked-payment-methods-for-woocommerce/example-notice",{name:"outlined",label:(0,o.__)("Outlined","locked-payment-methods-for-woocommerce")})})();
\ No newline at end of file
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting-rtl.css b/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting-rtl.css
deleted file mode 100644
index 4070942..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting-rtl.css
+++ /dev/null
@@ -1 +0,0 @@
-[data-locked-payment-methods-for-woocommerce=greeting]{font-weight:600}
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.asset.php b/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.asset.php
deleted file mode 100644
index 5b607f2..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.asset.php
+++ /dev/null
@@ -1,6 +0,0 @@
- array(
-
- ),
- 'version' => 'f7397bd60bff06cddc75'
-);
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.css b/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.css
deleted file mode 100644
index 4070942..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.css
+++ /dev/null
@@ -1 +0,0 @@
-[data-locked-payment-methods-for-woocommerce=greeting]{font-weight:600}
diff --git a/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.js b/plugins/locked-payment-methods-for-woocommerce/assets/build/greeting.js
deleted file mode 100644
index e69de29..0000000
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/blocks-manifest.php b/plugins/locked-payment-methods-for-woocommerce/blocks/build/blocks-manifest.php
deleted file mode 100644
index 99f4491..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/blocks-manifest.php
+++ /dev/null
@@ -1,26 +0,0 @@
- array(
- '$schema' => 'https://schemas.wp.org/trunk/block.json',
- 'apiVersion' => 3,
- 'name' => 'locked-payment-methods-for-woocommerce/example-notice',
- 'title' => 'Example Notice',
- 'category' => 'widgets',
- 'icon' => 'info',
- 'description' => 'Displays a static example notice.',
- 'attributes' => array(
-
- ),
- 'example' => array(
-
- ),
- 'supports' => array(
- 'html' => false
- ),
- 'textdomain' => 'locked-payment-methods-for-woocommerce',
- 'editorScript' => 'file:./index.js',
- 'editorStyle' => 'file:./index.css',
- 'style' => 'file:./style-index.css'
- )
-);
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/block.json b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/block.json
deleted file mode 100644
index 4fe0aa7..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/block.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "$schema": "https://schemas.wp.org/trunk/block.json",
- "apiVersion": 3,
- "name": "locked-payment-methods-for-woocommerce/example-notice",
- "title": "Example Notice",
- "category": "widgets",
- "icon": "info",
- "description": "Displays a static example notice.",
- "attributes": {},
- "example": {},
- "supports": {
- "html": false
- },
- "textdomain": "locked-payment-methods-for-woocommerce",
- "editorScript": "file:./index.js",
- "editorStyle": "file:./index.css",
- "style": "file:./style-index.css"
-}
\ No newline at end of file
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index-rtl.css b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index-rtl.css
deleted file mode 100644
index 0dde168..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index-rtl.css
+++ /dev/null
@@ -1 +0,0 @@
-.wp-block-locked-payment-methods-for-woocommerce-example-notice{outline:1px dashed currentcolor}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.asset.php b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.asset.php
deleted file mode 100644
index 7f9c68b..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.asset.php
+++ /dev/null
@@ -1,9 +0,0 @@
- array(
- 'react-jsx-runtime',
- 'wp-block-editor',
- 'wp-blocks',
- 'wp-i18n'
- ),
- 'version' => '8467c3ed7cc607ac19aa'
-);
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.css b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.css
deleted file mode 100644
index 0dde168..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.css
+++ /dev/null
@@ -1 +0,0 @@
-.wp-block-locked-payment-methods-for-woocommerce-example-notice{outline:1px dashed currentcolor}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.js b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.js
deleted file mode 100644
index 09daa8a..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/index.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{"use strict";var e={857(){const e=window.wp.blocks,o=window.wp.blockEditor,t=window.wp.i18n,n=window.ReactJSXRuntime,s=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":3,"name":"locked-payment-methods-for-woocommerce/example-notice","title":"Example Notice","category":"widgets","icon":"info","description":"Displays a static example notice.","attributes":{},"example":{},"supports":{"html":false},"textdomain":"locked-payment-methods-for-woocommerce","editorScript":"file:./index.js","editorStyle":"file:./index.css","style":"file:./style-index.css"}');(0,e.registerBlockType)(s,{edit:function(){return(0,n.jsx)("p",{...(0,o.useBlockProps)(),children:(0,t.__)("An example notice from Locked Payment Methods for WooCommerce.","locked-payment-methods-for-woocommerce")})},save:function(){return(0,n.jsx)("p",{...o.useBlockProps.save(),children:"An example notice from Locked Payment Methods for WooCommerce."})}})}};const o={};function t(n){const s=o[n];if(void 0!==s)return s.exports;const r=o[n]={exports:{}};return e[n](r,r.exports,t),r.exports}t.m=e,(()=>{const e=[];t.O=(o,n,s)=>{if(!n){for(var r=0;rObject.hasOwn(e,o),(()=>{const e={369:0,629:0};t.O.j=o=>0===e[o];const o=(o,n)=>{let[s,r,c]=n;var i,l,m=0;if(s.some(o=>0!==e[o])){for(i in r)t.o(r,i)&&(t.m[i]=r[i]);if(c)var a=c(t)}for(o&&o(n);mt(857));n=t.O(n)})();
\ No newline at end of file
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index-rtl.css b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index-rtl.css
deleted file mode 100644
index 9849cf9..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index-rtl.css
+++ /dev/null
@@ -1 +0,0 @@
-.wp-block-locked-payment-methods-for-woocommerce-example-notice{border-right:4px solid;padding:.5em 1em}.wp-block-locked-payment-methods-for-woocommerce-example-notice.is-style-outlined{border:1px solid}
diff --git a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index.css b/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index.css
deleted file mode 100644
index 4950830..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/blocks/build/example-notice/style-index.css
+++ /dev/null
@@ -1 +0,0 @@
-.wp-block-locked-payment-methods-for-woocommerce-example-notice{border-left:4px solid;padding:.5em 1em}.wp-block-locked-payment-methods-for-woocommerce-example-notice.is-style-outlined{border:1px solid}
From 3502dfa506f8973522a2a6eea78101acf73dbcb6 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:23:14 +0100
Subject: [PATCH 29/50] Distribute Locked Payment Methods through wordpress.org
v1's readme advertised a premium build, linked a knowledge base and a
contact form that no longer exist, said only enabled payment methods
can be locked and placed the settings on the Payments tab; v1's
version history lived only in that readme.
Locked Payment Methods stays listed on wordpress.org, so the entry
drops the Update URI header, which leaves the bundled updater inert,
and the text-domain loader, since language packs load just in time,
and states what the plugin does. readme.txt keeps v1's Description,
Installation, FAQ and Screenshots, updated: the five ways of unlocking
a payment method as plain features, WooCommerce 11.2 and the Advanced
tab, any registered payment method lockable, grants per site on a
network, support on the forum. Its Upgrade Notice names the floors,
the settings' move and the explicit-false unlock, and points at the
changelog. CHANGELOG.md carries v1's history between the markers that
changelog:write copies into readme.txt, through the Pronamic tool now
in require-dev, and 23 fragments record 2.0.0's changes.
X rulings 88 and 97.
Assisted-by: Claude Code:claude-opus-5-5
---
.../CHANGELOG.md | 81 ++++++++++++++++-
.../changelog/deprecated-functions | 4 +
.../changelog/deprecated-hooks | 4 +
.../changelog/downgrade | 4 +
.../changelog/empty-roles | 4 +
.../changelog/freemius | 4 +
.../changelog/grant-choices | 4 +
.../changelog/group-grants | 4 +
.../changelog/lock-hooks | 4 +
.../changelog/lock-list-filters | 4 +
.../changelog/lock-safeguard | 4 +
.../changelog/lockable-methods | 4 +
.../changelog/order-grants | 4 +
.../changelog/order-pay-key | 4 +
.../changelog/plan-grants | 4 +
.../changelog/removed-functions | 4 +
.../changelog/removed-hooks | 4 +
.../changelog/requirements | 4 +
.../changelog/settings-section | 4 +
.../changelog/settings-switches | 4 +
.../changelog/translations | 4 +
.../changelog/uninstall | 4 +
.../changelog/user-grants | 4 +
.../changelog/users-screen | 4 +
.../composer.json | 6 +-
.../composer.lock | 43 ++++++++-
...locked-payment-methods-for-woocommerce.php | 4 +-
.../readme.txt | 90 +++++++++++++++++++
28 files changed, 308 insertions(+), 8 deletions(-)
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-functions
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-hooks
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/downgrade
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/empty-roles
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/freemius
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/grant-choices
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/group-grants
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/lock-hooks
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/lock-list-filters
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/lock-safeguard
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/lockable-methods
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/order-grants
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/plan-grants
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/removed-functions
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/removed-hooks
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/requirements
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/settings-section
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/settings-switches
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/translations
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/uninstall
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/user-grants
create mode 100644 plugins/locked-payment-methods-for-woocommerce/changelog/users-screen
create mode 100644 plugins/locked-payment-methods-for-woocommerce/readme.txt
diff --git a/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md b/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md
index cc205c3..e46512e 100644
--- a/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md
+++ b/plugins/locked-payment-methods-for-woocommerce/CHANGELOG.md
@@ -2,9 +2,84 @@
All notable changes to this plugin are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-Pending entries live in [`changelog/`](./changelog) — add via `composer changelog:add`. Aggregate into a release with `composer changelog:write`.
+Pending entries live in [`changelog/`](./changelog) — add via `composer changelog:add`. Aggregate into a release with `composer changelog:write`, which also copies the versions between the markers into `readme.txt`.
-## 2.0.0 - unreleased
+
+
+## 1.3.5 - 2022-07-13
+
+### Changed
+- Tested up to WordPress 6.0.1.
+- Tested up to WooCommerce 6.7.0.
+- Updated Freemius SDK.
+
+## 1.3.4 - 2022-03-20
+
+### Changed
+- Tested up to WooCommerce 6.3.
+
+### Security
+- Updated Freemius SDK.
+
+## 1.3.3 - 2022-02-08
+
+### Changed
+- Tested up to WordPress 5.9.
+- Updated DWS framework.
+
+## 1.3.2 - 2022-01-14
+
+### Changed
+- Tested up to WooCommerce 6.1.
+- Tested up to PHP 8.1.
+- Updated DWS framework.
+
+### Fixed
+- Handling of a framework initialization failure.
+
+## 1.3.1 - 2021-12-02
+
+### Changed
+- Tested up to the latest version of WordPress.
+- Optimized the PSR-4 autoloader.
+- Hook tags can be generated before the plugin is initialized.
+- Updated DWS framework.
+
+## 1.3.0 - 2021-11-15
+
+### Changed
+- Tested up to the latest versions of WordPress and WooCommerce.
+- Additional licensing terms for redistribution.
+- Updated the dependencies' namespace.
+- Updated DWS framework.
+
+### Fixed
+- Readme formatting.
+
+## 1.2.1 - 2021-09-26
+
+### Fixed
+- Language domain inside the dependencies folder.
+- PHP 8 error on the settings page.
+
+## 1.2.0 - 2021-09-24
+
+### Changed
+- Tested up to the latest versions of WordPress and WooCommerce.
+- Updated DWS framework.
+- Added more automated tests.
+
+## 1.1.0 - 2021-07-26
+
+### Changed
+- Tested up to the latest versions of WordPress and WooCommerce.
+- Renamed the capabilities after WordPress core's.
+- Moved the bootstrap functions to the global namespace.
+- Updated DWS framework.
+
+## 1.0.0 - 2021-05-29
### Added
-- Initial release.
+- First official release.
+
+
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-functions b/plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-functions
new file mode 100644
index 0000000..3c6072c
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-functions
@@ -0,0 +1,4 @@
+Significance: minor
+Type: deprecated
+
+v1's root getters, hook-tag builders, access checks, per-strategy checks and setting readers, and DWS_LPMWC_NAME, DWS_LPMWC_VERSION, DWS_LPMWC_PATH, DWS_LPMWC_MIN_PHP and DWS_LPMWC_MIN_WP; DWS_LPMWC_NAME no longer carries v1's whitelabel prefix.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-hooks b/plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-hooks
new file mode 100644
index 0000000..3bbe753
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/deprecated-hooks
@@ -0,0 +1,4 @@
+Significance: minor
+Type: deprecated
+
+v1's lock, access, group-fields and value filters and dws_lpmwc_initialized and dws_lpmwc_initialization_failure fire deprecated, where v1 fired them and before their replacements.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/downgrade b/plugins/locked-payment-methods-for-woocommerce/changelog/downgrade
new file mode 100644
index 0000000..8764a35
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/downgrade
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+A downgrade to 1.3.5 finds no unlocked payment methods for customers, orders, groups or plans, and locks every payment method on v1's list for everyone without full access.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/empty-roles b/plugins/locked-payment-methods-for-woocommerce/changelog/empty-roles
new file mode 100644
index 0000000..c261419
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/empty-roles
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fixed
+
+An empty list of full-access roles unlocks nothing; v1 then unlocked every locked payment method for everyone, guests included.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/freemius b/plugins/locked-payment-methods-for-woocommerce/changelog/freemius
new file mode 100644
index 0000000..e5e4d9e
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/freemius
@@ -0,0 +1,4 @@
+Significance: major
+Type: removed
+
+Freemius and the premium build's licensing, with dws_lpmwc_fs(), dws_lpmwc_fs_init(), dws_lpmwc_fs_settings_url(), dws_lpmwc_fs_loaded and the Upgrade and Freemius links; every way of unlocking a payment method is part of the plugin.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/grant-choices b/plugins/locked-payment-methods-for-woocommerce/changelog/grant-choices
new file mode 100644
index 0000000..7b3dcd1
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/grant-choices
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fixed
+
+Saving a profile, an order, a group or a plan keeps the payment methods it unlocks that are no longer locked, which it lists marked as not locked.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/group-grants b/plugins/locked-payment-methods-for-woocommerce/changelog/group-grants
new file mode 100644
index 0000000..85a895f
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/group-grants
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+A Groups group's unlocked payment methods move from v1's option per group into one dws_lpmwc_groups_unlocked_payment_methods map, which deleting the group leaves no entry of.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/lock-hooks b/plugins/locked-payment-methods-for-woocommerce/changelog/lock-hooks
new file mode 100644
index 0000000..587fa85
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/lock-hooks
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+dws_lpmwc/payment_method/locked_ids, which locks more payment methods, dws_lpmwc/payment_method/should_lock, which judges a payment method for the paying user and the order being paid, and dws_lpmwc_is_payment_method_locked().
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/lock-list-filters b/plugins/locked-payment-methods-for-woocommerce/changelog/lock-list-filters
new file mode 100644
index 0000000..b01645a
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/lock-list-filters
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+A filter on the locked payment methods' ids only adds ids, so a callback that removes one from locked_payment_methods_for_woocommerce/lock_manager/locked_payment_methods or locked_payment_methods_for_woocommerce/general_settings/get_validated_option_value no longer unlocks it.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/lock-safeguard b/plugins/locked-payment-methods-for-woocommerce/changelog/lock-safeguard
new file mode 100644
index 0000000..28e1d88
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/lock-safeguard
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+The lock holds whenever the plugin is active, on a runtime below its floors, with WooCommerce below 11.2 and when the plugin fails to boot, until the plugin has booted; only an explicit false from dws_lpmwc/payment_method/should_lock unlocks a payment method, so a callback that returns null, 0 or an empty string no longer does.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/lockable-methods b/plugins/locked-payment-methods-for-woocommerce/changelog/lockable-methods
new file mode 100644
index 0000000..23fbde0
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/lockable-methods
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Any payment method registered with WooCommerce can be locked, disabled ones marked, so a locked payment method that is disabled stays locked once it is enabled again; v1 offered only enabled ones, and saving while a locked one was disabled unlocked it.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/order-grants b/plugins/locked-payment-methods-for-woocommerce/changelog/order-grants
new file mode 100644
index 0000000..f781256
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/order-grants
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+An order's unlocked payment methods are one list under the order meta _dws_lpmwc_unlocked_payment_methods, edited in a box on the order screen on either order storage; the box is read-only on a paid order, so saving it keeps them, and the first update moves v1's _dws_wc_lpm_grant_access_{gateway} order meta into the list and deletes it.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key b/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key
new file mode 100644
index 0000000..4a8f32b
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key
@@ -0,0 +1,4 @@
+Significance: patch
+Type: security
+
+An order unlocks its payment methods only on its own payment page and with its order key, so a checkout request naming another order's id no longer takes that order's unlocked payment methods.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/plan-grants b/plugins/locked-payment-methods-for-woocommerce/changelog/plan-grants
new file mode 100644
index 0000000..fdb63eb
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/plan-grants
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+A WooCommerce Memberships plan's unlocked payment methods move from the plan meta _dws_wc_lpm_unlocked_payment_methods to _dws_lpmwc_unlocked_payment_methods, and its tab saves under a nonce of its own.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/removed-functions b/plugins/locked-payment-methods-for-woocommerce/changelog/removed-functions
new file mode 100644
index 0000000..fe5ad98
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/removed-functions
@@ -0,0 +1,4 @@
+Significance: major
+Type: removed
+
+dws_lpmwc_di_container(), dws_lpmwc_instance(), dws_lpmwc_component(), dws_lpmwc_instance_initialize(), dws_lpmwc_plugin_activate() and dws_lpmwc_plugin_uninstall(), which exposed v1's framework internals.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/removed-hooks b/plugins/locked-payment-methods-for-woocommerce/changelog/removed-hooks
new file mode 100644
index 0000000..6875e64
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/removed-hooks
@@ -0,0 +1,4 @@
+Significance: major
+Type: removed
+
+v1's settings hooks locked_payment_methods_for_woocommerce/settings/{registered_options_page,registered_options_groups,get_option_value,update_option_value,delete_option_value,validate_option_value} and locked_payment_methods_for_woocommerce/{general,integrations,plugin}_settings/validate_option_value, v1's framework notice actions and the dws_wp_framework/*/init_status filters.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/requirements b/plugins/locked-payment-methods-for-woocommerce/changelog/requirements
new file mode 100644
index 0000000..d7346a9
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/requirements
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+Requires PHP 8.5, WordPress 7.1 and WooCommerce 11.2, and declares compatibility with WooCommerce's order tables.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/settings-section b/plugins/locked-payment-methods-for-woocommerce/changelog/settings-section
new file mode 100644
index 0000000..24b8927
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/settings-section
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+The settings move from an unlisted section of WooCommerce's Payments tab to Locked Payment Methods on its Advanced tab, under dws_lpmwc_ options that the first update copies from v1's; v1's options stay until uninstall, so a downgrade to 1.3.5 still locks what it locked, and v1's settings URL no longer opens them.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/settings-switches b/plugins/locked-payment-methods-for-woocommerce/changelog/settings-switches
new file mode 100644
index 0000000..de7eb90
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/settings-switches
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Each way of unlocking a payment method is a checkbox, and turning one off keeps what it unlocked for when it is turned on again; the Groups and WooCommerce Memberships checkboxes show only while their plugin is active.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/translations b/plugins/locked-payment-methods-for-woocommerce/changelog/translations
new file mode 100644
index 0000000..39952ea
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/translations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Translations come from translate.wordpress.org, and the plugin ships no bundled translation files.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/uninstall b/plugins/locked-payment-methods-for-woocommerce/changelog/uninstall
new file mode 100644
index 0000000..da7c381
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/uninstall
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+Uninstalling with Remove all data on uninstallation checked also deletes the unlocked payment methods of customers, orders, groups and plans, v1's options and the capabilities, on every site of a network that consents.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/user-grants b/plugins/locked-payment-methods-for-woocommerce/changelog/user-grants
new file mode 100644
index 0000000..211e4f0
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/user-grants
@@ -0,0 +1,4 @@
+Significance: major
+Type: changed
+
+A customer's unlocked payment methods are one list per site, under the user option dws_lpmwc_unlocked_payment_methods, edited on the profile behind unlock_dws_lpm_via_user_profiles; the first update moves v1's dws_wc_lpm_grant_access_{gateway} user meta into every site's list, so those keep applying network-wide, and deletes it, so a downgrade to 1.3.5 finds no customer's unlocked payment methods.
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/users-screen b/plugins/locked-payment-methods-for-woocommerce/changelog/users-screen
new file mode 100644
index 0000000..fa0e5de
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/users-screen
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fixed
+
+The Users screen names each locked payment method's state in text and escapes it, and its filter applies from either dropdown; v1's showed icons without text and read only the top dropdown.
diff --git a/plugins/locked-payment-methods-for-woocommerce/composer.json b/plugins/locked-payment-methods-for-woocommerce/composer.json
index 6c143c5..0de19c2 100644
--- a/plugins/locked-payment-methods-for-woocommerce/composer.json
+++ b/plugins/locked-payment-methods-for-woocommerce/composer.json
@@ -60,6 +60,7 @@
"php-di/php-di": "^7.1.1",
"php-stubs/woocommerce-stubs": "^11.2",
"phpunit/phpunit": "^13",
+ "pronamic/changelog-md-to-wordpress-plugin-readme-txt": "^1.0",
"wp-plugin/groups": "^4.8",
"wp-plugin/woocommerce": "^11.2"
},
@@ -134,7 +135,10 @@
"changelog:add": "@php vendor/bin/changelogger add",
"changelog:validate": "@php vendor/bin/changelogger validate",
- "changelog:write": "@php vendor/bin/changelogger write"
+ "changelog:write": [
+ "@php vendor/bin/changelogger write",
+ "@php vendor/bin/pronamic-changelog-md-to-wordpress-plugin-readme-txt"
+ ]
},
"extra": {
diff --git a/plugins/locked-payment-methods-for-woocommerce/composer.lock b/plugins/locked-payment-methods-for-woocommerce/composer.lock
index caf788f..c0e6158 100644
--- a/plugins/locked-payment-methods-for-woocommerce/composer.lock
+++ b/plugins/locked-payment-methods-for-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": "daf3375aeffc7b0510f18f581f61d7df",
+ "content-hash": "8096846a869e42b7074b39c03e99073f",
"packages": [],
"packages-dev": [
{
@@ -2859,6 +2859,47 @@
"time": "2026-10-27T04:16:22+00:00",
"published-time": "2026-10-27T04:18:40+00:00"
},
+ {
+ "name": "pronamic/changelog-md-to-wordpress-plugin-readme-txt",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt.git",
+ "reference": "9419cc48c0a348caeae74040c1c35e07535f63ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/zipball/9419cc48c0a348caeae74040c1c35e07535f63ed",
+ "reference": "9419cc48c0a348caeae74040c1c35e07535f63ed",
+ "shasum": ""
+ },
+ "bin": [
+ "bin/pronamic-changelog-md-to-wordpress-plugin-readme-txt"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Pronamic\\ChangelogMdToWordPressPluginReadmeTxt\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Pronamic",
+ "email": "info@pronamic.eu"
+ }
+ ],
+ "description": "Automates syncing of changelog entries from CHANGELOG.md to WordPress plugin readme.txt.",
+ "support": {
+ "issues": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/issues",
+ "source": "https://github.com/pronamic/pronamic-changelog-md-to-wordpress-plugin-readme-txt/tree/v1.0.0"
+ },
+ "time": "2024-10-01T13:41:34+00:00",
+ "published-time": "2024-10-02T09:31:51+00:00"
+ },
{
"name": "psr/clock",
"version": "1.0.0",
diff --git a/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php b/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php
index 43a547f..ea15055 100644
--- a/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php
+++ b/plugins/locked-payment-methods-for-woocommerce/locked-payment-methods-for-woocommerce.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Locked Payment Methods for WooCommerce
* Plugin URI: https://github.com/ahegyes/wc-locked-payment-methods
- * Description: WordPress plugins on the wp-framework packages start here, from one tested anatomy, so all are built alike and a fix found in one applies to all.
+ * Description: Hides the payment methods you lock from customers at checkout until you unlock them for a user, a role, an order, a group or a membership plan.
* Version: 2.0.0
* Requires at least: 7.1
* Requires PHP: 8.5
@@ -13,7 +13,6 @@
* Author URI: https://github.com/ahegyes
* License: GPL-3.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
- * Update URI: https://github.com/ahegyes/wc-locked-payment-methods
* Text Domain: locked-payment-methods-for-woocommerce
* Domain Path: /languages
*
@@ -26,7 +25,6 @@
defined( 'ABSPATH' ) || exit;
define( 'DWS_LPMWC_FILE', __FILE__ );
-load_plugin_textdomain( 'locked-payment-methods-for-woocommerce', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- WordPress registers languages/ itself only for plugins it does not network-activate.
require_once __DIR__ . '/includes/safeguards.php'; // Before the floor gate, so the restrictions hold on any runtime that parses the plugin.
diff --git a/plugins/locked-payment-methods-for-woocommerce/readme.txt b/plugins/locked-payment-methods-for-woocommerce/readme.txt
new file mode 100644
index 0000000..a682b09
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/readme.txt
@@ -0,0 +1,90 @@
+=== Locked Payment Methods for WooCommerce ===
+Contributors: ahegyes, deepwebsolutions
+Tags: payment methods, woocommerce, checkout, manual permission
+Tested up to: 7.1
+Stable tag: 2.0.0
+Requires at least: 7.1
+Requires PHP: 8.5
+License: GPLv3 or later
+License URI: https://www.gnu.org/licenses/gpl-3.0.html
+
+Hides the payment methods you lock from customers at checkout until you unlock them for a user, a role, an order, a group or a membership plan.
+
+== Description ==
+
+**Locked Payment Methods for WooCommerce hides the payment methods you lock from every customer you have not unlocked them for.**
+
+A typical use-case involves locking high-risk payment methods like *Cash on delivery* or *Pay by invoice*, but you can lock any payment method registered with WooCommerce, for any reason.
+
+Locked payment methods are removed from the checkout and the order payment page for every customer they are not unlocked for. You can then unlock them case by case, or by criteria for logged-in users you trust, while everyone else sees only the payment methods you leave unlocked.
+
+### Unlocking a payment method for your customers
+
+* Unlock payment methods for a customer from their user profile.
+* Unlock every payment method for all customers with given user roles.
+* Unlock payment methods for one unpaid order from its edit screen.
+* Unlock payment methods for every active member of a [WooCommerce Memberships](https://woocommerce.com/products/woocommerce-memberships/) plan.
+* Unlock payment methods for every member of a [Groups](https://wordpress.org/plugins/groups/) group and of its subgroups.
+
+Developers can lock and unlock payment methods by their own rules through the plugin's filters.
+
+== Installation ==
+
+This plugin requires WooCommerce 11.2 or newer. If you're running a lower version, please update it first.
+
+### INSTALL FROM WITHIN WORDPRESS
+
+1. Visit the plugins page within your dashboard and select `Add New Plugin`.
+1. Search for `Locked Payment Methods for WooCommerce` and click the `Install Now` button.
+1. Activate the plugin from within your `Plugins` page.
+
+### INSTALL MANUALLY
+
+1. Download the plugin from https://wordpress.org/plugins/locked-payment-methods-for-woocommerce/ and unzip the archive.
+1. Copy the unzipped `locked-payment-methods-for-woocommerce` folder into your site's `wp-content/plugins` folder, through SFTP or your host's file manager.
+1. Activate the plugin through the `Plugins` menu in WordPress.
+
+### AFTER ACTIVATION
+
+If WooCommerce 11.2 or newer is active, you will find a section called `Locked Payment Methods` under `WooCommerce` → `Settings` → `Advanced`, which the plugin's `Settings` link on the `Plugins` page also opens. There you will be able to:
+
+1. Choose which payment methods are locked by default.
+1. Choose which ways of unlocking them you use, and which user roles have access to every payment method.
+1. Choose whether to remove the plugin's data, the unlocked payment methods included, when it is uninstalled.
+
+== Frequently Asked Questions ==
+
+= What is a locked payment method? =
+
+That's what we call payment methods that you choose to hide from your customers by default. Granting a customer access is called *unlocking* the payment method.
+
+= Which payment methods can I lock? =
+
+Any payment method registered with WooCommerce, disabled ones included. A locked payment method that you disable stays locked, so it is still locked once you enable it again.
+
+= Are unlocked payment methods shared across a multisite network? =
+
+No. A customer's unlocked payment methods are kept per site, so unlocking a payment method for a customer on one store of a network leaves it locked on the others. Payment methods unlocked before version 2.0.0 keep applying on every site.
+
+= How can I get help if I'm stuck? =
+
+You can open a community support ticket here at [wordpress.org](https://wordpress.org/support/plugin/locked-payment-methods-for-woocommerce/).
+
+== Screenshots ==
+
+1. Plugin settings section within the WooCommerce settings.
+2. Example of how to unlock payment methods for all users of given roles.
+3. Example of how to unlock payment methods for a specific user.
+
+== Upgrade Notice ==
+
+= 2.0.0 =
+Requires PHP 8.5, WordPress 7.1 and WooCommerce 11.2. Settings move to WooCommerce → Settings → Advanced, and a customer's unlocked payment methods become per site. Only an explicit false unlocks a payment method. Read the changelog before updating.
+
+== Changelog ==
+
+
+
+
+
+[See the full changelog on GitHub.](https://github.com/ahegyes/wc-locked-payment-methods/blob/trunk/CHANGELOG.md)
From f5c05553bfa4141b85901a33da89030e49c8dac3 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:40:10 +0100
Subject: [PATCH 30/50] Unhook each strategy's test class after its rows
The Groups, Memberships and order rows hook their own strategy for the
whole class and never unhooked it, so every later class in the process
ran it too. In the root's shared environment, which has no Groups, the
leftover Groups strategy threw on Groups_User from the user profile
rows' checkout filter.
Each class clones WordPress's hook table before it hooks its strategy
and puts the copy back after its last row, as WordPress's own test case
does around each test.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/UnlockStrategies/GroupsStrategyTest.php | 4 ++++
.../tests/Integration/UnlockStrategies/OrderStrategyTest.php | 4 ++++
.../UnlockStrategies/WooCommerceMembershipsStrategyTest.php | 4 ++++
3 files changed, 12 insertions(+)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php
index e10b264..025a00a 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/GroupsStrategyTest.php
@@ -18,6 +18,8 @@ final class GroupsStrategyTest extends TestCase {
protected static GroupsStrategy $strategy;
+ protected static array $hooks = array();
+
protected static array $users = array();
protected array $stored = array();
@@ -29,6 +31,7 @@ final class GroupsStrategyTest extends TestCase {
public static function setUpBeforeClass(): void {
require_once ABSPATH . 'wp-admin/includes/user.php';
self::$strategy = new GroupsStrategy( new AdminNotices( 'dws_lpmwc', new PsrNullLogger() ) );
+ self::$hooks = \array_map( static fn ( \WP_Hook $hook ): \WP_Hook => clone $hook, $GLOBALS['wp_filter'] );
self::$strategy->register_hooks();
foreach ( array( 'administrator', 'shop_manager', 'customer', 'subscriber' ) as $role ) {
self::$users[ $role ] = (int) wp_insert_user(
@@ -47,6 +50,7 @@ public static function tearDownAfterClass(): void {
foreach ( self::$users as $user ) {
wp_delete_user( $user );
}
+ $GLOBALS['wp_filter'] = self::$hooks; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Unhooks the class's strategy and its form.
}
#[Before]
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
index a784e05..17ddcfe 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
@@ -24,6 +24,8 @@ final class OrderStrategyTest extends TestCase {
protected static OrderStrategy $strategy;
+ protected static array $hooks = array();
+
protected static int $admin;
protected array $stored = array();
@@ -33,6 +35,7 @@ final class OrderStrategyTest extends TestCase {
public static function setUpBeforeClass(): void {
require_once ABSPATH . 'wp-admin/includes/user.php';
self::$strategy = new OrderStrategy( new AdminNotices( 'dws_lpmwc', new PsrNullLogger() ) );
+ self::$hooks = \array_map( static fn ( \WP_Hook $hook ): \WP_Hook => clone $hook, $GLOBALS['wp_filter'] );
self::$strategy->register_hooks();
self::$admin = (int) wp_insert_user(
array(
@@ -47,6 +50,7 @@ public static function setUpBeforeClass(): void {
#[\Override]
public static function tearDownAfterClass(): void {
wp_delete_user( self::$admin );
+ $GLOBALS['wp_filter'] = self::$hooks; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Unhooks the class's strategy and its form.
}
#[Before]
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php
index 52b6fcb..a9c3ed1 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/WooCommerceMembershipsStrategyTest.php
@@ -24,6 +24,8 @@ final class WooCommerceMembershipsStrategyTest extends TestCase {
protected static WooCommerceMembershipsStrategy $strategy;
+ protected static array $hooks = array();
+
protected static array $users = array();
protected array $stored = array();
@@ -36,6 +38,7 @@ public static function setUpBeforeClass(): void {
require_once ABSPATH . 'wp-admin/includes/user.php';
register_post_type( 'wc_membership_plan' );
self::$strategy = new WooCommerceMembershipsStrategy( new AdminNotices( 'dws_lpmwc', new PsrNullLogger() ) );
+ self::$hooks = \array_map( static fn ( \WP_Hook $hook ): \WP_Hook => clone $hook, $GLOBALS['wp_filter'] );
self::$strategy->register_hooks();
foreach ( array( 'administrator', 'customer', 'subscriber' ) as $role ) {
self::$users[ $role ] = (int) wp_insert_user(
@@ -55,6 +58,7 @@ public static function tearDownAfterClass(): void {
wp_delete_user( $user );
}
unregister_post_type( 'wc_membership_plan' );
+ $GLOBALS['wp_filter'] = self::$hooks; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Unhooks the class's strategy and its form.
}
// The form evaluates its definition and saves an object once per request, so every row edits a plan of its own.
From 0d4231c9e06dd53dee5ca49e9dc1df5c287f909d Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:40:10 +0100
Subject: [PATCH 31/50] Declare Locked Payment Methods compatible with the
order tables
The template's declaration marks a new plugin incompatible with
WooCommerce's order tables until its own evidence says otherwise, so
WooCommerce warned on every Locked Payment Methods site that stores
orders there, though the plugin reads and writes orders only through
WC CRUD and its order rows run on both storages.
The plugin declares itself compatible, and a boot row checks the
declaration. It declares nothing for the cart and checkout blocks,
whose checkout the lock covers through the gateways filter they share.
X ruling 88.
Assisted-by: Claude Code:claude-opus-5-5
---
.../locked-payment-methods-for-woocommerce/functions.php | 4 ++--
.../tests/Integration/PluginBootTest.php | 6 ++++++
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/functions.php b/plugins/locked-payment-methods-for-woocommerce/functions.php
index b320d21..ddf3ffc 100644
--- a/plugins/locked-payment-methods-for-woocommerce/functions.php
+++ b/plugins/locked-payment-methods-for-woocommerce/functions.php
@@ -82,7 +82,7 @@ function dws_lpmwc_deactivate(): void {
}
/**
- * Declares whether the plugin works with WooCommerce's order tables, which each plugin built from the template sets from its own evidence.
+ * Declares that the plugin works with WooCommerce's order tables, which its tests cover on both order storages.
*
* @since 2.0.0
* @version 2.0.0
@@ -90,5 +90,5 @@ function dws_lpmwc_deactivate(): void {
* @return void
*/
function dws_lpmwc_declare_compatibility(): void {
- FeaturesUtil::declare_compatibility( 'custom_order_tables', DWS_LPMWC_FILE, false );
+ FeaturesUtil::declare_compatibility( 'custom_order_tables', DWS_LPMWC_FILE, true );
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
index f8694df..cacfda6 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
@@ -47,4 +47,10 @@ public function test_the_stored_data_is_at_the_revision_of_the_code(): void {
self::assertInstanceOf( MigrationRunner::class, dws_lpmwc_plugin()->migrations );
self::assertSame( \count( dws_lpmwc_get_migration_steps() ), MigrationRunner::get_revision( Plugin::PREFIX . '_revision' ) );
}
+
+ public function test_the_plugin_declares_itself_compatible_with_woocommerces_order_tables(): void {
+ $plugins = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'custom_order_tables' );
+
+ self::assertContains( plugin_basename( DWS_LPMWC_FILE ), $plugins['compatible'] );
+ }
}
From 4e9934a7ddb494fc4b919be406ce95cc77a98319 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:40:10 +0100
Subject: [PATCH 32/50] Check that the readme repeats the entry's version and
floors
wordpress.org reads the stable tag and the floors from readme.txt,
while WordPress and the release read them from the entry's headers,
and nothing kept the two in step.
A Unit row compares readme.txt's Stable tag, Requires at least and
Requires PHP with the entry's headers, and another checks the changelog
markers the readme sync fills, as Linked Orders' does.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Unit/ReadmeTest.php | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php
new file mode 100644
index 0000000..a17ccb3
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/ReadmeTest.php
@@ -0,0 +1,30 @@
+.+/s', $readme );
+ }
+
+ protected static function read_fields( string $file ): array {
+ \preg_match_all( '/^[\s*]*([A-Za-z ]+):\s*(\S.*?)\s*$/m', (string) \file_get_contents( \dirname( __DIR__, 2 ) . "/$file" ), $matches );
+
+ return \array_combine( $matches[1], $matches[2] );
+ }
+}
From 8f0723e4d5a6b84a32a8bbcd0a710ec9b7e8e950 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:40:11 +0100
Subject: [PATCH 33/50] Test the lock on the block checkout and the Store API
The Integration rows filter the gateways directly, so no row showed a
granted customer the block checkout or sent the Store API a checkout
naming a locked payment method.
An end-to-end spec locks cash on delivery and opens the store, which a
new WooCommerce install keeps as coming soon, then checks that a
customer it is granted to sees it on the block checkout, and that the
Store API refuses a guest's checkout naming it with
woocommerce_rest_checkout_payment_method_disabled.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/e2e/checkout.spec.ts | 178 ++++++++++++++++++
1 file changed, 178 insertions(+)
create mode 100644 plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts b/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts
new file mode 100644
index 0000000..73c34cf
--- /dev/null
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts
@@ -0,0 +1,178 @@
+///
+import { execFileSync } from 'node:child_process';
+import { expect, test } from '@wordpress/e2e-test-utils-playwright';
+
+// No screen enables a payment method, locks it or grants it in one step, so the site's WP-CLI stores them.
+const cli = ( ...args: string[] ): string =>
+ execFileSync(
+ 'node_modules/.bin/wp-env',
+ [ '--config', '.wp-env.tests.json', 'run', 'cli', 'wp', ...args ],
+ { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] }
+ )
+ .trim()
+ .split( '\n' )
+ .pop() ?? '';
+
+// A context takes the config's storage state, the administrator's session, unless it names its own.
+const LOGGED_OUT = { cookies: [], origins: [] };
+
+test.describe( 'Checkout', () => {
+ let product: number;
+ let customer: number;
+ let comingSoon: string;
+
+ test.beforeAll( async ( { requestUtils } ) => {
+ // A new store shows its pages as coming soon to everyone but its managers, so the rows open it while they run.
+ comingSoon = cli(
+ 'eval',
+ 'echo get_option( "woocommerce_coming_soon", "no" );'
+ );
+ cli( 'option', 'update', 'woocommerce_coming_soon', 'no' );
+ for ( const gateway of [ 'cod', 'bacs' ] ) {
+ cli(
+ 'wc',
+ 'payment_gateway',
+ 'update',
+ gateway,
+ '--enabled=true',
+ '--user=1'
+ );
+ }
+ cli(
+ 'option',
+ 'update',
+ 'dws_lpmwc_locked_payment_methods',
+ JSON.stringify( [ 'cod' ] ),
+ '--format=json'
+ );
+ product = (
+ await requestUtils.rest< { id: number } >( {
+ method: 'POST',
+ path: '/wc/v3/products',
+ data: {
+ name: 'Locked checkout',
+ type: 'simple',
+ regular_price: '10',
+ virtual: true,
+ },
+ } )
+ ).id;
+ customer = Number(
+ cli(
+ 'user',
+ 'create',
+ 'lpm-e2e-customer',
+ 'lpm-e2e-customer@example.org',
+ '--role=customer',
+ '--user_pass=password',
+ '--porcelain'
+ )
+ );
+ cli(
+ 'eval',
+ `update_user_option( ${ customer }, DWS_LPMWC_USER_OPTION_UNLOCKED_METHODS, array( 'cod' ) );`
+ );
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await requestUtils.rest( {
+ method: 'DELETE',
+ path: `/wc/v3/products/${ product }`,
+ params: { force: true },
+ } );
+ cli( 'user', 'delete', String( customer ), '--yes' );
+ cli(
+ 'option',
+ 'update',
+ 'dws_lpmwc_locked_payment_methods',
+ '[]',
+ '--format=json'
+ );
+ for ( const gateway of [ 'cod', 'bacs' ] ) {
+ cli(
+ 'wc',
+ 'payment_gateway',
+ 'update',
+ gateway,
+ '--enabled=false',
+ '--user=1'
+ );
+ }
+ cli( 'option', 'update', 'woocommerce_coming_soon', comingSoon );
+ } );
+
+ test( 'a customer granted a locked payment method may choose it on the block checkout', async ( {
+ browser,
+ baseURL,
+ } ) => {
+ const context = await browser.newContext( {
+ baseURL,
+ storageState: LOGGED_OUT,
+ } );
+
+ try {
+ const page = await context.newPage();
+ await page.goto( '/wp-login.php' );
+ await page
+ .getByLabel( 'Username or Email Address' )
+ .fill( 'lpm-e2e-customer' );
+ await page
+ .getByLabel( 'Password', { exact: true } )
+ .fill( 'password' );
+ await page.getByRole( 'button', { name: 'Log In' } ).click();
+ await page.waitForLoadState();
+ await page.goto( `/?add-to-cart=${ product }` );
+
+ await page.goto( '/checkout/' );
+ await expect(
+ page.getByLabel( 'Direct bank transfer' )
+ ).toBeVisible();
+ await expect( page.getByLabel( 'Cash on delivery' ) ).toBeVisible();
+ } finally {
+ await context.close();
+ }
+ } );
+
+ test( "the Store API refuses a guest's checkout with a locked payment method", async ( {
+ playwright,
+ baseURL,
+ } ) => {
+ const api = await playwright.request.newContext( {
+ baseURL,
+ storageState: LOGGED_OUT,
+ } );
+
+ try {
+ const nonce = (
+ await api.get( '/wp-json/wc/store/v1/cart' )
+ ).headers().nonce;
+ await api.post( '/wp-json/wc/store/v1/cart/add-item', {
+ headers: { Nonce: nonce },
+ data: { id: product, quantity: 1 },
+ } );
+ const checkout = await api.post( '/wp-json/wc/store/v1/checkout', {
+ headers: { Nonce: nonce },
+ data: {
+ billing_address: {
+ first_name: 'Guest',
+ last_name: 'Buyer',
+ address_1: '1 Main Street',
+ city: 'Springfield',
+ postcode: '12345',
+ country: 'US',
+ state: 'CA',
+ email: 'lpm-e2e-guest@example.org',
+ },
+ payment_method: 'cod',
+ },
+ } );
+
+ expect( checkout.status() ).toBe( 400 );
+ expect( ( await checkout.json() ).code ).toBe(
+ 'woocommerce_rest_checkout_payment_method_disabled'
+ );
+ } finally {
+ await api.dispose();
+ }
+ } );
+} );
From 6346388bdf983cbe6304f6a334ee71bc1a762ce5 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:40:11 +0100
Subject: [PATCH 34/50] Describe Locked Payment Methods in its own docs
The plugin's README, AGENTS.md, architecture and tests docs, SECURITY
scope and manifest descriptions still described the template: its
demo, its fill-in, its blocks and its GitHub distribution. docs/
recipes.md, the template's guide, came with the scaffold, and
phpcs.dist.xml kept the comment of the build exclusions the demo's
removal dropped.
Each now describes Locked Payment Methods: what it does and how other
plugins lock and unlock payment methods, its strategies, its lock and
v1's API, its data under dws_lpmwc_ and what the migrations move from
v1, its grants forms, order storage and v1-hook conventions, and its
wordpress.org distribution. Release passes wp-org: true and Quality
lints readme.txt, as Linked Orders' workflows do, which the plugin's
now match. They point at the template's recipes, which it maintains.
Assisted-by: Claude Code:claude-opus-5-5
---
.../.github/workflows/quality.yml | 14 +-
.../.github/workflows/release.yml | 3 +-
.../AGENTS.md | 68 +-
.../README.md | 53 +-
.../SECURITY.md | 3 +-
.../composer.json | 2 +-
.../docs/ARCHITECTURE.md | 40 +-
.../docs/recipes.md | 1293 -----------------
.../package.json | 2 +-
.../phpcs.dist.xml | 2 -
.../tests/README.md | 14 +-
11 files changed, 104 insertions(+), 1390 deletions(-)
delete mode 100644 plugins/locked-payment-methods-for-woocommerce/docs/recipes.md
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml
index 27586d6..27647ff 100644
--- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/quality.yml
@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- # GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares.
+ # The workflows read the slug the plugin declares, as the template's do, so a change to the template's applies here unedited.
- name: Read the slug from composer.json
id: slug
run: |
@@ -55,6 +55,18 @@ jobs:
php-versions: '["7.4"]'
paths: ${{ format('["{0}.php", "uninstall.php", "includes"]', needs.slug.outputs.slug) }}
+ readme-lint:
+ name: readme.txt lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: thetwopct/wporg-plugin-readme-linter@1f869aba959265ecc90935b2380c6569d56646ac # v1.0.2
+ with:
+ path: readme.txt
+ fail-on: error
+
changelog:
name: Changelog fragments
runs-on: ubuntu-latest
diff --git a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml
index da95cb5..9d43fc9 100644
--- a/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml
+++ b/plugins/locked-payment-methods-for-woocommerce/.github/workflows/release.yml
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- # GitHub refuses a GITHUB_TOKEN push that changes a workflow, so the fill-in leaves workflows alone and they read the slug the plugin declares.
+ # The workflows read the slug the plugin declares, as the template's do, so a change to the template's applies here unedited.
- name: Read the slug from composer.json
id: slug
run: |
@@ -49,5 +49,6 @@ jobs:
wp-env-config-file: '.wp-env.tests.json'
# Both parse every PHP file with php-parser 4, which stops at PHP 8.4 syntax such as protected(set).
exclude-checks: 'prefixing,direct_file_access'
+ wp-org: true
# A pull request or a dispatch on a branch rehearses the release without publishing it.
publish: ${{ github.ref_type == 'tag' }}
diff --git a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md
index c701853..b1dffc6 100644
--- a/plugins/locked-payment-methods-for-woocommerce/AGENTS.md
+++ b/plugins/locked-payment-methods-for-woocommerce/AGENTS.md
@@ -1,28 +1,35 @@
# wc-locked-payment-methods
-A WordPress plugin built on the five wp-framework packages (`ahegyes/wp-framework-{bootstrap,shared,core,settings,woocommerce}`), from the template that starts every such plugin. It ships its own copy of the framework, PHP-DI and their dependencies, prefixed under `DeepWebSolutions\LockedPaymentMethods\Scoped` by php-scoper through wordpress-configs into `vendor-prefixed/` on every development install. Its architecture follows the framework's conventions 1–10 in [wordpress-framework's AGENTS.md](https://github.com/ahegyes/wordpress-framework/blob/trunk/AGENTS.md); this file covers what is particular to the plugin. `docs/ARCHITECTURE.md` describes the boot, `docs/recipes.md` the common changes, `tests/README.md` the suites.
+Locked Payment Methods for WooCommerce, the v2 port of the wordpress.org plugin of that name, built on the five wp-framework packages (`ahegyes/wp-framework-{bootstrap,shared,core,settings,woocommerce}`) from the plugin template. It ships its own copy of the framework, PHP-DI and their dependencies, prefixed under `DeepWebSolutions\LockedPaymentMethods\Scoped` by php-scoper through wordpress-configs into `vendor-prefixed/` on every development install. Its architecture follows the framework's conventions 1–10 in [wordpress-framework's AGENTS.md](https://github.com/ahegyes/wordpress-framework/blob/trunk/AGENTS.md); this file covers what is particular to the plugin. `docs/ARCHITECTURE.md` describes the boot and the data, `tests/README.md` the suites, and the template's [recipes](https://github.com/ahegyes/wordpress-plugin-template/blob/trunk/docs/recipes.md) the common changes, under the template's `dws_tpl` prefix.
+
+v1 sites upgrade in place: the port moves v1's settings and grants under `dws_lpmwc_` through migration steps, keeps v1's capabilities, settings section id and helper API, and fires each v1 hook, deprecated, where v1 fired it. v1's premium build's strategies are part of the plugin.
## Layout
```
-locked-payment-methods-for-woocommerce.php the entry: safeguards, bootstrap load.php, the updater, the floor check, the build gate, the autoloader
-functions.php dws_lpmwc_plugin(), the boot on plugins_loaded 15, deactivation, the HPOS declaration (false), the includes/ loader
+locked-payment-methods-for-woocommerce.php the entry: safeguards, bootstrap load.php, the inert updater, the floor check, the build gate, the autoloader
+functions.php dws_lpmwc_plugin(), the boot on plugins_loaded 15, deactivation, the order-tables declaration, v1's boot hooks, the includes/ loader
uninstall.php the framework's uninstall engine over the footprint, with DWS_LPMWC_OPTION_DELETE_DATA's consent
-src/ the composition root and its components only
+src/ the composition root, its components and the plan tab host
├── Plugin.php SLUG, PREFIX (dws_lpmwc), COMPONENTS, the logger, the scheduler, the migration runner and the kernel
-├── Settings.php the WooCommerce settings tab dws_lpmwc: the greeting's fields and the uninstall consent
-├── Blocks.php the blocks from blocks/build/blocks-manifest.php and the block-editor script
-└── Feature/ the greeting: a composite gated on DWS_LPMWC_OPTION_FEATURE and its account-dashboard child
+├── Settings.php the dws-locked-payment-methods section on WooCommerce's Advanced tab, v1's three groups, and the plugin row's links
+├── UnlockStrategies/ one component per way of unlocking, each gated on its switch, answering should_lock and owning its screen
+│ ├── UserRoleStrategy.php every locked method for the full-access roles
+│ ├── UserProfileStrategy.php per-site user grants on the profile, the Users column and filter
+│ ├── OrderStrategy.php order grants in a box on the order screen, on either storage
+│ ├── GroupsStrategy.php group grants on Groups' own forms, gated on Groups too
+│ └── WooCommerceMembershipsStrategy.php plan grants in a tab of the plan data, gated on Memberships too
+└── EditScreen/Hosts/MembershipPlanTab.php an ObjectForm host for Memberships' plan data box
includes/ hook-free code as dws_lpmwc_ functions and DWS_LPMWC_ constants, loaded in name order, _-prefixed files skipped
-├── safeguards.php restrictions that hold whenever the plugin cannot boot; loaded by the entry before the floor check
-├── options.php the option keys, their defaults, the typed readers, the translated text and the validator
-├── footprint.php dws_lpmwc_get_footprint(), every kind of data the plugin stores
+├── safeguards.php the lock, its v1 filters and dws_lpmwc_should_lock(); loaded by the entry before the floor check
+├── capabilities.php v1's four capabilities and the roles CapabilityGrants gives them
+├── grants.php the grants' keys and readers, the grants fields' choices and dws_lpmwc_is_payment_method_locked()
+├── options.php the settings' keys, defaults and typed readers, and v1's settings map
+├── deprecated.php v1's root getters, hook-tag builders, constants, boot-hook relays, access checks and setting readers, deprecated
+├── footprint.php dws_lpmwc_get_footprint(), including what v1 stored under its own names
├── migrations.php dws_lpmwc_get_migration_steps() and one function per step, append-only
-├── assets.php the *.asset.php reader and the script and style registrars
└── metadata.php the slug, name and version getters, the plugin's surface for a peer plugin
-blocks/src/, assets/src/ TypeScript and SCSS, built into blocks/build/ and assets/build/ (untracked)
-models/, templates/ empty extension points
-.github/scripts/fill-in-scaffold.sh the fill-in's sweep, which fill-in-scaffold.yml runs once in a new plugin and Quality rehearses here
+changelog/ the pending entries changelog:write turns into CHANGELOG.md and readme.txt's changelog
```
## Commands
@@ -30,12 +37,12 @@ models/, templates/ empty extension points
```sh
composer packages-install # dependencies, then php-scoper into vendor-prefixed/
composer test:unit
-composer test:integration # starts .wp-env.tests.json, then runs test:integration:run, which builds the scripts first
+composer test:integration # starts .wp-env.tests.json, then runs test:integration:run
composer quality-check # manifest and changelog validation, PHPCS (both profiles), PHPStan (both profiles), Unit and Integration
-npm run setup # both installs, the scripts' build, then the development site
-npm run build # blocks and assets through the local wp-scripts; npm run start watches them in a POSIX shell
-npm run lint:scripts # ESLint, beside lint:styles (Stylelint) and lint:types (tsc)
-npm run test:e2e # Playwright on .wp-env.tests.json, starting it if needed; the scripts must be built
+composer changelog:add # stages an entry in changelog/; composer changelog:write, at release, also syncs readme.txt
+npm run setup # both installs, then the development site
+npm run lint:scripts # ESLint over tests/e2e, beside lint:types (tsc); the plugin builds no scripts or styles
+npm run test:e2e # Playwright on .wp-env.tests.json, starting it if needed
```
Run wp-scripts only through the npm scripts or `node_modules/.bin/wp-scripts`: `npx wp-scripts` can install an unrelated package of that name.
@@ -45,30 +52,35 @@ Run wp-scripts only through the npm scripts or `node_modules/.bin/wp-scripts`: `
- `.wp-env.json`: development, as `local`, on port 8826, behind `npm run setup` and `wp-env:*`. The framework rethrows boot and migration failures there.
- `.wp-env.tests.json`: Integration and end to end, as `production`, on port 8827, behind `wp-env:tests:*`, `composer test:integration` and `npm run test:e2e`. The end-to-end suite deactivates and reactivates WooCommerce there.
-Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk.
+Both run PHP 8.5 with WooCommerce and Groups from `vendor/` and WooCommerce's order tables on; CI runs Integration and end to end on every WordPress branch from the entry's `Requires at least` up, and nightly on WordPress trunk. The monorepo's shared environment runs the Integration suite beside Linked Orders and Internal Comments, without the `groups` and `wc-memberships` groups.
## Conventions
-1. **Placement.** `src/` holds only the composition root and its components. Code that registers no hooks, such as keys, readers, the footprint, migration steps and asset registrars, is `dws_lpmwc_` functions and `DWS_LPMWC_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match` or first-class callables there. Container definitions are composition and stay in `Plugin::get_definitions()`.
-2. **Variants.** A component that exists only in one build variant is a child in a branch of its parent's `get_child_component_classes()`, never a second entry in `COMPONENTS`.
+1. **Placement.** `src/` holds the composition root, its components and the plan tab host. Code that registers no hooks, such as keys, readers, v1's helper API, the footprint and migration steps, is `dws_lpmwc_` functions and `DWS_LPMWC_` constants in `includes/`, which parses on PHP 7.4: no `mixed` or union types, typed constants, `match`, nullsafe calls or first-class callables there. An include that reads `Plugin` while it loads must sort after `metadata.php`, so `capabilities.php` and `grants.php` name their keys as literals. Container definitions are composition and stay in `Plugin::get_definitions()`.
+2. **Strategies.** Each way of unlocking is a component in `src/UnlockStrategies/`, gated on its switch, and an integration on its plugin too through a static `is_available()` that the settings read. It answers `dws_lpmwc/payment_method/should_lock` from the arguments alone, through an `@internal` static `unlocks()` that v1's per-strategy checks call, and returns `false` or the lock it was given. The safeguard passes the current user and, on an order's own payment page with its key, that order.
3. **Request context.** `is_admin()` is false on REST requests and true in admin-ajax ones, so an admin-only feature decides the request context in its callbacks, never in `should_load()`.
4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. It reads a template-built base plugin's slug, name and version only through that plugin's `{prefix}_get_plugin_*()` getters, from `plugins_loaded` on, since the add-on may load first, and behind `function_exists()`, since a base plugin below its floors or with a partial build may stay active without defining them.
5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input.
-6. **Options and data.** Options are read only through `includes/options.php`'s typed readers, safeguards excepted. Everything stored lives under `dws_lpmwc_`, or `_dws_lpmwc_` for hidden meta. The footprint covers the prefix's options, transients and user meta; object meta needs a `meta_prefixes` entry per type, and anything else its own entry.
-7. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_lpmwc_is_relaxed()` (`docs/recipes.md`). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks.
-8. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails.
+6. **Options and data.** The settings are read only through `includes/options.php`'s typed readers, safeguards excepted, and each subject's grants only through `includes/grants.php`'s readers. Everything stored lives under `dws_lpmwc_`, or `_dws_lpmwc_` for hidden meta; a user's grants are a user option under the site's table prefix, read from that key alone. The plugin's settings fire no settings hooks of their own, since WordPress's `option_{key}` reaches every read; v1's value filters fire where v1 read through them.
+7. **Grants forms** offer the locked payment methods and the subject's other grants, through `dws_lpmwc_get_grant_choices()`, since a `checkboxes` save keeps only the choices it offers. The profile, order and plan forms are the framework's `ObjectForm`s; the Groups field rides Groups' own forms, whose nonce and capability Groups checks before its save actions, and checks the plugin's capability in each callback.
+8. **Order storage.** Order code goes through WC CRUD and WooCommerce's screen ids, never the posts table, and every Integration row whose outcome the order storage can change runs on all four storages through `OrderStorage`.
+9. **v1 hooks.** A hook v1 fired keeps its v1 literal, fired through `apply_filters_deprecated()` or `do_action_deprecated()` with `'2.0.0'` and its replacement, before the v2 name; a filter on the locked list only adds ids.
+10. **Safeguards** live in `includes/safeguards.php`, call only WordPress, WooCommerce and that file, and relax only through their own filter, through `dws_lpmwc_is_relaxed()` (the template's recipes). The entry loads them before the floor check, so they parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon`, the file's only PHPStan profile, checks with WooCommerce's stubs.
+11. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails.
## Style
- Every PHP file opens with `:`, never `Co-Authored-By`.
## Limitations
-- The fill-in pushes with the workflow's own token, which GitHub refuses for workflow changes, so it never edits `.github/workflows/`; the workflows read the slug from `composer.json`'s `extra.text-domain`.
- Integration and end to end share the tests site, so a row that changes shared data in a separate process flushes the parent's object cache.
-- The release zip carries the TypeScript and SCSS sources beside the built files, since a plugin's repository may be private and the zip is then the only place its users find them.
+- The Memberships rows run against a double of its one function and its three hooks, since Memberships is a paid plugin absent from WP Packages; they are unverified against a licensed copy.
+- No environment runs a multisite network, so the migration that copies v1's user grants into every site's list runs only on a single site in the suites.
+- The bundled GitHub updater ships inert, since the entry file has no `Update URI`, and wordpress.org's reviewers may ask to remove it.
diff --git a/plugins/locked-payment-methods-for-woocommerce/README.md b/plugins/locked-payment-methods-for-woocommerce/README.md
index 1574192..8dbe3ce 100644
--- a/plugins/locked-payment-methods-for-woocommerce/README.md
+++ b/plugins/locked-payment-methods-for-woocommerce/README.md
@@ -1,29 +1,29 @@
# Locked Payment Methods for WooCommerce
-TODO: one sentence saying why this plugin exists, its philosophy rather than a list of what it holds. The same sentence is `composer.json`'s and `package.json`'s `description`, the entry file's `Description` header and the repository's GitHub description.
+Hides the payment methods you lock from customers at checkout until you unlock them for a user, a role, an order, a group or a membership plan.
+
+A store locks a payment method it trusts only some customers with, such as cash on delivery or pay by invoice, and unlocks it per customer on their profile, for every customer with given roles, for one unpaid order, or for the members of a Groups group or a WooCommerce Memberships plan. The lock holds while the plugin cannot boot, so an outdated runtime or a broken deploy never shows a locked payment method to everyone.
## Requirements
-The entry file's headers state the floors: `Requires PHP`, `Requires at least` for WordPress, and `Requires Plugins` with `WC requires at least` for WooCommerce. On a site below them, WordPress shows its own notice and none of the plugin's features load.
+The entry file's headers state the floors: `Requires PHP`, `Requires at least` for WordPress, and `Requires Plugins` with `WC requires at least` for WooCommerce. On a site below them, WordPress shows its own notice and none of the plugin's features load, while the lock still holds.
## Installation
-Download `locked-payment-methods-for-woocommerce.zip` from the repository's latest release, then upload it under Plugins → Add New Plugin → Upload Plugin. Later releases arrive through WordPress's own update check, from the repository the entry file's `Update URI` names.
+Install the plugin from the WordPress plugin directory under Plugins → Add New Plugin, where its later releases arrive too, then open its section under WooCommerce → Settings → Advanced from the Settings link on its row of the Plugins screen. Each release also attaches `locked-payment-methods-for-woocommerce.zip` to the repository's GitHub release.
-## Development
+## Extending
-Development needs PHP and Composer at the `Requires PHP` floor or later, Node.js and npm at `package.json`'s `engines`, Docker for `@wordpress/env`, and a POSIX shell for `npm run start`.
+Other plugins lock more payment methods through `dws_lpmwc/payment_method/locked_ids`, unlock one for a user and an order by returning `false` from `dws_lpmwc/payment_method/should_lock`, and ask `dws_lpmwc_is_payment_method_locked()` whether one is locked. Every hook is named `dws_lpmwc/area/name`, and each one v1 fired under `locked_payment_methods_for_woocommerce/` still fires, deprecated, where v1 fired it; `CHANGELOG.md` lists the renames and the removals.
-Install the dependencies, prefix the framework into `vendor-prefixed/`, build the scripts and start the development site on :
+## Development
-```sh
-npm run setup
-```
+Development needs PHP and Composer at the `Requires PHP` floor or later, Node.js and npm at `package.json`'s `engines`, and Docker for `@wordpress/env`.
-Rebuild the blocks and assets on every change:
+Install the dependencies, prefix the framework into `vendor-prefixed/` and start the development site, with WooCommerce and Groups, on :
```sh
-npm run start
+npm run setup
```
Run the manifest and changelog checks, PHPCS, PHPStan, and the Unit and Integration suites, starting the tests site on as needed:
@@ -39,35 +39,10 @@ npx playwright install chromium
npm run test:e2e
```
-Lint the scripts, styles and types:
+Lint the end-to-end scripts and types:
```sh
-npm run lint:scripts && npm run lint:styles && npm run lint:types
+npm run lint:scripts && npm run lint:types
```
-`docs/ARCHITECTURE.md` describes how the plugin loads and boots, `docs/recipes.md` holds the step-by-step changes most plugins need, `tests/README.md` describes the test suites, and `AGENTS.md` holds the conventions for people and agents working on the code.
-
-## After the fill-in
-
-The fill-in renamed the plugin and kept the template's demo, which boots and passes every suite. Replace or delete each part as the plugin grows its own:
-
-| Part | Where it lives | Removing it |
-| --- | --- | --- |
-| The greeting | `src/Feature/`, the Greeting section of `src/Settings.php`, and the `DWS_LPMWC_OPTION_FEATURE`, `DWS_LPMWC_OPTION_GREETING` and `DWS_LPMWC_OPTION_GREETING_ORDERS` keys in `includes/options.php` | Delete `src/Feature/` and its row in `Plugin::COMPONENTS`, the Greeting section of `src/Settings.php`, `assets/src/greeting.scss` and its `greeting=` entry in `package.json`'s `build:assets` and `start:assets`, the three keys with their defaults, text and validator, their lines in `dws_lpmwc_add_settings()`, and `tests/Integration/Feature/`, then the rows naming them in `OptionsTest`, `MigrationsTest` and `SettingsTest`, and `PluginBootTest`'s expectation that the greeting is skipped. `tests/e2e/settings.spec.ts` drives the greeting's validated field, so point it at a validated field of the plugin's own. |
-| The example block | `blocks/src/example-notice/`, registered by `src/Blocks.php` from `blocks/build/blocks-manifest.php` | Delete `blocks/`, `build:blocks` and `start:blocks` with their calls in `build` and `start`, `register_blocks()` and its `init` hook in `src/Blocks.php`, and `BlocksTest`'s block rows. |
-| The editor script | `assets/src/editor.ts`, enqueued in the block editor by `src/Blocks.php` | Delete it, its `editor=` entry in `build:assets` and `start:assets`, `enqueue_editor_script()` and its hook in `src/Blocks.php`, and `BlocksTest`'s editor row. Once `assets/src/` holds nothing, delete `build:assets` and `start:assets` with their calls in `build` and `start`. Once the plugin registers no blocks either, delete `src/Blocks.php`, its row in `Plugin::COMPONENTS`, `BlocksTest`, and `Blocks` in `PluginBootTest`'s resolved components. |
-| `models/` and `templates/` | Empty extension points, for classes on Composer's classmap and for template partials | Delete the folders the plugin never fills, and `models/` from `composer.json`'s `autoload.classmap`, since Composer fails to dump the autoloader for a missing folder. |
-| The fill-in | `.github/workflows/fill-in-scaffold.yml`, `.github/scripts/fill-in-scaffold.sh` and Quality's `scaffold` job | Delete all three: GitHub refuses the fill-in's own token for workflow changes, so the fill-in cannot remove them, and the job skips itself outside the template. |
-
-Whichever parts go, four settings follow what is left, since a plugin removes them in any order:
-
-- The entry's build gate checks `blocks/build/blocks-manifest.php`, and the plugin stays off until that file exists, so point it at a file the remaining build writes, such as `assets/build/editor.asset.php`, or drop its built-file check once nothing is built.
-- `lint:scripts` names only folders that still hold a script, since ESLint fails on a path that holds none.
-- `lint:styles` names only folders that still hold SCSS. Once none does, it keeps one pattern, such as `'assets/src/**/*.scss'`, and takes `--allow-empty-input`, since Stylelint fails when nothing matches, Quality runs it, and without a pattern wp-scripts lints every stylesheet in the project.
-- `build` stays, even once it runs nothing, since `npm run setup` and the Integration run call it.
-
-Still to decide by hand:
-
-- The Composer package name: the fill-in set `ahegyes/locked-payment-methods-for-woocommerce`, while plugins published to Composer use `ahegyes/wc-` for WooCommerce and `ahegyes/wp-` otherwise.
-- WooCommerce's order tables: `dws_lpmwc_declare_compatibility()` in `functions.php` declares the plugin incompatible until its order code is verified on both storages.
-- `SECURITY.md`'s scope and the issue forms in `.github/ISSUE_TEMPLATE/`, whose wording describes this plugin's demo.
+`docs/ARCHITECTURE.md` describes how the plugin loads, boots and stores its data, `tests/README.md` describes the test suites, and `AGENTS.md` holds the conventions for people and agents working on the code. The plugin template's [recipes](https://github.com/ahegyes/wordpress-plugin-template/blob/trunk/docs/recipes.md) hold the step-by-step changes a plugin built from it makes, under the template's `dws_tpl` prefix.
diff --git a/plugins/locked-payment-methods-for-woocommerce/SECURITY.md b/plugins/locked-payment-methods-for-woocommerce/SECURITY.md
index bd45895..c0bb5bd 100644
--- a/plugins/locked-payment-methods-for-woocommerce/SECURITY.md
+++ b/plugins/locked-payment-methods-for-woocommerce/SECURITY.md
@@ -9,6 +9,5 @@ Report it privately through [GitHub's security advisory form](https://github.com
In scope: the plugin's PHP, JavaScript and CSS, its prefixing configuration (`scoper.inc.php`) and its CI workflows.
Out of scope, reported upstream instead:
-- plugins generated from this repository, to their maintainers;
- the wp-framework packages, to `ahegyes/wordpress-framework`;
-- WordPress, WooCommerce and the other Composer and npm dependencies, to their projects. Composer refuses to resolve a dependency version with a known advisory through `roave/security-advisories`. The Supply-Chain Audit workflow fails on an advisory anywhere in the Composer lock, and on a high-severity one among the npm runtime dependencies, of which the template has none; advisories in the npm development tools reach Dependabot alerts instead.
+- WordPress, WooCommerce and the other Composer and npm dependencies, to their projects. Composer refuses to resolve a dependency version with a known advisory through `roave/security-advisories`. The Supply-Chain Audit workflow fails on an advisory anywhere in the Composer lock, and on a high-severity one among the npm runtime dependencies, of which the plugin has none; advisories in the npm development tools reach Dependabot alerts instead.
diff --git a/plugins/locked-payment-methods-for-woocommerce/composer.json b/plugins/locked-payment-methods-for-woocommerce/composer.json
index 0de19c2..0ac8029 100644
--- a/plugins/locked-payment-methods-for-woocommerce/composer.json
+++ b/plugins/locked-payment-methods-for-woocommerce/composer.json
@@ -1,6 +1,6 @@
{
"name": "ahegyes/wc-locked-payment-methods",
- "description": "WordPress plugins on the wp-framework packages start here, from one tested anatomy, so all are built alike and a fix found in one applies to all.",
+ "description": "Hides the payment methods you lock from customers at checkout until you unlock them for a user, a role, an order, a group or a membership plan.",
"type": "wordpress-plugin",
"license": "GPL-3.0-or-later",
"homepage": "https://github.com/ahegyes/wc-locked-payment-methods",
diff --git a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
index 63274de..23b8abf 100644
--- a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
+++ b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
@@ -6,12 +6,12 @@ This document describes how the plugin loads, boots, stores its data and leaves
`locked-payment-methods-for-woocommerce.php` is the only file WordPress includes, and it parses on PHP 7.4 so that a site below the floors gets a notice instead of a fatal error:
-1. It defines `DWS_LPMWC_FILE` and registers `languages/`, which WordPress skips for a network-activated plugin.
-2. It loads `includes/safeguards.php`, whose restrictions hold on any runtime that parses the plugin: below the floors, on a partial deploy and when the boot fails.
-3. It requires `vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php`, registers the GitHub updater, then checks the `Requires PHP` and `Requires at least` headers. The updater comes first, so a release that lowers the floors still reaches a site below them. On a site below a floor, WordPress's own notice shows and nothing past the safeguards loads.
-4. It stops with a notice when the prefixed framework or the built blocks are missing. It then requires the autoloader and `functions.php`; a missing file throws, which local and development environments rethrow and every other environment logs, showing the same notice.
+1. It defines `DWS_LPMWC_FILE`; translations come from wordpress.org's language packs, which WordPress loads just in time.
+2. It loads `includes/safeguards.php`, whose lock holds on any runtime that parses the plugin: below the floors, on a partial deploy and when the boot fails.
+3. It requires `vendor-prefixed/ahegyes/wp-framework-bootstrap/load.php`, registers the GitHub updater, which stays inert because the entry file has no `Update URI` header, then checks the `Requires PHP` and `Requires at least` headers. On a site below a floor, WordPress's own notice shows and nothing past the safeguards loads.
+4. It stops with a notice when the prefixed framework is missing. It then requires the autoloader and `functions.php`; a missing file throws, which local and development environments rethrow and every other environment logs, showing the same notice.
-`functions.php` defines the `dws_lpmwc_plugin()` accessor, boots the plugin on `plugins_loaded` at priority 15, after WooCommerce and Action Scheduler have loaded, registers the deactivation callback, declares the plugin's WooCommerce feature compatibility, and requires every `includes/*.php` in name order except those whose names start with `_`.
+`functions.php` defines the `dws_lpmwc_plugin()` accessor, boots the plugin on `plugins_loaded` at priority 15, after WooCommerce and Action Scheduler have loaded, registers the deactivation callback, declares the plugin compatible with WooCommerce's order tables, relays the framework's boot hooks to v1's `dws_lpmwc_initialized` and `dws_lpmwc_initialization_failure`, and requires every `includes/*.php` in name order except those whose names start with `_`.
## Booting
@@ -20,26 +20,36 @@ This document describes how the plugin loads, boots, stores its data and leaves
- The scheduler registers its hooks first, so the plugin's recurring jobs keep their schedules while the plugin is blocked.
- `WooCommerceRequirement` checks WooCommerce against the `WC requires at least` header; without it, the boot is `blocked`, `dws_lpmwc/boot/blocked` fires, and users who can activate plugins see why the plugin stays off.
- The migration runner registers, and runs pending steps on `wp_loaded`.
-- The kernel builds each class in `Plugin::COMPONENTS` through a PHP-DI container and calls its `register_hooks()`; a composite adds its children, and a conditional component loads only when its static `should_load()` returns true.
+- The kernel builds each class in `Plugin::COMPONENTS` through a PHP-DI container and calls its `register_hooks()`; a conditional component, such as each unlock strategy, loads only when its static `should_load()` returns true.
+- `CapabilityGrants` gives administrators v1's four capabilities, and shop managers the user-profile and order ones, on `wp_loaded`, once per change of the declaration or the roles.
- Once every component has registered, the boot is `booted` and `dws_lpmwc/boot/succeeded` fires. A throw anywhere in the composition makes it `failed`: `dws_lpmwc/boot/failed` fires, the failure is logged, and administrators see a notice, while local and development environments rethrow it.
-The request that activates the plugin does not boot it, because WordPress includes the plugin's file after `plugins_loaded`; its post types, routes and migrations first run on the next request.
+The request that activates the plugin does not boot it, because WordPress includes the plugin's file after `plugins_loaded`; its strategies and migrations first run on the next request.
+
+## The lock
+
+`includes/safeguards.php` filters `woocommerce_available_payment_gateways` at 999, which WooCommerce applies on the classic checkout, the order payment page and the Store API's checkout:
+
+1. It reads `dws_lpmwc_locked_payment_methods`, or v1's `dws-wc-lpm_general_locked-payment-methods` while the plugin has stored none, since the migration that copies v1's list runs only once the plugin boots, then lets v1's value and list filters and `dws_lpmwc/payment_method/locked_ids` add ids.
+2. For each offered payment method on the list, it fires v1's two lock filters, then `dws_lpmwc/payment_method/should_lock` with the current user and, on an order's own payment page with that order's key, the order.
+3. It removes the payment method unless `should_lock` returns an explicit `false` after `dws_lpmwc/boot/succeeded`, so the lock binds everyone while the plugin cannot boot.
+
+Each unlock strategy in `src/UnlockStrategies/` hooks `should_lock` while its switch is on, and returns `false` when its subject unlocks the payment method: the user's roles, the user's grants on the site, the order's grants, the user's Groups groups or the plans the user is an active member of. `dws_lpmwc_is_payment_method_locked()` runs the same filter for a user and an order outside checkout.
## Where code lives
-- `src/` holds the composition root and its components: final classes that implement the framework's `ComponentInterface` and add their hooks in `register_hooks()`. A component takes ungated services in its constructor, such as the logger, the scheduler or `AdminNotices`, which `Plugin::get_definitions()` provides.
-- `includes/` holds code that registers no hooks and is not part of the tree, as prefixed functions and constants: the option keys, defaults and typed readers (`options.php`), the footprint (`footprint.php`), the migration steps (`migrations.php`), the asset registrars (`assets.php`) and the slug, name and version getters a peer plugin reads (`metadata.php`). Quality's syntax job parses the folder on PHP 7.4, because `safeguards.php` runs before the floor check, and `phpstan.safeguards.neon` analyzes it at PHP 7.4 and the oldest WordPress the bootstrap package runs on.
-- `blocks/src/` and `assets/src/` hold the TypeScript and SCSS sources, which `npm run build` compiles into `blocks/build/` and `assets/build/`; `includes/assets.php` reads the `*.asset.php` file wp-scripts writes beside each build, and `src/Blocks.php` registers the blocks from `blocks/build/blocks-manifest.php`.
-- `models/` and `templates/` are empty extension points, for classes on Composer's classmap and for template partials.
+- `src/` holds the composition root and its components: final classes that implement the framework's `ComponentInterface` and add their hooks in `register_hooks()`. `Settings` adds the `dws-locked-payment-methods` section to WooCommerce's Advanced tab; the strategies add their forms: `UserProfileStrategy` an `ObjectForm` on the profile and the Users screen's column and filter, `OrderStrategy` an `ObjectForm` in a box on the order screen on either storage, `GroupsStrategy` a field on Groups' own group forms, and `WooCommerceMembershipsStrategy` an `ObjectForm` in a tab of the plan data through `EditScreen\Hosts\MembershipPlanTab`. A component takes ungated services in its constructor, such as `AdminNotices`, which `Plugin::get_definitions()` provides.
+- `includes/` holds code that registers no hooks and is not part of the tree, as prefixed functions and constants: the lock (`safeguards.php`), v1's capabilities (`capabilities.php`), the grants' keys and readers (`grants.php`), the settings' keys, defaults, readers and v1's settings map (`options.php`), v1's deprecated helper API (`deprecated.php`), the footprint (`footprint.php`), the migration steps (`migrations.php`) and the slug, name and version getters a peer plugin reads (`metadata.php`). Quality's syntax job parses the folder on PHP 7.4, because `safeguards.php` runs before the floor check, and `phpstan.safeguards.neon` analyzes that file at PHP 7.4 and the oldest WordPress the bootstrap package runs on.
## Data
-- **Options** are prefixed constants in `includes/options.php`, read only through its typed readers, which guard each stored value and fall back to the key's default. Translated defaults come from `dws_lpmwc_get_option_text()` after `init`; a field shows one as its placeholder and stores an empty value, which its reader reads as the default, so no save stores the admin's language.
+- **Settings** are flat options under `dws_lpmwc_`, as constants in `includes/options.php`: the locked list, a switch per strategy, the full-access roles and the uninstall consent. The first migration step copies each of v1's eight settings from its `dws-wc-lpm_{group}_{field}` row where it holds a value of the setting's kind, and leaves v1's rows until uninstall, so a downgrade to 1.3.5 still locks what it locked.
+- **Grants** are one list of payment method ids per subject: the user option `dws_lpmwc_unlocked_payment_methods` under each site's table prefix, the order meta and the Memberships plan meta `_dws_lpmwc_unlocked_payment_methods`, and the `dws_lpmwc_groups_unlocked_payment_methods` map from Groups group id to list, from which deleting a group removes its entry. The second to fifth migration steps move v1's grants into these shapes and delete v1's rows: user meta into every site's list, so v1's grants keep their network reach, order meta through WC CRUD on either storage, the per-group options and the plan meta.
- **Migrations** are the append-only steps `dws_lpmwc_get_migration_steps()` returns. The framework's runner runs each pending step once per site on `wp_loaded`, behind a lock, and records the revision reached in `dws_lpmwc_revision`. A step converges from any partial state and throws when a write fails, so a failed step runs again on a later request.
-- **The footprint**, `dws_lpmwc_get_footprint()`, names every kind of data the plugin stores: option and user meta prefixes, network options, scheduled hooks and Action Scheduler groups. Options, transients and user meta under the `dws_lpmwc_` prefix are covered; meta on posts, terms, comments or orders needs a `meta_prefixes` entry per object type, and data under another name its own entry, which no check enforces.
+- **The footprint**, `dws_lpmwc_get_footprint()`, names every kind of data the plugin stores: the `dws_lpmwc_` and `dws-wc-lpm_` option prefixes, v1's version and notice rows, the order meta under `_dws_lpmwc_` and `_dws_wc_lpm_` on both storages, the plan meta under both keys, the site user meta under `dws_lpmwc_`, v1's global user grants under `dws_wc_lpm_`, network options, the capabilities, scheduled hooks and Action Scheduler groups. Freemius's `fs_` rows from v1 stay, since they belong to Freemius.
- **Deactivation** unschedules every hook in the footprint's `cron_hooks` and `as_hooks` on the current site.
-- **Uninstall** runs `uninstall.php`, which calls the framework's uninstall engine over the footprint, with WooCommerce's order meta types, on every site whose `dws_lpmwc_delete_data` setting consents; a site that never consented keeps its data. Below the floors or with an incomplete build, a web request deletes the files, keeps the data and logs why, while WP-CLI fails and keeps both for a rerun.
+- **Uninstall** runs `uninstall.php`, which calls the framework's uninstall engine over the footprint, with WooCommerce's order meta types, on every site whose `dws_lpmwc_delete_data` setting consents, which the first migration step copies from v1's; a site that never consented keeps its data. Below the floors or with an incomplete build, a web request deletes the files, keeps the data and logs why, while WP-CLI fails and keeps both for a rerun.
## Prefixing and distribution
-Every development install runs php-scoper through wordpress-configs, which copies the framework, PHP-DI and their dependencies into `vendor-prefixed/` under `DeepWebSolutions\LockedPaymentMethods\Scoped\`, so each plugin carries its own copy and never meets another plugin's. The release workflow builds `locked-payment-methods-for-woocommerce.zip` from a production install, checks it with Plugin Check, the archive and header checks, activates it on a site and runs the end-to-end suite against it. A pull request rehearses the same build without publishing. A tag publishes a GitHub release, which the bundled updater offers through WordPress's own update check while the `Update URI` header points at the repository.
+Every development install runs php-scoper through wordpress-configs, which copies the framework, PHP-DI and their dependencies into `vendor-prefixed/` under `DeepWebSolutions\LockedPaymentMethods\Scoped\`, so each plugin carries its own copy and never meets another plugin's. The release workflow builds `locked-payment-methods-for-woocommerce.zip` from a production install, checks it with Plugin Check, the archive and header checks, activates it on a site and runs the end-to-end suite against it. A pull request rehearses the same build without publishing. A tag publishes a GitHub release and deploys the zip to the wordpress.org directory, which offers it to sites; `composer changelog:write` turns `changelog/` into `CHANGELOG.md` and copies its versions into `readme.txt`. The listing's banners, icons and screenshots stay in the directory's SVN, since the plugin ships no `.wordpress-org/`.
diff --git a/plugins/locked-payment-methods-for-woocommerce/docs/recipes.md b/plugins/locked-payment-methods-for-woocommerce/docs/recipes.md
deleted file mode 100644
index b7fcfd7..0000000
--- a/plugins/locked-payment-methods-for-woocommerce/docs/recipes.md
+++ /dev/null
@@ -1,1293 +0,0 @@
-# Recipes
-
-These are the changes most plugins built from this template make, each as the files to add or change. A recipe that stores data lists its keys for `includes/footprint.php`, which no check enforces. Options, transients and user meta under the plugin's `dws_lpmwc_` prefix are in the footprint already; meta on posts, terms, comments or orders needs its own `meta_prefixes` entry per object type.
-
-- [A safeguard](#a-safeguard)
-- [A one-time notice](#a-one-time-notice)
-- [A WordPress settings page](#a-wordpress-settings-page)
-- [A product data tab](#a-product-data-tab)
-- [A form on an edit screen](#a-form-on-an-edit-screen)
-- [A section on WooCommerce's Payments tab](#a-section-on-woocommerces-payments-tab)
-- [Recurring jobs](#recurring-jobs)
-- [Rewrite rules after activation](#rewrite-rules-after-activation)
-- [A script module](#a-script-module)
-- [Renaming a public hook](#renaming-a-public-hook)
-- [Distribution through wordpress.org](#distribution-through-wordpressorg)
-- [Without WooCommerce](#without-woocommerce)
-
-## A safeguard
-
-A safeguard is a restriction that holds even when the rest of the plugin fails to load, such as a capability that stays locked unless the plugin unlocks it. It lives in `includes/safeguards.php`, which the entry file loads before the floor gate and the autoloader, so it calls only WordPress and what that file defines. The plugin relaxes it only through the safeguard's own filter, with an explicit `false`, and only after `dws_lpmwc/boot/succeeded`, which fires once the plugin has booted, so a half-loaded build never relaxes it.
-
-Replace `includes/safeguards.php` with the helper and the safeguard. The file runs on any runtime that parses the plugin, so it must parse on PHP 7.4 and call only WordPress APIs present at the oldest WordPress the bootstrap package runs on, or guard them, which `phpstan.safeguards.neon` checks:
-
-```php
-export(); // Checks current_user_can( 'dws_lpmwc_export_data' ).
-} finally {
- remove_filter( 'dws_lpmwc/data_export/should_restrict', '__return_false' );
-}
-```
-
-Add the safeguard's failure test as `tests/Integration/SafeguardsTest.php`. The plugin has booted when the Integration suite runs, so the last row takes `dws_lpmwc/boot/succeeded` back for its duration:
-
-```php
- array( array(), false ),
- 'null' => array( array( 'relaxation' => null ), false ),
- 'zero' => array( array( 'relaxation' => 0 ), false ),
- 'empty-string' => array( array( 'relaxation' => '' ), false ),
- 'false' => array( array( 'relaxation' => false ), true ),
- );
- }
-
- #[DataProvider( 'relaxations' )]
- public function test_only_an_explicit_false_relaxes_the_export_lock( array $spec, bool $relaxed ): void {
- $relaxation = static fn (): mixed => $spec['relaxation'];
- if ( \array_key_exists( 'relaxation', $spec ) ) {
- add_filter( self::FILTER, $relaxation );
- }
-
- try {
- self::assertSame( $relaxed ? array( 'manage_options' ) : array( 'do_not_allow' ), map_meta_cap( 'dws_lpmwc_export_data', 0 ) );
- } finally {
- remove_filter( self::FILTER, $relaxation );
- }
- }
-
- public function test_an_explicit_false_does_not_relax_the_export_lock_before_the_plugin_booted(): void {
- global $wp_actions;
- self::assertGreaterThan( 0, did_action( 'dws_lpmwc/boot/succeeded' ) );
- $fired = $wp_actions['dws_lpmwc/boot/succeeded'];
- unset( $wp_actions['dws_lpmwc/boot/succeeded'] );
- add_filter( self::FILTER, '__return_false' );
-
- try {
- self::assertSame( array( 'do_not_allow' ), map_meta_cap( 'dws_lpmwc_export_data', 0 ) );
- } finally {
- remove_filter( self::FILTER, '__return_false' );
- $wp_actions['dws_lpmwc/boot/succeeded'] = $fired;
- }
- }
-}
-```
-
-## A one-time notice
-
-A one-time notice shows once, on the user's next admin screen, such as after a form post redirects. The plugin's `AdminNotices` is in `Plugin::COMPONENTS` and in the container, so a component takes it in its constructor:
-
-```php
-use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\AdminNotices;
-use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\Notice;
-use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\NoticeType;
-
-// ...
-
- /**
- * Constructor.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param AdminNotices $notices The plugin's admin notices.
- */
- public function __construct(
- protected AdminNotices $notices
- ) {}
-
- // ...
-
- /**
- * Tells the current user, on their next admin screen, how many rows the export wrote.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param int $rows The number of rows the export wrote.
- *
- * @return void
- */
- protected function report_export( int $rows ): void {
- $this->notices->add(
- new Notice(
- Plugin::PREFIX . '-export-' . time(), // A viewer's delivery outlives the notice, so each occasion takes a new id.
- /* translators: %d: The number of rows. */
- esc_html( sprintf( _n( 'The export wrote %d row.', 'The export wrote %d rows.', $rows, 'locked-payment-methods-for-woocommerce' ), $rows ) ),
- NoticeType::Success
- )
- );
- }
-```
-
-The notice's scope defaults to the current user and its message is HTML, so escape it. A notice for every administrator of the site takes `NoticeScope::Site`, a capability and `per_viewer: true`, since without it the first administrator's viewing consumes it for everyone; one that stays until dismissed takes `persistent: true`.
-
-## A WordPress settings page
-
-An options page adds a screen under a WordPress admin menu and stores each tab's fields in one option through the Settings API. WooCommerce settings belong on a WooCommerce tab, as `src/Settings.php` does.
-
-Add the option, its field and its default, a reader that guards the field and the validator to `includes/options.php`, which stays PHP 7.4-parseable. The non-WooCommerce recipe below uses the same names, with the uninstall consent as a second field:
-
-```php
-/**
- * The option that holds the settings page's fields, keyed by field id.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @var string
- */
-const DWS_LPMWC_OPTION_SETTINGS = Plugin::PREFIX . '_settings';
-
-/**
- * The settings field that holds the limit.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @var string
- */
-const DWS_LPMWC_SETTING_LIMIT = 'limit';
-
-/**
- * The value each settings field reads as while nothing valid is stored.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @var array
- */
-const DWS_LPMWC_SETTING_DEFAULTS = array(
- DWS_LPMWC_SETTING_LIMIT => 1,
-);
-
-/**
- * Returns a settings field as an integer.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string $field The field's id.
- *
- * @return int
- */
-function dws_lpmwc_get_int_setting( string $field ): int {
- $default = DWS_LPMWC_SETTING_DEFAULTS[ $field ] ?? null;
- $stored = get_option( DWS_LPMWC_OPTION_SETTINGS );
- $stored = is_array( $stored ) && array_key_exists( $field, $stored ) ? $stored[ $field ] : $default;
-
- return filter_var( $stored, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? ( is_int( $default ) ? $default : 0 );
-}
-
-/**
- * Returns the limit as the settings page stores it, or an error when the submission is not a whole number of at least 1.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param mixed $submitted The submitted value.
- *
- * @return int|WP_Error
- */
-function dws_lpmwc_validate_limit( $submitted ) {
- $limit = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false;
-
- return false === $limit ? new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'locked-payment-methods-for-woocommerce' ) ) : $limit;
-}
-```
-
-Add the page as a component, `src/SettingsPage.php`, and list it in `Plugin::COMPONENTS`:
-
-```php
-register_hooks();
- }
-
- /**
- * {@inheritDoc}
- *
- * @since 2.0.0
- * @version 2.0.0
- */
- #[\Override]
- public function get_options_page( string $page_id ): array {
- return array(
- 'parent' => 'options-general.php',
- 'capability' => 'manage_options',
- 'page_title' => __( 'Locked Payment Methods for WooCommerce', 'locked-payment-methods-for-woocommerce' ),
- 'menu_title' => __( 'Locked Payment Methods for WooCommerce', 'locked-payment-methods-for-woocommerce' ),
- 'tabs' => array(
- array(
- 'slug' => 'general',
- 'title' => __( 'General', 'locked-payment-methods-for-woocommerce' ),
- 'option' => DWS_LPMWC_OPTION_SETTINGS,
- 'fields' => array(
- array(
- 'type' => 'number',
- 'id' => DWS_LPMWC_SETTING_LIMIT,
- 'title' => __( 'Limit', 'locked-payment-methods-for-woocommerce' ),
- 'default' => (string) DWS_LPMWC_SETTING_DEFAULTS[ DWS_LPMWC_SETTING_LIMIT ],
- 'custom_attributes' => array(
- 'min' => '1',
- 'step' => '1',
- ),
- 'validate' => 'dws_lpmwc_validate_limit',
- ),
- ),
- ),
- ),
- );
- }
-
- // endregion
-}
-```
-
-Read the limit with `dws_lpmwc_get_int_setting( DWS_LPMWC_SETTING_LIMIT )`, which returns the default until a valid limit is stored. The option is under the plugin's prefix, so the footprint covers it; a migration step that stores `DWS_LPMWC_SETTING_DEFAULTS` autoloaded saves the first request's query, as the non-WooCommerce recipe's step does.
-
-## A product data tab
-
-A product data tab adds a panel to the product editor's Product data box, whose fields WooCommerce renders and saves as the product's meta. Add it as a component, `src/ProductData.php`, and list it in `Plugin::COMPONENTS`:
-
-```php
-register_hooks();
- }
-
- /**
- * {@inheritDoc}
- *
- * @since 2.0.0
- * @version 2.0.0
- */
- #[\Override]
- public function get_product_data_tab( string $tab_id, \WC_Product $product ): array {
- return array(
- 'label' => __( 'Packing', 'locked-payment-methods-for-woocommerce' ),
- 'groups' => array(
- array(
- 'fields' => array(
- array(
- 'type' => 'text',
- 'id' => '_' . Plugin::PREFIX . '_box',
- 'label' => __( 'Box', 'locked-payment-methods-for-woocommerce' ),
- ),
- ),
- ),
- ),
- );
- }
-
- // endregion
-}
-```
-
-Read the stored value with `ProductMeta::get_value( $product, '_dws_lpmwc_box' )`. The leading underscore hides the key from WordPress's Custom Fields box. Post meta is not in the footprint until its type is, so declare the prefix for uninstall in `includes/footprint.php`:
-
-```php
-'meta_prefixes' => array( 'post' => array( '_' . Plugin::PREFIX . '_' ) ),
-```
-
-A field with its own `render` callable returns its control as inline markup, escapes, names and describes it, and prints a hidden empty input before it unless the field is disabled. Without that input, a cleared control posts nothing, and the save leaves the stored value untouched. It also takes a `validate` callable that returns the value to store or a `WP_Error`, since the framework refuses to save a custom control's value that nothing validates.
-
-## A form on an edit screen
-
-An object form adds fields to an edit screen, such as a post's meta box, a user's profile or a term's form, and stores each field on the edited object. It reports a rejected value through the plugin's `AdminNotices`. Add it as a component, `src/PostNote.php`, and list it in `Plugin::COMPONENTS`:
-
-```php
-notices )->register_hooks();
- }
-
- /**
- * {@inheritDoc}
- *
- * @since 2.0.0
- * @version 2.0.0
- */
- #[\Override]
- public function get_object_form( string $form_id, int $object_id ): array {
- return array(
- 'title' => __( 'Editorial note', 'locked-payment-methods-for-woocommerce' ),
- 'capability' => 'edit_posts',
- 'fields' => array(
- array(
- 'type' => 'textarea',
- 'id' => '_' . Plugin::PREFIX . '_note',
- 'title' => __( 'Note', 'locked-payment-methods-for-woocommerce' ),
- ),
- ),
- );
- }
-
- // endregion
-}
-```
-
-The host checks that the user can edit the post itself, and the form's `capability` is one more capability the user needs, checked without an object, so it names a primitive capability such as `edit_posts` rather than `edit_post`. The form verifies its own nonce before it asks for the definition on a save. Declare the meta for uninstall in `includes/footprint.php`, as the product data tab does. A field with its own `render` callable prints a hidden empty input before its control unless the field is disabled, and takes a `validate` callable, for the reasons that recipe gives.
-
-## A section on WooCommerce's Payments tab
-
-A section on an existing WooCommerce tab, here the Payments tab (`checkout`), holds settings that belong beside WooCommerce's own. WooCommerce renders the Payments tab as its own page and lists no extension sections there, so the section is reached through its direct link, which the plugin's row on the Plugins screen carries. Add it as a component, `src/PaymentSettings.php`, and list it in `Plugin::COMPONENTS`:
-
-```php
- 'title',
- 'id' => Plugin::PREFIX . '_payment_options',
- 'title' => __( 'Locked Payment Methods for WooCommerce', 'locked-payment-methods-for-woocommerce' ),
- ),
- array(
- 'type' => 'checkbox',
- 'id' => Plugin::PREFIX . '_payment_notes',
- 'title' => __( 'Payment notes', 'locked-payment-methods-for-woocommerce' ),
- 'desc' => __( 'Add a note to each order when its payment completes', 'locked-payment-methods-for-woocommerce' ),
- ),
- array(
- 'type' => 'sectionend',
- 'id' => Plugin::PREFIX . '_payment_options',
- ),
- );
- }
-
- // endregion
-
- // region HOOKS
-
- /**
- * Registers the plugin's section on the Payments tab.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param SettingsSectionRegistry $registry WooCommerce's registry of settings sections.
- *
- * @return void
- */
- public function register_section( SettingsSectionRegistry $registry ): void {
- $registry->register(
- new SettingsSection(
- Plugin::PREFIX,
- array(
- 'tab' => 'checkout',
- 'label' => __( 'Locked Payment Methods for WooCommerce', 'locked-payment-methods-for-woocommerce' ),
- ),
- $this
- )
- );
- }
-
- /**
- * Adds the link to the plugin's section to its row on the Plugins screen.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string[] $links The row's action links.
- *
- * @return string[]
- */
- public function add_settings_link( array $links ): array {
- array_unshift( $links, sprintf( '%s', esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=' . Plugin::PREFIX ) ), esc_html__( 'Settings', 'locked-payment-methods-for-woocommerce' ) ) );
-
- return $links;
- }
-
- // endregion
-}
-```
-
-WooCommerce stores each field in its own option under its id, flat rather than in a group, and a WooCommerce checkbox stores `yes` or `no`; read it with `dws_lpmwc_get_bool_option()` once the key and its default are in `includes/options.php`.
-
-## Recurring jobs
-
-A recurring job runs a hook at an interval through the plugin's scheduler, which uses Action Scheduler where it is loaded and WP-Cron otherwise. Either can run an invocation twice or skip one, so the job's callback leaves its data consistent after a duplicate or a missed run.
-
-Add the repair gate's option to `includes/options.php`:
-
-```php
-/**
- * The option that holds when the plugin last ensured its recurring jobs, which lets a request skip ensuring them for an hour.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @var string
- */
-const DWS_LPMWC_OPTION_RECURRING_ENSURED_AT = Plugin::PREFIX . '_recurring_ensured_at';
-```
-
-Add the job as a component, `src/Pruning.php`, and list it in `Plugin::COMPONENTS`. It ensures the schedule on `wp_loaded` at most once an hour, writing the time first so concurrent requests skip, and again when Action Scheduler asks every day:
-
-```php
-ensure_schedule();
- }
-
- /**
- * Schedules the daily prune unless it runs at that interval already, logging a scheduler that refuses.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @return void
- */
- public function ensure_schedule(): void {
- $ensured = $this->scheduler->ensure_recurring( new ScheduledHook( self::HOOK ), Recurrence::from_interval( DAY_IN_SECONDS ) );
- if ( true !== $ensured ) {
- $this->logger->warning(
- 'Could not schedule {hook}: {error}',
- array(
- 'hook' => self::HOOK,
- 'error' => $ensured->get_error_message(),
- 'exception' => $ensured->get_error_data(),
- )
- );
- }
- }
-
- /**
- * Deletes the records that have expired.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @return void
- */
- public function prune(): void {
- // Delete by expiry, so a second run in a day finds nothing left and a missed day catches up.
- }
-
- // endregion
-}
-```
-
-Then:
-- Add `Plugin::PREFIX . '/records/prune'` to `cron_hooks` in `includes/footprint.php`. Deactivation unschedules every hook there through the plugin's scheduler, and uninstall clears WP-Cron's copies, while Action Scheduler's are in the footprint's `as_groups` already.
-- Delete the repair gate's option on activation, so the next request ensures the jobs: add `register_activation_hook( DWS_LPMWC_FILE, 'dws_lpmwc_activate' );` to `functions.php` beside the deactivation hook, and a `dws_lpmwc_activate()` that calls `delete_option( DWS_LPMWC_OPTION_RECURRING_ENSURED_AT )`.
-- Change an interval in code only: `ensure_recurring()` replaces a schedule whose interval differs on the next pass of the repair gate. A "run now" schedules the same hook with an argument that sets it apart, or calls `prune()` directly.
-
-## Rewrite rules after activation
-
-WordPress builds its rewrite rules from the post types and taxonomies registered on `init`. The request that activates the plugin does not boot it, because WordPress includes the plugin's file after `plugins_loaded`, so flushing there would drop the plugin's rules. Delete the stored rules instead: the next request rebuilds them once the plugin has registered its types.
-
-In `functions.php`, beside the deactivation hook:
-
-```php
-register_activation_hook( DWS_LPMWC_FILE, 'dws_lpmwc_activate' );
-
-// ...
-
-/**
- * Lets the next request rebuild the site's rewrite rules with the plugin's post types and taxonomies.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @return void
- */
-function dws_lpmwc_activate(): void {
- delete_option( 'rewrite_rules' );
-}
-```
-
-Add the same `delete_option( 'rewrite_rules' )` to `dws_lpmwc_deactivate()`, so the next request rebuilds the rules without them. Both act on the current site only, as WordPress's own activation does; a network activation leaves the other sites' rules until they next rebuild them.
-
-## A script module
-
-A script module is a JavaScript module WordPress loads through its import map, such as a block's view script for the Interactivity API. wp-scripts builds script modules only from a block's `block.json`, and WordPress registers each one from the block's metadata, with the imports wp-scripts recorded, and enqueues it when the block renders, so the plugin registers nothing itself.
-
-Name the module in the block's `block.json`, here `blocks/src/example-notice/block.json`:
-
-```json
-"viewScriptModule": "file:./view.js"
-```
-
-Add the module beside it, as `blocks/src/example-notice/view.ts`:
-
-```ts
-import { store } from '@wordpress/interactivity';
-
-store( 'locked-payment-methods-for-woocommerce/example-notice', {} );
-```
-
-Declare the module's import, which today arrives only through `@wordpress/block-editor`:
-
-```sh
-npm install --save-dev @wordpress/interactivity
-```
-
-Build the blocks' modules with their scripts, by adding `--experimental-modules` to `build:blocks` and `start:blocks` in `package.json`:
-
-```json
-"build:blocks": "wp-scripts build --webpack-src-dir=blocks/src --output-path=blocks/build --blocks-manifest --experimental-modules",
-"start:blocks": "wp-scripts start --webpack-src-dir=blocks/src --output-path=blocks/build --blocks-manifest --experimental-modules"
-```
-
-The build writes `view.js` with a `view.asset.php` whose `type` is `module`, and `src/Blocks.php`'s registration from the manifest picks it up.
-
-## Renaming a public hook
-
-A public hook's name is part of the plugin's contract, as its arguments, its options and its REST routes are. To rename one in a released plugin:
-
-1. Rename the literal at every `do_action()` or `apply_filters()` and in its docblock, which keeps its `@since` and moves its `@version` to the release.
-2. Add a changelog fragment with `composer changelog:add` that names the old and the new hook, with a major significance.
-3. Migrate stored data that holds the name, such as a scheduled job's hook, in a migration step that converges from any partial state: unschedule the old name, then ensure the new one.
-
-Ship no deprecated alias: the major release is the migration path.
-
-## Distribution through wordpress.org
-
-A plugin listed on wordpress.org gets its updates and its language packs from there, so it drops the GitHub updater's header and adds the directory's `readme.txt`.
-
-1. In the entry file, delete the `Update URI` header, which turns the bundled updater inert, and the `load_plugin_textdomain()` line, since language packs load just in time.
-2. Add `readme.txt`, whose `Stable tag` the release checks against the `Version` header, whose floors repeat the entry file's headers, and whose changelog block `composer changelog:write` fills:
-
-```text
-=== Locked Payment Methods for WooCommerce ===
-Contributors: TODO
-Tags: TODO
-Tested up to: TODO, the newest WordPress branch the plugin is tested on
-Stable tag: TODO, the Version header
-Requires at least: TODO, the Requires at least header
-Requires PHP: TODO, the Requires PHP header
-License: GPLv3 or later
-License URI: https://www.gnu.org/licenses/gpl-3.0.html
-
-TODO: the plugin's one sentence, at most 150 characters.
-
-== Description ==
-
-TODO: what the plugin does, for the directory's listing.
-
-== Installation ==
-
-1. Install the plugin from Plugins → Add New Plugin.
-2. Activate it.
-
-== Changelog ==
-
-
-
-
-
-[See the full changelog on GitHub.](https://github.com/ahegyes/wc-locked-payment-methods/blob/trunk/CHANGELOG.md)
-```
-
-3. Wrap `CHANGELOG.md`'s version blocks in the same `` and `` markers, require `pronamic/changelog-md-to-wordpress-plugin-readme-txt` in `require-dev`, and make `changelog:write` sync the file:
-
-```json
-"changelog:write": [
- "@php vendor/bin/changelogger write",
- "@php vendor/bin/pronamic-changelog-md-to-wordpress-plugin-readme-txt"
-]
-```
-
-4. Lint the file in `.github/workflows/quality.yml`:
-
-```yaml
- readme-lint:
- name: readme.txt lint
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- persist-credentials: false
- - uses: thetwopct/wporg-plugin-readme-linter@1f869aba959265ecc90935b2380c6569d56646ac # v1.0.2
- with:
- path: readme.txt
- fail-on: error
-```
-
-5. In `.github/workflows/release.yml`, pass `wp-org: true` to the reusable release. A tag then also deploys the zip and the `.wordpress-org/` assets to the directory's SVN from the `wp-org-release` environment: create it, store `SVN_USERNAME` and `SVN_PASSWORD` there and only there, and protect it with required reviewers.
-
-The wordpress.org build runs Plugin Check's full set but for the checks `exclude-checks` names; Plugin Check's `prefixing` and `direct_file_access` stop at PHP 8.4 syntax such as `protected(set)`, which wordpress.org's own scan may meet too. The bundled updater ships inert under `vendor-prefixed/`, which wordpress.org's reviewers may still ask to remove.
-
-## Without WooCommerce
-
-A plugin that does not need WooCommerce drops the WooCommerce package, the WooCommerce demo and its tooling, and keeps its settings on a WordPress options page.
-
-1. In `composer.json`, delete the `vcs` repository entry whose `url` is `https://github.com/ahegyes/wp-framework-woocommerce`. Then remove the WooCommerce packages, which leaves every other locked version as it is:
-
-```sh
-composer remove --dev --ignore-platform-req=php+ --no-interaction ahegyes/wp-framework-woocommerce php-stubs/woocommerce-stubs wp-plugin/woocommerce
-```
-
-2. In `phpstan.dist.neon`, delete the `scanFiles` block that names the WooCommerce stubs:
-
-```neon
- scanFiles:
- - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php
- - vendor/php-stubs/woocommerce-stubs/woocommerce-packages-stubs.php
-```
-
-3. In `locked-payment-methods-for-woocommerce.php`, delete the `Requires Plugins`, `WC requires at least` and `WC tested up to` headers.
-
-4. In `functions.php`, delete the `FeaturesUtil` import, the `before_woocommerce_init` hook and `dws_lpmwc_declare_compatibility()`, and drop "declares its WooCommerce feature compatibility" from the file's summary:
-
-```php
-use DeepWebSolutions\LockedPaymentMethods\Plugin;
-
-defined( 'ABSPATH' ) || exit;
-
-add_action( 'plugins_loaded', 'dws_lpmwc_boot', 15 );
-register_deactivation_hook( DWS_LPMWC_FILE, 'dws_lpmwc_deactivate' );
-```
-
-5. In `src/Plugin.php`, log through core's `ErrorLogLogger`, which drops records below warning unless `WP_DEBUG` is on, so `DWS_LPMWC_DEBUG` goes. Compose without the WooCommerce check, and leave the greeting out of `COMPONENTS`:
-
-```php
-use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\AdminNotice\AdminNotices;
-use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\Logging\ErrorLogLogger;
-use DeepWebSolutions\LockedPaymentMethods\Scoped\DeepWebSolutions\Framework\Core\Migration\MigrationRunner;
-// ...
-
- public const array COMPONENTS = array(
- Scheduler::class,
- AdminNotices::class,
- Settings::class,
- Blocks::class,
- );
-
- /**
- * The scheduler, which the components and the deactivation callback share.
- // ...
- */
- public readonly Scheduler $scheduler;
-
- // ...
-
- public function __construct() {
- $this->logger = new ErrorLogLogger( self::SLUG );
- $this->scheduler = new Scheduler( self::PREFIX );
- $this->boot = new PluginBoot( DWS_LPMWC_FILE, self::PREFIX, $this->logger );
- }
-
- // ...
-
- /**
- * Registers the scheduler and the migrations, then builds the kernel and boots the plugin's components.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @return bool
- */
- protected function compose(): bool {
- $this->scheduler->register_hooks();
- $this->migrations = new MigrationRunner( DWS_LPMWC_FILE, self::PREFIX . '_revision', dws_lpmwc_get_migration_steps(), $this->logger, $this->scheduler );
- $this->migrations->register_hooks();
-
- $this->kernel = new PluginKernel( new ContainerBuilder()->addDefinitions( $this->get_definitions( $this->logger, $this->scheduler ) )->build() );
- $this->kernel->boot( self::COMPONENTS );
-
- return true;
- }
-```
-
-6. Delete the greeting: `src/Feature/`, `tests/Integration/Feature/` and `assets/src/greeting.scss`, then its entries in `package.json`:
-
-```json
-"build:assets": "wp-scripts build editor=./assets/src/editor.ts --output-path=assets/build",
-"start:assets": "wp-scripts start editor=./assets/src/editor.ts --output-path=assets/build"
-```
-
-7. Replace `src/Settings.php` with an options page under Settings, which stores both fields in one option through the Settings API:
-
-```php
-register_hooks();
- }
-
- /**
- * {@inheritDoc}
- *
- * @since 2.0.0
- * @version 2.0.0
- */
- #[\Override]
- public function get_options_page( string $page_id ): array {
- return array(
- 'parent' => 'options-general.php',
- 'capability' => 'manage_options',
- 'page_title' => __( 'Locked Payment Methods for WooCommerce', 'locked-payment-methods-for-woocommerce' ),
- 'menu_title' => __( 'Locked Payment Methods for WooCommerce', 'locked-payment-methods-for-woocommerce' ),
- 'tabs' => array(
- array(
- 'slug' => 'general',
- 'title' => __( 'General', 'locked-payment-methods-for-woocommerce' ),
- 'option' => DWS_LPMWC_OPTION_SETTINGS,
- 'fields' => array(
- array(
- 'type' => 'number',
- 'id' => DWS_LPMWC_SETTING_LIMIT,
- 'title' => __( 'Limit', 'locked-payment-methods-for-woocommerce' ),
- 'default' => (string) DWS_LPMWC_SETTING_DEFAULTS[ DWS_LPMWC_SETTING_LIMIT ],
- 'custom_attributes' => array(
- 'min' => '1',
- 'step' => '1',
- ),
- 'validate' => 'dws_lpmwc_validate_limit',
- ),
- array(
- 'type' => 'checkbox',
- 'id' => DWS_LPMWC_SETTING_DELETE_DATA,
- 'title' => __( 'Data on uninstall', 'locked-payment-methods-for-woocommerce' ),
- 'desc' => __( 'Delete the plugin\'s data when it is uninstalled', 'locked-payment-methods-for-woocommerce' ),
- ),
- ),
- ),
- ),
- );
- }
-
- // endregion
-}
-```
-
-8. Replace `includes/options.php` with the one settings option, its fields, their defaults, and readers that guard each field. The file stays PHP 7.4-parseable, because Quality's syntax job lints `includes/`:
-
-```php
-
- */
-const DWS_LPMWC_SETTING_DEFAULTS = array(
- DWS_LPMWC_SETTING_DELETE_DATA => false,
- DWS_LPMWC_SETTING_LIMIT => 1,
-);
-
-/**
- * Returns a settings field as a boolean, which reads checkbox values such as 'yes' and 'no'.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string $field The field's id.
- *
- * @return bool
- */
-function dws_lpmwc_get_bool_setting( string $field ): bool {
- $default = dws_lpmwc_get_setting_default( $field );
-
- return filter_var( dws_lpmwc_get_setting( $field, $default ), FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE ) ?? ( true === $default );
-}
-
-/**
- * Returns a settings field as an integer.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string $field The field's id.
- *
- * @return int
- */
-function dws_lpmwc_get_int_setting( string $field ): int {
- $default = dws_lpmwc_get_setting_default( $field );
-
- return filter_var( dws_lpmwc_get_setting( $field, $default ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? ( is_int( $default ) ? $default : 0 );
-}
-
-/**
- * Returns the limit as the settings page stores it, or an error when the submission is not a whole number of at least 1.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param mixed $submitted The submitted value.
- *
- * @return int|WP_Error
- */
-function dws_lpmwc_validate_limit( $submitted ) {
- $limit = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false;
-
- return false === $limit ? new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'locked-payment-methods-for-woocommerce' ) ) : $limit;
-}
-
-/**
- * Returns a settings field's stored value, or the fallback while the settings hold none.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string $field The field's id.
- * @param mixed $fallback The value to return while nothing is stored.
- *
- * @return mixed
- */
-function dws_lpmwc_get_setting( string $field, $fallback ) {
- $settings = get_option( DWS_LPMWC_OPTION_SETTINGS );
-
- return is_array( $settings ) && array_key_exists( $field, $settings ) ? $settings[ $field ] : $fallback;
-}
-
-/**
- * Returns a settings field's default, or null for a field without one.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string $field The field's id.
- *
- * @return mixed
- */
-function dws_lpmwc_get_setting_default( string $field ) {
- return DWS_LPMWC_SETTING_DEFAULTS[ $field ] ?? null;
-}
-```
-
-9. In `includes/migrations.php`, the first step stores the defaults as they are. A stored `false` renders as an unchecked checkbox, the readers guard it, and uninstall consents only on `'yes'`:
-
-```php
-/**
- * Stores the settings' defaults, autoloaded, so a site that never saved the settings page reads them without a query.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @throws RuntimeException Thrown when the defaults could not be stored.
- *
- * @return void
- */
-function dws_lpmwc_add_settings(): void {
- if ( false === get_option( DWS_LPMWC_OPTION_SETTINGS ) && ! add_option( DWS_LPMWC_OPTION_SETTINGS, DWS_LPMWC_SETTING_DEFAULTS, '', true ) ) {
- throw new RuntimeException( 'The option ' . DWS_LPMWC_OPTION_SETTINGS . ' could not be stored.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing, never rendered.
- }
-}
-```
-
-10. In `uninstall.php`, delete the `MetaTypes` import, and take consent from the settings row's `delete_data` key:
-
-```php
- $dws_lpmwc_footprint = dws_lpmwc_get_footprint();
- $dws_lpmwc_consent = array(
- 'option' => DWS_LPMWC_OPTION_SETTINGS,
- 'key' => DWS_LPMWC_SETTING_DELETE_DATA,
- );
-// ...
-
-Uninstaller::run(
- $dws_lpmwc_file,
- $dws_lpmwc_footprint,
- array( 'consent' => $dws_lpmwc_consent )
-);
-```
-
-11. In `.wp-env.json` and `.wp-env.tests.json`, delete the WooCommerce mapping, and let `afterStart` only activate the plugin; `.wp-env.json`'s command takes no `--config`:
-
-```json
-"mappings": {
- "wp-content/plugins/locked-payment-methods-for-woocommerce": "."
-},
-"lifecycleScripts": {
- "afterStart": "node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate locked-payment-methods-for-woocommerce"
-}
-```
-
-12. Adapt the tests:
-- Delete `tests/e2e/without-woocommerce.spec.ts`.
-- `OptionsTest` reads through `dws_lpmwc_get_bool_setting()` and `dws_lpmwc_get_int_setting()` from a `DWS_LPMWC_OPTION_SETTINGS` row; a malformed limit reads its default 1; the validator rows call `dws_lpmwc_validate_limit()`, which returns integers.
-- `MigrationsTest` expects `DWS_LPMWC_SETTING_DEFAULTS` stored autoloaded, a stored row kept as it is, and the throw when that option's `INSERT` fails.
-- `UninstallTest` consents through `array( DWS_LPMWC_SETTING_DELETE_DATA => 'yes' )`, seeds and asserts no Action Scheduler action, and expects the defaults stored again afterwards.
-- `PluginBootTest` expects nothing in `$plugin->kernel->skipped`.
-- `SettingsTest` builds `new OptionsPage( Plugin::PREFIX, new Settings() )` and calls its `register_settings()` in `setUpBeforeClass()`, which first requires `wp-admin/includes/template.php`. `tearDownAfterClass()` removes the page's `pre_update_option_` and `option_page_capability_` filters and calls `unregister_setting()`. Each test resets `$GLOBALS['wp_settings_errors']`, renders through `render_page()` and saves as `options.php` does:
-
-```php
- // options.php saves a submission through update_option(), whose filter the page hooks for its own option page only.
- protected function save( array $posted ): void {
- $GLOBALS['pagenow'] = 'options.php';
- $_REQUEST['option_page'] = DWS_LPMWC_OPTION_SETTINGS;
-
- try {
- update_option( DWS_LPMWC_OPTION_SETTINGS, $posted );
- } finally {
- unset( $GLOBALS['pagenow'], $_REQUEST['option_page'] );
- }
- }
-```
-
-- `tests/e2e/settings.spec.ts` drives the page's limit field with the same checks: it focuses `page.getByLabel( 'Limit' )`, tabs to the `Save Changes` button and scopes axe to the form:
-
-```ts
-const SAVED = 'Settings saved.';
-const REJECTED =
- 'Limit was not saved, so it keeps its previous value. Enter a whole number of 1 or more, such as 3.';
-const FORM = 'form[action="options.php"]';
-
-// ...
-
- test.beforeEach( async ( { admin } ) => {
- await admin.visitAdminPage( 'options-general.php', 'page=dws_lpmwc' );
- } );
-```
-
-13. Update `AGENTS.md`, `README.md` and `docs/ARCHITECTURE.md` wherever they name WooCommerce, the greeting, the settings tab or the order-tables declaration.
diff --git a/plugins/locked-payment-methods-for-woocommerce/package.json b/plugins/locked-payment-methods-for-woocommerce/package.json
index a309608..6a8b9c2 100644
--- a/plugins/locked-payment-methods-for-woocommerce/package.json
+++ b/plugins/locked-payment-methods-for-woocommerce/package.json
@@ -1,7 +1,7 @@
{
"name": "@ahegyes/locked-payment-methods-for-woocommerce",
"private": true,
- "description": "DWS WordPress plugin template — wp-env, script and style tooling.",
+ "description": "Hides the payment methods you lock from customers at checkout until you unlock them for a user, a role, an order, a group or a membership plan.",
"license": "GPL-3.0-or-later",
"repository": {
"type": "git",
diff --git a/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml b/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml
index 4416f8d..cc94b8b 100644
--- a/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml
+++ b/plugins/locked-payment-methods-for-woocommerce/phpcs.dist.xml
@@ -3,8 +3,6 @@
-
-
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/README.md b/plugins/locked-payment-methods-for-woocommerce/tests/README.md
index f9e51a2..dc89259 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/README.md
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/README.md
@@ -6,9 +6,9 @@ This document describes the plugin's three test suites, the environments they ru
| Suite | Where | Runs on | Covers |
| --- | --- | --- | --- |
-| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree and the footprint's shape and coverage |
-| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, the options and their readers, the migrations, deactivation, uninstall, the assets and blocks, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws |
-| End to end | `tests/e2e/` | the tests site, in a browser through Playwright | the settings tab by keyboard, a rejected and a corrected value, accessibility through axe, and the plugin with WooCommerce deactivated |
+| Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage of v1's data, and the readme's version and floors |
+| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce, Groups and order tables loaded | the boot, the lock and its relaxation, fresh requests below the PHP and WooCommerce floors, every strategy and its form, the settings, the capabilities, v1's deprecated functions and hooks, the migrations from v1, deactivation, uninstall, the peer getters, and a partial deploy: a missing build file or engine, or an include that throws; every row whose outcome the order storage can change runs on all four storages through `OrderStorage`, the Groups rows run in the `groups` group against Groups itself, and the Memberships rows in the `wc-memberships` group against a double of its one function |
+| End to end | `tests/e2e/` | the tests site, in a browser through Playwright | a locked payment method a customer is granted on the block checkout, the Store API's refusal of a guest's checkout with a locked payment method, and the plugin with WooCommerce deactivated; the monorepo's shared environment checks a locked payment method's absence from a guest's classic and block checkout |
PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`.
@@ -17,7 +17,7 @@ PHPUnit runs both PHP suites in random order, so each row sets up what it needs
- `.wp-env.json` is the development site on , as `local`, where the framework rethrows boot and migration failures.
- `.wp-env.tests.json` is the tests site on , as `production`, which the Integration and end-to-end suites share. A row whose separate process changes the database flushes the parent's object cache, and the end-to-end specs set their own preconditions, because either suite may run first.
-Both start WordPress on PHP 8.5 with WooCommerce from `vendor/` and its order tables on.
+Both start WordPress on PHP 8.5 with WooCommerce and Groups from `vendor/` and WooCommerce's order tables on.
## Running
@@ -27,13 +27,13 @@ Run the Unit suite:
composer test:unit
```
-Start the tests site, build the scripts and run the Integration suite:
+Start the tests site and run the Integration suite:
```sh
composer test:integration
```
-Run the end-to-end suite, which starts the tests site when it is down and needs the built scripts and, once per machine, Playwright's Chromium:
+Run the end-to-end suite, which starts the tests site when it is down and needs, once per machine, Playwright's Chromium:
```sh
npx playwright install chromium
@@ -51,7 +51,7 @@ composer quality-check
- **Tests** runs Unit once, and Integration and the end-to-end suite once per WordPress branch, from the entry file's `Requires at least` up to the newest release, which WordPress's version API lists, so a new release joins without an edit.
- **Tests (nightly)** runs Integration and the end-to-end suite on WordPress trunk every day, outside pull requests.
- **Release** builds the zip on every pull request as a tag would, runs Plugin Check and the archive checks, then activates the zip on a site and runs the end-to-end suite against it.
-- **Quality** runs PHPCS, PHPStan, the PHP 7.4 parse of the entry file, `uninstall.php` and `includes/`, the script, style and type lints, the `block.json` schema and the changelog fragments.
+- **Quality** runs PHPCS, PHPStan, the PHP 7.4 parse of the entry file, `uninstall.php` and `includes/`, the script and type lints, the `readme.txt` lint and the changelog fragments.
## Conventions
From c0767f92bdc24c273c9d2274acc95f0b4e906828 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:40:11 +0100
Subject: [PATCH 35/50] Add Locked Payment Methods to the root's shared
environment
The root's shared environment, its Integration run and its plugins:*
fan-outs named Linked Orders and Internal Comments alone, so Locked
Payment Methods would have joined the CI lanes but not the shared site
where the plugins are tested side by side. The shared smoke check only
checked that the plugins were active, and the README's table listed
Linked Orders alone.
The shared environment maps and activates Locked Payment Methods, and
test:integration:run runs its Integration suite there without the
groups and wc-memberships groups, whose plugins the site lacks; every
Composer and npm fan-out lists it. The shared checks add that every
plugin boots from its own prefixed framework, and that a guest sees a
locked payment method on neither the classic nor the block checkout.
AGENTS.md and the README name the plugin, and the README Internal
Comments too.
X ruling 99.
Assisted-by: Claude Code:claude-opus-5-5
---
.wp-env.tests.json | 3 +-
AGENTS.md | 9 ++-
README.md | 4 +-
composer.json | 27 ++++---
package.json | 10 +--
tests/e2e/shared-environment.spec.ts | 114 +++++++++++++++++++++++++--
6 files changed, 140 insertions(+), 27 deletions(-)
diff --git a/.wp-env.tests.json b/.wp-env.tests.json
index c2bfbbb..2578648 100644
--- a/.wp-env.tests.json
+++ b/.wp-env.tests.json
@@ -7,6 +7,7 @@
"mappings": {
"wp-content/plugins/internal-comments": "./plugins/internal-comments",
"wp-content/plugins/linked-orders-for-woocommerce": "./plugins/linked-orders-for-woocommerce",
+ "wp-content/plugins/locked-payment-methods-for-woocommerce": "./plugins/locked-payment-methods-for-woocommerce",
"wp-content/plugins/woocommerce": "./vendor/wp-plugin/woocommerce"
},
"config": {
@@ -15,6 +16,6 @@
"WP_ENVIRONMENT_TYPE": false
},
"lifecycleScripts": {
- "afterStart": "node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate woocommerce && node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate linked-orders-for-woocommerce internal-comments"
+ "afterStart": "node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate woocommerce && node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp wc hpos enable --ignore-plugin-compatibility && node_modules/.bin/wp-env --config .wp-env.tests.json run cli wp plugin activate linked-orders-for-woocommerce internal-comments locked-payment-methods-for-woocommerce"
}
}
diff --git a/AGENTS.md b/AGENTS.md
index 9fe9590..ddf223b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,9 +8,10 @@ The monorepo of the DWS v2 WordPress plugins, each built on the wp-framework pac
wordpress-plugins/
├── plugins/
│ ├── linked-orders-for-woocommerce/ Linked Orders for WooCommerce → wc-linked-orders, ports 8822 and 8823, on wordpress.org
-│ └── internal-comments/ Internal Comments → wp-internal-comments, ports 8824 and 8825, on wordpress.org
+│ ├── internal-comments/ Internal Comments → wp-internal-comments, ports 8824 and 8825, on wordpress.org
+│ └── locked-payment-methods-for-woocommerce/ Locked Payment Methods for WooCommerce → wc-locked-payment-methods, ports 8826 and 8827, on wordpress.org
├── tests/
-│ ├── e2e/ the shared environment's smoke check, over the plugins .wp-env.tests.json maps
+│ ├── e2e/ the shared environment's checks: every plugin .wp-env.tests.json maps is active and boots, and the plugins work side by side
│ └── Fixtures/multi-plugin-smoke/ reserved for a fixture plugin the shared environment mounts
├── .github/workflows/
│ ├── plugins.yml lists the plugins, their mirrors and readmes and each one's WordPress versions for the other workflows
@@ -28,7 +29,7 @@ wordpress-plugins/
npm run setup # every install, then every plugin's build
composer plugins:quality-check # each plugin's quality-check, in its own environments
composer test:integration # starts the shared environment and runs each plugin's Integration suite in it
-npm run test:e2e # the shared environment's smoke check
+npm run test:e2e # the shared environment's checks
npm run plugins:test:e2e # each plugin's end-to-end suite, in its own environment
actionlint .github/workflows/*.yml plugins/*/.github/workflows/*.yml && zizmor .github/ plugins/*/.github/
```
@@ -49,4 +50,4 @@ Each plugin's `AGENTS.md` states its code style, the template's. Markdown keeps
- CI runs twice per change: the root's lanes on the pull request, and the mirror's own after the split.
- The root's workflow checks cover the root's workflows; a plugin's own are checked on its mirror after the split, and locally by the command above.
-- Each plugin's Integration suite runs twice: in the plugin's own environments and in the shared one.
+- Each plugin's Integration suite runs twice: in the plugin's own environments and in the shared one, which leaves out the rows of a plugin the shared environment does not install, such as Locked Payment Methods' Groups and WooCommerce Memberships groups.
diff --git a/README.md b/README.md
index 4d96be7..d96e595 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,8 @@ Plugins built on the wp-framework packages are developed side by side here, so a
| Plugin | Mirror | Distribution |
| --- | --- | --- |
| [Linked Orders for WooCommerce](./plugins/linked-orders-for-woocommerce) | [`wc-linked-orders`](https://github.com/ahegyes/wc-linked-orders) | wordpress.org |
+| [Internal Comments](./plugins/internal-comments) | [`wp-internal-comments`](https://github.com/ahegyes/wp-internal-comments) | wordpress.org |
+| [Locked Payment Methods for WooCommerce](./plugins/locked-payment-methods-for-woocommerce) | [`wc-locked-payment-methods`](https://github.com/ahegyes/wc-locked-payment-methods) | wordpress.org |
Each plugin under `plugins//` is a complete plugin repository, started from [wordpress-plugin-template](https://github.com/ahegyes/wordpress-plugin-template): its own Composer and npm manifests, prefixed framework, test environments, CI and release workflows. Every push to `trunk` splits it to its mirror, which runs the plugin's own CI and releases it.
@@ -29,7 +31,7 @@ composer test:integration
npm run test:e2e
```
-`composer test:integration` runs each plugin's Integration suite in the shared environment, and `npm run test:e2e` its smoke check.
+`composer test:integration` runs each plugin's Integration suite in the shared environment, and `npm run test:e2e` its checks of the plugins side by side.
## Releases
diff --git a/composer.json b/composer.json
index 2634a67..7799df3 100644
--- a/composer.json
+++ b/composer.json
@@ -59,7 +59,8 @@
"@putenv CI=1",
"npm --prefix=. run plugins:build",
"node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/linked-orders-for-woocommerce vendor/bin/phpunit --testsuite=Integration",
- "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration"
+ "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/internal-comments vendor/bin/phpunit --testsuite=Integration",
+ "node_modules/.bin/wp-env --config .wp-env.tests.json run cli --env-cwd=wp-content/plugins/locked-payment-methods-for-woocommerce vendor/bin/phpunit --testsuite=Integration --exclude-group groups --exclude-group wc-memberships"
],
"quality-check": [
@@ -70,38 +71,46 @@
"plugins:packages-install": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce packages-install",
- "@composer --working-dir=plugins/internal-comments packages-install"
+ "@composer --working-dir=plugins/internal-comments packages-install",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce packages-install"
],
"plugins:packages-update": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce packages-update",
- "@composer --working-dir=plugins/internal-comments packages-update"
+ "@composer --working-dir=plugins/internal-comments packages-update",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce packages-update"
],
"plugins:format:php": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce format:php",
- "@composer --working-dir=plugins/internal-comments format:php"
+ "@composer --working-dir=plugins/internal-comments format:php",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce format:php"
],
"plugins:lint:php": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce lint:php",
- "@composer --working-dir=plugins/internal-comments lint:php"
+ "@composer --working-dir=plugins/internal-comments lint:php",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce lint:php"
],
"plugins:test": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce test",
- "@composer --working-dir=plugins/internal-comments test"
+ "@composer --working-dir=plugins/internal-comments test",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce test"
],
"plugins:test:unit": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce test:unit",
- "@composer --working-dir=plugins/internal-comments test:unit"
+ "@composer --working-dir=plugins/internal-comments test:unit",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce test:unit"
],
"plugins:test:integration": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce test:integration",
- "@composer --working-dir=plugins/internal-comments test:integration"
+ "@composer --working-dir=plugins/internal-comments test:integration",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce test:integration"
],
"plugins:quality-check": [
"@composer --working-dir=plugins/linked-orders-for-woocommerce quality-check",
- "@composer --working-dir=plugins/internal-comments quality-check"
+ "@composer --working-dir=plugins/internal-comments quality-check",
+ "@composer --working-dir=plugins/locked-payment-methods-for-woocommerce quality-check"
],
"changelog:add": "@php vendor/bin/changelogger add",
diff --git a/package.json b/package.json
index ae6ea95..1f23ba0 100644
--- a/package.json
+++ b/package.json
@@ -28,11 +28,11 @@
"packages-update": "npm update",
"packages-update:wp": "wp-scripts packages-update",
- "plugins:packages-install": "npm --prefix=plugins/linked-orders-for-woocommerce run packages-install && npm --prefix=plugins/internal-comments run packages-install",
- "plugins:packages-update": "npm --prefix=plugins/linked-orders-for-woocommerce run packages-update && npm --prefix=plugins/internal-comments run packages-update",
- "plugins:packages-update:wp": "npm --prefix=plugins/linked-orders-for-woocommerce run packages-update:wp && npm --prefix=plugins/internal-comments run packages-update:wp",
+ "plugins:packages-install": "npm --prefix=plugins/linked-orders-for-woocommerce run packages-install && npm --prefix=plugins/internal-comments run packages-install && npm --prefix=plugins/locked-payment-methods-for-woocommerce run packages-install",
+ "plugins:packages-update": "npm --prefix=plugins/linked-orders-for-woocommerce run packages-update && npm --prefix=plugins/internal-comments run packages-update && npm --prefix=plugins/locked-payment-methods-for-woocommerce run packages-update",
+ "plugins:packages-update:wp": "npm --prefix=plugins/linked-orders-for-woocommerce run packages-update:wp && npm --prefix=plugins/internal-comments run packages-update:wp && npm --prefix=plugins/locked-payment-methods-for-woocommerce run packages-update:wp",
- "plugins:build": "npm --prefix=plugins/linked-orders-for-woocommerce run build && npm --prefix=plugins/internal-comments run build",
+ "plugins:build": "npm --prefix=plugins/linked-orders-for-woocommerce run build && npm --prefix=plugins/internal-comments run build && npm --prefix=plugins/locked-payment-methods-for-woocommerce run build",
"wp-env:tests:start": "wp-env --config .wp-env.tests.json start",
"wp-env:tests:stop": "wp-env --config .wp-env.tests.json stop",
@@ -45,7 +45,7 @@
"check:licenses": "wp-scripts check-licenses",
"test:e2e": "playwright test --config=playwright.config.js",
- "plugins:test:e2e": "npm --prefix=plugins/linked-orders-for-woocommerce run test:e2e && npm --prefix=plugins/internal-comments run test:e2e",
+ "plugins:test:e2e": "npm --prefix=plugins/linked-orders-for-woocommerce run test:e2e && npm --prefix=plugins/internal-comments run test:e2e && npm --prefix=plugins/locked-payment-methods-for-woocommerce run test:e2e",
"test:all": "composer test:integration && npm run test:e2e",
"quality-check:all": "composer quality-check && npm run test:e2e"
diff --git a/tests/e2e/shared-environment.spec.ts b/tests/e2e/shared-environment.spec.ts
index 0613cdf..db77c82 100644
--- a/tests/e2e/shared-environment.spec.ts
+++ b/tests/e2e/shared-environment.spec.ts
@@ -32,7 +32,7 @@ test( 'every mapped plugin is active beside the others and the dashboard loads',
).toBeVisible();
} );
-// No page shows a boot's outcome or the Internal Comments settings, so the site's WP-CLI reads and stores them.
+// No page shows a boot's outcome or the plugins' settings, so the site's WP-CLI reads and stores them.
const cli = ( ...args: string[] ): string =>
execFileSync(
'node_modules/.bin/wp-env',
@@ -43,17 +43,19 @@ const cli = ( ...args: string[] ): string =>
.split( '\n' )
.pop() ?? '';
-test( 'Linked Orders and Internal Comments each boot and share an order screen', async ( {
- page,
- requestUtils,
-} ) => {
+test( 'every plugin boots from its own scoped framework', () => {
expect(
cli(
'eval',
- 'echo wp_json_encode( array( did_action( "dws_lowc/boot/succeeded" ), did_action( "dws_ic/boot/succeeded" ) ) );'
+ 'echo wp_json_encode( array( did_action( "dws_lowc/boot/succeeded" ), did_action( "dws_ic/boot/succeeded" ), did_action( "dws_lpmwc/boot/succeeded" ), class_exists( "DeepWebSolutions\\LinkedOrders\\Scoped\\DeepWebSolutions\\Framework\\Core\\PluginKernel", false ), class_exists( "DeepWebSolutions\\InternalComments\\Scoped\\DeepWebSolutions\\Framework\\Core\\PluginKernel", false ), class_exists( "DeepWebSolutions\\LockedPaymentMethods\\Scoped\\DeepWebSolutions\\Framework\\Core\\PluginKernel", false ) ) );'
)
- ).toBe( '[1,1]' );
+ ).toBe( '[1,1,1,true,true,true]' );
+} );
+test( 'Linked Orders and Internal Comments share an order screen', async ( {
+ page,
+ requestUtils,
+} ) => {
cli(
'option',
'update',
@@ -88,3 +90,101 @@ test( 'Linked Orders and Internal Comments each boot and share an order screen',
);
}
} );
+
+test( 'a locked payment method stays hidden from a guest on the classic and the block checkout', async ( {
+ browser,
+ baseURL,
+ requestUtils,
+} ) => {
+ // A new store shows its pages to visitors as coming soon, so the row opens it while it runs.
+ const comingSoon = cli(
+ 'eval',
+ 'echo get_option( "woocommerce_coming_soon", "no" );'
+ );
+ cli( 'option', 'update', 'woocommerce_coming_soon', 'no' );
+ for ( const gateway of [ 'cod', 'bacs' ] ) {
+ cli(
+ 'wc',
+ 'payment_gateway',
+ 'update',
+ gateway,
+ '--enabled=true',
+ '--user=1'
+ );
+ }
+ cli(
+ 'option',
+ 'update',
+ 'dws_lpmwc_locked_payment_methods',
+ JSON.stringify( [ 'cod' ] ),
+ '--format=json'
+ );
+ const product = await requestUtils.rest< { id: number } >( {
+ method: 'POST',
+ path: '/wc/v3/products',
+ data: {
+ name: 'Locked checkout',
+ type: 'simple',
+ regular_price: '10',
+ virtual: true,
+ },
+ } );
+ const classic = await requestUtils.rest< { id: number; link: string } >( {
+ method: 'POST',
+ path: '/wp/v2/pages',
+ data: {
+ title: 'Classic checkout',
+ status: 'publish',
+ content:
+ '[woocommerce_checkout]',
+ },
+ } );
+ // A context takes the config's storage state, the administrator's session, unless it names its own.
+ const guest = await browser.newContext( {
+ baseURL,
+ storageState: { cookies: [], origins: [] },
+ } );
+
+ try {
+ const page = await guest.newPage();
+ await page.goto( `/?add-to-cart=${ product.id }` );
+
+ await page.goto( classic.link );
+ await expect( page.locator( '#payment_method_bacs' ) ).toBeAttached();
+ await expect( page.locator( '#payment_method_cod' ) ).toHaveCount( 0 );
+
+ await page.goto( '/checkout/' );
+ await expect( page.getByLabel( 'Direct bank transfer' ) ).toBeVisible();
+ await expect( page.getByLabel( 'Cash on delivery' ) ).toHaveCount( 0 );
+ } finally {
+ await guest.close();
+ await requestUtils.rest( {
+ method: 'DELETE',
+ path: `/wp/v2/pages/${ classic.id }`,
+ params: { force: true },
+ } );
+ await requestUtils.rest( {
+ method: 'DELETE',
+ path: `/wc/v3/products/${ product.id }`,
+ params: { force: true },
+ } );
+ cli(
+ 'option',
+ 'update',
+ 'dws_lpmwc_locked_payment_methods',
+ '[]',
+ '--format=json'
+ );
+ for ( const gateway of [ 'cod', 'bacs' ] ) {
+ cli(
+ 'wc',
+ 'payment_gateway',
+ 'update',
+ gateway,
+ '--enabled=false',
+ '--user=1'
+ );
+ }
+ cli( 'option', 'update', 'woocommerce_coming_soon', comingSoon );
+ }
+} );
From d90f4ee74f6a5cafb9fea017169b9a88bda3a4e6 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:50:43 +0100
Subject: [PATCH 36/50] Keep a payment method locked unless a callback returns
false
Only an explicit false from dws_lpmwc/payment_method/should_lock is
meant to unlock a payment method, but the strategies typed the lock
as bool. WordPress calls them in weak mode, so a 0 or an empty string
an earlier callback returned became false and unlocked the method for
everyone, guests included, and a null threw a TypeError that broke the
checkout. The lock itself took the gateways as an array, so another
plugin's gateways filter returning anything else broke it too.
Each strategy takes the lock untyped and returns it unchanged unless
its subject unlocks the method, and the lock passes on a gateways
value that is not an array, which WooCommerce casts to an empty list.
Rows hook a null, a 0 and an empty string before the strategies, and
a null in place of the gateways.
Assisted-by: Claude Code:claude-opus-5-5
---
.../includes/safeguards.php | 18 +++++++----
.../src/UnlockStrategies/GroupsStrategy.php | 6 ++--
.../src/UnlockStrategies/OrderStrategy.php | 6 ++--
.../UnlockStrategies/UserProfileStrategy.php | 6 ++--
.../src/UnlockStrategies/UserRoleStrategy.php | 6 ++--
.../WooCommerceMembershipsStrategy.php | 6 ++--
.../tests/Integration/SafeguardsTest.php | 32 ++++++++++++++++++-
7 files changed, 57 insertions(+), 23 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
index a6ad992..81aee81 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
@@ -140,11 +140,15 @@ function dws_lpmwc_get_paid_order_id(): int {
* @since 2.0.0
* @version 2.0.0
*
- * @param array $gateways The payment gateways offered, by id.
+ * @param mixed $gateways The payment gateways offered, by id, or what an earlier callback returned in their place, which WooCommerce casts to a list.
*
- * @return array
+ * @return mixed
*/
-function dws_lpmwc_remove_locked_payment_methods( array $gateways ): array {
+function dws_lpmwc_remove_locked_payment_methods( $gateways ) {
+ if ( ! is_array( $gateways ) ) {
+ return $gateways;
+ }
+
$ids = dws_lpmwc_get_locked_payment_method_ids();
/**
@@ -154,8 +158,8 @@ function dws_lpmwc_remove_locked_payment_methods( array $gateways ): array {
* @version 2.0.0
* @deprecated 2.0.0 Use dws_lpmwc/payment_method/locked_ids.
*
- * @param string[] $ids The locked payment methods' ids.
- * @param array $gateways The payment gateways offered, by id.
+ * @param string[] $ids The locked payment methods' ids.
+ * @param array $gateways The payment gateways offered, by id.
*/
$ids = dws_lpmwc_add_locked_ids( $ids, apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/lock_manager/locked_payment_methods', array( $ids, $gateways ), '2.0.0', 'dws_lpmwc/payment_method/locked_ids' ) );
@@ -165,8 +169,8 @@ function dws_lpmwc_remove_locked_payment_methods( array $gateways ): array {
* @since 2.0.0
* @version 2.0.0
*
- * @param string[] $ids The locked payment methods' ids.
- * @param array $gateways The payment gateways offered, by id.
+ * @param string[] $ids The locked payment methods' ids.
+ * @param array $gateways The payment gateways offered, by id.
*/
$ids = dws_lpmwc_add_locked_ids( $ids, apply_filters( 'dws_lpmwc/payment_method/locked_ids', $ids, $gateways ) ); // @phpstan-ignore WPCompat.parameterNotAvailable.applyfilters.args (WordPress 6.0 only formalized the arguments that older versions already passed on.)
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php
index a29b200..69b5ee5 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/GroupsStrategy.php
@@ -147,13 +147,13 @@ public static function unlocks( string $gateway_id, int $user_id ): bool {
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
+ * @param mixed $lock Whether the payment method stays locked, as earlier callbacks left it, passed on unchanged since only an explicit false unlocks.
* @param string $gateway_id The payment method's id.
* @param int $user_id The paying user, or 0 for a guest.
*
- * @return bool
+ * @return mixed
*/
- public function unlock_group_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ public function unlock_group_methods( mixed $lock, string $gateway_id, int $user_id ): mixed {
return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php
index f4ccf2c..2dce681 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/OrderStrategy.php
@@ -133,14 +133,14 @@ public static function unlocks( string $gateway_id, int $order_id ): bool {
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
+ * @param mixed $lock Whether the payment method stays locked, as earlier callbacks left it, passed on unchanged since only an explicit false unlocks.
* @param string $gateway_id The payment method's id.
* @param int $user_id The paying user, or 0 for a guest.
* @param int $order_id The order being paid, or 0 off the order-pay page.
*
- * @return bool
+ * @return mixed
*/
- public function unlock_granted_methods( bool $lock, string $gateway_id, int $user_id, int $order_id ): bool {
+ public function unlock_granted_methods( mixed $lock, string $gateway_id, int $user_id, int $order_id ): mixed {
return self::unlocks( $gateway_id, $order_id ) ? false : $lock;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
index b288fb6..cf590e4 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
@@ -144,13 +144,13 @@ public static function unlocks( string $gateway_id, int $user_id ): bool {
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
+ * @param mixed $lock Whether the payment method stays locked, as earlier callbacks left it, passed on unchanged since only an explicit false unlocks.
* @param string $gateway_id The payment method's id.
* @param int $user_id The paying user, or 0 for a guest.
*
- * @return bool
+ * @return mixed
*/
- public function unlock_granted_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ public function unlock_granted_methods( mixed $lock, string $gateway_id, int $user_id ): mixed {
return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php
index b078b29..ae71926 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserRoleStrategy.php
@@ -76,13 +76,13 @@ public static function unlocks( string $gateway_id, int $user_id ): bool {
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
+ * @param mixed $lock Whether the payment method stays locked, as earlier callbacks left it, passed on unchanged since only an explicit false unlocks.
* @param string $gateway_id The payment method's id.
* @param int $user_id The paying user, or 0 for a guest.
*
- * @return bool
+ * @return mixed
*/
- public function unlock_for_full_access_roles( bool $lock, string $gateway_id, int $user_id ): bool {
+ public function unlock_for_full_access_roles( mixed $lock, string $gateway_id, int $user_id ): mixed {
return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
index 51076c9..72cdf5d 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/WooCommerceMembershipsStrategy.php
@@ -156,13 +156,13 @@ public static function unlocks( string $gateway_id, int $user_id ): bool {
* @since 2.0.0
* @version 2.0.0
*
- * @param bool $lock Whether the payment method stays locked.
+ * @param mixed $lock Whether the payment method stays locked, as earlier callbacks left it, passed on unchanged since only an explicit false unlocks.
* @param string $gateway_id The payment method's id.
* @param int $user_id The paying user, or 0 for a guest.
*
- * @return bool
+ * @return mixed
*/
- public function unlock_member_methods( bool $lock, string $gateway_id, int $user_id ): bool {
+ public function unlock_member_methods( mixed $lock, string $gateway_id, int $user_id ): mixed {
return self::unlocks( $gateway_id, $user_id ) ? false : $lock;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
index a833b41..e9fd4a2 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
@@ -31,6 +31,14 @@ public static function relaxations(): array {
);
}
+ public static function early_relaxations(): array {
+ return array(
+ 'null' => array( null ),
+ 'zero' => array( 0 ),
+ 'empty-string' => array( '' ),
+ );
+ }
+
public static function order_keys(): array {
return array(
'missing' => array( null ),
@@ -94,6 +102,28 @@ public function test_only_an_explicit_false_from_should_lock_unlocks_a_locked_me
}
}
+ #[DataProvider( 'early_relaxations' )]
+ public function test_a_callback_before_the_strategies_unlocks_nothing_without_an_explicit_false( mixed $relaxation ): void {
+ $early = static fn (): mixed => $relaxation;
+ add_filter( self::FILTER, $early, 5 );
+
+ try {
+ self::assertArrayNotHasKey( 'cod', self::available() );
+ } finally {
+ remove_filter( self::FILTER, $early, 5 );
+ }
+ }
+
+ public function test_the_lock_passes_on_a_gateways_value_that_is_not_a_list(): void {
+ add_filter( 'woocommerce_available_payment_gateways', '__return_null' );
+
+ try {
+ self::assertNull( self::available() );
+ } finally {
+ remove_filter( 'woocommerce_available_payment_gateways', '__return_null' );
+ }
+ }
+
public function test_an_explicit_false_does_not_unlock_before_the_plugin_booted(): void {
global $wp_actions;
self::assertGreaterThan( 0, did_action( 'dws_lpmwc/boot/succeeded' ) );
@@ -282,7 +312,7 @@ public function test_the_lock_holds_and_nothing_relaxes_it_while_the_plugin_cann
self::assertSame( $expected, $request['last'] );
}
- protected static function available( array $gateways = array() ): array {
+ protected static function available( array $gateways = array() ): ?array {
return apply_filters( 'woocommerce_available_payment_gateways', $gateways + WC()->payment_gateways()->payment_gateways() );
}
From eb8171a1f8d83beef6e7b89134b06cbe37b735f5 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:51:37 +0100
Subject: [PATCH 37/50] Say which list the lock check and the grants forms read
dws_lpmwc_is_payment_method_locked() and the grants forms read the
settings' locked list, while checkout also locks the ids the list
filters add, and their docblocks called it "the locked list", which
reads as the list checkout uses.
The docblocks and the architecture name the settings' list and say
why the list filters stay out: they judge the payment methods a
checkout offers, as v1's did, so a callback written for a cart never
runs on a profile or the Users screen.
Assisted-by: Claude Code:claude-opus-5-5
---
.../docs/ARCHITECTURE.md | 2 +-
.../includes/grants.php | 6 ++++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
index 23b8abf..921f98f 100644
--- a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
+++ b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
@@ -34,7 +34,7 @@ The request that activates the plugin does not boot it, because WordPress includ
2. For each offered payment method on the list, it fires v1's two lock filters, then `dws_lpmwc/payment_method/should_lock` with the current user and, on an order's own payment page with that order's key, the order.
3. It removes the payment method unless `should_lock` returns an explicit `false` after `dws_lpmwc/boot/succeeded`, so the lock binds everyone while the plugin cannot boot.
-Each unlock strategy in `src/UnlockStrategies/` hooks `should_lock` while its switch is on, and returns `false` when its subject unlocks the payment method: the user's roles, the user's grants on the site, the order's grants, the user's Groups groups or the plans the user is an active member of. `dws_lpmwc_is_payment_method_locked()` runs the same filter for a user and an order outside checkout.
+Each unlock strategy in `src/UnlockStrategies/` hooks `should_lock` while its switch is on, and returns `false` when its subject unlocks the payment method: the user's roles, the user's grants on the site, the order's grants, the user's Groups groups or the plans the user is an active member of. `dws_lpmwc_is_payment_method_locked()` runs the same filter for a user and an order outside checkout, over the settings' list alone, because the list filters judge the payment methods a checkout offers; the grants forms offer the same list.
## Where code lives
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
index 97611b9..cfadfc7 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
@@ -96,7 +96,7 @@ function dws_lpmwc_get_payment_method_title( string $gateway_id ): string {
}
/**
- * Returns the choices of a grants field by payment method id: the locked methods and the subject's other grants, since a save keeps only the choices a field offers, each marked when it is not locked or when another rule already unlocks it for the user.
+ * Returns the choices of a grants field by payment method id: the methods the settings lock and the subject's other grants, since a save keeps only the choices a field offers, each marked when it is not locked or when another rule already unlocks it for the user.
*
* @since 2.0.0
* @version 2.0.0
@@ -171,7 +171,9 @@ function dws_lpmwc_get_plan_unlocked_payment_method_ids( int $plan_id ): array {
}
/**
- * Returns whether a payment method is locked for a user and an order: whether it is on the locked list and the plugin's strategies do not unlock it.
+ * Returns whether a payment method is locked for a user and an order: whether the settings lock it and dws_lpmwc/payment_method/should_lock does not relax it.
+ *
+ * The ids the list filters add are left out, because those filters judge the payment methods a checkout offers.
*
* @since 2.0.0
* @version 2.0.0
From 7d174cd535809d6ca93e4cf58812a031113ea7cb Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:52:53 +0100
Subject: [PATCH 38/50] Filter only the Users list by a granted payment method
The Users screen's filter narrowed every user query run on users.php
through pre_get_users, as v1's did, so another plugin's user query on
that screen, such as a dropdown or a count, saw only the users granted
the filtered payment method.
The filter adds its meta query on users_list_table_query_args, which
only the list tables apply to their own query, and a row checks that a
query of its own on the Users screen keeps every user.
Assisted-by: Claude Code:claude-opus-5-5
---
.../UnlockStrategies/UserProfileStrategy.php | 28 +++++++++---------
.../UserProfileStrategyTest.php | 29 ++++++++++++++-----
2 files changed, 34 insertions(+), 23 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
index cf590e4..afa89d9 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
@@ -85,7 +85,7 @@ public function register_hooks(): void {
add_filter( 'manage_users_columns', array( $this, 'add_users_column' ) );
add_filter( 'manage_users_custom_column', array( $this, 'render_users_column' ), 10, 3 );
add_action( 'restrict_manage_users', array( $this, 'render_users_filter' ) );
- add_action( 'pre_get_users', array( $this, 'filter_users' ) );
+ add_filter( 'users_list_table_query_args', array( $this, 'filter_users' ) );
}
/**
@@ -230,33 +230,31 @@ public function render_users_filter( string $which ): void {
}
/**
- * Limits the Users screen's list to the users holding the filtered payment method on the profile.
+ * Returns the Users screen's query arguments limited to the users holding the filtered payment method on the profile.
*
* @since 2.0.0
* @version 2.0.0
*
- * @global string $pagenow The admin screen's file.
+ * @param array $args The list table's query arguments.
*
- * @param \WP_User_Query $query The users query.
- *
- * @return void
+ * @return array
*/
- public function filter_users( \WP_User_Query $query ): void {
- global $pagenow;
-
- $active = 'users.php' === $pagenow && is_admin() ? $this->get_active_filter() : '';
+ public function filter_users( array $args ): array {
+ $active = $this->get_active_filter();
if ( '' === $active ) {
- return;
+ return $args;
}
- $meta_query = $query->get( 'meta_query' );
- $meta_query = \is_array( $meta_query ) ? $meta_query : array();
- $meta_query[] = array(
+ $meta_query = $args['meta_query'] ?? array();
+ $meta_query = \is_array( $meta_query ) ? $meta_query : array();
+ $meta_query[] = array(
'key' => dws_lpmwc_get_user_grants_key(),
'value' => '"' . $active . '"', // Matches the id as one element of the stored, serialized list.
'compare' => 'LIKE',
);
- $query->set( 'meta_query', $meta_query );
+ $args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- The filter matches a user meta value, which only a meta query reaches.
+
+ return $args;
}
// endregion
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php
index a64d86a..4c93f22 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php
@@ -219,6 +219,15 @@ public function test_the_users_screen_filters_by_a_method_granted_on_the_profile
self::assertSame( array( self::$users['editor'] ), self::listed_users() );
}
+ public function test_the_users_screen_filter_leaves_other_user_queries_alone(): void {
+ update_user_meta( self::$users['customer'], self::key(), array( 'cod' ) );
+ $GLOBALS['pagenow'] = 'users.php';
+ $_GET['dws_lpmwc_unlocked_for_top'] = 'cod';
+ set_current_screen( 'users' );
+
+ self::assertSame( \array_values( self::$users ), self::queried_users() );
+ }
+
protected static function create_user( string $role ): int {
$login = \uniqid( "lpm-profiles-$role-" );
@@ -266,14 +275,18 @@ protected static function save( int $user_id, array|string $methods ): void {
}
protected static function listed_users(): array {
- $query = new \WP_User_Query(
- array(
- 'include' => \array_values( self::$users ),
- 'fields' => 'ID',
- 'orderby' => 'ID',
- )
- );
+ return self::queried_users( apply_filters( 'users_list_table_query_args', self::query_args() ) );
+ }
- return \array_map( 'intval', $query->get_results() );
+ protected static function queried_users( ?array $args = null ): array {
+ return \array_map( 'intval', ( new \WP_User_Query( $args ?? self::query_args() ) )->get_results() );
+ }
+
+ protected static function query_args(): array {
+ return array(
+ 'include' => \array_values( self::$users ),
+ 'fields' => 'ID',
+ 'orderby' => 'ID',
+ );
}
}
From 55e4e3e91fefd477ae0d0774adb86f2444022646 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 00:59:27 +0100
Subject: [PATCH 39/50] Load the footprint's constants when uninstalling
WordPress uninstalls a plugin in a request that never included it,
and uninstall.php loaded footprint.php and options.php but not the
capabilities and grants files whose constants the footprint names. The
footprint threw on an undefined constant, so a web uninstall kept every
row despite the site's consent, and WP-CLI failed. The uninstall rows
ran in processes that had loaded the plugin, which defined them.
uninstall.php requires capabilities.php and grants.php too, and a row
uninstalls through a WP-CLI request that skips the plugin, as
WordPress's own uninstall request does.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/UninstallTest.php | 22 +++++++++++++++++++
.../tests/Support/RunsRequestsTrait.php | 4 ++--
.../uninstall.php | 2 ++
3 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
index e0ee6a2..f31de77 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
@@ -3,6 +3,7 @@
namespace DeepWebSolutions\LockedPaymentMethods\Tests\Integration;
use DeepWebSolutions\LockedPaymentMethods\Plugin;
+use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RunsRequestsTrait;
use PHPUnit\Framework\Attributes\AfterClass;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
@@ -10,6 +11,8 @@
#[CoversNothing]
final class UninstallTest extends TestCase {
+ use RunsRequestsTrait;
+
protected const int USER = 1;
#[AfterClass]
@@ -73,6 +76,25 @@ public function test_a_consenting_uninstall_removes_a_row_of_every_kind_the_fram
self::assertSame( 'no', get_option( DWS_LPMWC_OPTION_DELETE_DATA ) );
}
+ #[RunInSeparateProcess]
+ public function test_an_uninstall_in_a_request_that_never_loaded_the_plugin_removes_its_data(): void {
+ update_option( DWS_LPMWC_OPTION_DELETE_DATA, 'yes' );
+ update_option( DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS, array( 7 => array( 'cod' ) ) );
+
+ try {
+ // WordPress uninstalls a plugin in a request that never included it, as WP-CLI's --skip-plugins does here.
+ $request = self::request( 'require_once ABSPATH . "wp-admin/includes/plugin.php"; echo wp_json_encode( array( uninstall_plugin( "' . plugin_basename( DWS_LPMWC_FILE ) . '" ), get_option( "' . DWS_LPMWC_OPTION_GROUPS_UNLOCKED_METHODS . '" ) ) );', '--skip-plugins=' . Plugin::SLUG );
+
+ self::assertSame( '[true,false]', $request['last'], $request['output'] );
+ } finally {
+ wp_cache_flush();
+ wp_roles()->for_site();
+ // The other tests share the database, so the runner stores again what a new site starts with, and the grants act again.
+ dws_lpmwc_plugin()->migrations?->run();
+ do_action( 'wp_loaded' );
+ }
+ }
+
#[RunInSeparateProcess]
public function test_a_consenting_uninstall_removes_the_grants_v1s_rows_and_the_capabilities(): void {
global $wpdb;
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php
index ed52ba0..2042376 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Support/RunsRequestsTrait.php
@@ -6,8 +6,8 @@ trait RunsRequestsTrait {
/**
* @return array{status: int, output: string, last: string}
*/
- protected static function request( string $php ): array {
- \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . ' eval ' . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin.
+ protected static function request( string $php, string $flags = '' ): array {
+ \exec( 'wp --path=' . \escapeshellarg( ABSPATH ) . " $flags eval " . \escapeshellarg( $php ) . ' 2>&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin.
$output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) );
return array(
diff --git a/plugins/locked-payment-methods-for-woocommerce/uninstall.php b/plugins/locked-payment-methods-for-woocommerce/uninstall.php
index 6794020..52ede59 100644
--- a/plugins/locked-payment-methods-for-woocommerce/uninstall.php
+++ b/plugins/locked-payment-methods-for-woocommerce/uninstall.php
@@ -27,7 +27,9 @@
}
require_once __DIR__ . '/vendor/autoload.php';
+ require_once __DIR__ . '/includes/capabilities.php';
require_once __DIR__ . '/includes/footprint.php';
+ require_once __DIR__ . '/includes/grants.php';
require_once __DIR__ . '/includes/options.php';
$dws_lpmwc_footprint = dws_lpmwc_get_footprint();
$dws_lpmwc_consent = DWS_LPMWC_OPTION_DELETE_DATA;
From 3d98a539d8c65dad1f6762679638f1b80008cc1e Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 01:01:36 +0100
Subject: [PATCH 40/50] Store an order's grants list before deleting v1's rows
The order grants step queued the deletion of v1's grant rows and the
new list in one save, and WooCommerce deletes an order's meta before
it adds any. When the list's insert failed, the step threw after v1's
rows were gone, so its retry found nothing to move and the order lost
its grants.
The step saves the merged list and verifies it, then deletes v1's rows
in a second save and verifies that, so a failure at either save leaves
what a retry needs. A row refuses the list's insert on the posts
storage, then checks that v1's grant survives and the retry moves it.
Assisted-by: Claude Code:claude-opus-5-5
---
.../includes/migrations.php | 57 +++++++++++++------
.../UnlockStrategies/OrderStrategyTest.php | 23 ++++++++
2 files changed, 64 insertions(+), 16 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
index 03d8480..ed1fa1b 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
@@ -126,12 +126,14 @@ function dws_lpmwc_move_user_grants(): void {
/**
* Moves v1's order grants, one 'yes' row per payment method, into each order's list beside the grants it holds, through WooCommerce's order data, so either order storage keeps them, then deletes every v1 grant row of the order.
*
+ * The list is saved before v1's rows are deleted, because one save deletes an order's meta before it adds any, so a failed write would lose the grants a retry needs.
+ *
* @since 2.0.0
* @version 2.0.0
*
* @global wpdb $wpdb WordPress's database access.
*
- * @throws RuntimeException Thrown when v1's order grants could not be read or an order's grants could not be stored.
+ * @throws RuntimeException Thrown when v1's order grants could not be read, or an order's grants could not be stored or its v1 rows deleted.
*
* @return void
*/
@@ -159,34 +161,57 @@ function dws_lpmwc_move_order_grants(): void {
foreach ( array_unique( $ids ) as $order_id ) {
$order = wc_get_order( $order_id );
- if ( ! $order instanceof WC_Order ) {
+ $rows = $order instanceof WC_Order ? dws_lpmwc_get_v1_order_grant_rows( $order ) : array();
+ if ( array() === $rows ) {
continue;
}
$granted = dws_lpmwc_get_order_unlocked_payment_method_ids( $order_id );
- $moved = false;
- foreach ( $order->get_meta_data() as $meta ) {
- $data = $meta instanceof WC_Meta_Data ? $meta->get_data() : array();
- if ( is_string( $data['key'] ?? null ) && str_starts_with( $data['key'], $prefix ) ) {
- if ( 'yes' === ( $data['value'] ?? null ) ) {
- $granted[] = substr( $data['key'], strlen( $prefix ) );
- }
- $order->delete_meta_data( $data['key'] );
- $moved = true;
+ foreach ( $rows as $key => $value ) {
+ if ( 'yes' === $value ) {
+ $granted[] = substr( $key, strlen( $prefix ) );
}
}
- if ( ! $moved ) {
- continue;
+ $granted = array_values( array_unique( $granted ) );
+ $order->update_meta_data( DWS_LPMWC_META_UNLOCKED_METHODS, $granted );
+ $order->save();
+ if ( dws_lpmwc_get_order_unlocked_payment_method_ids( $order_id ) !== $granted ) {
+ throw new RuntimeException( "The payment methods unlocked for order $order_id could not be stored." ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing, never rendered.
}
- $order->update_meta_data( DWS_LPMWC_META_UNLOCKED_METHODS, array_values( array_unique( $granted ) ) );
+ foreach ( array_keys( $rows ) as $key ) {
+ $order->delete_meta_data( $key );
+ }
$order->save();
- if ( dws_lpmwc_get_order_unlocked_payment_method_ids( $order_id ) !== array_values( array_unique( $granted ) ) ) {
- throw new RuntimeException( "The payment methods unlocked for order $order_id could not be stored." ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing, never rendered.
+ $order = wc_get_order( $order_id );
+ if ( $order instanceof WC_Order && array() !== dws_lpmwc_get_v1_order_grant_rows( $order ) ) {
+ throw new RuntimeException( "The v1 grants of order $order_id could not be deleted." ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing, never rendered.
}
}
}
+/**
+ * Returns an order's v1 grant rows, by meta key.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param WC_Order $order The order.
+ *
+ * @return array
+ */
+function dws_lpmwc_get_v1_order_grant_rows( WC_Order $order ): array {
+ $rows = array();
+ foreach ( $order->get_meta_data() as $meta ) {
+ $data = $meta instanceof WC_Meta_Data ? $meta->get_data() : array();
+ if ( is_string( $data['key'] ?? null ) && str_starts_with( $data['key'], '_dws_wc_lpm_grant_access_' ) ) {
+ $rows[ $data['key'] ] = $data['value'] ?? null;
+ }
+ }
+
+ return $rows;
+}
+
/**
* Moves v1's Groups grants, one option per group, into the map of every group's grants beside the grants it holds, then deletes v1's options.
*
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
index 17ddcfe..6bc35df 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
@@ -185,6 +185,29 @@ public function test_the_grants_step_moves_v1s_order_grants_into_the_orders_list
}
}
+ public function test_a_grants_step_whose_write_fails_keeps_v1s_grants_for_its_retry(): void {
+ $this->use_storage( false, false );
+ $order = $this->create_order();
+ $order->update_meta_data( '_dws_wc_lpm_grant_access_cod', 'yes' );
+ $order->save();
+ $refuse = static fn ( mixed $check, int $object_id, string $key ): mixed => DWS_LPMWC_META_UNLOCKED_METHODS === $key ? false : $check;
+ add_filter( 'add_post_metadata', $refuse, 10, 3 );
+
+ $failed = false;
+ try {
+ dws_lpmwc_move_order_grants();
+ } catch ( \RuntimeException ) {
+ $failed = true;
+ } finally {
+ remove_filter( 'add_post_metadata', $refuse );
+ }
+
+ self::assertTrue( $failed );
+ self::assertSame( 'yes', wc_get_order( $order->get_id() )->get_meta( '_dws_wc_lpm_grant_access_cod' ) );
+ dws_lpmwc_move_order_grants();
+ self::assertSame( array( 'cod' ), dws_lpmwc_get_order_unlocked_payment_method_ids( $order->get_id() ) );
+ }
+
protected static function should_lock( string $gateway_id, int $order_id ): mixed {
return apply_filters( 'dws_lpmwc/payment_method/should_lock', true, $gateway_id, 0, $order_id );
}
From 75148bd8bd55b7dd012783e99ce11d61df3fb894 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 01:02:40 +0100
Subject: [PATCH 41/50] Leave no orders behind the checkout and uninstall rows
The block checkout and the Store API's checkout leave a draft order
each time the checkout spec runs, and an uninstall row created an order
before checking a capability outside its try, so a failed check left
the order behind. The tests site's start enables WooCommerce's order
tables, whose check refuses while any order is unsynced, so the next
quality-check failed to start the site.
The checkout spec deletes every draft order after its rows, and the
uninstall row checks the capability inside the try whose finally
deletes the order.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/UninstallTest.php | 2 +-
.../tests/e2e/checkout.spec.ts | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
index f31de77..8815cf8 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UninstallTest.php
@@ -120,9 +120,9 @@ public function test_a_consenting_uninstall_removes_the_grants_v1s_rows_and_the_
);
update_post_meta( $plan, DWS_LPMWC_META_UNLOCKED_METHODS, array( 'cod' ) );
update_post_meta( $plan, '_dws_wc_lpm_unlocked_payment_methods', array( 'cod' ) );
- self::assertTrue( get_role( 'administrator' )?->has_cap( DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS ) );
try {
+ self::assertTrue( get_role( 'administrator' )?->has_cap( DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS ) );
self::assertTrue( uninstall_plugin( plugin_basename( DWS_LPMWC_FILE ) ) );
wp_cache_flush();
wp_roles()->for_site();
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts b/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts
index 73c34cf..94a07ec 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/e2e/checkout.spec.ts
@@ -81,6 +81,11 @@ test.describe( 'Checkout', () => {
params: { force: true },
} );
cli( 'user', 'delete', String( customer ), '--yes' );
+ // The block checkout and the Store API's checkout leave draft orders, which the next start's order tables check counts as unsynced.
+ cli(
+ 'eval',
+ 'foreach ( wc_get_orders( array( "status" => "checkout-draft", "limit" => -1 ) ) as $order ) { $order->delete( true ); }'
+ );
cli(
'option',
'update',
From 9e85659b10e55cf380b7e7a3a196fcc63525b9f3 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 01:08:47 +0100
Subject: [PATCH 42/50] Keep order grants off checkouts that pay a new order
The lock passed should_lock the order an order-pay URL names once the
request carried that order's key. WordPress parses the order-pay query
var on AJAX and REST requests too, and WooCommerce's AJAX checkout
counts as the checkout, so a customer posting the cart checkout to
?wc-ajax=checkout&order-pay=&key= of an order they hold had
that order's unlocked payment methods offered for a new order from
the cart. The Store API's checkout took the same path.
The order-pay order counts only outside AJAX and REST requests, where
the order-pay page and its pay form run, and rows give an AJAX and a
REST request a valid key. The changelog's security entry says so.
Assisted-by: Claude Code:claude-opus-5-5
---
.../changelog/order-pay-key | 2 +-
.../includes/safeguards.php | 4 ++-
.../tests/Integration/SafeguardsTest.php | 27 +++++++++++++++++++
3 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key b/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key
index 4a8f32b..1eec1d3 100644
--- a/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key
+++ b/plugins/locked-payment-methods-for-woocommerce/changelog/order-pay-key
@@ -1,4 +1,4 @@
Significance: patch
Type: security
-An order unlocks its payment methods only on its own payment page and with its order key, so a checkout request naming another order's id no longer takes that order's unlocked payment methods.
+An order unlocks its payment methods only on its own payment page with its order key, never in an AJAX or REST request, so a checkout of a new order that names an existing order's id, or its key too, no longer takes that order's unlocked payment methods.
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
index 81aee81..a5df1f2 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
@@ -114,6 +114,8 @@ function dws_lpmwc_should_lock( bool $lock, string $gateway_id, int $user_id, in
/**
* Returns the order the order-pay page pays, or 0 elsewhere or without that order's key, which WooCommerce requires before it takes the payment, so a request naming another order's id gains none of its grants.
*
+ * AJAX and REST requests count as elsewhere, because the checkouts they run pay a new order from the cart whatever order the URL names.
+ *
* @since 2.0.0
* @version 2.0.0
*
@@ -124,7 +126,7 @@ function dws_lpmwc_should_lock( bool $lock, string $gateway_id, int $user_id, in
function dws_lpmwc_get_paid_order_id(): int {
global $wp;
- if ( ! function_exists( 'is_checkout_pay_page' ) || ! is_checkout_pay_page() ) {
+ if ( wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ! function_exists( 'is_checkout_pay_page' ) || ! is_checkout_pay_page() ) {
return 0;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
index e9fd4a2..82c03f5 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;
#[CoversNothing]
@@ -283,6 +284,32 @@ public function test_should_lock_judges_the_order_its_order_pay_page_pays_given_
}
}
+ public function test_should_lock_judges_no_order_in_an_ajax_checkout_that_names_an_order_and_its_key(): void {
+ $order = wc_create_order();
+ self::visit_order_pay( $order->get_id(), $order->get_order_key() );
+ add_filter( 'wp_doing_ajax', '__return_true' );
+
+ try {
+ self::assertSame( array( true, 'cod', 0, 0 ), self::should_lock_arguments() );
+ } finally {
+ remove_filter( 'wp_doing_ajax', '__return_true' );
+ $order->delete( true );
+ }
+ }
+
+ #[RunInSeparateProcess]
+ public function test_should_lock_judges_no_order_in_a_rest_checkout_that_names_an_order_and_its_key(): void {
+ \define( 'REST_REQUEST', true );
+ $order = wc_create_order();
+ self::visit_order_pay( $order->get_id(), $order->get_order_key() );
+
+ try {
+ self::assertSame( array( true, 'cod', 0, 0 ), self::should_lock_arguments() );
+ } finally {
+ $order->delete( true );
+ }
+ }
+
#[DataProvider( 'order_keys' )]
public function test_should_lock_judges_no_order_on_an_order_pay_page_without_the_orders_key( ?string $key ): void {
$order = wc_create_order();
From ba10de848c751f2ed7ffd976a7b93bf3f58efce5 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 01:09:56 +0100
Subject: [PATCH 43/50] Test the underscore include skip with an include that
exists
The boot row asserted that includes/_disabled-example.php never loads,
but no such file exists, so the row passed even with the loader's
underscore check removed.
The row writes an underscore-prefixed include defining a constant and
checks in a fresh request that the constant stays undefined, as Linked
Orders' row does; it fails once the loader's check is removed.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/PluginBootTest.php | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
index cacfda6..077adae 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
@@ -13,11 +13,14 @@
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserProfileStrategy;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\UserRoleStrategy;
use DeepWebSolutions\LockedPaymentMethods\UnlockStrategies\WooCommerceMembershipsStrategy;
+use DeepWebSolutions\LockedPaymentMethods\Tests\Support\RunsRequestsTrait;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;
#[CoversNothing]
final class PluginBootTest extends TestCase {
+ use RunsRequestsTrait;
+
public function test_the_plugin_boots_every_component_whose_switch_is_on(): void {
$plugin = dws_lpmwc_plugin();
@@ -30,7 +33,14 @@ public function test_the_entry_loads_every_include_but_those_whose_names_start_w
$includes = \dirname( DWS_LPMWC_FILE ) . '/includes/';
self::assertContains( $includes . 'index.php', \get_included_files() );
- self::assertNotContains( $includes . '_disabled-example.php', \get_included_files() );
+ \file_put_contents( $includes . '_lpmwc-skipped.php', "
Date: Sun, 1 Nov 2026 01:14:18 +0100
Subject: [PATCH 44/50] Fail the order grants step when its table probe fails
The order grants step probed for each meta table with SHOW TABLES and
skipped a table the probe did not name, so a failed probe read as an
absent table. The step then finished, the runner recorded it, and the
orders in that table kept v1's grants, which v2 never reads.
A probe that fails throws, so the runner runs the step again on a later
request, and a row breaks the probe and expects the throw.
Assisted-by: Claude Code:claude-opus-5-5
---
.../includes/migrations.php | 6 +++++-
.../UnlockStrategies/OrderStrategyTest.php | 15 +++++++++++++++
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
index ed1fa1b..9308ff5 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/migrations.php
@@ -148,7 +148,11 @@ function dws_lpmwc_move_order_grants(): void {
);
foreach ( $tables as $table => $column ) {
$wpdb->flush();
- if ( $table !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- The order tables exist only once WooCommerce created them.
+ $listed = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- The order tables exist only once WooCommerce created them.
+ if ( null === $listed && '' !== $wpdb->last_error ) {
+ throw new RuntimeException( 'The v1 order grants could not be read.' );
+ }
+ if ( $table !== $listed ) {
continue;
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
index 6bc35df..3b9dc4e 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/OrderStrategyTest.php
@@ -185,6 +185,21 @@ public function test_the_grants_step_moves_v1s_order_grants_into_the_orders_list
}
}
+ public function test_the_grants_step_throws_when_it_cannot_tell_whether_an_order_table_exists(): void {
+ global $wpdb;
+
+ $fail = static fn ( string $query ): string => \str_starts_with( \ltrim( $query ), 'SHOW TABLES' ) ? "SELECT * FROM {$wpdb->prefix}dws_lpmwc_missing" : $query;
+ $suppressed = $wpdb->suppress_errors();
+ add_filter( 'query', $fail );
+ try {
+ $this->expectExceptionObject( new \RuntimeException( 'The v1 order grants could not be read.' ) );
+ dws_lpmwc_move_order_grants();
+ } finally {
+ remove_filter( 'query', $fail );
+ $wpdb->suppress_errors( $suppressed );
+ }
+ }
+
public function test_a_grants_step_whose_write_fails_keeps_v1s_grants_for_its_retry(): void {
$this->use_storage( false, false );
$order = $this->create_order();
From 41021c7920288ba28917c7772d2a2f997e2269c3 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 01:19:29 +0100
Subject: [PATCH 45/50] Narrow the uninstall's v1 user meta prefix to the
grants
The footprint swept every user meta key starting with dws_wc_lpm_,
though v1 stored only its grants, dws_wc_lpm_grant_access_{gateway},
under it. A user meta prefix sweep also matches WordPress's own
per-site keys, such as {table prefix}capabilities, when the site's
table prefix starts with it, so the wider prefix put every user's
roles at risk on a site whose table prefix begins dws_wc_lpm_.
The footprint names v1's grants prefix alone, and the Unit row and the
architecture say so.
Assisted-by: Claude Code:claude-opus-5-5
---
.../locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md | 2 +-
.../includes/footprint.php | 2 +-
.../tests/Unit/FootprintTest.php | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
index 921f98f..4564b32 100644
--- a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
+++ b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
@@ -46,7 +46,7 @@ Each unlock strategy in `src/UnlockStrategies/` hooks `should_lock` while its sw
- **Settings** are flat options under `dws_lpmwc_`, as constants in `includes/options.php`: the locked list, a switch per strategy, the full-access roles and the uninstall consent. The first migration step copies each of v1's eight settings from its `dws-wc-lpm_{group}_{field}` row where it holds a value of the setting's kind, and leaves v1's rows until uninstall, so a downgrade to 1.3.5 still locks what it locked.
- **Grants** are one list of payment method ids per subject: the user option `dws_lpmwc_unlocked_payment_methods` under each site's table prefix, the order meta and the Memberships plan meta `_dws_lpmwc_unlocked_payment_methods`, and the `dws_lpmwc_groups_unlocked_payment_methods` map from Groups group id to list, from which deleting a group removes its entry. The second to fifth migration steps move v1's grants into these shapes and delete v1's rows: user meta into every site's list, so v1's grants keep their network reach, order meta through WC CRUD on either storage, the per-group options and the plan meta.
- **Migrations** are the append-only steps `dws_lpmwc_get_migration_steps()` returns. The framework's runner runs each pending step once per site on `wp_loaded`, behind a lock, and records the revision reached in `dws_lpmwc_revision`. A step converges from any partial state and throws when a write fails, so a failed step runs again on a later request.
-- **The footprint**, `dws_lpmwc_get_footprint()`, names every kind of data the plugin stores: the `dws_lpmwc_` and `dws-wc-lpm_` option prefixes, v1's version and notice rows, the order meta under `_dws_lpmwc_` and `_dws_wc_lpm_` on both storages, the plan meta under both keys, the site user meta under `dws_lpmwc_`, v1's global user grants under `dws_wc_lpm_`, network options, the capabilities, scheduled hooks and Action Scheduler groups. Freemius's `fs_` rows from v1 stay, since they belong to Freemius.
+- **The footprint**, `dws_lpmwc_get_footprint()`, names every kind of data the plugin stores: the `dws_lpmwc_` and `dws-wc-lpm_` option prefixes, v1's version and notice rows, the order meta under `_dws_lpmwc_` and `_dws_wc_lpm_` on both storages, the plan meta under both keys, the site user meta under `dws_lpmwc_`, v1's global user grants under `dws_wc_lpm_grant_access_`, network options, the capabilities, scheduled hooks and Action Scheduler groups. Freemius's `fs_` rows from v1 stay, since they belong to Freemius.
- **Deactivation** unschedules every hook in the footprint's `cron_hooks` and `as_hooks` on the current site.
- **Uninstall** runs `uninstall.php`, which calls the framework's uninstall engine over the footprint, with WooCommerce's order meta types, on every site whose `dws_lpmwc_delete_data` setting consents, which the first migration step copies from v1's; a site that never consented keeps its data. Below the floors or with an incomplete build, a web request deletes the files, keeps the data and logs why, while WP-CLI fails and keeps both for a rerun.
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php b/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
index ae53bad..64b4d47 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/footprint.php
@@ -27,7 +27,7 @@ function dws_lpmwc_get_footprint(): array {
'meta' => array( 'post' => array( DWS_LPMWC_META_UNLOCKED_METHODS, '_dws_wc_lpm_unlocked_payment_methods' ) ),
'meta_prefixes' => array( 'order' => array( '_' . Plugin::PREFIX . '_', '_dws_wc_lpm_' ) ),
'user_meta_site_prefixes' => array( Plugin::PREFIX . '_' ),
- 'user_meta_global_prefixes' => array( Plugin::PREFIX . '_', 'dws_wc_lpm_' ),
+ 'user_meta_global_prefixes' => array( Plugin::PREFIX . '_', 'dws_wc_lpm_grant_access_' ),
'user_meta_global' => array( '_locked_payment_methods_for_woocommerce_admin_notices' ),
'network_options' => array( Plugin::PREFIX . '_network_notices' ),
'capabilities' => array( DWS_LPMWC_CAP_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_CAP_UNLOCK_VIA_ORDERS, DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS, DWS_LPMWC_CAP_UNLOCK_VIA_WC_MEMBERSHIPS ),
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
index 8514628..e9e4d0a 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Unit/FootprintTest.php
@@ -73,7 +73,7 @@ public function test_it_covers_what_v1_stored_and_the_grants_and_capabilities():
self::assertSame( array( 'locked_payment_methods_for_woocommerce_version', 'locked_payment_methods_for_woocommerce_original_version', '_locked_payment_methods_for_woocommerce_admin_notices' ), $footprint['options'] );
self::assertSame( array( '_' . Plugin::PREFIX . '_', '_dws_wc_lpm_' ), $footprint['meta_prefixes']['order'] );
self::assertSame( array( DWS_LPMWC_META_UNLOCKED_METHODS, '_dws_wc_lpm_unlocked_payment_methods' ), $footprint['meta']['post'] );
- self::assertContains( 'dws_wc_lpm_', $footprint['user_meta_global_prefixes'] );
+ self::assertContains( 'dws_wc_lpm_grant_access_', $footprint['user_meta_global_prefixes'] );
self::assertSame( array( '_locked_payment_methods_for_woocommerce_admin_notices' ), $footprint['user_meta_global'] );
self::assertSame( array( DWS_LPMWC_CAP_UNLOCK_VIA_USER_PROFILES, DWS_LPMWC_CAP_UNLOCK_VIA_ORDERS, DWS_LPMWC_CAP_UNLOCK_VIA_GROUPS, DWS_LPMWC_CAP_UNLOCK_VIA_WC_MEMBERSHIPS ), $footprint['capabilities'] );
}
From d9239cadf01e94b3937aec955cd9250fa6655428 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 01:24:14 +0100
Subject: [PATCH 46/50] Judge the gateways as WooCommerce casts them
The lock passed on a gateways value that was not an array, so an
earlier callback that returned the gateways as an object skipped the
lock, and WooCommerce, which casts the filtered value to an array,
then offered every locked payment method.
The lock casts the value as WooCommerce does before it judges it, and
rows replace the gateways with null and with an object.
Assisted-by: Claude Code:claude-opus-5-5
---
.../includes/safeguards.php | 10 ++++-----
.../tests/Integration/SafeguardsTest.php | 21 ++++++++++++++-----
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
index a5df1f2..824345f 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
@@ -142,14 +142,12 @@ function dws_lpmwc_get_paid_order_id(): int {
* @since 2.0.0
* @version 2.0.0
*
- * @param mixed $gateways The payment gateways offered, by id, or what an earlier callback returned in their place, which WooCommerce casts to a list.
+ * @param mixed $gateways The payment gateways offered, by id, or what an earlier callback returned in their place.
*
- * @return mixed
+ * @return array
*/
-function dws_lpmwc_remove_locked_payment_methods( $gateways ) {
- if ( ! is_array( $gateways ) ) {
- return $gateways;
- }
+function dws_lpmwc_remove_locked_payment_methods( $gateways ): array {
+ $gateways = (array) $gateways; // WooCommerce casts the filtered value to a list, so the lock judges what it would offer.
$ids = dws_lpmwc_get_locked_payment_method_ids();
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
index 82c03f5..62caed5 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
@@ -32,6 +32,13 @@ public static function relaxations(): array {
);
}
+ public static function gateway_values(): array {
+ return array(
+ 'null' => array( 'null' ),
+ 'object' => array( 'object' ),
+ );
+ }
+
public static function early_relaxations(): array {
return array(
'null' => array( null ),
@@ -115,13 +122,17 @@ public function test_a_callback_before_the_strategies_unlocks_nothing_without_an
}
}
- public function test_the_lock_passes_on_a_gateways_value_that_is_not_a_list(): void {
- add_filter( 'woocommerce_available_payment_gateways', '__return_null' );
+ #[DataProvider( 'gateway_values' )]
+ public function test_the_lock_judges_a_gateways_value_that_is_not_a_list_as_woocommerce_casts_it( string $kind ): void {
+ $replace = static fn ( array $gateways ): ?object => 'object' === $kind ? (object) $gateways : null;
+ add_filter( 'woocommerce_available_payment_gateways', $replace );
try {
- self::assertNull( self::available() );
+ $offered = (array) self::available();
+ self::assertArrayNotHasKey( 'cod', $offered );
+ self::assertSame( 'object' === $kind, \array_key_exists( 'bacs', $offered ) );
} finally {
- remove_filter( 'woocommerce_available_payment_gateways', '__return_null' );
+ remove_filter( 'woocommerce_available_payment_gateways', $replace );
}
}
@@ -339,7 +350,7 @@ public function test_the_lock_holds_and_nothing_relaxes_it_while_the_plugin_cann
self::assertSame( $expected, $request['last'] );
}
- protected static function available( array $gateways = array() ): ?array {
+ protected static function available( array $gateways = array() ): mixed {
return apply_filters( 'woocommerce_available_payment_gateways', $gateways + WC()->payment_gateways()->payment_gateways() );
}
From 58ac0a3ed9f5ca51fc4df038482db37244b49731 Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 10:51:45 +0100
Subject: [PATCH 47/50] Test that every reader of the locked list counts the
locked_ids filter
dws_lpmwc/payment_method/locked_ids fired only in the checkout's
gateways filter, so a payment method it locked was hidden at checkout
but reported unlocked by dws_lpmwc_is_payment_method_locked() and
offered on no grants form, leaving it to full-access roles and
should_lock callbacks.
The rows check that the lock check and the profile form count an id
the filter adds, and that the filter runs right after v1's value
filter with the ids alone, before v1's list filter at checkout.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/SafeguardsTest.php | 25 ++++++++++++++-----
.../UserProfileStrategyTest.php | 21 ++++++++++++++++
2 files changed, 40 insertions(+), 6 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
index 62caed5..ba418ad 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/SafeguardsTest.php
@@ -197,17 +197,30 @@ public function test_the_locked_ids_filter_locks_more_methods_but_cannot_unlock_
self::assertArrayNotHasKey( 'bacs', $available );
}
- public function test_v1s_list_filters_fire_as_deprecated_before_the_locked_ids_filter(): void {
+ public function test_the_lock_check_counts_the_methods_the_locked_ids_filter_adds(): void {
+ $add = static fn ( array $ids ): array => array( ...$ids, 'bacs' );
+ add_filter( 'dws_lpmwc/payment_method/locked_ids', $add );
+
+ try {
+ self::assertTrue( dws_lpmwc_is_payment_method_locked( 'bacs', 0 ) );
+ } finally {
+ remove_filter( 'dws_lpmwc/payment_method/locked_ids', $add );
+ }
+
+ self::assertFalse( dws_lpmwc_is_payment_method_locked( 'bacs', 0 ) );
+ }
+
+ public function test_the_locked_ids_filter_follows_v1s_value_filter_and_v1s_list_filter_follows_it_at_checkout(): void {
$seen = array();
$gateways = WC()->payment_gateways()->payment_gateways();
$hooks = array(
'locked_payment_methods_for_woocommerce/general_settings/get_validated_option_value' => 'first',
- 'locked_payment_methods_for_woocommerce/lock_manager/locked_payment_methods' => 'second',
- 'dws_lpmwc/payment_method/locked_ids' => 'third',
+ 'dws_lpmwc/payment_method/locked_ids' => 'second',
+ 'locked_payment_methods_for_woocommerce/lock_manager/locked_payment_methods' => 'third',
);
$adders = array();
foreach ( $hooks as $hook => $id ) {
- $adders[ $hook ] = static function ( array $ids, mixed $context ) use ( &$seen, $hook, $id ): array {
+ $adders[ $hook ] = static function ( array $ids, mixed $context = null ) use ( &$seen, $hook, $id ): array {
$seen[] = array( $hook, $ids, \is_array( $context ) ? \array_keys( $context ) : $context );
return array( ...$ids, $id );
@@ -227,12 +240,12 @@ public function test_v1s_list_filters_fire_as_deprecated_before_the_locked_ids_f
self::assertSame(
array(
array( $hooks[0], array( 'cod' ), 'locked-payment-methods' ),
- array( $hooks[1], array( 'cod', 'first' ), \array_keys( $gateways ) ),
+ array( $hooks[1], array( 'cod', 'first' ), null ),
array( $hooks[2], array( 'cod', 'first', 'second' ), \array_keys( $gateways ) ),
),
$seen
);
- self::assertSame( array( array( $hooks[0], $hooks[2] ), array( $hooks[1], $hooks[2] ) ), $this->deprecated );
+ self::assertSame( array( array( $hooks[0], $hooks[1] ), array( $hooks[2], $hooks[1] ) ), $this->deprecated );
}
public function test_v1s_lock_filters_fire_as_deprecated_before_should_lock_under_v1s_safe_id(): void {
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php
index 4c93f22..dfc4749 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/UnlockStrategies/UserProfileStrategyTest.php
@@ -121,6 +121,27 @@ public function test_the_profile_offers_the_locked_methods_and_the_users_other_g
self::assertSame( 'Locked Payment Methods', \trim( (string) $form->querySelector( 'h2' )?->textContent ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- A DOM property.
}
+ public function test_the_profile_offers_the_methods_the_locked_ids_filter_adds_as_locked(): void {
+ $add = static fn ( array $ids ): array => array( ...$ids, 'cheque' );
+ add_filter( 'dws_lpmwc/payment_method/locked_ids', $add );
+ wp_set_current_user( self::$users['administrator'] );
+
+ try {
+ $form = self::render( self::$users['customer'] );
+ } finally {
+ remove_filter( 'dws_lpmwc/payment_method/locked_ids', $add );
+ }
+
+ self::assertSame(
+ array(
+ 'cod' => array( 'Cash on delivery', false ),
+ 'bacs' => array( 'Direct bank transfer', false ),
+ 'cheque' => array( 'Check payments', false ),
+ ),
+ self::choices( $form )
+ );
+ }
+
public function test_the_profile_marks_the_methods_another_rule_already_unlocks_for_the_user(): void {
wp_set_current_user( self::$users['administrator'] );
From 2f5885eb64bb924be11dd3220bce17193d55554a Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 10:52:59 +0100
Subject: [PATCH 48/50] Filter the locked list wherever Locked Payment Methods
reads it
dws_lpmwc/payment_method/locked_ids fired only in the checkout's
gateways filter with the offered gateways, so the lock check, the
grants forms and the Users filter read a shorter list than checkout
did, and a payment method locked from code could be unlocked only by
full-access roles or should_lock callbacks.
The filter fires inside dws_lpmwc_get_locked_payment_method_ids() with
the ids alone, after v1's value filter, so every reader sees the ids it
adds; v1's deprecated list filter still fires only at checkout, with
the offered gateways, as v1 fired it.
Assisted-by: Claude Code:claude-opus-5-5
---
.../docs/ARCHITECTURE.md | 4 ++--
.../includes/grants.php | 6 ++---
.../includes/safeguards.php | 24 +++++++++----------
3 files changed, 15 insertions(+), 19 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
index 4564b32..4855f51 100644
--- a/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
+++ b/plugins/locked-payment-methods-for-woocommerce/docs/ARCHITECTURE.md
@@ -30,11 +30,11 @@ The request that activates the plugin does not boot it, because WordPress includ
`includes/safeguards.php` filters `woocommerce_available_payment_gateways` at 999, which WooCommerce applies on the classic checkout, the order payment page and the Store API's checkout:
-1. It reads `dws_lpmwc_locked_payment_methods`, or v1's `dws-wc-lpm_general_locked-payment-methods` while the plugin has stored none, since the migration that copies v1's list runs only once the plugin boots, then lets v1's value and list filters and `dws_lpmwc/payment_method/locked_ids` add ids.
+1. It reads `dws_lpmwc_locked_payment_methods`, or v1's `dws-wc-lpm_general_locked-payment-methods` while the plugin has stored none, since the migration that copies v1's list runs only once the plugin boots, then lets v1's value filter and `dws_lpmwc/payment_method/locked_ids` add ids, as every read of the list does, and v1's list filter add more.
2. For each offered payment method on the list, it fires v1's two lock filters, then `dws_lpmwc/payment_method/should_lock` with the current user and, on an order's own payment page with that order's key, the order.
3. It removes the payment method unless `should_lock` returns an explicit `false` after `dws_lpmwc/boot/succeeded`, so the lock binds everyone while the plugin cannot boot.
-Each unlock strategy in `src/UnlockStrategies/` hooks `should_lock` while its switch is on, and returns `false` when its subject unlocks the payment method: the user's roles, the user's grants on the site, the order's grants, the user's Groups groups or the plans the user is an active member of. `dws_lpmwc_is_payment_method_locked()` runs the same filter for a user and an order outside checkout, over the settings' list alone, because the list filters judge the payment methods a checkout offers; the grants forms offer the same list.
+Each unlock strategy in `src/UnlockStrategies/` hooks `should_lock` while its switch is on, and returns `false` when its subject unlocks the payment method: the user's roles, the user's grants on the site, the order's grants, the user's Groups groups or the plans the user is an active member of. `dws_lpmwc_is_payment_method_locked()` runs the same filter for a user and an order outside checkout, over the same list without v1's list filter, which v1 fired only at checkout; the grants forms offer that list.
## Where code lives
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
index cfadfc7..d56499c 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
@@ -96,7 +96,7 @@ function dws_lpmwc_get_payment_method_title( string $gateway_id ): string {
}
/**
- * Returns the choices of a grants field by payment method id: the methods the settings lock and the subject's other grants, since a save keeps only the choices a field offers, each marked when it is not locked or when another rule already unlocks it for the user.
+ * Returns the choices of a grants field by payment method id: the locked methods and the subject's other grants, since a save keeps only the choices a field offers, each marked when it is not locked or when another rule already unlocks it for the user.
*
* @since 2.0.0
* @version 2.0.0
@@ -171,9 +171,7 @@ function dws_lpmwc_get_plan_unlocked_payment_method_ids( int $plan_id ): array {
}
/**
- * Returns whether a payment method is locked for a user and an order: whether the settings lock it and dws_lpmwc/payment_method/should_lock does not relax it.
- *
- * The ids the list filters add are left out, because those filters judge the payment methods a checkout offers.
+ * Returns whether a payment method is locked for a user and an order: whether the locked list holds it and dws_lpmwc/payment_method/should_lock does not relax it.
*
* @since 2.0.0
* @version 2.0.0
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
index 824345f..ecb9d19 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/safeguards.php
@@ -27,7 +27,7 @@ function dws_lpmwc_is_relaxed( $filtered ): bool {
}
/**
- * Returns the ids of the locked payment methods: the plugin's list, or v1's while the plugin has stored none, because the migration that copies v1's list runs only once the plugin boots.
+ * Returns the ids of the locked payment methods: the plugin's list, or v1's while the plugin has stored none, because the migration that copies v1's list runs only once the plugin boots, with the ids the plugin's list filters add.
*
* @since 2.0.0
* @version 2.0.0
@@ -37,8 +37,17 @@ function dws_lpmwc_is_relaxed( $filtered ): bool {
function dws_lpmwc_get_locked_payment_method_ids(): array {
$stored = get_option( 'dws_lpmwc_locked_payment_methods' );
$ids = dws_lpmwc_add_locked_ids( array(), false === $stored ? get_option( 'dws-wc-lpm_general_locked-payment-methods' ) : $stored );
+ $ids = dws_lpmwc_add_locked_ids( $ids, dws_lpmwc_apply_v1_setting_filter( $ids, 'general', 'locked-payment-methods', 'dws_lpmwc/payment_method/locked_ids' ) );
- return dws_lpmwc_add_locked_ids( $ids, dws_lpmwc_apply_v1_setting_filter( $ids, 'general', 'locked-payment-methods', 'dws_lpmwc/payment_method/locked_ids' ) );
+ /**
+ * Filters the locked payment methods' ids wherever the plugin reads them, at checkout, in the lock check and on the grants forms; a callback adds the ids it locks, and an id it leaves out stays locked.
+ *
+ * @since 2.0.0
+ * @version 2.0.0
+ *
+ * @param string[] $ids The locked payment methods' ids.
+ */
+ return dws_lpmwc_add_locked_ids( $ids, apply_filters( 'dws_lpmwc/payment_method/locked_ids', $ids ) );
}
/**
@@ -163,17 +172,6 @@ function dws_lpmwc_remove_locked_payment_methods( $gateways ): array {
*/
$ids = dws_lpmwc_add_locked_ids( $ids, apply_filters_deprecated( 'locked_payment_methods_for_woocommerce/lock_manager/locked_payment_methods', array( $ids, $gateways ), '2.0.0', 'dws_lpmwc/payment_method/locked_ids' ) );
- /**
- * Filters the locked payment methods' ids, to which a callback adds the ids it locks; an id it leaves out stays locked.
- *
- * @since 2.0.0
- * @version 2.0.0
- *
- * @param string[] $ids The locked payment methods' ids.
- * @param array $gateways The payment gateways offered, by id.
- */
- $ids = dws_lpmwc_add_locked_ids( $ids, apply_filters( 'dws_lpmwc/payment_method/locked_ids', $ids, $gateways ) ); // @phpstan-ignore WPCompat.parameterNotAvailable.applyfilters.args (WordPress 6.0 only formalized the arguments that older versions already passed on.)
-
$user_id = get_current_user_id();
$order_id = dws_lpmwc_get_paid_order_id();
foreach ( $ids as $id ) {
From ae2e3f312e89ba2196d24d9379714849fc735b7f Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 11:05:26 +0100
Subject: [PATCH 49/50] Judge a locked method for a user without reading the
list again
The Users column and the grants choices asked
dws_lpmwc_is_payment_method_locked() about ids they had just read from
the locked list, so every row and payment method read the options and
ran the list filters again, locked_ids callbacks included.
Both call dws_lpmwc_should_lock() for the id they already know is
locked, which judges it the same way.
Assisted-by: Claude Code:claude-opus-5-5
---
.../includes/grants.php | 2 +-
.../src/UnlockStrategies/UserProfileStrategy.php | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
index d56499c..93eebb0 100644
--- a/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
+++ b/plugins/locked-payment-methods-for-woocommerce/includes/grants.php
@@ -114,7 +114,7 @@ function dws_lpmwc_get_grant_choices( array $granted, int $user_id ): array {
if ( ! in_array( $id, $locked, true ) ) {
/* translators: %s: payment method title. */
$title = sprintf( __( '%s (not locked)', 'locked-payment-methods-for-woocommerce' ), $title );
- } elseif ( 0 !== $user_id && ! in_array( $id, $granted, true ) && ! dws_lpmwc_is_payment_method_locked( $id, $user_id ) ) {
+ } elseif ( 0 !== $user_id && ! in_array( $id, $granted, true ) && ! dws_lpmwc_should_lock( true, $id, $user_id, 0 ) ) {
/* translators: %s: payment method title. */
$title = sprintf( __( '%s (unlocked by another rule)', 'locked-payment-methods-for-woocommerce' ), $title );
}
diff --git a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
index afa89d9..e258854 100644
--- a/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
+++ b/plugins/locked-payment-methods-for-woocommerce/src/UnlockStrategies/UserProfileStrategy.php
@@ -192,9 +192,9 @@ public function render_users_column( string $output, string $column, int $user_i
$lines = array();
foreach ( dws_lpmwc_get_locked_payment_method_ids() as $id ) {
$state = match ( true ) {
- \in_array( $id, $granted, true ) => __( 'unlocked on the profile', 'locked-payment-methods-for-woocommerce' ),
- ! dws_lpmwc_is_payment_method_locked( $id, $user_id ) => __( 'unlocked by another rule', 'locked-payment-methods-for-woocommerce' ),
- default => __( 'locked', 'locked-payment-methods-for-woocommerce' ),
+ \in_array( $id, $granted, true ) => __( 'unlocked on the profile', 'locked-payment-methods-for-woocommerce' ),
+ ! dws_lpmwc_should_lock( true, $id, $user_id, 0 ) => __( 'unlocked by another rule', 'locked-payment-methods-for-woocommerce' ),
+ default => __( 'locked', 'locked-payment-methods-for-woocommerce' ),
};
/* translators: 1: payment method title, 2: the payment method's state for the user. */
$lines[] = esc_html( \sprintf( __( '%1$s: %2$s', 'locked-payment-methods-for-woocommerce' ), dws_lpmwc_get_payment_method_title( $id ), $state ) );
From aa509c6cb7dbba0224c934b95ff8ec4927122cce Mon Sep 17 00:00:00 2001
From: Tony Hegyes
Date: Sun, 1 Nov 2026 11:05:26 +0100
Subject: [PATCH 50/50] Check that the plugin booted in the underscore include
row
The row read "skipped" from a fresh request whenever the constant the
underscore include defines stayed undefined, which also happens when
the plugin never loads or boots in that request or the include was
never written, so the row could pass without testing the loader.
The row checks the write and the request's exit status, and prints
"not booted" unless the plugin's boot succeeded.
Assisted-by: Claude Code:claude-opus-5-5
---
.../tests/Integration/PluginBootTest.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
index 077adae..f580892 100644
--- a/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
+++ b/plugins/locked-payment-methods-for-woocommerce/tests/Integration/PluginBootTest.php
@@ -33,13 +33,14 @@ public function test_the_entry_loads_every_include_but_those_whose_names_start_w
$includes = \dirname( DWS_LPMWC_FILE ) . '/includes/';
self::assertContains( $includes . 'index.php', \get_included_files() );
- \file_put_contents( $includes . '_lpmwc-skipped.php', "