Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions projects/packages/stats-admin/.gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Files to include in the mirror repo, but excluded via gitignore
# Remember to end all directories with `/**` to properly tag every file.
# /src/js/example.min.js production-include
build/** production-include

# Files to exclude from the mirror repo, but included in the monorepo.
# (see also entries in the monorepo root .gitattributes)
Expand Down
2 changes: 2 additions & 0 deletions projects/packages/stats-admin/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ vendor/
node_modules/
dist/
wordpress/
build/
.cache/
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add Stats pricing grid for new site installations without a plan, allowing users to choose between free and paid tiers directly from wp-admin.
17 changes: 15 additions & 2 deletions projects/packages/stats-admin/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,21 @@
],
"test-php": "@composer phpunit",
"test-php-coverage": "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"",
"build-production": "echo 'Add your build step to composer.json, please!'",
"build-development": "echo 'Add your build step to composer.json, please!'"
"build": [
"Composer\\Config::disableProcessTimeout",
"pnpm run build"
],
"build-development": [
"pnpm run build-development"
],
"build-production": [
"pnpm run build-production"
],
"test-js": "pnpm run test",
"watch": [
"Composer\\Config::disableProcessTimeout",
"pnpm run watch"
]
},
"repositories": [
{
Expand Down
38 changes: 33 additions & 5 deletions projects/packages/stats-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,38 @@
"license": "GPL-2.0-or-later",
"author": "Automattic",
"scripts": {
"build": "echo 'Not implemented.'",
"build-js": "echo 'Not implemented.'",
"build-production": "echo 'Not implemented.'",
"build-production-js": "echo 'Not implemented.'",
"clean": "true"
"build": "pnpm run clean && pnpm run build-pricing-grid",
"build-development": "NODE_ENV=development BABEL_ENV=development pnpm run build",
"build-pricing-grid": "webpack --config ./tools/webpack.pricing-grid.config.js",
"build-production": "NODE_ENV=production BABEL_ENV=production pnpm run build && pnpm run validate",
"clean": "rm -rf build/",
"test": "jest --passWithNoTests",
"validate": "pnpm exec validate-es --no-error-on-unmatched-pattern build/",
"watch": "pnpm run build-pricing-grid --watch"
},
"browserslist": [
"extends @wordpress/browserslist-config"
],
"dependencies": {
"@automattic/jetpack-components": "workspace:*",
"@wordpress/element": "8.3.0",
"@wordpress/i18n": "6.24.0"
},
"devDependencies": {
"@automattic/jetpack-base-styles": "workspace:*",
"@automattic/jetpack-webpack-config": "workspace:*",
"@babel/core": "7.29.7",
"@wordpress/browserslist-config": "6.51.0",
"@wordpress/theme": "1.1.0",
"autoprefixer": "10.4.20",
"jest": "30.4.2",
"postcss": "8.5.23",
"postcss-loader": "8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"sass-embedded": "1.97.3",
"sass-loader": "16.0.5",
"webpack": "5.107.2",
"webpack-cli": "7.2.1"
}
}
6 changes: 6 additions & 0 deletions projects/packages/stats-admin/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = () => ( {
plugins: [
require( '@wordpress/theme/postcss-plugins/postcss-ds-token-fallbacks' ).default,
require( 'autoprefixer' ),
],
} );
151 changes: 113 additions & 38 deletions projects/packages/stats-admin/src/class-dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Automattic\Jetpack\Stats_Admin;

use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Stats\Options as Stats_Options;

/**
Expand Down Expand Up @@ -84,59 +85,133 @@ public function add_wp_admin_menu() {
* Override render funtion
*/
public function render() {
// Record the number of views of the stats dashboard on the initial several loads for the purpose of showing feedback notice.
$views = intval( Stats_Options::get_option( 'views' ) ) + 1;
if ( $views <= Notices::VIEWS_TO_SHOW_FEEDBACK ) {
Stats_Options::set_option( 'views', $views );
}
// Check if we should show the pricing grid for new installations without a plan.
if ( $this->should_show_pricing_grid() ) {
// Render pricing grid mount point.
?>
<div id="jp-stats-pricing-grid" class="jp-stats-pricing-grid">
<div class="hide-if-js"><?php esc_html_e( 'The Jetpack Stats pricing grid requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
</div>
<?php
} else {
// Record the number of views of the stats dashboard on the initial several loads for the purpose of showing feedback notice.
$views = intval( Stats_Options::get_option( 'views' ) ) + 1;
if ( $views <= Notices::VIEWS_TO_SHOW_FEEDBACK ) {
Stats_Options::set_option( 'views', $views );
}

?>
<div id="wpcom" class="jp-stats-dashboard" style="min-height: calc(100vh - 100px);">
<div class="hide-if-js"><?php esc_html_e( 'Your Jetpack Stats dashboard requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
<div class="hide-if-no-js" style="height: 100%">
<img
class="jp-stats-dashboard-loading-spinner"
width="32"
height="32"
style="position: absolute; left: 50%; top: 50%;"
alt=<?php echo esc_attr( __( 'Loading', 'jetpack-stats-admin' ) ); ?>
src="//en.wordpress.com/i/loading/loading-64.gif"
/>
// Render Odyssey mount point.
?>
<div id="wpcom" class="jp-stats-dashboard" style="min-height: calc(100vh - 100px);">
<div class="hide-if-js"><?php esc_html_e( 'Your Jetpack Stats dashboard requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
<div class="hide-if-no-js" style="height: 100%">
<img
class="jp-stats-dashboard-loading-spinner"
width="32"
height="32"
style="position: absolute; left: 50%; top: 50%;"
alt=<?php echo esc_attr( __( 'Loading', 'jetpack-stats-admin' ) ); ?>
src="//en.wordpress.com/i/loading/loading-64.gif"
/>
</div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
// Load SVG sprite.
$.get("https://widgets.wp.com/odyssey-stats/common/gridicons-506499ddac13811fee8e.svg", function(data) {
var div = document.createElement("div");
div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
div.style = 'display: none';
document.body.insertBefore(div, document.body.childNodes[0]);
<script>
jQuery(document).ready(function($) {
// Load SVG sprite.
$.get("https://widgets.wp.com/odyssey-stats/common/gridicons-506499ddac13811fee8e.svg", function(data) {
var div = document.createElement("div");
div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
div.style = 'display: none';
document.body.insertBefore(div, document.body.childNodes[0]);
});
// we intercept on all anchor tags and change it to hashbang style.
$("#wpcom").on('click', 'a', function (e) {
const link = e && e.currentTarget && e.currentTarget.attributes && e.currentTarget.attributes.href && e.currentTarget.attributes.href.value;
if( link && link.startsWith( '/stats' ) ) {
location.hash = `#!${link}`;
return false;
}
});
});
// we intercept on all anchor tags and change it to hashbang style.
$("#wpcom").on('click', 'a', function (e) {
const link = e && e.currentTarget && e.currentTarget.attributes && e.currentTarget.attributes.href && e.currentTarget.attributes.href.value;
if( link && link.startsWith( '/stats' ) ) {
location.hash = `#!${link}`;
return false;
}
});
});
</script>
<?php
</script>
<?php
}
}

/**
* Initialize the admin resources.
*/
public function admin_init() {
add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );

// Landing on an Odyssey view means the user chose a plan from the
// pricing grid; don't show the grid again on later visits.
if ( $this->is_odyssey_view() ) {
Pricing_Grid\Eligibility::dismiss();
}
}

/**
* Load the admin scripts.
*/
public function load_admin_scripts() {
( new Odyssey_Assets() )->load_admin_scripts( 'jp-stats-dashboard', 'build.min', array( 'config_variable_name' => 'jetpackStatsOdysseyAppConfigData' ) );
// Check if we should show the pricing grid for new installations without a plan.
if ( $this->should_show_pricing_grid() ) {
$this->load_pricing_grid_scripts();
} else {
( new Odyssey_Assets() )->load_admin_scripts( 'jp-stats-dashboard', 'build.min', array( 'config_variable_name' => 'jetpackStatsOdysseyAppConfigData' ) );
}
}

/**
* Check if the pricing grid should be shown.
*
* @return bool True if the pricing grid should be shown.
*/
private function should_show_pricing_grid() {
// Odyssey shares this page; the pricing grid's CTAs add a `view` flag
// (purchase for the paid tier route, traffic for the free dashboard)
// so the dashboard renders Odyssey there instead of the grid.
if ( $this->is_odyssey_view() ) {
return false;
}

return Pricing_Grid\Eligibility::should_show_pricing_grid();
}

/**
* Whether the current request targets an Odyssey view via the pricing
* grid's `view` query flag.
*
* @return bool True for an Odyssey view.
*/
private function is_odyssey_view() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['view'] ) && in_array( $_GET['view'], array( 'purchase', 'traffic' ), true );
}

/**
* Load the pricing grid scripts and initial state.
*/
private function load_pricing_grid_scripts() {
$handle = 'jp-stats-pricing-grid';

if ( ! file_exists( __DIR__ . '/../build/pricing-grid/jp-stats-pricing-grid.js' ) ) {
return;
}

Assets::register_script(
$handle,
'../build/pricing-grid/jp-stats-pricing-grid.js',
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-stats-admin',
)
);
Assets::enqueue_script( $handle );

// Inject the initial state before the bundle runs.
wp_add_inline_script( $handle, ( new Pricing_Grid\Initial_State() )->render(), 'before' );
}
}
12 changes: 12 additions & 0 deletions projects/packages/stats-admin/src/class-main.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ public static function init() {
*/
private function __construct() {
add_action( 'rest_api_init', array( REST_Controller::class, 'register' ) );
add_action( 'jetpack_site_registered', array( Pricing_Grid\Eligibility::class, 'record_connection_time' ) );

// The stats module normally registers the Stats menu, but modules only
// load once the site is connected. Register the dashboard here for
// unconnected sites only, so eligible new sites see the pricing grid
// before connecting. Connected sites go exclusively through the stats
// module (modules/stats.php), which also owns the noheader chart-image
// paths used by the adminbar and the dashboard widget.
if ( is_admin() && ! ( new Manager() )->is_connected() && Pricing_Grid\Eligibility::is_eligible_site() ) {
Dashboard::init();
}

// Disable JITM assets on the Stats page.
// JITM is handled separately by Stats: https://github.com/Automattic/wp-calypso/pull/95273.
add_filter(
Expand Down
1 change: 1 addition & 0 deletions projects/packages/stats-admin/src/class-notices.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Notices {
const NEW_STATS_FEEDBACK_NOTICE_ID = 'new_stats_feedback';
const OPT_IN_NEW_STATS_NOTICE_ID = 'opt_in_new_stats';
const GDPR_COOKIE_CONSENT_NOTICE_ID = 'gdpr_cookie_consent';
const PRICING_GRID_DISMISSED_NOTICE_ID = 'pricing_grid_dismissed';

const VIEWS_TO_SHOW_FEEDBACK = 3;
const POSTPONE_OPT_IN_NOTICE_DAYS = 30;
Expand Down
Loading
Loading