diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b7c2d9918e93..90afd1b2febdb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1938,6 +1938,9 @@ importers:
'@automattic/typography':
specifier: 1.0.0
version: 1.0.0
+ '@preact/signals':
+ specifier: ^1.2.2
+ version: 1.2.2(preact@10.19.3)
'@sentry/browser':
specifier: 7.80.1
version: 7.80.1
@@ -1949,7 +1952,7 @@ importers:
version: 4.39.0
'@wordpress/components':
specifier: 25.14.0
- version: 25.14.0(react-dom@18.2.0)(react@18.2.0)
+ version: 25.14.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@wordpress/data':
specifier: 9.18.0
version: 9.18.0(react@18.2.0)
@@ -1964,10 +1967,16 @@ importers:
version: 4.48.0
'@wordpress/plugins':
specifier: 6.16.0
- version: 6.16.0(react-dom@18.2.0)(react@18.2.0)
+ version: 6.16.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
'@wordpress/url':
specifier: 3.49.0
version: 3.49.0
+ preact:
+ specifier: ^10.13.1
+ version: 10.19.3
+ wpcom-proxy-request:
+ specifier: ^7.0.3
+ version: 7.0.5
optionalDependencies:
react:
specifier: ^18.2.0
@@ -1982,9 +1991,27 @@ importers:
'@babel/core':
specifier: 7.23.5
version: 7.23.5
+ '@babel/plugin-transform-react-jsx':
+ specifier: 7.23.4
+ version: 7.23.4(@babel/core@7.23.5)
'@babel/preset-react':
specifier: 7.23.3
version: 7.23.3(@babel/core@7.23.5)
+ '@types/node':
+ specifier: ^20.4.2
+ version: 20.10.7
+ '@types/react':
+ specifier: ^18.2.28
+ version: 18.2.33
+ '@types/react-dom':
+ specifier: 18.2.0
+ version: 18.2.0
+ babel-plugin-transform-rename-properties:
+ specifier: 0.1.0
+ version: 0.1.0(@babel/core@7.23.5)
+ copy-webpack-plugin:
+ specifier: 11.0.0
+ version: 11.0.0(webpack@5.76.0)
sass:
specifier: 1.64.1
version: 1.64.1
@@ -5631,6 +5658,12 @@ packages:
dependencies:
regenerator-runtime: 0.14.1
+ /@babel/runtime@7.23.8:
+ resolution: {integrity: sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.14.1
+
/@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
@@ -11326,6 +11359,12 @@ packages:
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
dev: true
+ /@types/react-dom@18.2.0:
+ resolution: {integrity: sha512-8yQrvS6sMpSwIovhPOwfyNf2Wz6v/B62LFSVYQ85+Rq3tLsBIG7rP5geMxaijTUxSkrO6RzN/IRuIAADYQsleA==}
+ dependencies:
+ '@types/react': 18.2.33
+ dev: true
+
/@types/react-dom@18.2.14:
resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==}
dependencies:
@@ -11888,7 +11927,7 @@ packages:
resolution: {integrity: sha512-87GhllJcdlxqLugQUx/hL+PE4z7Aqf+AFs8CgzN5/V7INq9IFlIjcbm5TpI4WrGVDSa2puA0tMrjhR/FWXF3NQ==}
engines: {node: '>=12'}
dependencies:
- '@babel/runtime': 7.23.5
+ '@babel/runtime': 7.23.8
'@wordpress/i18n': 4.48.0
'@wordpress/url': 3.49.0
@@ -12990,7 +13029,7 @@ packages:
resolution: {integrity: sha512-vFmjpq/XN2bYgz67BS2ZC0n4P1FZUi0UPv8PTMKK+dzCPhQRYrJb8DRhBafwu2mXRzw4rO7vmVTCNJQM6xVObQ==}
engines: {node: '>=12'}
dependencies:
- '@babel/runtime': 7.23.5
+ '@babel/runtime': 7.23.8
/@wordpress/html-entities@3.48.0:
resolution: {integrity: sha512-La4ErNVZPV6kt1jj0Thay/YlR16GkAOYZlaFXtKeYgwiyOzMsIVlghpExk7rvEj2ygRMeWoMSU0+cbciv6QeBg==}
@@ -13003,7 +13042,7 @@ packages:
engines: {node: '>=12'}
hasBin: true
dependencies:
- '@babel/runtime': 7.23.5
+ '@babel/runtime': 7.23.8
'@wordpress/hooks': 3.48.0
gettext-parser: 1.4.0
memize: 2.1.0
@@ -13583,7 +13622,7 @@ packages:
resolution: {integrity: sha512-AARE9FMGEf3bf/EKb+OhFivgps38s5fRGFMqeHImP8JvKAt6xc7Q6IrpFOTXkI2BOWA4ERK//PAygR8PR5bgVA==}
engines: {node: '>=12'}
dependencies:
- '@babel/runtime': 7.23.5
+ '@babel/runtime': 7.23.8
remove-accents: 0.5.0
/@wordpress/viewport@5.25.0(react@18.2.0):
@@ -14318,6 +14357,14 @@ packages:
- supports-color
dev: true
+ /babel-plugin-transform-rename-properties@0.1.0(@babel/core@7.23.5):
+ resolution: {integrity: sha512-uBSvAC8qH81TsXsWYD20ME4qg9ICflMLjsNfeuSxrKkJkym4Riqne1BrCCW15lcM/t9lfEiz4FJbVeUoaOVGWA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ dependencies:
+ '@babel/core': 7.23.5
+ dev: true
+
/babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.5):
resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
peerDependencies:
@@ -14517,6 +14564,10 @@ packages:
engines: {node: '>=6'}
dev: true
+ /builtin-status-codes@2.0.0:
+ resolution: {integrity: sha512-8KPx+JfZWi0K8L5sycIOA6/ZFZbaFKXDeUIXaqwUnhed1Ge1cB0wyq+bNDjKnL9AR2Uj3m/khkF6CDolsyMitA==}
+ dev: false
+
/bytes-iec@3.1.1:
resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==}
engines: {node: '>= 0.8'}
@@ -14554,6 +14605,11 @@ packages:
pascal-case: 3.1.2
tslib: 2.5.0
+ /camelcase@1.2.1:
+ resolution: {integrity: sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
@@ -18780,6 +18836,7 @@ packages:
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ requiresBuild: true
/js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
@@ -19281,6 +19338,7 @@ packages:
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ requiresBuild: true
dependencies:
js-tokens: 4.0.0
@@ -21409,6 +21467,7 @@ packages:
/react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
+ requiresBuild: true
peerDependencies:
react: ^18.2.0
dependencies:
@@ -21842,6 +21901,7 @@ packages:
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
+ requiresBuild: true
dependencies:
loose-envify: 1.4.0
@@ -22019,7 +22079,7 @@ packages:
/regenerator-transform@0.15.2:
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
dependencies:
- '@babel/runtime': 7.23.5
+ '@babel/runtime': 7.23.8
/regexp.prototype.flags@1.5.1:
resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
@@ -23923,6 +23983,12 @@ packages:
dependencies:
tslib: 2.5.0
+ /uppercamelcase@1.1.0:
+ resolution: {integrity: sha512-C7YEMvhgrvTEKEEVqA7LXNID/1TvvIwYZqNIKLquS6y/MGSkRQAav9LnTTILlC1RqUM8eTVBOe1U/fnB652PRA==}
+ dependencies:
+ camelcase: 1.2.1
+ dev: false
+
/uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
@@ -24490,11 +24556,28 @@ packages:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: true
+ /wp-error@1.3.0:
+ resolution: {integrity: sha512-6Mn8fIBgWYgKJveRpB5oR+T9JEXxUawq5Om35ZE0yvCh5p3SQ+2OCH+KH39k0ZMxvNh9CI7LyfihtQH6itHbdQ==}
+ dependencies:
+ builtin-status-codes: 2.0.0
+ uppercamelcase: 1.1.0
+ dev: false
+
/wp-prettier@3.0.3:
resolution: {integrity: sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==}
engines: {node: '>=14'}
hasBin: true
+ /wpcom-proxy-request@7.0.5:
+ resolution: {integrity: sha512-IJ0AYAxA6zOaIt8mD3aKDcaTQrOQiaV41/AKU42fL+GkH2chV7MGYf02hQrtMifLAjdAg0Xwm7IlGCfv+cy9XQ==}
+ dependencies:
+ debug: 4.3.4
+ uuid: 8.3.2
+ wp-error: 1.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/wrap-ansi@3.0.1:
resolution: {integrity: sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==}
engines: {node: '>=4'}
diff --git a/projects/packages/jetpack-mu-wpcom/.eslintrc.js b/projects/packages/jetpack-mu-wpcom/.eslintrc.js
index f7f2667eff363..6fa5bb2795b96 100644
--- a/projects/packages/jetpack-mu-wpcom/.eslintrc.js
+++ b/projects/packages/jetpack-mu-wpcom/.eslintrc.js
@@ -14,5 +14,10 @@ module.exports = {
allowedTextDomain: 'jetpack-mu-wpcom',
},
],
+ 'testing-library/prefer-screen-queries': 'off',
+ 'react/jsx-no-bind': 'off',
+ // Not needed for TypeScript.
+ 'jsdoc/require-param-type': 'off',
+ 'jsdoc/require-returns-type': 'off',
},
};
diff --git a/projects/packages/jetpack-mu-wpcom/changelog/add-verbum-comment-into-jetpack-mu-wpcom b/projects/packages/jetpack-mu-wpcom/changelog/add-verbum-comment-into-jetpack-mu-wpcom
new file mode 100644
index 0000000000000..ba2b7abb8bfe0
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/changelog/add-verbum-comment-into-jetpack-mu-wpcom
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Add Verbum Comments in jetpack-mu-wpcom plugin
diff --git a/projects/packages/jetpack-mu-wpcom/composer.json b/projects/packages/jetpack-mu-wpcom/composer.json
index a51d5caed9ed9..91ad6babc4e09 100644
--- a/projects/packages/jetpack-mu-wpcom/composer.json
+++ b/projects/packages/jetpack-mu-wpcom/composer.json
@@ -49,7 +49,7 @@
},
"autotagger": true,
"branch-alias": {
- "dev-trunk": "5.9.x-dev"
+ "dev-trunk": "5.10.x-dev"
},
"textdomain": "jetpack-mu-wpcom",
"version-constants": {
diff --git a/projects/packages/jetpack-mu-wpcom/package.json b/projects/packages/jetpack-mu-wpcom/package.json
index f0fb3c0fc04d9..bbc2b5d486fe6 100644
--- a/projects/packages/jetpack-mu-wpcom/package.json
+++ b/projects/packages/jetpack-mu-wpcom/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@automattic/jetpack-mu-wpcom",
- "version": "5.9.0",
+ "version": "5.10.0-alpha",
"description": "Enhances your site with features powered by WordPress.com",
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/jetpack-mu-wpcom/#readme",
"bugs": {
@@ -19,6 +19,7 @@
"build-js": "pnpm clean && webpack",
"build-production": "echo 'Not implemented.'",
"build-production-js": "NODE_ENV=production BABEL_ENV=production pnpm build-js",
+ "lint": "eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx .",
"clean": "rm -rf src/build/"
},
"optionalDependencies": {
@@ -28,7 +29,13 @@
"devDependencies": {
"@automattic/jetpack-webpack-config": "workspace:*",
"@babel/core": "7.23.5",
+ "@babel/plugin-transform-react-jsx": "7.23.4",
"@babel/preset-react": "7.23.3",
+ "@types/node": "^20.4.2",
+ "@types/react": "^18.2.28",
+ "@types/react-dom": "18.2.0",
+ "babel-plugin-transform-rename-properties": "0.1.0",
+ "copy-webpack-plugin": "11.0.0",
"sass": "1.64.1",
"sass-loader": "12.4.0",
"typescript": "^5.0.4",
@@ -37,15 +44,20 @@
},
"dependencies": {
"@automattic/typography": "1.0.0",
+ "@preact/signals": "^1.2.2",
"@sentry/browser": "7.80.1",
"@wordpress/api-fetch": "6.45.0",
"@wordpress/base-styles": "4.39.0",
"@wordpress/components": "25.14.0",
- "@wordpress/dom-ready": "^3.48.0",
"@wordpress/data": "9.18.0",
+ "@wordpress/dom-ready": "^3.48.0",
"@wordpress/hooks": "3.48.0",
"@wordpress/i18n": "4.48.0",
"@wordpress/plugins": "6.16.0",
- "@wordpress/url": "3.49.0"
+ "@wordpress/url": "3.49.0",
+ "preact": "^10.13.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "wpcom-proxy-request": "^7.0.3"
}
}
diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php
index 5dfcdd7ea8cdc..a21279c60964f 100644
--- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php
+++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php
@@ -13,7 +13,7 @@
* Jetpack_Mu_Wpcom main class.
*/
class Jetpack_Mu_Wpcom {
- const PACKAGE_VERSION = '5.9.0';
+ const PACKAGE_VERSION = '5.10.0-alpha';
const PKG_DIR = __DIR__ . '/../';
const BASE_DIR = __DIR__ . '/';
const BASE_FILE = __FILE__;
@@ -42,6 +42,8 @@ public static function init() {
add_action( 'plugins_loaded', array( __CLASS__, 'load_first_posts_stream_helpers' ) );
+ add_action( 'plugins_loaded', array( __CLASS__, 'load_verbum_comments' ) );
+
// Unified navigation fix for changes in WordPress 6.2.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'unbind_focusout_on_wp_admin_bar_menu_toggle' ) );
@@ -218,4 +220,42 @@ public static function load_marketplace_products_updater() {
public static function load_first_posts_stream_helpers() {
require_once __DIR__ . '/features/first-posts-stream/first-posts-stream-helpers.php';
}
+
+ /**
+ * Determine whether to disable the comment experience.
+ *
+ * @param int $blog_id The blog ID.
+ * @return boolean
+ */
+ private function should_disable_comment_experience( $blog_id ) {
+ require_once WP_CONTENT_DIR . '/lib/wpforteams/functions.php';
+ // This covers both P2 and P2020 themes.
+ $is_p2 = str_contains( get_stylesheet(), 'pub/p2' ) || function_exists( '\WPForTeams\is_wpforteams_site' ) && is_wpforteams_site( $blog_id );
+ $is_forums = str_contains( get_stylesheet(), 'a8c/supportforums' ); // Not in /forums
+
+ // Don't load any comment experience in the Reader, GlotPress, wp-admin, or P2.
+ return ( 1 === $blog_id || TRANSLATE_BLOG_ID === $blog_id || is_admin() || $is_p2 || $is_forums );
+ }
+
+ /**
+ * Load Verbum Comments.
+ */
+ public static function load_verbum_comments() {
+ if ( class_exists( 'Verbum_Comments' ) ) {
+ return;
+ } else {
+ $blog_id = get_current_blog_id();
+ // Jetpack loads Verbum though an iframe from jetpack.wordpress.com.
+ // So we need to check the GET request for the blogid.
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['blogid'] ) ) {
+ $blog_id = intval( $_GET['blogid'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
+ if ( should_disable_comment_experience( $blog_id ) ) {
+ return false;
+ }
+ require_once __DIR__ . '/build/verbum-comments/class-verbum-comments.php';
+ new \Automattic\Jetpack\Verbum_Comments();
+ }
+ }
}
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/README.md b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/README.md
new file mode 100644
index 0000000000000..b731ce4d71543
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/README.md
@@ -0,0 +1,135 @@
+# Verbum Comments
+
+A word, discourse, or reason; connoting an appeal to rational discourse.
+
+## Description
+
+Verbum is the comment UX for WordPress.com and Jetpack and is the successor to [Highlander Comments](../highlander-comments/highlander-comments.php). It is built with [Preact](https://preactjs.com/) and uses [Vite](https://vitejs.dev/) for bundling. This was chosen to limit the size of the bundle and minimize impact on the page performance metrics.
+
+## Technical Details
+
+### Basics
+
+Page performance impact was highly considered in this plugin. For that reason Preact was chosen to minimize the bundle size as the basic ReactDOM modules needed for rendering were very large. Preact is best utilized with Vite which compiles and bundles everything into the `dist` directory.
+
+From a developer experience it functions the same as React with Webpack. You can use all the typical npm scripts like `start`, `build`, and `lint`.
+
+Because we are using a Preact based plugin to manage the user experience, we overwrite and remove any of the basic WordPress comment section hooks. We remove everything that is not needed and output our plugin in place of the submit button. This also means that we do not utilize any of the default settings for the comment section that would allow others to overwrite them. These are typically used to change the wording on buttons and headings or inject your own custom components. If someone were to selfishly overwrite the submit button it would prevent the entire comment form from loading.
+
+With the introduction of block based themes and the site editor, not all themes come with the comment section added to the single-post template. If the template does not add the comment form they will need to add the block in order for Verbum to load.
+
+### Dynamic Loading
+
+Verbum does not load any scripts until the comment section is visible on the screen. This is done from [dynamic-loader](./dynamic-loader.js) using `IntersectionObserver` and [`WP_Enqueue_Dynamic_Script`](../wp-enqueue-dynamic-script.php). In the index.php the main script (dist/index.js) for the plugin is registered and dynamically enqueued rather than enqueuing as normal. Then when we decide we are ready to load we call the `loadScript()` method to inject and run the script.
+
+### Handling Login
+
+When the user chooses to log in via WordPress.com or Facebook, a separate pop-up is opened to the remote login url (r-login.wordpress.com). After the user succeeds in authenticating a `wpc_` cookie is added and the window is closed. Before the login, a nonce token is created for posting a comment. Once the user successfully logs in, that nonce is no longer valid as well as the logout URL which includes a nonce. To capture this new nonce, there is a tiny bit of JS that exists in `public.api/connect/index.php` that updates the nonce in the hidden input field and sets the new logout URL in the VerbumComments object.
+
+### Jetpack
+
+Currently when a user has enhanced comments enabled on Jetpack they will also get Verbum Comments on their site. Rather than injecting the plugin, Jetpack adds an iframe to `jetpack.wordpress.com` where the plugin is loaded and handles everything from there. Because of this we cannot use typical functions like `get_current_blog_id()` because in Jetpack we will retrieve the details for `jetpack.wordpress.com`. Jetpack passes all the relevant data through the GET request so it can be retrieved from there if needed.
+
+Most of the Jetpack logic can be found in `wp-content/mu-plugins/jetpack/class.jetpack-renderer.php`
+
+### Managing State
+Our global state is defined in state.jsx. Whenever we need to define new state it should be done there, with the appropriate comment to describe it. We use preact [signal api](https://preactjs.com/guide/v10/signals/) to do this.
+
+You can define a signal with the `signal()` function which takes the default value of the state as argument. To access it, import it from the state file and use `signal.value` in your components. This creates a subscription which will automatically update your components when the signal's value has changed.
+
+When a signal's value depends on other pieces of the global state you can use the `computed` function.
+```js
+const todos = signal([
+ { text: "Buy groceries", completed: true },
+ { text: "Walk the dog", completed: false },
+]);
+
+// create a signal computed from other signals
+const completed = computed(() => {
+ // When `todos` changes, this re-runs automatically:
+ return todos.value.filter(todo => todo.completed).length;
+});
+
+```
+To run arbitrary code in response to signal changes, we can use effect(fn). Similar to computed signals, effects track which signals are accessed and re-run their callback when those signals change.
+```js
+const name = signal("Jane");
+const surname = signal("Doe");
+const fullName = computed(() => `${name.value} ${surname.value}`);
+
+// Logs name every time it changes:
+effect(() => console.log(fullName.value));
+// Logs: "Jane Doe"
+
+// Updating `name` updates `fullName`, which triggers the effect again:
+name.value = "John";
+// Logs: "John Doe"
+```
+You can destroy an effect and unsubscribe from all signals it accessed by calling the returned function.
+```js
+const dispose = effect(() => console.log(fullName.value));
+// Logs: "Jane Doe"
+
+// Destroy effect and subscriptions:
+dispose();
+
+```
+In case you want to use the signal's value without subscribing to it, use signal.peek().
+```js
+const delta = signal(0);
+
+const count = signal(0);
+
+effect(() => {
+// Update `count` without subscribing to `count`:
+count.value = count.peek() + delta.value;
+});
+```
+
+## Development
+
+Verbum is built and managed from this repository, but is then deployed to WPCOM inside `wp-content/mu-plugins/verbum`. To keep these both in sync it is important to deploy your changes to WPCOM after merging any changes. Here is the workflow you can use.
+
+### Commands
+
+**In order for Verbum to sync properly to your sandbox you need to have `wpcom` set as a host in your SSH config pointed to your sandbox**
+
+* `npm run build` - Build Verbum.
+* `npm run build:sync` - Build Verbum and sync to sandbox.
+* `npm run start` - Build and watch Verbum.
+* `npm run start:sync` - Build and watch Verbum, while syncing to sandbox.
+* `npm run lint` - Look for lint issues.
+* `npm run lint:fix` - Look for lint issues and fix easily fixable issues.
+* `npm run build:editor` - Build the editor app inside `/editor`, necessary when changing the editor files.
+
+### After Merge - Deploy process
+
+Since this is not deployed to WPCOM automatically you need to manually deploy your changes with the following steps:
+
+1. `git checkout trunk && git reset --hard && git clean -fd` - After your merge make sure you have the freshest version on trunk.
+2. `git checkout trunk && git reset --hard && git clean -fd` - Log in to your sandbox and do the same thing.
+3. `npm run build:editor && npm run build:sync` - From Verbum run the sync command to move the latest version of Verbum to your sandbox. You can also use only `npm run build:sync` for non editor changes (it will be faster for Intel CPUs)
+4. `arc diff --only` - From your sandbox create a new diff for WordPress.com
+5. Your changes should have already been tested on WordPress.com before merging but check for smoke and failing test.
+6. If there are any LINT issues or errors when deploying please be sure to update the REPO as well to avoid future breaking.
+7. `arc land` - Land your changes
+8. `deploy wpcom` - Deploy and done!
+
+### Testing
+
+#### Setup
+
+1. Please sandbox the following sites before running the tests
+ - jetpack.wordpress.com
+ - e2esiteopencommentstoeveryone.wordpress.com
+ - e2ecommentauthormustfilloutnameandemail.wordpress.com
+ - e2eusersmustberegisteredandloggedintocomment.wordpress.com
+
+2. Run `npx playwright install` to install the browser.
+3. Run `npm i` to install the pre-push git hook.
+
+Now the tests will run on every push.
+
+The tests live in /tests folder. To run them, you can run `npm run e2e-tests`.
+
+If you want to watch the tests unfold, you can run `npx playwright test --ui`.
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php
new file mode 100644
index 0000000000000..57ab457e6f691
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php
@@ -0,0 +1,130 @@
+ 'core/quote',
+ 'h1' => 'core/heading',
+ 'h2' => 'core/heading',
+ 'h3' => 'core/heading',
+ 'img' => 'core/image',
+ 'ul' => 'core/list',
+ 'ol' => 'core/list',
+ 'pre' => 'core/code',
+ );
+
+ foreach ( array_keys( $allowedtags ) as $tag ) {
+ if ( isset( $convert[ $tag ] ) ) {
+ $allowed_blocks[] = $convert[ $tag ];
+ }
+ }
+
+ return $allowed_blocks;
+ }
+}
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-wpcom-rest-api-v2-verbum-auth.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-wpcom-rest-api-v2-verbum-auth.php
new file mode 100644
index 0000000000000..1c85e6c9f5f59
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-wpcom-rest-api-v2-verbum-auth.php
@@ -0,0 +1,80 @@
+namespace = 'wpcom/v2';
+ $this->rest_base = '/verbum/auth';
+ $this->wpcom_is_wpcom_only_endpoint = false;
+ $this->wpcom_is_site_specific_endpoint = false;
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Register the routes for the objects of the controller.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base,
+ array(
+ 'show_in_index' => false,
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_auth' ),
+ 'permission_callback' => '__return_true',
+ )
+ );
+ }
+
+ /**
+ * Authorize user based on their WordPress credentials or Facebook cookies.
+ *
+ * @return array|WP_Error
+ */
+ public function get_auth() {
+ $user = wp_get_current_user();
+ if ( $user->ID ) {
+ list( $wordpress_avatar_url ) = wpcom_get_avatar_url( $user->user_email, 60, '', true );
+ return array(
+ 'account' => $user->user_login,
+ 'avatar' => $wordpress_avatar_url,
+ 'email' => $user->user_email,
+ 'link' => ( ! empty( $user->user_url ) ? esc_url_raw( $user->user_url ) : esc_url_raw( 'http://gravatar.com/' . $user->user_login ) ),
+ 'name' => ( ! empty( $user->display_name ) ? $user->display_name : $user->user_login ),
+ 'uid' => $user->ID,
+ 'service' => 'wordpress',
+ );
+ } else {
+ $fb = \Automattic\Jetpack\Verbum_Comments::verify_facebook_identity();
+ if ( ! is_wp_error( $fb ) ) {
+ return array(
+ 'account' => $fb->name,
+ 'avatar' => $fb->picture->data->url,
+ 'email' => $fb->email,
+ 'link' => esc_url_raw( 'http://gravatar.com/' . $fb->email ),
+ 'name' => $fb->name,
+ 'uid' => $user->id,
+ 'service' => 'facebook',
+ );
+ }
+ }
+ return new \WP_Error( '403', 'Not Authorized', array( 'status' => 403 ) );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Verbum_Auth' );
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-wpcom-rest-api-v2-verbum-oembed.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-wpcom-rest-api-v2-verbum-oembed.php
new file mode 100644
index 0000000000000..44a9ac0423e19
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-wpcom-rest-api-v2-verbum-oembed.php
@@ -0,0 +1,77 @@
+namespace = 'wpcom/v2';
+ $this->rest_base = '/verbum/embed';
+ $this->wpcom_is_wpcom_only_endpoint = false;
+ $this->wpcom_is_site_specific_endpoint = false;
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Register the routes for the objects of the controller.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base,
+ array(
+ 'show_in_index' => false,
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_embed_data' ),
+ 'permission_callback' => array( $this, 'permission_callback' ),
+ )
+ );
+ }
+
+ /**
+ * Check if the user is authenticated.
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return bool
+ */
+ public function permission_callback( WP_REST_Request $request ) {
+ $nonce = $request->get_param( 'embed_nonce' );
+
+ return wp_verify_nonce( $nonce, 'embed_nonce' );
+ }
+
+ /**
+ * Get the embed data for the embed block.
+ *
+ * @param WP_REST_Request $request The request object.
+ * @return array|\WP_Error
+ */
+ public function get_embed_data( WP_REST_Request $request ) {
+ $url = sanitize_url( $request->get_param( 'embed_url' ) );
+ $instance = new WP_oEmbed();
+ $embed_data = $instance->get_data( $url, array() );
+
+ // Return error if the embed data is empty.
+ // This matches the core response.
+ if ( false === $embed_data ) {
+ return new \WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) );
+ }
+
+ return $embed_data;
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Verbum_oEmbed' );
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/dynamic-loader.js b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/dynamic-loader.js
new file mode 100644
index 0000000000000..4f123fd34001a
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/dynamic-loader.js
@@ -0,0 +1,24 @@
+/* global WP_Enqueue_Dynamic_Script VerbumComments */
+window.addEventListener( 'DOMContentLoaded', function () {
+ // Lazy load the comment form when clicking the comment field
+ const commentForm = document.querySelector( '#commentform' );
+ if ( commentForm ) {
+ // only load Verbum if the comment field is visible or the browser doesn't support IntersectionObserver
+ if ( window.IntersectionObserver ) {
+ new IntersectionObserver( function ( entries ) {
+ if ( entries.some( el => el.isIntersecting ) ) {
+ const startedLoadingAt = Date.now();
+
+ WP_Enqueue_Dynamic_Script.loadScript( 'verbum' ).then( () => {
+ const finishedLoadingAt = Date.now();
+
+ VerbumComments.fullyLoadedTime = finishedLoadingAt - startedLoadingAt;
+ } );
+ this.disconnect();
+ }
+ } ).observe( commentForm );
+ } else {
+ WP_Enqueue_Dynamic_Script.loadScript( 'verbum' );
+ }
+ }
+} );
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php
new file mode 100644
index 0000000000000..e335e0cfdb39d
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php
@@ -0,0 +1,594 @@
+blog_id = get_current_blog_id();
+
+ // Jetpack loads the app via an iframe, so we need to get the blog id from the query string.
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['blogid'] ) ) {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $this->blog_id = intval( $_GET['blogid'] );
+ }
+
+ // Selfishly remove everything from the existing comment form
+ add_filter( 'comment_form_field_comment', '__return_false', 11 );
+ add_filter( 'comment_form_logged_in', '__return_empty_string' );
+ add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ), 20 );
+ remove_action( 'comment_form', 'subscription_comment_form' );
+ remove_all_filters( 'comment_form_default_fields' );
+ add_filter( 'comment_form_default_fields', array( $this, 'comment_form_default_fields' ) );
+ add_action( 'clear_auth_cookie', array( $this, 'clear_fb_cookies' ) );
+
+ // Fix comment reply link when `comment_registration` is required.
+ add_filter( 'comment_reply_link', array( $this, 'comment_reply_link' ), 10, 4 );
+
+ // Add Verbum.
+ add_action( 'comment_form_must_log_in_after', array( $this, 'verbum_render_element' ) );
+ add_filter( 'comment_form_submit_field', array( $this, 'verbum_render_element' ) );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
+
+ // Do things before the comment is accepted.
+ add_action( 'pre_comment_on_post', array( $this, 'check_comment_allowed' ) );
+ add_action( 'pre_comment_on_post', array( $this, 'allow_logged_out_user_to_comment_as_external' ), 100 ); // Set priority high to run after check to make sure they are logged in to the external service.
+ add_filter( 'preprocess_comment', array( $this, 'verify_external_account' ), 0 );
+
+ // After the comment is saved, we add meta data to the comment.
+ add_action( 'comment_post', array( $this, 'add_verbum_meta_data' ) );
+
+ // Load the Gutenberg editor for comments.
+ if (
+ $this->should_load_gutenberg_comments()
+ ) {
+ new \Verbum_Gutenberg_Editor();
+ }
+ }
+
+ /**
+ * Load the div where Verbum app is rendered.
+ */
+ public function verbum_render_element() {
+ $color_scheme = get_blog_option( $this->blog_id, 'jetpack_comment_form_color_scheme' );
+ $comment_url = site_url( '/wp-comments-post.php' );
+
+ if ( ! $color_scheme || '' === $color_scheme ) {
+ // Default to transparent because it is more adaptable than white or dark.
+ $color_scheme = 'transparent';
+ }
+
+ $verbum = '
' . $this->hidden_fields();
+
+ // If the blog requires login, Verbum need to be wrapped in a ";
+ } else {
+ return $verbum;
+ }
+ }
+
+ /**
+ * Enqueue Assets
+ */
+ public function enqueue_assets() {
+ if (
+ ! ( is_singular() && comments_open() )
+ && ! ( is_front_page() && is_page() && comments_open() )
+ ) {
+ return;
+ }
+
+ $version_js = filemtime( __DIR__ . '/verbum-comments.js' );
+ $version_css = filemtime( __DIR__ . '/verbum-comments.css' );
+ $connect_url = site_url( '/public.api/connect/?action=request' );
+ $primary_redirect = get_primary_redirect();
+
+ if ( strpos( $primary_redirect, '.wordpress.com' ) === false ) {
+ $connect_url = add_query_arg( 'domain', $primary_redirect, $connect_url );
+ }
+
+ // Enqueue styles
+ wp_enqueue_style( 'verbum', plugins_url( '/verbum-comments.css', __FILE__ ), array(), $version_css );
+
+ // Enqueue scripts
+ wp_register_script(
+ 'verbum',
+ plugins_url( 'verbum-comments.js', __FILE__ ),
+ array(),
+ $version_js,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ \WP_Enqueue_Dynamic_Script::enqueue_script( 'verbum' );
+
+ // Enqueue settings separately since the main script is dynamic.
+ // We need the VerbumComments object to be available before the main script is loaded.
+ wp_register_script(
+ 'verbum-settings',
+ false,
+ array(),
+ $version_js,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+
+ $blog_details = get_blog_details( $this->blog_id );
+ $is_blog_atomic = is_blog_atomic( $blog_details );
+ $is_blog_jetpack = is_blog_jetpack( $blog_details );
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $subscribe_to_blog = isset( $_GET['stb_enabled'] ) ? boolval( $_GET['stb_enabled'] ) : false;
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $subscribe_to_comment = isset( $_GET['stc_enabled'] ) ? boolval( $_GET['stc_enabled'] ) : false;
+
+ // If it is simple, we set it to true. Simple sites return inconsistent results.
+ if ( ! $is_blog_atomic && ! $is_blog_jetpack ) {
+ $subscribe_to_blog = true;
+ $subscribe_to_comment = true;
+ }
+
+ // Jetpack Comments client side logged in user data
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $__get = stripslashes_deep( $_GET );
+ $email_hash = isset( $__get['hc_useremail'] ) && is_string( $__get['hc_useremail'] ) ? $__get['hc_useremail'] : '';
+ $jetpack_username = isset( $__get['hc_username'] ) && is_string( $__get['hc_username'] ) ? $__get['hc_username'] : '';
+ $jetpack_user_id = isset( $__get['hc_userid'] ) && is_numeric( $__get['hc_userid'] ) ? (int) $__get['hc_userid'] : 0;
+ $jetpack_signature = isset( $__get['sig'] ) && is_string( $__get['sig'] ) ? $__get['sig'] : '';
+ list( $jetpack_avatar ) = wpcom_get_avatar_url( "$email_hash@md5.gravatar.com" );
+ $comment_registration_enabled = boolval( get_blog_option( $this->blog_id, 'comment_registration' ) );
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : get_queried_object_id();
+ $locale = get_locale();
+
+ $vbe_cache_buster = filemtime( ABSPATH . '/widgets.wp.com/verbum-block-editor/build_meta.json' );
+
+ wp_add_inline_script(
+ 'verbum-settings',
+ 'window.VerbumComments = ' . wp_json_encode(
+ array(
+ 'Log in or provide your name and email to leave a reply.' => __( 'Log in or provide your name and email to leave a reply.', 'jetpack-mu-wpcom' ),
+ 'Receive web and mobile notifications for posts on this site.' => __( 'Receive web and mobile notifications for posts on this site.', 'jetpack-mu-wpcom' ),
+ 'Name' => __( 'Name', 'jetpack-mu-wpcom' ),
+ 'Email (address never made public)' => __( 'Email (address never made public)', 'jetpack-mu-wpcom' ),
+ 'Website (optional)' => __( 'Website (optional)', 'jetpack-mu-wpcom' ),
+ 'Leave a reply. (log in optional)' => __( 'Leave a reply. (log in optional)', 'jetpack-mu-wpcom' ),
+ 'Log in to leave a reply.' => __( 'Log in to leave a reply.', 'jetpack-mu-wpcom' ),
+ /* translators: %s is the name of the provider (WordPress, Facebook, Twitter) */
+ 'Logged in via %s' => __( 'Logged in via %s', 'jetpack-mu-wpcom' ),
+ 'Log out' => __( 'Log out', 'jetpack-mu-wpcom' ),
+ 'Email' => __( 'Email', 'jetpack-mu-wpcom' ),
+ '(Address never made public)' => __( '(Address never made public)', 'jetpack-mu-wpcom'), // phpcs:ignore PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket
+ 'Instantly' => __( 'Instantly', 'jetpack-mu-wpcom' ),
+ 'Daily' => __( 'Daily', 'jetpack-mu-wpcom' ),
+ 'Reply' => __( 'Reply', 'jetpack-mu-wpcom' ),
+ 'WordPress' => __( 'WordPress', 'jetpack-mu-wpcom' ),
+ 'Weekly' => __( 'Weekly', 'jetpack-mu-wpcom' ),
+ 'Notify me of new posts' => __( 'Notify me of new posts', 'jetpack-mu-wpcom' ),
+ 'Email me new posts' => __( 'Email me new posts', 'jetpack-mu-wpcom' ),
+ 'Email me new comments' => __( 'Email me new comments', 'jetpack-mu-wpcom' ),
+ 'Cancel' => __( 'Cancel', 'jetpack-mu-wpcom' ),
+ 'Write a comment...' => __( 'Write a Comment...', 'jetpack-mu-wpcom' ),
+ 'Website' => __( 'Website', 'jetpack-mu-wpcom' ),
+ 'Optional' => __( 'Optional', 'jetpack-mu-wpcom' ),
+ /* translators: Success message of a modal when user subscribes */
+ 'We\'ll keep you in the loop!' => __( 'We\'ll keep you in the loop!', 'jetpack-mu-wpcom' ),
+ 'Loading your comment...' => __( 'Loading your comment...', 'jetpack-mu-wpcom' ),
+ /* translators: %s is the name of the site */
+ 'Discover more from' => sprintf( __( 'Discover more from %s', 'jetpack-mu-wpcom' ), get_bloginfo( 'name' ) ),
+ 'Subscribe now to keep reading and get access to the full archive.' => __( 'Subscribe now to keep reading and get access to the full archive.', 'jetpack-mu-wpcom' ),
+ 'Continue reading' => __( 'Continue reading', 'jetpack-mu-wpcom' ),
+ 'Never miss a beat!' => __( 'Never miss a beat!', 'jetpack-mu-wpcom' ),
+ 'Interested in getting blog post updates? Simply click the button below to stay in the loop!' => __( 'Interested in getting blog post updates? Simply click the button below to stay in the loop!', 'jetpack-mu-wpcom' ),
+ 'Enter your email address' => __( 'Enter your email address', 'jetpack-mu-wpcom' ),
+ 'Subscribe' => __( 'Subscribe', 'jetpack-mu-wpcom' ),
+ 'Comment sent successfully' => __( 'Comment sent successfully', 'jetpack-mu-wpcom' ),
+ 'Save my name, email, and website in this browser for the next time I comment.' => __( 'Save my name, email, and website in this browser for the next time I comment.', 'jetpack-mu-wpcom' ),
+ 'siteId' => $this->blog_id,
+ 'postId' => $post_id,
+ 'mustLogIn' => $comment_registration_enabled && ! is_user_logged_in(),
+ 'requireNameEmail' => boolval( get_blog_option( $this->blog_id, 'require_name_email' ) ),
+ 'commentRegistration' => $comment_registration_enabled,
+ 'connectURL' => $connect_url,
+ 'logoutURL' => html_entity_decode( wp_logout_url(), ENT_COMPAT ),
+ 'homeURL' => home_url( '/' ),
+ 'subscribeToBlog' => $subscribe_to_blog,
+ 'subscribeToComment' => $subscribe_to_comment,
+ 'isJetpackCommentsLoggedIn' => is_jetpack_comments() && is_jetpack_comments_user_logged_in(),
+ 'jetpackUsername' => $jetpack_username,
+ 'jetpackUserId' => $jetpack_user_id,
+ 'jetpackSignature' => $jetpack_signature,
+ 'jetpackAvatar' => $jetpack_avatar,
+ 'enableBlocks' => boolval( $this->should_load_gutenberg_comments() ),
+ 'enableSubscriptionModal' => boolval( $this->should_show_subscription_modal() ),
+ 'currentLocale' => $locale,
+ 'isJetpackComments' => is_jetpack_comments(),
+ 'allowedBlocks' => \Verbum_Gutenberg_Editor::get_allowed_blocks(),
+ 'embedNonce' => wp_create_nonce( 'embed_nonce' ),
+ 'verbumBundleUrl' => plugins_url( 'dist/index.js', __FILE__ ),
+ 'isRTL' => is_rtl( $locale ),
+ 'vbeCacheBuster' => $vbe_cache_buster,
+ )
+ ),
+ 'before'
+ );
+
+ wp_enqueue_script( 'verbum-settings' );
+
+ wp_enqueue_script(
+ 'verbum-dynamic-loader',
+ plugins_url( 'assets/dynamic-loader.js', __FILE__ ),
+ array(),
+ $version_js,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ }
+
+ /**
+ * Remove some of the default comment_form args because they are not needed.
+ *
+ * @param array $args - The default comment form arguments.
+ */
+ public function comment_form_defaults( $args ) {
+ return array_merge(
+ $args,
+ array(
+ 'comment_field' => '',
+ 'must_log_in' => '',
+ 'logged_in_as' => '',
+ 'comment_notes_before' => '',
+ 'comment_notes_after' => '',
+ 'title_reply' => __( 'Leave a Reply', 'jetpack-mu-wpcom' ),
+ /* translators: % is the original posters name */
+ 'title_reply_to' => __( 'Leave a Reply to %s', 'jetpack-mu-wpcom' ),
+ 'cancel_reply_link' => __( 'Cancel reply', 'jetpack-mu-wpcom' ),
+ )
+ );
+ }
+
+ /**
+ * Set comment reply link.
+ * This is to fix the reply link when comment registration is required.
+ *
+ * @param string $reply_link - HTML for reply link.
+ * @param array $args - Default options for reply link.
+ * @param object $comment - Comment being replied to.
+ * @param object $post - PostID or WP_Post object comment is going to be displayed on.
+ */
+ public function comment_reply_link( $reply_link, $args, $comment, $post ) {
+ // This is only necessary if comment_registration is required to post comments
+ if ( ! get_option( 'comment_registration' ) ) {
+ return $reply_link;
+ }
+
+ $comment = get_comment( $comment );
+ $respond_id = esc_attr( $args['respond_id'] );
+ $add_below = esc_attr( $args['add_below'] );
+ /* This is to accommodate some themes that add an SVG to the Reply link like twenty-seventeen. */
+ $reply_text = wp_kses(
+ $args['reply_text'],
+ array(
+ 'svg' => array(
+ 'class' => true,
+ 'aria-hidden' => true,
+ 'aria-labelledby' => true,
+ 'role' => true,
+ 'xmlns' => true,
+ 'width' => true,
+ 'height' => true,
+ 'viewbox' => true,
+ ),
+ 'use' => array(
+ 'href' => true,
+ 'xlink:href' => true,
+ ),
+ )
+ );
+ $before_link = wp_kses( $args['before'], wp_kses_allowed_html( 'post' ) );
+ $after_link = wp_kses( $args['after'], wp_kses_allowed_html( 'post' ) );
+
+ $reply_url = esc_url( add_query_arg( 'replytocom', $comment->comment_ID . '#' . $respond_id ) );
+
+ $link = <<$reply_text
+ $after_link
+HTML;
+
+ return $link;
+ }
+
+ /**
+ * Loop through all available fields and remove them.
+ *
+ * @param array $fields - Default comment fields.
+ * @return array $fields with no HTML.
+ */
+ public function comment_form_default_fields( $fields ) {
+ foreach ( $fields as $field => $html ) {
+ remove_all_filters( "comment_form_field_{$field}" );
+ add_filter( "comment_form_field_{$field}", '__return_false', 100 );
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Clear FB comments on logout. wp-login.php doesn't clear these by default.
+ *
+ * @return void
+ */
+ public function clear_fb_cookies() {
+ setcookie( 'wpc_fbc', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
+ }
+
+ /**
+ * Check Facebook token and return the user data.
+ */
+ public static function verify_facebook_identity() {
+ $data = isset( $_COOKIE['wpc_fbc'] ) ? wp_parse_args( sanitize_text_field( wp_unslash( $_COOKIE['wpc_fbc'] ) ) ) : array();
+
+ if ( empty( $data['access_token'] ) ) {
+ return new \WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
+ }
+
+ // Make a new request using the access token we were given.
+ $request = wp_remote_get( 'https://graph.facebook.com/v6.0/me?fields=name,email,picture,id&access_token=' . rawurlencode( $data['access_token'] ) );
+ if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
+ return new \WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
+ }
+
+ $body = wp_remote_retrieve_body( $request );
+ $json = json_decode( $body );
+
+ if ( ! $body || ! $json ) {
+ return new \WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
+ }
+
+ return $json;
+ }
+
+ /**
+ * Allows a logged out user to leave a comment as a facebook credentialed user.
+ * Overrides WordPress' core comment_registration option to treat the commenter as "registered" (verified) users.
+ */
+ public function allow_logged_out_user_to_comment_as_external() {
+ $service = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
+
+ if ( $service !== 'facebook' ) {
+ return;
+ }
+
+ add_filter( 'pre_option_comment_registration', '__return_zero' );
+ add_filter( 'pre_option_require_name_email', '__return_zero' );
+ }
+
+ /**
+ * Check if the comment is allowed by verifying the Facebook token.
+ *
+ * @param array $comment_data - The comment data.
+ * @return WP_Error|comment_data The comment data if the comment is allowed, or a WP_Error if not.
+ */
+ public function verify_external_account( $comment_data ) {
+ $service = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
+
+ if ( $service === 'facebook' ) {
+ $fb_comment_data = self::verify_facebook_identity();
+
+ if ( is_wp_error( $fb_comment_data ) ) {
+ wp_die( esc_html( $fb_comment_data->get_error_message() ) );
+ }
+
+ $comment_data['highlander'] = 'facebook';
+ }
+
+ return $comment_data;
+ }
+
+ /**
+ * Verify nonce before accepting comment.
+ *
+ * @return WP_Error|void
+ */
+ public function check_comment_allowed() {
+ // Don't check if we're using Jetpack Comments.
+ if ( is_jetpack_comments() ) {
+ return;
+ }
+
+ // Check for Highlander Nonce.
+ if (
+ isset( $_POST['highlander_comment_nonce'] ) &&
+ wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['highlander_comment_nonce'] ), 'highlander_comment' ) )
+ ) {
+ return;
+ }
+
+ return new \WP_Error( 'verbum', __( 'Error: please try commenting again.', 'jetpack-mu-wpcom' ) );
+ }
+
+ /**
+ * Add all our custom fields to the comment meta after it is saved.
+ *
+ * @param int $comment_id The comment ID.
+ */
+ public function add_verbum_meta_data( $comment_id ) {
+ $comment_meta = array();
+ // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
+ $allowed_subscription_modal_statuses = array( 'showed', 'hidden_is_blog_member', 'hidden_jetpack', 'hidden_disabled', 'hidden_cookies_disabled', 'hidden_subscribe_not_enabled', 'hidden_already_subscribed', 'hidden_views_limit' );
+ $hc_avatar = isset( $_POST['hc_avatar'] ) ? esc_url_raw( wp_unslash( $_POST['hc_avatar'] ) ) : '';
+ $hc_userid = isset( $_POST['hc_foreign_user_id'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_foreign_user_id'] ) ) : '';
+ $service = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : '';
+ $verbum_loaded_editor = isset( $_POST['verbum_loaded_editor'] ) ? sanitize_text_field( wp_unslash( $_POST['verbum_loaded_editor'] ) ) : '';
+ $verbum_subscription_modal_show = isset( $_POST['verbum_show_subscription_modal'] ) && in_array( $_POST['verbum_show_subscription_modal'], $allowed_subscription_modal_statuses, true ) ? sanitize_text_field( wp_unslash( $_POST['verbum_show_subscription_modal'] ) ) : '';
+ // phpcs:enable WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
+ $allowed_comments_sources = array( 'gutenberg', 'textarea', 'textarea-slow-connection' );
+ if ( in_array( $verbum_loaded_editor, $allowed_comments_sources, true ) ) {
+ bump_stats_extras( 'verbum-comment-editor', $verbum_loaded_editor );
+ }
+ if ( $verbum_subscription_modal_show ) {
+ bump_stats_extras( 'verbum-subscription-modal', $verbum_subscription_modal_show );
+ }
+ switch ( $service ) {
+ case 'facebook':
+ $comment_meta['hc_post_as'] = 'facebook';
+ $comment_meta['hc_avatar'] = $hc_avatar;
+ $comment_meta['hc_foreign_user_id'] = $hc_userid;
+
+ bump_stats_extras( 'verbum-comment-posted', 'facebook' );
+ break;
+
+ case 'wordpress': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
+ if ( 'wpcom' === wpcom_blog_site_id_label() ) {
+ do_action( 'highlander_wpcom_post_comment_bump_stat', $comment_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+ }
+ bump_stats_extras( 'verbum-comment-posted', 'wordpress' ); // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
+ break;
+
+ case 'jetpack':
+ if ( is_jetpack_comments() && is_jetpack_comments_user_logged_in() ) {
+ $comment_meta['hc_post_as'] = 'jetpack';
+ $comment_meta['hc_avatar'] = check_and_return_post_string( 'hc_avatar' );
+ $comment_meta['hc_foreign_user_id'] = check_and_return_post_string( 'hc_userid' );
+
+ bump_stats_extras( 'verbum-comment-posted', 'jetpack' );
+ } else {
+ jetpack_comments_die( 'JPC_HIGHLANDER_ADD_COMMENT_META' );
+ }
+
+ break;
+ default:
+ if ( is_user_logged_in() ) {
+ bump_stats_extras( 'verbum-comment-posted', 'guest-logged-in' );
+ } else {
+ bump_stats_extras( 'verbum-comment-posted', 'guest' );
+ }
+ break;
+ }
+
+ foreach ( $comment_meta as $key => $value ) {
+ add_comment_meta( $comment_id, $key, $value, true );
+ }
+ }
+
+ /**
+ * Get the hidden fields for the comment form.
+ */
+ public function hidden_fields() {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : get_queried_object_id();
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $is_current_user_subscribed = isset( $_GET['is_current_user_subscribed'] ) ? intval( $_GET['is_current_user_subscribed'] ) : 0;
+ $nonce = wp_create_nonce( 'highlander_comment' );
+ $hidden_fields = get_comment_id_fields( $post_id ) . '
+
+ ';
+
+ if ( is_jetpack_comments() ) {
+ $hidden_fields .= '
+
+
+ ';
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $jetpack_nonce = isset( $_GET['jetpack_comments_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack_comments_nonce'] ) ) : false;
+ if ( $jetpack_nonce ) {
+ $hidden_fields .= '';
+ }
+ }
+
+ return '
' . $hidden_fields . '
';
+ }
+
+ /***
+ * Check if we should load the Gutenberg comments.
+ *
+ * Block should be carefully loaded to avoid Forums, P2, etc.
+ */
+ public function should_load_gutenberg_comments() {
+ // Don't load when jetpack or atomic for now, it does not look cool on dark themes.
+ $is_jetpack_site = 522232 === get_current_blog_id();
+ if ( $is_jetpack_site ) {
+ return false;
+ }
+
+ $blog_id = verbum_get_blog_id();
+ $e2e_tests = has_blog_sticker( 'a8c-e2e-test-blog', $blog_id );
+ $has_blocks_flag = has_blog_sticker( 'verbum-block-comments', $blog_id );
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $gutenberg_query_param = isset( $_GET['verbum_gutenberg'] ) ? intval( $_GET['verbum_gutenberg'] ) : null;
+ // This will release to 10% of sites.
+ $blog_in_10_percent = $blog_id % 100 >= 90;
+ // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $is_proxied = isset( $_SERVER['A8C_PROXIED_REQUEST'] )
+ ? sanitize_text_field( wp_unslash( $_SERVER['A8C_PROXIED_REQUEST'] ) )
+ : defined( 'A8C_PROXIED_REQUEST' ) && A8C_PROXIED_REQUEST;
+
+ // Check if the parameter is set and its value is either 0 or 1, if any random value is passed, it is ignored.
+ if ( $gutenberg_query_param !== null ) {
+ return $gutenberg_query_param === 1;
+ }
+
+ return $has_blocks_flag || $e2e_tests || $blog_in_10_percent;
+ }
+
+ /**
+ * Check if we should show the subscription modal.
+ */
+ public function should_show_subscription_modal() {
+ $modal_enabled = get_option( 'jetpack_verbum_subscription_modal', true );
+ return ! is_user_member_of_blog( '', $this->blog_id ) && $modal_enabled;
+ }
+
+ /**
+ * Get the status of the subscription modal.
+ */
+ public function subscription_modal_status() {
+ if ( is_user_member_of_blog( '', $this->blog_id ) ) {
+ return 'hidden_is_blog_member';
+ }
+ if ( is_jetpack_comments() ) {
+ return 'hidden_jetpack';
+ }
+ if ( ! get_option( 'jetpack_verbum_subscription_modal', true ) ) {
+ return 'hidden_disabled';
+ }
+ return '';
+ }
+}
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/playwright.config.ts b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/playwright.config.ts
new file mode 100644
index 0000000000000..039b3fe251970
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/playwright.config.ts
@@ -0,0 +1,81 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig( {
+ testDir: './tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !! process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ expect: {
+ timeout: 30000,
+ },
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://127.0.0.1:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ // Use FF because it respects the hosts file.
+ name: 'firefox',
+ use: { ...devices[ 'Desktop Firefox' ] },
+ },
+
+ // {
+ // name: 'firefox',
+ // use: { ...devices['Desktop Firefox'] },
+ // },
+
+ // {
+ // name: 'webkit',
+ // use: { ...devices['Desktop Safari'] },
+ // },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // url: 'http://127.0.0.1:3000',
+ // reuseExistingServer: !process.env.CI,
+ // },
+} );
diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx
new file mode 100644
index 0000000000000..751f5daa6438e
--- /dev/null
+++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/src/components/EmailForm/email-form-cookie-consent.tsx
@@ -0,0 +1,31 @@
+import { translate } from '../../i18n';
+import { shouldStoreEmailData } from '../../state';
+import { ToggleControl } from '../ToggleControl';
+
+const handleChange = ( e: boolean ) => {
+ shouldStoreEmailData.value = e;
+};
+
+export const EmailFormCookieConsent = () => {
+ const label = (
+
+
+ { translate(
+ 'Save my name, email, and website in this browser for the next time I comment.'
+ ) }
+