Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.72% covered (danger)
1.72%
1 / 58
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Initial_State
1.72% covered (danger)
1.72%
1 / 58
14.29% covered (danger)
14.29%
1 / 7
481.39
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 enqueue
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_data
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
42
 get_pricing_data
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 has_videopress_access
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 has_videopress_feature
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 render
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * The modernized VideoPress dashboard React initial state.
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8namespace Automattic\Jetpack\VideoPress;
9
10use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
11use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12use Automattic\Jetpack\My_Jetpack\Product;
13use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products;
14use Automattic\Jetpack\Status;
15use Automattic\Jetpack\Status\Host;
16use Jetpack_Options;
17use function admin_url;
18use function esc_url_raw;
19use function get_bloginfo;
20use function get_locale;
21use function get_option;
22use function get_site_url;
23use function plugins_url;
24use function rest_url;
25use function wp_add_inline_script;
26use function wp_create_nonce;
27use function wp_json_encode;
28use function wp_parse_url;
29use function wp_script_is;
30
31/**
32 * The modernized VideoPress dashboard React initial state.
33 *
34 * Mirrors the Activity Log dashboard pattern: a small structured payload
35 * inlined as `var JPVIDEOPRESS_INITIAL_STATE=...` before the wp-build boot
36 * script runs. Phases 6 and 8 will consume the payload via per-file
37 * `declare const` ambient types.
38 */
39class Initial_State {
40
41    const SCRIPT_HANDLE = 'jetpack-videopress-dashboard-wp-admin-prerequisites';
42
43    /**
44     * WPCOM product slug purchased by the dashboard's upgrade CTA.
45     *
46     * Mirrors the product `Plan::get_product()` resolves to
47     * (`$products->jetpack_videopress`). Kept as a constant rather than read
48     * from `Plan::get_product()` because that helper issues a synchronous
49     * WPCOM request, which we don't want to incur on every page render.
50     */
51    const VIDEOPRESS_PRODUCT_SLUG = 'jetpack_videopress';
52
53    /**
54     * Register the inline-state enqueue hook.
55     *
56     * Hooks `admin_enqueue_scripts` at priority 11 so the wp-build page
57     * (`build/pages/jetpack-videopress-dashboard/page-wp-admin.php`) has
58     * already registered the prerequisites handle at its default priority 10.
59     * The `wp_script_is( ..., 'registered' )` check inside the callback
60     * doubles as the page guard.
61     *
62     * @return void
63     */
64    public static function init() {
65        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue' ), 11 );
66    }
67
68    /**
69     * Inline the initial-state payload before the wp-build boot script.
70     *
71     * @return void
72     */
73    public static function enqueue() {
74        if ( ! Admin_UI::is_modernized() ) {
75            return;
76        }
77
78        if ( ! wp_script_is( self::SCRIPT_HANDLE, 'registered' ) ) {
79            return;
80        }
81
82        wp_add_inline_script( self::SCRIPT_HANDLE, ( new self() )->render(), 'before' );
83
84        // Hydrate the JP connection store (`window.JP_CONNECTION_INITIAL_STATE`)
85        // so shared connection-aware hooks — notably
86        // `useProductCheckoutWorkflow`, which powers the upgrade CTA — work on
87        // the modernized dashboard exactly as they do on the legacy one. This
88        // also sets `window.jpTracksContext.blog_id` for `@automattic/jetpack-analytics`.
89        Connection_Initial_State::render_script( self::SCRIPT_HANDLE );
90    }
91
92    /**
93     * Get the initial state data.
94     *
95     * @return array
96     */
97    private function get_data() {
98        $gmt_offset      = get_option( 'gmt_offset' );
99        $timezone_string = get_option( 'timezone_string' );
100        $home_host       = wp_parse_url( get_site_url(), PHP_URL_HOST );
101
102        return array(
103            'API'                    => array(
104                'WP_API_root'  => esc_url_raw( rest_url() ),
105                'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
106                'contentNonce' => wp_create_nonce( 'videopress-content-nonce' ),
107            ),
108            'jetpackStatus'          => array(
109                'calypsoSlug' => ( new Status() )->get_site_suffix(),
110            ),
111            'product'                => array(
112                // Fed to `useProductCheckoutWorkflow` as the product to purchase.
113                'slug' => self::VIDEOPRESS_PRODUCT_SLUG,
114            ),
115            'siteData'               => array(
116                'id'                    => Jetpack_Options::get_option( 'id' ),
117                'title'                 => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(),
118                'adminUrl'              => esc_url_raw( admin_url() ),
119                'slug'                  => is_string( $home_host ) ? $home_host : '',
120                'gmtOffset'             => is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0,
121                'timezoneString'        => is_string( $timezone_string ) ? $timezone_string : '',
122                'locale'                => str_replace( '_', '-', (string) get_locale() ),
123                // Paid-tier capability check. Drives the free-tier UX (callout /
124                // DataViews configuration) once designer input lands. Backed by
125                // `Product::get_site_features_from_wpcom()`, which caches the WPCOM
126                // `/sites/%d/features` response in a 15-second site transient. On a
127                // cache miss this issues a synchronous WPCOM request that blocks
128                // page rendering until it returns.
129                'hasVideoPressAccess'   => self::has_videopress_access(),
130                'isVideoPress1TB'       => self::has_videopress_feature( 'videopress-1tb-storage' ),
131                'isVideoPressUnlimited' => self::has_videopress_feature( 'videopress-unlimited-storage' ),
132            ),
133            'assets'                 => array(
134                'buildUrl' => plugins_url( '../build/', __FILE__ ),
135            ),
136            // Authoritative map of accepted upload types (extension => mimetype),
137            // so the dashboard's drag-and-drop filter accepts exactly what the
138            // VideoPress backend supports rather than guessing client-side.
139            'allowedVideoExtensions' => Admin_UI::get_allowed_video_extensions(),
140            // Product/pricing payload for the pre-connection upsell. Only
141            // populated when the site isn't connected — the only time the
142            // connection gate renders the upsell — so connected dashboards never
143            // incur the synchronous WPCOM pricing request `get_pricing_data()`
144            // makes.
145            'pricing'                => ( new Connection_Manager() )->is_connected() ? null : $this->get_pricing_data(),
146        );
147    }
148
149    /**
150     * Product description, feature list, and yearly price for the
151     * pre-connection upsell (a port of the legacy dashboard's pricing table).
152     *
153     * Backed by `Plan::get_product_price()`, which issues a synchronous WPCOM
154     * request, so this only runs for disconnected sites. Returns null when the
155     * product or price data isn't available (e.g. the WPCOM request fails), in
156     * which case the gate falls back to the plain connect screen.
157     *
158     * @return array|null The upsell payload, or null when it can't be built.
159     */
160    private function get_pricing_data() {
161        $site_product  = My_Jetpack_Products::get_product( 'videopress' );
162        $product_price = Plan::get_product_price();
163
164        if ( ! is_array( $site_product ) || ! isset( $product_price['yearly'] ) ) {
165            return null;
166        }
167
168        return array(
169            'title'    => isset( $site_product['description'] ) ? (string) $site_product['description'] : '',
170            'features' => isset( $site_product['features'] ) ? array_values( (array) $site_product['features'] ) : array(),
171            'yearly'   => $product_price['yearly'],
172        );
173    }
174
175    /**
176     * Whether the site has any paid VideoPress feature flag active.
177     *
178     * Matches the active-features check used by the existing
179     * `videopress/v1/features` REST endpoint: any of the storage tiers
180     * counts as paid access. On WPCOM the legacy `videopress` slug also
181     * grants access.
182     *
183     * @return bool
184     */
185    public static function has_videopress_access() {
186        return self::has_videopress_feature( 'videopress-1tb-storage' )
187            || self::has_videopress_feature( 'videopress-unlimited-storage' )
188            || ( ( new Host() )->is_wpcom_platform() && self::has_videopress_feature( 'videopress' ) );
189    }
190
191    /**
192     * Whether the named feature appears in the WPCOM active-features list.
193     *
194     * @param string $feature_slug Feature slug as returned by WPCOM (e.g. `videopress-1tb-storage`).
195     * @return bool
196     */
197    private static function has_videopress_feature( $feature_slug ) {
198        $features = Product::get_site_features_from_wpcom();
199
200        if ( is_wp_error( $features ) ) {
201            return false;
202        }
203
204        $active = $features['active'] ?? array();
205
206        return in_array( $feature_slug, $active, true );
207    }
208
209    /**
210     * Render the initial state into a JavaScript variable.
211     *
212     * @return string
213     */
214    public function render() {
215        return 'var JPVIDEOPRESS_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
216    }
217}