Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.63% covered (warning)
72.63%
69 / 95
78.95% covered (warning)
78.95%
15 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stats
73.12% covered (warning)
73.12%
68 / 93
78.95% covered (warning)
78.95%
15 / 19
89.91
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
50.00% covered (danger)
50.00%
9 / 18
0.00% covered (danger)
0.00%
0 / 1
30.00
 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's highest tier and bundle subscriptions are not upgradable here.
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                // Stats changes for bundle owners go through plan management.
220                if (
221                    str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ||
222                    str_starts_with( $purchase->product_slug, 'jetpack_pro_' ) ||
223                    str_starts_with( $purchase->product_slug, 'jetpack_growth' )
224                ) {
225                    return false;
226                } elseif (
227                    // Stats commercial purchased with highest tier cannot be upgraded.
228                    in_array(
229                        $purchase->product_slug,
230                        array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
231                        true
232                    ) && $purchase->current_price_tier_slug === 'more_than_1m_views'
233                ) {
234                    return false;
235                } elseif (
236                    // If user already has Stats PWYW, we won't push them to upgrade.
237                    $purchase->product_slug === 'jetpack_stats_pwyw_yearly'
238                ) {
239                    return false;
240                }
241            }
242        }
243        return true;
244    }
245
246    /**
247     * Get the product-slugs of the paid plans for this product (not including bundles)
248     *
249     * @return array
250     */
251    public static function get_paid_plan_product_slugs() {
252        return array(
253            'jetpack_stats_yearly',
254            'jetpack_stats_monthly',
255            'jetpack_stats_bi_yearly',
256            'jetpack_stats_pwyw_yearly',
257        );
258    }
259
260    /**
261     * Returns a productType parameter for an upgrade URL, determining whether
262     * to show the PWYW upgrade interstitial or commercial upgrade interstitial.
263     *
264     * @return string
265     */
266    public static function get_url_product_type() {
267        $purchases_data     = Wpcom_Products::get_site_current_purchases();
268        $is_commercial_site = Initializer::is_commercial_site();
269        if ( is_wp_error( $purchases_data ) ) {
270            return $is_commercial_site ? '&productType=commercial' : '';
271        }
272        if ( $is_commercial_site ) {
273            return '&productType=commercial';
274        }
275        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
276            foreach ( $purchases_data as $purchase ) {
277                if (
278                    str_starts_with( $purchase->product_slug, static::get_wpcom_free_product_slug() )
279                ) {
280                    return '&productType=personal';
281                } elseif (
282                    in_array(
283                        $purchase->product_slug,
284                        array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
285                        true
286                    ) &&
287                    ! empty( $purchase->current_price_tier_slug )
288                ) {
289                    return '&productType=commercial';
290                }
291            }
292        }
293        return '';
294    }
295
296    /**
297     * Checks whether the product supports trial or not.
298     * Since Jetpack Stats has been widely available as a free product in the past, it "supports" a trial.
299     *
300     * @return boolean
301     */
302    public static function has_trial_support() {
303        return true;
304    }
305
306    /**
307     * Mirrors `Analytics::MENU_PAGE_SLUG`, spelled out because My Jetpack does not
308     * depend on the premium-analytics package.
309     *
310     * @since 5.42.0
311     */
312    const PREMIUM_ANALYTICS_PAGE_SLUG = 'jetpack-premium-analytics-wp-admin';
313
314    /**
315     * Whether the Premium Analytics dashboard is the site's analytics UI.
316     *
317     * Guarded like the other `class_exists( 'Jetpack' )` checks here: My Jetpack
318     * also ships in plugins without the Jetpack plugin. Public so the UI flags
319     * report the same answer the URLs are built from.
320     *
321     * @since 5.42.0
322     *
323     * @return bool
324     */
325    public static function is_premium_analytics_enabled() {
326        return class_exists( 'Jetpack' )
327            && method_exists( 'Jetpack', 'is_premium_analytics_enabled' )
328            && \Jetpack::is_premium_analytics_enabled();
329    }
330
331    /**
332     * Get the WordPress.com URL for purchasing Jetpack Stats for the current site.
333     *
334     * Null once Premium Analytics is the analytics UI: the tier purchase
335     * screen was a Calypso route inside the Odyssey bundle, so it left with that
336     * dashboard. Null is also the base-class default, which falls the action
337     * button back to the existing `#/add-stats` interstitial.
338     *
339     * @return ?string
340     */
341    public static function get_purchase_url() {
342        if ( self::is_premium_analytics_enabled() ) {
343            return null;
344        }
345
346        $status = static::get_status();
347        if ( $status === Products::STATUS_NEEDS_FIRST_SITE_CONNECTION ) {
348            return null;
349        }
350        // The returning URL could be customized by changing the `redirect_uri` param with relative path.
351        return sprintf(
352            '%s#!/stats/purchase/%d?from=jetpack-my-jetpack%s&redirect_uri=%s',
353            admin_url( 'admin.php?page=stats' ),
354            Jetpack_Options::get_option( 'id' ),
355            static::get_url_product_type(),
356            rawurlencode( 'admin.php?page=stats' )
357        );
358    }
359
360    /**
361     * Get the URL the user is taken to after activating the product
362     *
363     * @return ?string
364     */
365    public static function get_post_activation_url() {
366        // Names the plan the Free-vs-Paid question was already answered with here, so the Stats
367        // dashboard's own pricing grid renders the dashboard instead of asking it a second time,
368        // and records the choice as free rather than as one it cannot name.
369        return add_query_arg( 'stats_plan_chosen', 'free', static::get_manage_url() );
370    }
371
372    /**
373     * Get the URL where the user manages the product
374     *
375     * @return ?string
376     */
377    public static function get_manage_url() {
378        if ( self::is_premium_analytics_enabled() ) {
379            return admin_url( 'admin.php?page=' . self::PREMIUM_ANALYTICS_PAGE_SLUG );
380        }
381
382        return admin_url( 'admin.php?page=stats' );
383    }
384
385    /**
386     * Return product bundles list
387     * that supports the product.
388     *
389     * @return boolean|array Products bundle list.
390     */
391    public static function is_upgradable_by_bundle() {
392        return array( 'growth', 'complete' );
393    }
394}