Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (warning)
80.00%
56 / 70
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Initial_State
80.00% covered (warning)
80.00%
56 / 70
37.50% covered (danger)
37.50%
3 / 8
34.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 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 attach_to
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_data
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
7
 get_pricing_data
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
12.17
 has_videopress_access
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 has_videopress_feature
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 render
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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        self::attach_to( self::SCRIPT_HANDLE );
83    }
84
85    /**
86     * Inline the initial-state payload, and the connection state, before a script.
87     *
88     * @since $$next-version$$
89     *
90     * @param string $handle A registered script handle.
91     * @return void
92     */
93    public static function attach_to( $handle ) {
94        wp_add_inline_script( $handle, ( new self() )->render(), 'before' );
95
96        // Hydrate the JP connection store (`window.JP_CONNECTION_INITIAL_STATE`)
97        // so shared connection-aware hooks — notably
98        // `useProductCheckoutWorkflow`, which powers the upgrade CTA — work on
99        // the modernized dashboard exactly as they do on the legacy one. This
100        // also sets `window.jpTracksContext.blog_id` for `@automattic/jetpack-analytics`.
101        Connection_Initial_State::render_script( $handle );
102    }
103
104    /**
105     * Get the initial state data.
106     *
107     * @return array
108     */
109    private function get_data() {
110        $gmt_offset      = get_option( 'gmt_offset' );
111        $timezone_string = get_option( 'timezone_string' );
112        $home_host       = wp_parse_url( get_site_url(), PHP_URL_HOST );
113
114        return array(
115            'API'                    => array(
116                'WP_API_root'  => esc_url_raw( rest_url() ),
117                'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
118                'contentNonce' => wp_create_nonce( 'videopress-content-nonce' ),
119            ),
120            'jetpackStatus'          => array(
121                'calypsoSlug' => ( new Status() )->get_site_suffix(),
122            ),
123            'product'                => array(
124                // Fed to `useProductCheckoutWorkflow` as the product to purchase.
125                'slug' => self::VIDEOPRESS_PRODUCT_SLUG,
126            ),
127            'siteData'               => array(
128                'id'                    => Jetpack_Options::get_option( 'id' ),
129                'title'                 => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(),
130                'adminUrl'              => esc_url_raw( admin_url() ),
131                'slug'                  => is_string( $home_host ) ? $home_host : '',
132                'gmtOffset'             => is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0,
133                'timezoneString'        => is_string( $timezone_string ) ? $timezone_string : '',
134                'locale'                => str_replace( '_', '-', (string) get_locale() ),
135                // Paid-tier capability check. Drives the free-tier UX (callout /
136                // DataViews configuration) once designer input lands. Backed by
137                // `Product::get_site_features_from_wpcom()`, which caches the WPCOM
138                // `/sites/%d/features` response in a 15-second site transient. On a
139                // cache miss this issues a synchronous WPCOM request that blocks
140                // page rendering until it returns.
141                'hasVideoPressAccess'   => self::has_videopress_access(),
142                'isVideoPress1TB'       => self::has_videopress_feature( 'videopress-1tb-storage' ),
143                'isVideoPressUnlimited' => self::has_videopress_feature( 'videopress-unlimited-storage' ),
144            ),
145            'assets'                 => array(
146                'buildUrl' => plugins_url( '../build/', __FILE__ ),
147            ),
148            // Feature gates mirrored from the PHP-side filters so the client can
149            // hide gated UI without a round trip.
150            'features'               => array(
151                'chaptersEditor' => Admin_UI::is_chapters_editor_enabled(),
152                'trimCut'        => Admin_UI::is_trim_cut_enabled(),
153            ),
154            // Authoritative map of accepted upload types (extension => mimetype),
155            // so the dashboard's drag-and-drop filter accepts exactly what the
156            // VideoPress backend supports rather than guessing client-side.
157            'allowedVideoExtensions' => Admin_UI::get_allowed_video_extensions(),
158            // Product/pricing payload for the pre-connection upsell. Only
159            // populated when the site isn't connected — the only time the
160            // connection gate renders the upsell — so connected dashboards never
161            // incur the synchronous WPCOM pricing request `get_pricing_data()`
162            // makes. Skipped on WordPress.com Simple, where the connection gate is
163            // bypassed (Simple sites are inherently wpcom-connected).
164            'pricing'                => ( ( new Host() )->is_wpcom_simple() || ( new Connection_Manager() )->is_connected() ) ? null : $this->get_pricing_data(),
165        );
166    }
167
168    /**
169     * Product description, feature list, and yearly price for the
170     * pre-connection upsell (a port of the legacy dashboard's pricing table).
171     *
172     * Backed by `Plan::get_product_price()`, which issues a synchronous WPCOM
173     * request, so this only runs for disconnected sites. Returns null when the
174     * product or price data isn't available (e.g. the WPCOM request fails), in
175     * which case the gate falls back to the plain connect screen.
176     *
177     * @return array|null The upsell payload, or null when it can't be built.
178     */
179    private function get_pricing_data() {
180        $site_product  = My_Jetpack_Products::get_product( 'videopress' );
181        $product_price = Plan::get_product_price();
182
183        // Plan::get_product_price() returns a WP_Error when the WPCOM products
184        // request doesn't come back 200, and `isset()` on a non-ArrayAccess
185        // object is a fatal in PHP 8 — so the array check has to come first.
186        if ( ! is_array( $site_product ) || ! is_array( $product_price ) || ! isset( $product_price['yearly'] ) ) {
187            return null;
188        }
189
190        return array(
191            'title'    => isset( $site_product['description'] ) ? (string) $site_product['description'] : '',
192            'features' => isset( $site_product['features'] ) ? array_values( (array) $site_product['features'] ) : array(),
193            'yearly'   => $product_price['yearly'],
194        );
195    }
196
197    /**
198     * Whether the site has any paid VideoPress feature flag active.
199     *
200     * Matches the active-features check used by the existing
201     * `videopress/v1/features` REST endpoint: any of the storage tiers
202     * counts as paid access. On WPCOM the legacy `videopress` slug also
203     * grants access.
204     *
205     * @return bool|null Null when site features could not be retrieved.
206     */
207    public static function has_videopress_access() {
208        $feature_slugs = array( 'videopress-1tb-storage', 'videopress-unlimited-storage' );
209        if ( ( new Host() )->is_wpcom_platform() ) {
210            $feature_slugs[] = 'videopress';
211        }
212
213        foreach ( $feature_slugs as $feature_slug ) {
214            $has_feature = self::has_videopress_feature( $feature_slug );
215            if ( null === $has_feature || $has_feature ) {
216                return $has_feature;
217            }
218        }
219
220        return false;
221    }
222
223    /**
224     * Whether the named feature appears in the WPCOM active-features list.
225     *
226     * @param string $feature_slug Feature slug as returned by WPCOM (e.g. `videopress-1tb-storage`).
227     * @return bool|null Null when site features could not be retrieved.
228     */
229    private static function has_videopress_feature( $feature_slug ) {
230        if ( ( new Host() )->is_wpcom_simple() ) {
231            return function_exists( 'wpcom_site_has_feature' ) && (bool) wpcom_site_has_feature( $feature_slug );
232        }
233
234        $features = Product::get_site_features_from_wpcom();
235
236        if ( is_wp_error( $features ) ) {
237            return null;
238        }
239
240        $active = $features['active'] ?? array();
241
242        return in_array( $feature_slug, $active, true );
243    }
244
245    /**
246     * Render the initial state into a JavaScript variable.
247     *
248     * @return string
249     */
250    public function render() {
251        return 'var JPVIDEOPRESS_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
252    }
253}