Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.43% covered (success)
98.43%
250 / 254
87.50% covered (warning)
87.50%
14 / 16
CRAP
n/a
0 / 0
Automattic\Jetpack\PremiumAnalytics\get_stats_plan_tier
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
Automattic\Jetpack\PremiumAnalytics\get_wpcom_stats_plan_tier
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
Automattic\Jetpack\PremiumAnalytics\get_jetpack_stats_plan_tier
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
Automattic\Jetpack\PremiumAnalytics\has_cached_jetpack_plan
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
Automattic\Jetpack\PremiumAnalytics\has_jetpack_stats_commercial_purchase
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
Automattic\Jetpack\PremiumAnalytics\get_locked_stats_features
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
Automattic\Jetpack\PremiumAnalytics\get_locked_stats_sections
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\get_stats_section_copy
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\get_stats_feature_copy
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
1 / 1
16
Automattic\Jetpack\PremiumAnalytics\localize_stats_support_url
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
Automattic\Jetpack\PremiumAnalytics\get_stats_upgrade_url
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
6
Automattic\Jetpack\PremiumAnalytics\get_stats_plan_gates
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
8
Automattic\Jetpack\PremiumAnalytics\configure_stats_plan_gates
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\inject_stats_plan_gates_script_data
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
Automattic\Jetpack\PremiumAnalytics\maybe_invalidate_stats_caches
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
Automattic\Jetpack\PremiumAnalytics\maybe_refresh_jetpack_plan
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * Which paid Stats features the site's plan lacks, following classic Stats' gates.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics;
9
10use Automattic\Jetpack\Current_Plan;
11use Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller;
12use Automattic\Jetpack\Stats\Options as Stats_Options;
13use Automattic\Jetpack\Status;
14use Automattic\Jetpack\Status\Host;
15
16// Guarded on a symbol the file declares; see the include block in Analytics::load_dashboard_components().
17if ( ! function_exists( __NAMESPACE__ . '\\ensure_widget_registry_ready' ) ) {
18    require_once __DIR__ . '/widget-modules.php';
19}
20
21/**
22 * Filter overriding the Stats plan tier.
23 *
24 * @var string
25 */
26const STATS_PLAN_TIER_FILTER = 'jetpack_premium_analytics_stats_plan_tier';
27
28const STATS_TIER_COMMERCIAL   = 'commercial';
29const STATS_TIER_LEGACY_FREE  = 'legacy-free';
30const STATS_TIER_PAID         = 'paid';
31const STATS_TIER_LEGACY_BASIC = 'legacy-basic';
32const STATS_TIER_FREE         = 'free';
33const STATS_TIER_JETPACK_FREE = 'jetpack-free';
34const STATS_TIER_UNKNOWN      = 'unknown';
35
36const STATS_TIERS = array( STATS_TIER_COMMERCIAL, STATS_TIER_LEGACY_FREE, STATS_TIER_PAID, STATS_TIER_LEGACY_BASIC, STATS_TIER_FREE, STATS_TIER_JETPACK_FREE, STATS_TIER_UNKNOWN );
37
38/**
39 * Jetpack products that support commercial use. PWYW and Free Stats do not.
40 */
41const JETPACK_STATS_COMMERCIAL_PRODUCTS = array( 'jetpack_stats_monthly', 'jetpack_stats_yearly', 'jetpack_stats_bi_yearly' );
42
43/**
44 * Jetpack plan families that bundle commercial Stats.
45 */
46const JETPACK_STATS_COMMERCIAL_PLAN_PREFIXES = array( 'jetpack_business', 'jetpack_complete', 'jetpack_growth' );
47
48/**
49 * The Stats plan tier the dashboard is gated by.
50 *
51 * @since 0.13.0
52 *
53 * @return string One of the STATS_TIER_* values.
54 */
55function get_stats_plan_tier() {
56    $host = new Host();
57
58    if ( $host->is_wpcom_platform() ) {
59        $tier = get_wpcom_stats_plan_tier( $host->is_woa_site() );
60    } elseif ( $host->is_vip_site() ) {
61        $tier = STATS_TIER_COMMERCIAL;
62    } else {
63        $tier = get_jetpack_stats_plan_tier();
64    }
65
66    /**
67     * Filters the Stats plan tier Premium Analytics gates the dashboard by.
68     *
69     * @since 0.13.0
70     *
71     * @param string $tier One of `commercial`, `legacy-free`, `paid`, `legacy-basic`, `free`, `jetpack-free` or
72     *                     `unknown`, which locks nothing. Any other value is ignored.
73     */
74    $filtered = apply_filters( STATS_PLAN_TIER_FILTER, $tier );
75
76    // A misspelled tier would otherwise fall through to `unknown` and unlock every paid feature.
77    if ( ! in_array( $filtered, STATS_TIERS, true ) ) {
78        // One line: tools/replace-next-version-tag.sh only rewrites the token in a single-line call.
79        _doing_it_wrong( __FUNCTION__, esc_html( sprintf( /* translators: %s: Filter name. */ __( 'The %s filter must return one of the STATS_TIER_* values.', 'jetpack-premium-analytics-pkg' ), STATS_PLAN_TIER_FILTER ) ), 'jetpack-premium-analytics-0.13.0' );
80        return $tier;
81    }
82
83    return $filtered;
84}
85
86/**
87 * The tier of a WordPress.com site, from its stats feature.
88 *
89 * @since 0.13.0
90 *
91 * @param bool $is_atomic Whether the site is Atomic.
92 * @return string One of the STATS_TIER_* values.
93 */
94function get_wpcom_stats_plan_tier( $is_atomic ) {
95    if ( ! function_exists( 'wpcom_site_has_feature' ) ) {
96        return STATS_TIER_UNKNOWN;
97    }
98
99    // Unsynced Atomic data reads like a site that bought nothing; see Current_Plan::get_wpcom_site_specific_features().
100    if ( $is_atomic && function_exists( 'wpcom_get_site_purchases' ) && ! \wpcom_get_site_purchases() ) {
101        return STATS_TIER_UNKNOWN;
102    }
103
104    // Calypso's shouldGateStats() order: legacy free outranks paid.
105    $tiers = array(
106        'stats-commercial' => STATS_TIER_COMMERCIAL,
107        'stats-free'       => STATS_TIER_LEGACY_FREE,
108        'stats-paid'       => STATS_TIER_PAID,
109        'stats-basic'      => STATS_TIER_LEGACY_BASIC,
110    );
111    foreach ( $tiers as $feature => $tier ) {
112        if ( \wpcom_site_has_feature( $feature ) ) {
113            return $tier;
114        }
115    }
116
117    return STATS_TIER_FREE;
118}
119
120/**
121 * The tier of a self-hosted site, from its cached plan.
122 *
123 * @since 0.13.0
124 *
125 * @return string One of the STATS_TIER_* values.
126 */
127function get_jetpack_stats_plan_tier() {
128    if ( ! has_cached_jetpack_plan() ) {
129        return STATS_TIER_UNKNOWN;
130    }
131
132    return has_jetpack_stats_commercial_purchase() ? STATS_TIER_COMMERCIAL : STATS_TIER_JETPACK_FREE;
133}
134
135/**
136 * Whether the plan cache holds a plan. An empty one cannot tell a paid site from a free one (STATS-475).
137 *
138 * @since 0.13.0
139 *
140 * @return bool
141 */
142function has_cached_jetpack_plan() {
143    $plan = get_option( Current_Plan::PLAN_OPTION );
144
145    return is_array( $plan ) && is_array( $plan['features']['active'] ?? null ) && $plan['features']['active'];
146}
147
148/**
149 * Whether a self-hosted site's cached plan or products support commercial use.
150 *
151 * @since 0.13.0
152 *
153 * @return bool
154 */
155function has_jetpack_stats_commercial_purchase() {
156    $plan     = get_option( Current_Plan::PLAN_OPTION );
157    $products = get_option( Current_Plan::SITE_PRODUCTS_OPTION, array() );
158    $products = is_array( $products ) ? $products : array();
159
160    $slugs = array_column( $products, 'product_slug' );
161    if ( isset( $plan['product_slug'] ) ) {
162        $slugs[] = $plan['product_slug'];
163    }
164
165    foreach ( $slugs as $slug ) {
166        if ( in_array( $slug, JETPACK_STATS_COMMERCIAL_PRODUCTS, true ) ) {
167            return true;
168        }
169        foreach ( JETPACK_STATS_COMMERCIAL_PLAN_PREFIXES as $prefix ) {
170            if ( 0 === strpos( $slug, $prefix ) ) {
171                return true;
172            }
173        }
174    }
175
176    return in_array( 'stats-commercial', $plan['features']['active'] ?? array(), true );
177}
178
179/**
180 * The features classic Stats gates on WordPress.com Personal: the newer, commercial modules.
181 */
182const STATS_COMMERCIAL_FEATURES = array( 'authors', 'search-terms', 'videos', 'utm', 'devices', 'regions-cities' );
183
184/**
185 * The features gated on legacy-free sites and on self-hosted sites without a commercial purchase.
186 */
187const STATS_ADVANCED_FEATURES = array( 'utm', 'devices', 'regions-cities' );
188
189/**
190 * The features a tier locks.
191 *
192 * @since 0.13.0
193 *
194 * @param string $tier One of the STATS_TIER_* values.
195 * @return string[] Feature keys.
196 */
197function get_locked_stats_features( $tier ) {
198    switch ( $tier ) {
199        case STATS_TIER_LEGACY_FREE:
200        case STATS_TIER_JETPACK_FREE:
201            return STATS_ADVANCED_FEATURES;
202        case STATS_TIER_PAID:
203            return STATS_COMMERCIAL_FEATURES;
204        case STATS_TIER_LEGACY_BASIC:
205            return array_merge( STATS_COMMERCIAL_FEATURES, array( 'referrers', 'clicks', 'csv', 'intervals', 'long-date-presets' ) );
206        case STATS_TIER_FREE:
207            return array_merge(
208                STATS_COMMERCIAL_FEATURES,
209                array( 'top-posts', 'referrers', 'countries', 'clicks', 'file-downloads', 'insights', 'tags', 'csv', 'intervals', 'long-date-presets', 'recent-7-days' )
210            );
211        default:
212            return array();
213    }
214}
215
216/**
217 * The dashboard sections a tier replaces with one upgrade prompt, as classic Stats replaces its Insights and Traffic pages.
218 *
219 * @since $$next-version$$
220 *
221 * @param string $tier One of the STATS_TIER_* values.
222 * @return string[] Section slugs.
223 */
224function get_locked_stats_sections( $tier ) {
225    return STATS_TIER_FREE === $tier ? array( 'traffic', 'insights' ) : array();
226}
227
228/**
229 * What a locked section's upgrade prompt says: classic Stats' upsell, without its price.
230 *
231 * @since $$next-version$$
232 *
233 * @param string $section Section slug.
234 * @return array{title:string, description:string, features:string[], upgrade_label:string, expand_label:string, support_url:string} The copy.
235 */
236function get_stats_section_copy( $section ) {
237    $plan_features = array(
238        __( 'Keep your data private and GDPR-compliant', 'jetpack-premium-analytics-pkg' ),
239        __( '14-day money-back guarantee', 'jetpack-premium-analytics-pkg' ),
240        __( '6 GB storage', 'jetpack-premium-analytics-pkg' ),
241        __( 'Unlimited pages, posts, users, and visitors', 'jetpack-premium-analytics-pkg' ),
242        __( 'Free domain for one year', 'jetpack-premium-analytics-pkg' ),
243        __( 'Ad-free browsing experience for your visitors', 'jetpack-premium-analytics-pkg' ),
244        __( 'All premium themes', 'jetpack-premium-analytics-pkg' ),
245        __( 'Fast support from our expert team', 'jetpack-premium-analytics-pkg' ),
246        __( 'Customize fonts and colors sitewide', 'jetpack-premium-analytics-pkg' ),
247    );
248
249    if ( 'insights' === $section ) {
250        $title    = __( 'Unlock site insights', 'jetpack-premium-analytics-pkg' );
251        $features = array(
252            __( 'Monitor your posting activity', 'jetpack-premium-analytics-pkg' ),
253            __( 'Track your all-time highlights and insights', 'jetpack-premium-analytics-pkg' ),
254        );
255    } else {
256        $title    = __( 'Unlock site growth analytics', 'jetpack-premium-analytics-pkg' );
257        $features = array(
258            __( 'View trends and data from any time period', 'jetpack-premium-analytics-pkg' ),
259            __( 'Detailed stats about posts, referrers, clicks and more', 'jetpack-premium-analytics-pkg' ),
260        );
261    }
262
263    return array(
264        'title'         => $title,
265        'description'   => __( 'Get detailed insights into your site’s performance with a Personal plan.', 'jetpack-premium-analytics-pkg' ),
266        'features'      => array_merge( $features, $plan_features ),
267        'upgrade_label' => __( 'Upgrade to Personal', 'jetpack-premium-analytics-pkg' ),
268        'expand_label'  => __( 'Show all Personal features', 'jetpack-premium-analytics-pkg' ),
269        'support_url'   => 'https://wordpress.com/support/stats/',
270    );
271}
272
273/**
274 * What a locked feature says, worded as classic Stats' upsell; `<link>` wraps the words its support page explains.
275 *
276 * @since 0.13.0
277 *
278 * @param string $feature Feature key.
279 * @return array{description:string, support_url:string} The copy, generic for a feature without its own.
280 */
281function get_stats_feature_copy( $feature ) {
282    $traffic_support = 'https://jetpack.com/support/jetpack-stats/traffic-dashboard/';
283
284    switch ( $feature ) {
285        case 'top-posts':
286            return array(
287                'description' => __( 'Discover your <link>post and pages</link> traffic in detail and learn what content resonates the most.', 'jetpack-premium-analytics-pkg' ),
288                'support_url' => $traffic_support . '#analyzing-popular-posts-pages-and-archives',
289            );
290        case 'referrers':
291            return array(
292                'description' => __( 'Find out where your <link>visitors come from</link> to optimize your content strategy.', 'jetpack-premium-analytics-pkg' ),
293                'support_url' => $traffic_support . '#referrers',
294            );
295        case 'countries':
296            return array(
297                'description' => __( 'Discover where your <link>visitors are located</link> and identify where your traffic is coming from.', 'jetpack-premium-analytics-pkg' ),
298                'support_url' => $traffic_support . '#views-by-locations',
299            );
300        case 'regions-cities':
301            return array(
302                'description' => __( 'Access to <link>region and city stats</link> for more accurate analysis.', 'jetpack-premium-analytics-pkg' ),
303                'support_url' => $traffic_support . '#regions-and-cities',
304            );
305        case 'clicks':
306            return array(
307                'description' => __( 'Learn what <link>external links</link> your visitors click on your site to reveal their areas of interest.', 'jetpack-premium-analytics-pkg' ),
308                'support_url' => $traffic_support . '#clicks',
309            );
310        case 'file-downloads':
311            return array(
312                'description' => __( 'Discover the most <link>downloaded files</link> by your visitors.', 'jetpack-premium-analytics-pkg' ),
313                'support_url' => 'https://wordpress.com/support/stats/analyze-content-performance/#track-file-downloads',
314            );
315        case 'authors':
316            return array(
317                'description' => __( 'Identify your audience’s <link>favorite writers</link> and perspectives.', 'jetpack-premium-analytics-pkg' ),
318                'support_url' => $traffic_support . '#authors',
319            );
320        case 'search-terms':
321            return array(
322                'description' => __( 'Discover the <link>terms and phrases</link> your visitors use to find your site.', 'jetpack-premium-analytics-pkg' ),
323                'support_url' => $traffic_support . '#search-terms',
324            );
325        case 'videos':
326            return array(
327                'description' => __( 'Discover your <link>most popular videos</link> and find out how they performed.', 'jetpack-premium-analytics-pkg' ),
328                'support_url' => 'https://jetpack.com/support/jetpack-videopress/add-video-block-editor/video-stats/',
329            );
330        case 'utm':
331            return array(
332                'description' => __( 'Generate UTM parameters and track your <link>campaign performance data</link>.', 'jetpack-premium-analytics-pkg' ),
333                'support_url' => $traffic_support . '#harnessing-utm-stats-for-precision-tracking',
334            );
335        case 'devices':
336            return array(
337                'description' => __( 'See which <link>devices, browsers and OS</link> your visitors are using.', 'jetpack-premium-analytics-pkg' ),
338                'support_url' => $traffic_support . '#devices-stats',
339            );
340        case 'tags':
341            return array(
342                'description' => __( 'Track the most viewed <link>tags & categories</link>.', 'jetpack-premium-analytics-pkg' ),
343                'support_url' => 'https://wordpress.com/support/stats/learn-insights-about-your-website/#all-time-insights',
344            );
345        case 'recent-7-days':
346            return array(
347                'description' => __( 'See more than the last 7 days of your stats.', 'jetpack-premium-analytics-pkg' ),
348                'support_url' => '',
349            );
350        case 'long-date-presets':
351            return array(
352                'description' => __( 'See periods beyond the last 30 days in one click.', 'jetpack-premium-analytics-pkg' ),
353                'support_url' => '',
354            );
355        default:
356            return array(
357                'description' => __( 'Upgrade your plan to unlock Jetpack Stats.', 'jetpack-premium-analytics-pkg' ),
358                'support_url' => '',
359            );
360    }
361}
362
363/**
364 * Locales with translated WordPress.com support articles: Calypso's `supportSiteLocales`, which classic Stats links with.
365 */
366const WPCOM_SUPPORT_LOCALES = array( 'ar', 'de', 'es', 'fr', 'he', 'id', 'it', 'ja', 'ko', 'nl', 'pt-br', 'ru', 'sv', 'tr', 'zh-cn', 'zh-tw' );
367
368/**
369 * A WordPress.com support URL in the current user's language, where that support site has one.
370 *
371 * Leaves jetpack.com URLs in English: its localized support docs are missing for most locales.
372 *
373 * @since 0.13.0
374 *
375 * @param string $url Support URL, in English.
376 * @return string
377 */
378function localize_stats_support_url( $url ) {
379    $prefix = 'https://wordpress.com/support/';
380    if ( ! str_starts_with( $url, $prefix ) ) {
381        return $url;
382    }
383
384    $locale = strtolower( str_replace( '_', '-', get_user_locale() ) );
385    if ( ! in_array( $locale, WPCOM_SUPPORT_LOCALES, true ) ) {
386        $locale = strtok( $locale, '-' );
387    }
388    if ( ! in_array( $locale, WPCOM_SUPPORT_LOCALES, true ) ) {
389        return $url;
390    }
391
392    return 'https://wordpress.com/' . $locale . '/support/' . substr( $url, strlen( $prefix ) );
393}
394
395/**
396 * Where to buy a locked feature: classic Stats' purchase destinations.
397 *
398 * @since 0.13.0
399 *
400 * @param string $feature Feature key, or the slug of a locked section, which no commercial feature shares.
401 * @param string $tier    One of the STATS_TIER_* values.
402 * @return string The URL, or an empty string when there is nowhere to buy it.
403 */
404function get_stats_upgrade_url( $feature, $tier ) {
405    $return_path = 'admin.php?page=' . Analytics::MENU_PAGE_SLUG;
406
407    if ( STATS_TIER_JETPACK_FREE === $tier ) {
408        // The purchase flow lives in Odyssey Stats; with it off, `page=stats` shows only an opt-in notice.
409        $stats_page = function_exists( 'menu_page_url' ) ? menu_page_url( 'stats', false ) : '';
410        if ( ! $stats_page || ! Stats_Options::get_option( 'enable_odyssey_stats' ) ) {
411            return '';
412        }
413
414        $query = http_build_query(
415            array(
416                'from'         => 'jetpack-premium-analytics',
417                'productType'  => 'commercial',
418                'redirect_uri' => $return_path,
419            ),
420            '',
421            '&',
422            PHP_QUERY_RFC3986
423        );
424
425        return $stats_page . '#!/stats/purchase/' . (int) \Jetpack_Options::get_option( 'id' ) . '?' . $query;
426    }
427
428    $plan      = in_array( $feature, STATS_COMMERCIAL_FEATURES, true ) ? 'premium' : 'personal';
429    $site_slug = ( new Status() )->get_site_suffix();
430
431    return 'https://wordpress.com/checkout/' . rawurlencode( $site_slug ) . '/' . $plan . '?redirect_to=' . rawurlencode( admin_url( $return_path ) );
432}
433
434/**
435 * The gates the client applies: each locked feature's copy and upgrade URL, the feature of each widget type it locks, and each locked section's prompt.
436 *
437 * @since 0.13.0
438 *
439 * @return array{tier:string, locked_features:array<string,array{description:string, support_url:string, upgrade_url:string}>, locked_widget_types:array<string,string>, locked_sections:array<string,array{title:string, description:string, features:string[], upgrade_label:string, expand_label:string, support_url:string, upgrade_url:string, open_widget_types:string[]}>}
440 */
441function get_stats_plan_gates() {
442    $tier     = get_stats_plan_tier();
443    $features = get_locked_stats_features( $tier );
444
445    $locked_features = array();
446    foreach ( $features as $feature ) {
447        $copy                        = get_stats_feature_copy( $feature );
448        $copy['support_url']         = localize_stats_support_url( $copy['support_url'] );
449        $locked_features[ $feature ] = $copy + array( 'upgrade_url' => get_stats_upgrade_url( $feature, $tier ) );
450    }
451
452    $locked_widget_types = array();
453    if ( $locked_features ) {
454        ensure_widget_registry_ready();
455
456        foreach ( get_available_widget_types() as $name => $widget_type ) {
457            if ( isset( $widget_type->plan_feature ) && isset( $locked_features[ $widget_type->plan_feature ] ) ) {
458                $locked_widget_types[ $name ] = $widget_type->plan_feature;
459            }
460        }
461    }
462
463    $locked_sections = array();
464    foreach ( get_locked_stats_sections( $tier ) as $section ) {
465        $copy                        = get_stats_section_copy( $section );
466        $copy['support_url']         = localize_stats_support_url( $copy['support_url'] );
467        $locked_sections[ $section ] = $copy + array(
468            'upgrade_url'       => get_stats_upgrade_url( $section, $tier ),
469            // An allowlist, so a widget type no plan feature names is hidden too.
470            'open_widget_types' => 'traffic' === $section ? array( 'jpa/traffic-chart' ) : array(),
471        );
472    }
473
474    return array(
475        'tier'                => $tier,
476        'locked_features'     => $locked_features,
477        'locked_widget_types' => $locked_widget_types,
478        'locked_sections'     => $locked_sections,
479    );
480}
481
482/**
483 * Configures the plan gates script data.
484 *
485 * @since 0.13.0
486 *
487 * @return void
488 */
489function configure_stats_plan_gates() {
490    add_filter( 'jetpack_admin_js_script_data', __NAMESPACE__ . '\\inject_stats_plan_gates_script_data', 20 );
491    add_action( 'admin_init', __NAMESPACE__ . '\\maybe_refresh_jetpack_plan' );
492    add_action( 'admin_init', __NAMESPACE__ . '\\maybe_invalidate_stats_caches' );
493}
494
495/**
496 * Injects the plan gates into JetpackScriptData.
497 *
498 * @since 0.13.0
499 *
500 * @param array $data The script data passed by the assets package.
501 * @return array
502 */
503function inject_stats_plan_gates_script_data( array $data ): array {
504    // Script data prints on every Jetpack admin page; only the dashboard reads the gates.
505    if ( ! Analytics::is_dashboard_request() ) {
506        return $data;
507    }
508
509    if ( ! isset( $data['premium_analytics'] ) || ! is_array( $data['premium_analytics'] ) ) {
510        $data['premium_analytics'] = array();
511    }
512
513    $gates = get_stats_plan_gates();
514
515    // Objects, so an empty map reaches JS as `{}` rather than `[]`.
516    $data['premium_analytics']['plan_gates'] = array(
517        'tier'                => $gates['tier'],
518        'locked_features'     => (object) $gates['locked_features'],
519        'locked_widget_types' => (object) $gates['locked_widget_types'],
520        'locked_sections'     => (object) $gates['locked_sections'],
521    );
522
523    return $data;
524}
525
526/**
527 * Transient throttling the refresh of an empty plan cache.
528 */
529const EMPTY_PLAN_REFRESH_TRANSIENT = 'jetpack_premium_analytics_empty_plan_refresh';
530
531/**
532 * Transient throttling the refresh after a purchase.
533 */
534const PURCHASE_PLAN_REFRESH_TRANSIENT = 'jetpack_premium_analytics_purchase_plan_refresh';
535
536/**
537 * Transient throttling the cache invalidation after a purchase.
538 */
539const PURCHASE_CACHE_INVALIDATION_TRANSIENT = 'jetpack_premium_analytics_purchase_cache_invalidation';
540
541/**
542 * Drops the cached Stats replies a checkout made stale, before the dashboard it returns to asks for them.
543 *
544 * @since 0.13.0
545 *
546 * @return void
547 */
548function maybe_invalidate_stats_caches() {
549    // Simple serves the dashboard from WordPress.com, with neither cache.
550    if ( ! Analytics::is_dashboard_request() || '' === get_stats_purchase_success_type() || ( new Host() )->is_wpcom_simple() ) {
551        return;
552    }
553
554    // The marker is a plain GET parameter, so a crafted link could otherwise drop the caches on every visit.
555    if ( get_transient( PURCHASE_CACHE_INVALIDATION_TRANSIENT ) ) {
556        return;
557    }
558    set_transient( PURCHASE_CACHE_INVALIDATION_TRANSIENT, 1, MINUTE_IN_SECONDS );
559
560    Api_Proxy_Controller::invalidate_cache();
561    delete_transient( Notices::NOTICES_CACHE_KEY );
562}
563
564/**
565 * Refreshes a self-hosted site's cached plan on the dashboard when it is empty or a checkout just returned.
566 *
567 * Script data is final once printed, so the gates can only change before it.
568 *
569 * @since 0.13.0
570 *
571 * @return void
572 */
573function maybe_refresh_jetpack_plan() {
574    if ( ! Analytics::is_dashboard_request() || ( new Host() )->is_wpcom_platform() ) {
575        return;
576    }
577
578    if ( '' !== get_stats_purchase_success_type() ) {
579        $transient = PURCHASE_PLAN_REFRESH_TRANSIENT;
580        $ttl       = MINUTE_IN_SECONDS;
581    } elseif ( ! has_cached_jetpack_plan() ) {
582        $transient = EMPTY_PLAN_REFRESH_TRANSIENT;
583        $ttl       = 15 * MINUTE_IN_SECONDS;
584    } else {
585        return;
586    }
587
588    if ( get_transient( $transient ) ) {
589        return;
590    }
591    set_transient( $transient, 1, $ttl );
592
593    Current_Plan::refresh_from_wpcom( array( 'timeout' => 5 ) );
594}