Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.67% covered (danger)
1.67%
1 / 60
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Initial_State
1.67% covered (danger)
1.67%
1 / 60
14.29% covered (danger)
14.29%
1 / 7
619.27
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
56
 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 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 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. Skipped on WordPress.com Simple, where the connection gate is
145            // bypassed (Simple sites are inherently wpcom-connected).
146            'pricing'                => ( ( new Host() )->is_wpcom_simple() || ( new Connection_Manager() )->is_connected() ) ? null : $this->get_pricing_data(),
147        );
148    }
149
150    /**
151     * Product description, feature list, and yearly price for the
152     * pre-connection upsell (a port of the legacy dashboard's pricing table).
153     *
154     * Backed by `Plan::get_product_price()`, which issues a synchronous WPCOM
155     * request, so this only runs for disconnected sites. Returns null when the
156     * product or price data isn't available (e.g. the WPCOM request fails), in
157     * which case the gate falls back to the plain connect screen.
158     *
159     * @return array|null The upsell payload, or null when it can't be built.
160     */
161    private function get_pricing_data() {
162        $site_product  = My_Jetpack_Products::get_product( 'videopress' );
163        $product_price = Plan::get_product_price();
164
165        if ( ! is_array( $site_product ) || ! isset( $product_price['yearly'] ) ) {
166            return null;
167        }
168
169        return array(
170            'title'    => isset( $site_product['description'] ) ? (string) $site_product['description'] : '',
171            'features' => isset( $site_product['features'] ) ? array_values( (array) $site_product['features'] ) : array(),
172            'yearly'   => $product_price['yearly'],
173        );
174    }
175
176    /**
177     * Whether the site has any paid VideoPress feature flag active.
178     *
179     * Matches the active-features check used by the existing
180     * `videopress/v1/features` REST endpoint: any of the storage tiers
181     * counts as paid access. On WPCOM the legacy `videopress` slug also
182     * grants access.
183     *
184     * @return bool
185     */
186    public static function has_videopress_access() {
187        // Any paid storage tier grants access; on the WPCOM platform (Simple or
188        // Atomic) the legacy `videopress` slug does too. No Simple special-case is
189        // needed here: each has_videopress_feature() call already routes through the
190        // Simple-local wpcom_site_has_feature() check under IS_WPCOM, and
191        // is_wpcom_platform() is true on Simple, so the third term reduces to
192        // wpcom_site_has_feature( 'videopress' ) there — the check the old early
193        // return performed, minus the redundancy.
194        return self::has_videopress_feature( 'videopress-1tb-storage' )
195            || self::has_videopress_feature( 'videopress-unlimited-storage' )
196            || ( ( new Host() )->is_wpcom_platform() && self::has_videopress_feature( 'videopress' ) );
197    }
198
199    /**
200     * Whether the named feature appears in the WPCOM active-features list.
201     *
202     * @param string $feature_slug Feature slug as returned by WPCOM (e.g. `videopress-1tb-storage`).
203     * @return bool
204     */
205    private static function has_videopress_feature( $feature_slug ) {
206        if ( ( new Host() )->is_wpcom_simple() ) {
207            return function_exists( 'wpcom_site_has_feature' ) && (bool) wpcom_site_has_feature( $feature_slug );
208        }
209
210        $features = Product::get_site_features_from_wpcom();
211
212        if ( is_wp_error( $features ) ) {
213            return false;
214        }
215
216        $active = $features['active'] ?? array();
217
218        return in_array( $feature_slug, $active, true );
219    }
220
221    /**
222     * Render the initial state into a JavaScript variable.
223     *
224     * @return string
225     */
226    public function render() {
227        return 'var JPVIDEOPRESS_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
228    }
229}