Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.09% covered (warning)
68.09%
64 / 94
78.95% covered (warning)
78.95%
15 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stats
68.48% covered (warning)
68.48%
63 / 92
78.95% covered (warning)
78.95%
15 / 19
112.27
0.00% covered (danger)
0.00%
0 / 1
 get_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_long_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_features
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 get_pricing_for_ui
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 get_wpcom_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_wpcom_pwyw_product_slug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_wpcom_free_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_status
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 is_upgradable
23.53% covered (danger)
23.53%
4 / 17
0.00% covered (danger)
0.00%
0 / 1
65.11
 get_paid_plan_product_slugs
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 get_url_product_type
22.22% covered (danger)
22.22%
4 / 18
0.00% covered (danger)
0.00%
0 / 1
57.05
 has_trial_support
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_premium_analytics_enabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get_purchase_url
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 get_post_activation_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_manage_url
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_upgradable_by_bundle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Jetpack Stats product
4 *
5 * @package my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack\Products;
9
10use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
11use Automattic\Jetpack\My_Jetpack\Initializer;
12use Automattic\Jetpack\My_Jetpack\Products;
13use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
14use Automattic\Jetpack\Status\Host;
15use Jetpack_Options;
16
17if ( ! defined( 'ABSPATH' ) ) {
18    exit( 0 );
19}
20
21/**
22 * Class responsible for handling the Jetpack Stats product
23 */
24class Stats extends Hybrid_Product {
25    /**
26     * The product slug
27     *
28     * @var string
29     */
30    public static $slug = 'stats';
31
32    /**
33     * The Jetpack module name associated with this product
34     *
35     * @var string|null
36     */
37    public static $module_name = 'stats';
38
39    /**
40     * The category of the product
41     *
42     * @var string
43     */
44    public static $category = 'growth';
45
46    /**
47     * The Plugin slug associated with stats
48     *
49     * @var string|null
50     */
51    public static $plugin_slug = 'jetpack-stats';
52
53    /**
54     * The Plugin file associated with stats
55     *
56     * @var string[]
57     */
58    public static $plugin_filename = array(
59        'jetpack-stats/jetpack-stats.php',
60        'stats/jetpack-stats.php',
61        'jetpack-stats-dev/jetpack-stats.php',
62    );
63
64    /**
65     * Stats only requires site connection, not user connection
66     *
67     * @var bool
68     */
69    public static $requires_user_connection = false;
70
71    /**
72     * Whether this product has a free offering
73     *
74     * @var bool
75     */
76    public static $has_free_offering = true;
77
78    /**
79     * The feature slug that identifies the paid plan
80     *
81     * @var string
82     */
83    public static $feature_identifying_paid_plan = 'stats-paid';
84
85    /**
86     * Get the product name
87     *
88     * @return string
89     */
90    public static function get_name() {
91        return 'Stats';
92    }
93
94    /**
95     * Get the product title
96     *
97     * @return string
98     */
99    public static function get_title() {
100        return 'Jetpack Stats';
101    }
102
103    /**
104     * Get the internationalized product description
105     *
106     * @return string
107     */
108    public static function get_description() {
109        return __( 'Clear, concise, and actionable analysis of your site performance.', 'jetpack-my-jetpack' );
110    }
111
112    /**
113     * Get the internationalized product long description
114     *
115     * @return string
116     */
117    public static function get_long_description() {
118        return __( 'With Jetpack Stats, you don’t need to be a data scientist to see how your site is performing, understand your visitors, and grow your site.', 'jetpack-my-jetpack' );
119    }
120
121    /**
122     * Get the internationalized features list
123     *
124     * @return array Stats features list
125     */
126    public static function get_features() {
127        return array(
128            __( 'Real-time data on visitors', 'jetpack-my-jetpack' ),
129            __( 'Traffic stats and trends for post and pages', 'jetpack-my-jetpack' ),
130            __( 'Detailed statistics about links leading to your site', 'jetpack-my-jetpack' ),
131            __( 'GDPR compliant', 'jetpack-my-jetpack' ),
132            /* translators: UTM refers to the Urchin Tracking Module campaign parameters appended to a URL. */
133            __( 'UTM tracking', 'jetpack-my-jetpack' ),
134            __( 'Device stats', 'jetpack-my-jetpack' ),
135            __( 'Region and city locations', 'jetpack-my-jetpack' ),
136            __( 'Priority support', 'jetpack-my-jetpack' ),
137        );
138    }
139
140    /**
141     * Get the product pricing details
142     * Only showing the pricing details for the commercial product
143     *
144     * @return array Pricing details
145     */
146    public static function get_pricing_for_ui() {
147        return array_merge(
148            array(
149                'available'          => true,
150                'wpcom_product_slug' => static::get_wpcom_product_slug(),
151            ),
152            Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
153        );
154    }
155
156    /**
157     * Get the WPCOM product slug used to make the purchase
158     *
159     * @return ?string
160     */
161    public static function get_wpcom_product_slug() {
162        return 'jetpack_stats_yearly';
163    }
164
165    /**
166     * Get the WPCOM Pay Whatever You Want product slug used to make the purchase
167     *
168     * @return ?string
169     */
170    public static function get_wpcom_pwyw_product_slug() {
171        return 'jetpack_stats_pwyw_yearly';
172    }
173
174    /**
175     * Get the WPCOM free product slug
176     *
177     * @return ?string
178     */
179    public static function get_wpcom_free_product_slug() {
180        return 'jetpack_stats_free_yearly';
181    }
182
183    /**
184     * Gets the 'status' of the Stats product
185     *
186     * @return string
187     */
188    public static function get_status() {
189        $status = parent::get_status();
190        if ( in_array( $status, array( Products::STATUS_MODULE_DISABLED, Products::STATUS_NEEDS_PLAN ), true ) && ! Initializer::is_registered() ) {
191            // If the site has never been connected before, show the "Learn more" CTA,
192            // that points to the add Stats product interstitial.
193            return Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
194        }
195        if ( Products::STATUS_NEEDS_PLAN === $status ) {
196            // Recognizing the standalone plugin makes the base class ask an unowned site to
197            // buy a plan while that plugin is inactive, but Stats is free from the Jetpack
198            // plugin, so the card keeps offering activation.
199            $status = Products::STATUS_NEEDS_ACTIVATION;
200        }
201        return $status;
202    }
203    /**
204     * Checks whether the product can be upgraded to a different product.
205     * Stats Commercial plan (highest tier) & Jetpack Complete are not upgradable.
206     * Also we don't push PWYW users to upgrade.
207     *
208     * @return boolean
209     */
210    public static function is_upgradable() {
211        // For now, atomic sites with stats are not in a position to upgrade
212        if ( ( new Host() )->is_woa_site() ) {
213            return false;
214        }
215
216        $purchases_data = Wpcom_Products::get_site_current_purchases();
217        if ( ! is_wp_error( $purchases_data ) && is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
218            foreach ( $purchases_data as $purchase ) {
219                // Jetpack complete includes Stats commercial & cannot be upgraded
220                if (
221                    str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ||
222                    str_starts_with( $purchase->product_slug, 'jetpack_growth' )
223                ) {
224                    return false;
225                } elseif (
226                    // Stats commercial purchased with highest tier cannot be upgraded.
227                    in_array(
228                        $purchase->product_slug,
229                        array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
230                        true
231                    ) && $purchase->current_price_tier_slug === 'more_than_1m_views'
232                ) {
233                    return false;
234                } elseif (
235                    // If user already has Stats PWYW, we won't push them to upgrade.
236                    $purchase->product_slug === 'jetpack_stats_pwyw_yearly'
237                ) {
238                    return false;
239                }
240            }
241        }
242        return true;
243    }
244
245    /**
246     * Get the product-slugs of the paid plans for this product (not including bundles)
247     *
248     * @return array
249     */
250    public static function get_paid_plan_product_slugs() {
251        return array(
252            'jetpack_stats_yearly',
253            'jetpack_stats_monthly',
254            'jetpack_stats_bi_yearly',
255            'jetpack_stats_pwyw_yearly',
256        );
257    }
258
259    /**
260     * Returns a productType parameter for an upgrade URL, determining whether
261     * to show the PWYW upgrade interstitial or commercial upgrade interstitial.
262     *
263     * @return string
264     */
265    public static function get_url_product_type() {
266        $purchases_data     = Wpcom_Products::get_site_current_purchases();
267        $is_commercial_site = Initializer::is_commercial_site();
268        if ( is_wp_error( $purchases_data ) ) {
269            return $is_commercial_site ? '&productType=commercial' : '';
270        }
271        if ( $is_commercial_site ) {
272            return '&productType=commercial';
273        }
274        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
275            foreach ( $purchases_data as $purchase ) {
276                if (
277                    str_starts_with( $purchase->product_slug, static::get_wpcom_free_product_slug() )
278                ) {
279                    return '&productType=personal';
280                } elseif (
281                    in_array(
282                        $purchase->product_slug,
283                        array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
284                        true
285                    ) &&
286                    ! empty( $purchase->current_price_tier_slug )
287                ) {
288                    return '&productType=commercial';
289                }
290            }
291        }
292        return '';
293    }
294
295    /**
296     * Checks whether the product supports trial or not.
297     * Since Jetpack Stats has been widely available as a free product in the past, it "supports" a trial.
298     *
299     * @return boolean
300     */
301    public static function has_trial_support() {
302        return true;
303    }
304
305    /**
306     * Mirrors `Analytics::MENU_PAGE_SLUG`, spelled out because My Jetpack does not
307     * depend on the premium-analytics package.
308     *
309     * @since 5.42.0
310     */
311    const PREMIUM_ANALYTICS_PAGE_SLUG = 'jetpack-premium-analytics-wp-admin';
312
313    /**
314     * Whether the Premium Analytics dashboard is the site's analytics UI.
315     *
316     * Guarded like the other `class_exists( 'Jetpack' )` checks here: My Jetpack
317     * also ships in plugins without the Jetpack plugin. Public so the UI flags
318     * report the same answer the URLs are built from.
319     *
320     * @since 5.42.0
321     *
322     * @return bool
323     */
324    public static function is_premium_analytics_enabled() {
325        return class_exists( 'Jetpack' )
326            && method_exists( 'Jetpack', 'is_premium_analytics_enabled' )
327            && \Jetpack::is_premium_analytics_enabled();
328    }
329
330    /**
331     * Get the WordPress.com URL for purchasing Jetpack Stats for the current site.
332     *
333     * Null once Premium Analytics is the analytics UI: the tier purchase
334     * screen was a Calypso route inside the Odyssey bundle, so it left with that
335     * dashboard. Null is also the base-class default, which falls the action
336     * button back to the existing `#/add-stats` interstitial.
337     *
338     * @return ?string
339     */
340    public static function get_purchase_url() {
341        if ( self::is_premium_analytics_enabled() ) {
342            return null;
343        }
344
345        $status = static::get_status();
346        if ( $status === Products::STATUS_NEEDS_FIRST_SITE_CONNECTION ) {
347            return null;
348        }
349        // The returning URL could be customized by changing the `redirect_uri` param with relative path.
350        return sprintf(
351            '%s#!/stats/purchase/%d?from=jetpack-my-jetpack%s&redirect_uri=%s',
352            admin_url( 'admin.php?page=stats' ),
353            Jetpack_Options::get_option( 'id' ),
354            static::get_url_product_type(),
355            rawurlencode( 'admin.php?page=stats' )
356        );
357    }
358
359    /**
360     * Get the URL the user is taken to after activating the product
361     *
362     * @return ?string
363     */
364    public static function get_post_activation_url() {
365        // Names the plan the Free-vs-Paid question was already answered with here, so the Stats
366        // dashboard's own pricing grid renders the dashboard instead of asking it a second time,
367        // and records the choice as free rather than as one it cannot name.
368        return add_query_arg( 'stats_plan_chosen', 'free', static::get_manage_url() );
369    }
370
371    /**
372     * Get the URL where the user manages the product
373     *
374     * @return ?string
375     */
376    public static function get_manage_url() {
377        if ( self::is_premium_analytics_enabled() ) {
378            return admin_url( 'admin.php?page=' . self::PREMIUM_ANALYTICS_PAGE_SLUG );
379        }
380
381        return admin_url( 'admin.php?page=stats' );
382    }
383
384    /**
385     * Return product bundles list
386     * that supports the product.
387     *
388     * @return boolean|array Products bundle list.
389     */
390    public static function is_upgradable_by_bundle() {
391        return array( 'growth', 'complete' );
392    }
393}