Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.03% covered (warning)
54.03%
114 / 211
61.11% covered (warning)
61.11%
22 / 36
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Ai
54.07% covered (warning)
54.07%
113 / 209
61.11% covered (warning)
61.11%
22 / 36
1152.19
0.00% covered (danger)
0.00%
0 / 1
 get_plugin_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_plugin_filename
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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_tiers
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 get_features_by_tier
n/a
0 / 0
n/a
0 / 0
4
 get_current_usage_tier
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 get_next_usage_tier
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
14.11
 get_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_long_description_by_usage_tier
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 get_long_description
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_features_by_usage_tier
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 get_features
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_pricing_for_ui_by_usage_tier
15.38% covered (danger)
15.38%
4 / 26
0.00% covered (danger)
0.00%
0 / 1
58.07
 get_pricing_for_ui
22.22% covered (danger)
22.22%
8 / 36
0.00% covered (danger)
0.00%
0 / 1
11.53
 get_wpcom_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_wpcom_monthly_product_slug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_wpcom_bi_yearly_product_slug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_paid_plan_product_slugs
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 is_upgradable
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
12.03
 get_post_checkout_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_post_activation_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_feature_ui_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_feature_ui_waiting_for_owner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_feature_ui_ready
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 get_manage_url
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_plugin_installed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_plugin_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_active
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_activated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_module_active
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_ai_assistant_feature
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
6.17
 are_tier_plans_enabled
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 is_site_connected
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extend_plugin_action_links
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 admin_enqueue_scripts
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 add_ai_block
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2/**
3 * AI product
4 *
5 * @package my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack\Products;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\My_Jetpack\Initializer;
12use Automattic\Jetpack\My_Jetpack\Module_Product;
13use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
14use Automattic\Jetpack\Status\Host;
15use WP_Post;
16
17if ( ! defined( 'ABSPATH' ) ) {
18    exit( 0 );
19}
20
21/**
22 * Class responsible for handling the Jetpack AI product
23 */
24class Jetpack_Ai extends Module_Product {
25
26    const CURRENT_TIER_SLUG  = 'free';
27    const UPGRADED_TIER_SLUG = 'upgraded';
28
29    /**
30     * How many requests a site gets before it has to upgrade.
31     */
32    const FREE_REQUESTS = 20;
33
34    /**
35     * The product slug
36     *
37     * @var string
38     */
39    public static $slug = 'jetpack-ai';
40
41    /**
42     * The Jetpack module name associated with this product
43     *
44     * @var string
45     */
46    public static $module_name = 'ai';
47
48    /**
49     * The category of the product
50     *
51     * @var string
52     */
53    public static $category = 'create';
54
55    /**
56     * Whether this product has a free offering
57     *
58     * @var bool
59     */
60    public static $has_free_offering = true;
61
62    /**
63     * The feature slug that identifies the paid plan
64     *
65     * @var string
66     */
67    public static $feature_identifying_paid_plan = 'ai-assistant';
68
69    /**
70     * Get the plugin slug - ovewrite it and return Jetpack's
71     *
72     * @return ?string
73     */
74    public static function get_plugin_slug() {
75        return self::JETPACK_PLUGIN_SLUG;
76    }
77
78    /**
79     * Get the plugin filename - ovewrite it and return Jetpack's
80     *
81     * @return ?string
82     */
83    public static function get_plugin_filename() {
84        return self::JETPACK_PLUGIN_FILENAME;
85    }
86
87    /**
88     * Get the product name
89     *
90     * @return string
91     */
92    public static function get_name() {
93        return 'AI';
94    }
95
96    /**
97     * Get the product title
98     *
99     * @return string
100     */
101    public static function get_title() {
102        return 'Jetpack AI';
103    }
104
105    /**
106     * Get the product's available tiers
107     *
108     * @return string[] Slugs of the available tiers
109     */
110    public static function get_tiers() {
111        if ( ! self::are_tier_plans_enabled() ) {
112            return parent::get_tiers();
113        }
114
115        return array(
116            self::UPGRADED_TIER_SLUG,
117            self::CURRENT_TIER_SLUG,
118        );
119    }
120
121    /**
122     * Get the internationalized comparison of free vs upgraded features
123     *
124     * @deprecated 6.10.0
125     *
126     * @return array[] Jetpack AI features comparison
127     */
128    public static function get_features_by_tier() {
129        _deprecated_function( __METHOD__, 'jetpack-my-jetpack-6.10.0' );
130
131        if ( ! self::are_tier_plans_enabled() ) {
132            return array();
133        }
134
135        $current_tier        = self::get_current_usage_tier();
136        $current_description = 0 === $current_tier
137            /* translators: %d is the number of free requests, such as 20. */
138            ? sprintf( _n( 'Up to %d request', 'Up to %d requests', self::FREE_REQUESTS, 'jetpack-my-jetpack' ), self::FREE_REQUESTS )
139            /* translators: number of requests */
140            : sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $current_tier );
141        $next_tier        = self::get_next_usage_tier();
142        $next_description = $next_tier === null
143            ? __( 'Let\'s get in touch', 'jetpack-my-jetpack' )
144            /* translators: number of requests */
145            : sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $next_tier );
146
147        return array(
148            array(
149                'name'  => __( 'Number of requests', 'jetpack-my-jetpack' ),
150                'info'  => array(
151                    'title'   => __( 'Requests', 'jetpack-my-jetpack' ),
152                    'content' => __( 'Increase your monthly request limit. Upgrade now and have the option to further increase your requests with additional upgrades.', 'jetpack-my-jetpack' ),
153                ),
154                'tiers' => array(
155                    self::CURRENT_TIER_SLUG  => array(
156                        'included'    => true,
157                        'description' => $current_description,
158                    ),
159                    self::UPGRADED_TIER_SLUG => array(
160                        'included'    => true,
161                        'description' => $next_description,
162                    ),
163                ),
164            ),
165            array(
166                'name'  => __( 'Generate and edit content', 'jetpack-my-jetpack' ),
167                'tiers' => array(
168                    self::CURRENT_TIER_SLUG  => array( 'included' => true ),
169                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
170                ),
171            ),
172            array(
173                'name'  => __( 'Build forms from prompts', 'jetpack-my-jetpack' ),
174                'tiers' => array(
175                    self::CURRENT_TIER_SLUG  => array( 'included' => true ),
176                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
177                ),
178            ),
179            array(
180                'name'  => __( 'Get feedback on posts', 'jetpack-my-jetpack' ),
181                'tiers' => array(
182                    self::CURRENT_TIER_SLUG  => array( 'included' => true ),
183                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
184                ),
185            ),
186            array(
187                'name'  => __( 'Generate featured images', 'jetpack-my-jetpack' ),
188                'tiers' => array(
189                    self::CURRENT_TIER_SLUG  => array( 'included' => true ),
190                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
191                ),
192            ),
193        );
194    }
195
196    /**
197     * Get the current usage tier
198     *
199     * @return int
200     */
201    public static function get_current_usage_tier() {
202        if ( ! self::is_site_connected() ) {
203            return 0;
204        }
205
206        $info = self::get_ai_assistant_feature();
207
208        // Bail early if it's not possible to fetch the feature data.
209        if ( is_wp_error( $info ) ) {
210            return 0;
211        }
212
213        $current_tier = $info['current-tier']['value'] ?? null;
214
215        return $current_tier;
216    }
217
218    /**
219     * Get the next usage tier
220     *
221     * @return int
222     */
223    public static function get_next_usage_tier() {
224        if ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) {
225            // without site connection we can't know if tiers are enabled or not,
226            // hence we can't know if the next tier is 100 or 1 (unlimited).
227            return 100;
228        }
229
230        $info = self::get_ai_assistant_feature();
231
232        // Bail early if it's not possible to fetch the feature data or if it's included in a plan.
233        if ( is_wp_error( $info ) || empty( $info ) ) {
234            return null;
235        }
236
237        // Trust the next tier provided by the feature data.
238        $next_tier = $info['next-tier']['value'] ?? null;
239
240        return $next_tier;
241    }
242
243    /**
244     * Get the internationalized product description
245     *
246     * @return string
247     */
248    public static function get_description() {
249        return __( 'Turn your ideas into ready‑to‑publish content at lightspeed.', 'jetpack-my-jetpack' );
250    }
251
252    /**
253     * Get the internationalized usage tier long description by tier
254     *
255     * @param int|null $tier The usage tier.
256     * @return string
257     */
258    public static function get_long_description_by_usage_tier( $tier ) {
259        switch ( (int) $tier ) {
260            case 1:
261                return __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' );
262            case 100:
263                return __( 'The most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
264            default:
265                return __( 'Upgrade and increase the amount of your available monthly requests to continue using the most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
266        }
267    }
268
269    /**
270     * Get the internationalized product long description
271     *
272     * @return string
273     */
274    public static function get_long_description() {
275        $next_tier = self::get_next_usage_tier();
276
277        return self::get_long_description_by_usage_tier( $next_tier );
278    }
279
280    /**
281     * Get the internationalized usage tier features by tier
282     *
283     * @param int $tier The usage tier.
284     * @return string
285     */
286    public static function get_features_by_usage_tier( $tier ) {
287        $is_tier_plan = $tier && intval( $tier ) > 1;
288
289        if ( $tier === 100 && ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) ) {
290            // in these cases, get_next_usage_tier() will return 100
291            // 100 is fine as default when tiered plans are enabled, but not otherwise
292            $is_tier_plan = false;
293        }
294
295        $features = array(
296            __( 'High request capacity *', 'jetpack-my-jetpack' ),
297            __( 'Generate text, tables, lists, and forms', 'jetpack-my-jetpack' ),
298            __( 'Easily refine content to your liking', 'jetpack-my-jetpack' ),
299            __( 'Make your content easier to read', 'jetpack-my-jetpack' ),
300            __( 'Generate images with one-click', 'jetpack-my-jetpack' ),
301            __( 'Optimize your titles for better performance', 'jetpack-my-jetpack' ),
302            __( 'Priority support', 'jetpack-my-jetpack' ),
303        );
304
305        $tiered_features = array(
306            __( 'Prompt based content generation', 'jetpack-my-jetpack' ),
307            __( 'Generate text, tables, and lists', 'jetpack-my-jetpack' ),
308            __( 'Adaptive tone adjustment', 'jetpack-my-jetpack' ),
309            __( 'Superior spelling and grammar correction', 'jetpack-my-jetpack' ),
310            __( 'Title & summary generation', 'jetpack-my-jetpack' ),
311            __( 'Priority support', 'jetpack-my-jetpack' ),
312            /* translators: %d is the number of requests. */
313            sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $tier ),
314        );
315
316        return $is_tier_plan ? $tiered_features : $features;
317    }
318
319    /**
320     * Get the internationalized features list
321     *
322     * @return array Jetpack AI features list
323     */
324    public static function get_features() {
325        $next_tier = self::get_next_usage_tier();
326
327        return self::get_features_by_usage_tier( $next_tier );
328    }
329
330    /**
331     * Get the product pricing details by tier
332     *
333     * @param int|null $tier The usage tier.
334     * @return array Pricing details
335     */
336    public static function get_pricing_for_ui_by_usage_tier( $tier ) {
337        if ( $tier === null ) {
338            return array();
339        }
340
341        $product = Wpcom_Products::get_product( static::get_wpcom_product_slug() );
342
343        if ( empty( $product ) ) {
344            return array();
345        }
346
347        $tier_plans_enabled = self::are_tier_plans_enabled();
348
349        /*
350         * when tiers are enabled and the price tier list is empty,
351         * we may need to renew the cache for the product data so
352         * we get the new price tier list.
353         *
354         * if the list is still empty after the fresh data, we will
355         * default to empty pricing (by returning an empty array).
356         */
357        if ( empty( $product->price_tier_list ) && $tier_plans_enabled ) {
358            $product = Wpcom_Products::get_product( static::get_wpcom_product_slug(), true );
359        }
360
361        // get the base pricing for the unlimited plan, for compatibility
362        $base_pricing = Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() );
363
364        $price_tier_list = $product->price_tier_list;
365        $yearly_prices   = array();
366
367        foreach ( $price_tier_list as $price_tier ) {
368            if ( isset( $price_tier->maximum_units ) && isset( $price_tier->maximum_price ) ) {
369                    // The prices are in cents
370                    $yearly_prices[ $price_tier->maximum_units ] = $price_tier->maximum_price / 100;
371            }
372        }
373
374        // add the base pricing to the list
375        $prices = array( 1 => $base_pricing );
376
377        foreach ( $yearly_prices as $units => $price ) {
378            $prices[ $units ] = array_merge(
379                $base_pricing,
380                array(
381                    'full_price'            => $price,
382                    'discount_price'        => $price,
383                    'is_introductory_offer' => false,
384                    'introductory_offer'    => null,
385                )
386            );
387        }
388
389        return $prices[ $tier ] ?? array();
390    }
391
392    /**
393     * Get the product pricing details
394     *
395     * @return array Pricing details
396     */
397    public static function get_pricing_for_ui() {
398        // no tiers
399        if ( ! self::are_tier_plans_enabled() ) {
400            return array_merge(
401                array(
402                    'available'          => true,
403                    'wpcom_product_slug' => static::get_wpcom_product_slug(),
404                ),
405                // hardcoding 1 as next tier if tiers are not enabled
406                self::get_pricing_for_ui_by_usage_tier( 1 )
407            );
408        }
409
410        $next_tier              = self::get_next_usage_tier();
411        $current_tier           = self::get_current_usage_tier();
412        $current_call_to_action = $current_tier === 0
413            ? __( 'Continue for free', 'jetpack-my-jetpack' )
414            : __( 'I\'m fine with my plan, thanks', 'jetpack-my-jetpack' );
415        $next_call_to_action    = $next_tier === null
416            ? __( 'Contact Us', 'jetpack-my-jetpack' )
417            : __( 'Upgrade', 'jetpack-my-jetpack' );
418
419        return array(
420            'tiers' => array(
421                self::CURRENT_TIER_SLUG  => array_merge(
422                    self::get_pricing_for_ui_by_usage_tier( $current_tier ),
423                    array(
424                        'available'      => true,
425                        'is_free'        => true,
426                        'call_to_action' => $current_call_to_action,
427                    )
428                ),
429                self::UPGRADED_TIER_SLUG => array_merge(
430                    self::get_pricing_for_ui_by_usage_tier( $next_tier ),
431                    array(
432                        'wpcom_product_slug' => static::get_wpcom_product_slug(),
433                        'quantity'           => $next_tier,
434                        'call_to_action'     => $next_call_to_action,
435                    )
436                ),
437            ),
438        );
439    }
440
441    /**
442     * Get the WPCOM product slug used to make the purchase
443     *
444     * @return string
445     */
446    public static function get_wpcom_product_slug() {
447        return 'jetpack_ai_yearly';
448    }
449
450    /**
451     * Get the WPCOM monthly product slug used to make the purchase
452     *
453     * @return string
454     */
455    public static function get_wpcom_monthly_product_slug() {
456        return 'jetpack_ai_monthly';
457    }
458
459    /**
460     * Get the WPCOM bi-yearly product slug used to make the purchase
461     *
462     * @return string
463     */
464    public static function get_wpcom_bi_yearly_product_slug() {
465        return 'jetpack_ai_bi_yearly';
466    }
467
468    /**
469     * Get the product-slugs of the paid plans for this product.
470     * (Do not include bundle plans, unless it's a bundle plan itself).
471     *
472     * @return array
473     */
474    public static function get_paid_plan_product_slugs() {
475        return array(
476            'jetpack_ai_yearly',
477            'jetpack_ai_monthly',
478            'jetpack_ai_bi_yearly',
479        );
480    }
481
482    /**
483     * Checks whether the product can be upgraded to a different product.
484     *
485     * @return boolean
486     */
487    public static function is_upgradable() {
488        $has_ai_feature     = static::does_site_have_feature( 'ai-assistant' );
489        $tier_plans_enabled = self::are_tier_plans_enabled();
490        $current_tier       = self::get_current_usage_tier();
491
492        if ( $has_ai_feature && ! $tier_plans_enabled && $current_tier >= 1 ) {
493            return false;
494        }
495
496        $next_tier = self::get_next_usage_tier();
497
498        // The check below is debatable, not having the feature should not flag as not upgradable.
499        // If user is free (tier = 0), not unlimited (tier = 1) and has a next tier, then it's upgradable.
500        if ( $current_tier !== null && $current_tier !== 1 && $next_tier ) {
501            return true;
502        }
503
504        // Mark as not upgradable if user is on unlimited tier or does not have any plan.
505        if ( ! $has_ai_feature || null === $current_tier || 1 === $current_tier ) {
506            return false;
507        }
508
509        return true;
510    }
511
512    /**
513     * Get the URL the user is taken after purchasing the product through the checkout
514     *
515     * @return ?string
516     */
517    public static function get_post_checkout_url() {
518        return self::get_manage_url();
519    }
520
521    /**
522     * Get the URL the user is taken after activating the product through the checkout
523     *
524     * @return ?string
525     */
526    public static function get_post_activation_url() {
527        return self::get_manage_url();
528    }
529
530    /**
531     * Whether My Jetpack should surface the AI feature controls.
532     *
533     * @return bool
534     */
535    public static function is_feature_ui_enabled() {
536        return self::is_feature_ui_ready() && ( new Connection_Manager() )->has_connected_owner();
537    }
538
539    /**
540     * Whether My Jetpack's AI feature controls are waiting only for the site's owner to connect.
541     *
542     * @since $$next-version$$
543     *
544     * @return bool True when Jetpack is active with its AI module and settings, the site is registered,
545     *              it is self-hosted or an Atomic internal testing environment, and no owner is connected yet.
546     */
547    public static function is_feature_ui_waiting_for_owner() {
548        return self::is_feature_ui_ready() && ! ( new Connection_Manager() )->has_connected_owner();
549    }
550
551    /**
552     * Whether the AI feature controls could show on this site, leaving out whether its owner is connected.
553     *
554     * @return bool
555     */
556    private static function is_feature_ui_ready() {
557        if (
558            ! self::is_plugin_active()
559            || ! method_exists( '\\Jetpack', 'is_module' )
560            || ! \Jetpack::is_module( 'ai' )
561            // @phan-suppress-next-line PhanUndeclaredClassReference -- Supplied by the optional Jetpack plugin.
562            || ! method_exists( '\\Jetpack_AI_Settings', 'is_feature_enabled' )
563            || ! self::is_site_connected()
564        ) {
565            return false;
566        }
567
568        $host = new Host();
569        if ( ! $host->is_wpcom_platform() ) {
570            return true;
571        }
572
573        return $host->is_woa_site()
574            && function_exists( 'jetpack_is_internal_testing_environment' )
575            && jetpack_is_internal_testing_environment();
576    }
577
578    /**
579     * Get the URL where the user manages the product.
580     *
581     * @return string
582     */
583    public static function get_manage_url() {
584        if ( self::is_feature_ui_enabled() ) {
585            return admin_url( 'admin.php?page=jetpack-ai' );
586        }
587
588        return admin_url( 'admin.php?page=my-jetpack#/jetpack-ai' );
589    }
590
591    /**
592     * Checks whether the plugin is installed
593     *
594     * @return boolean
595     */
596    public static function is_plugin_installed() {
597        return self::is_jetpack_plugin_installed();
598    }
599
600    /**
601     * Checks whether the plugin is active
602     *
603     * @return boolean
604     */
605    public static function is_plugin_active() {
606        return (bool) static::is_jetpack_plugin_active();
607    }
608
609    /**
610     * Checks whether the Product is active
611     *
612     * Overrides Module_Product::is_active() to also respect the jetpack_ai_enabled
613     * filter. The parent already checks that the Jetpack plugin and the 'ai' module
614     * are active; this override layers the host/master-off signal on top so the
615     * product card reflects it.
616     *
617     * @return boolean
618     */
619    public static function is_active() {
620        /**
621         * Filter to enable/disable Jetpack AI.
622         *
623         * @since 5.28.3
624         *
625         * @param boolean $enabled True if Jetpack AI should be enabled, false otherwise. Default true.
626         */
627        $is_enabled = apply_filters( 'jetpack_ai_enabled', true );
628
629        return $is_enabled && parent::is_active();
630    }
631
632    /**
633     * Checks whether the site has switched the product on, respecting the jetpack_ai_enabled filter.
634     *
635     * @return boolean
636     */
637    public static function is_activated() {
638        /** This filter is documented in projects/packages/my-jetpack/src/products/class-jetpack-ai.php */
639        return apply_filters( 'jetpack_ai_enabled', true ) && parent::is_activated();
640    }
641
642    /**
643     * Whether the 'ai' module backs this product's UI state.
644     *
645     * @return bool
646     */
647    public static function is_module_active() {
648        if ( ! self::is_feature_ui_enabled() ) {
649            return true;
650        }
651
652        return parent::is_module_active();
653    }
654
655    /**
656     * Get data about the AI Assistant feature
657     *
658     * @return array
659     */
660    public static function get_ai_assistant_feature() {
661        // Bail early if the plugin is not active.
662        if ( ! self::is_jetpack_plugin_installed() ) {
663            return array();
664        }
665
666        // Check if the global constant is defined.
667        if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
668            return array();
669        }
670
671        // Bail early if the site is not connected.
672        if ( ! self::is_site_connected() ) {
673            return array();
674        }
675
676        // Check if class exists. If not, try to require it once.
677        if ( ! class_exists( 'Jetpack_AI_Helper' ) ) {
678            $class_file_path = JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php';
679
680            // Check whether the file exists
681            if ( ! file_exists( $class_file_path ) ) {
682                return array();
683            }
684
685            require_once $class_file_path;
686        }
687
688        return \Jetpack_AI_Helper::get_ai_assistance_feature();
689    }
690
691    /**
692     * Get the AI Assistant tiered plans status
693     *
694     * @return boolean
695     */
696    public static function are_tier_plans_enabled() {
697        $info = self::get_ai_assistant_feature();
698        if ( is_wp_error( $info ) ) {
699            // this is another faulty default value, we'll assume disabled while
700            // production is enabled
701            return false;
702        }
703
704        if ( ! empty( $info ) && isset( $info['tier-plans-enabled'] ) ) {
705            return boolval( $info['tier-plans-enabled'] );
706        }
707        return false;
708    }
709
710    /**
711     * Checks whether the site is connected to WordPress.com.
712     *
713     * @return boolean
714     */
715    private static function is_site_connected() {
716        return ( new Connection_Manager() )->is_connected();
717    }
718
719    /**
720     * Get the URL where the user manages the product
721     *
722     * NOTE: this method is the only thing that resembles an initialization for the product.
723     *
724     * @return void
725     */
726    public static function extend_plugin_action_links() {
727        add_action( 'myjetpack_enqueue_scripts', array( static::class, 'admin_enqueue_scripts' ) );
728        add_filter( 'default_content', array( static::class, 'add_ai_block' ), 10, 2 );
729    }
730
731    /**
732     * Enqueue the AI Assistant script
733     *
734     * The script is just a global variable used for the nonce, needed for the create post link.
735     *
736     * @return void
737     */
738    public static function admin_enqueue_scripts() {
739        wp_register_script(
740            'my_jetpack_ai_app',
741            false,
742            array(),
743            Initializer::PACKAGE_VERSION,
744            array( 'in_footer' => true )
745        );
746        wp_localize_script(
747            'my_jetpack_ai_app',
748            'jetpackAi',
749            array(
750                'nonce' => wp_create_nonce( 'ai-assistant-content-nonce' ),
751            )
752        );
753        wp_enqueue_script( 'my_jetpack_ai_app' );
754    }
755
756    /**
757     * Add AI block to the post content
758     *
759     * Used only from the link on the product page, the filter will insert an AI Assistant block in the post content.
760     *
761     * @param string  $content The post content.
762     * @param WP_Post $post The post object.
763     * @return string
764     */
765    public static function add_ai_block( $content, $post ) {
766        if ( isset( $_GET['use_ai_block'] ) && isset( $_GET['_wpnonce'] )
767            && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ai-assistant-content-nonce' )
768            && ! empty( $post )
769            && ! is_wp_error( $post )
770            && current_user_can( 'edit_post', $post->ID )
771            && '' === $content
772        ) {
773            return '<!-- wp:jetpack/ai-assistant /-->';
774        }
775        return $content;
776    }
777}