Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.10% covered (danger)
3.10%
7 / 226
5.26% covered (danger)
5.26%
1 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Solutions
3.10% covered (danger)
3.10%
7 / 226
5.26% covered (danger)
5.26%
1 / 19
2057.03
0.00% covered (danger)
0.00%
0 / 1
 add_solutions_admin_body_class
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 init_entitilements_apis
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_plugins_solutions_menu_link
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 add_nfd_subnav
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 render_solutions_page
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add_dummy_solutions_menu_link
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 handle_solutions_redirect
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 old_solutions_redirect
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 solutions_page_assets
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
20
 solutions_page_component_assets
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 addnew_brand_solutions_tab
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 render_nfd_solutions_tab
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 addnew_plugins_solutions_assets
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
12
 get_newfold_solutions_payload
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 add_branding_primary_inline_style
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_enhanced_entitlment_data
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 check_jetpack_connection_redirect
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 add_jetpack_menu_link
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace NewfoldLabs\WP\Module\Solutions;
4
5use NewfoldLabs\WP\ModuleLoader\Container;
6use NewfoldLabs\WP\Module\Solutions\I18nService;
7use NewfoldLabs\WP\Module\Data\HiiveConnection;
8use NewfoldLabs\WP\Module\Data\SiteCapabilities;
9
10use function NewfoldLabs\WP\ModuleLoader\container;
11
12/**
13 * Manages all the functionalities for the module.
14 */
15class Solutions {
16    /**
17     * Dependency injection container.
18     *
19     * @var Container
20     */
21    protected $container;
22
23    /**
24     * Entitlements API class instance.
25     *
26     * @var EntitlementsApi
27     */
28    protected static $entitlements_api;
29
30    /**
31     * Body class for standalone Solutions admin screens loaded by this module.
32     *
33     * @param string $classes Space-separated class list from WordPress core.
34     * @return string
35     */
36    public static function add_solutions_admin_body_class( $classes ) {
37        if ( ! is_admin() ) {
38            return $classes;
39        }
40        $screen = function_exists( 'get_current_screen' ) ? \get_current_screen() : null;
41        if ( isset( $screen->id ) && false !== strpos( $screen->id, 'solution' ) ) {
42            return trim( $classes . ' nfd-solutions-admin' );
43        }
44
45        return $classes;
46    }
47
48    /**
49     * Constructor for the Solutions class.
50     *
51     * @param Container $container The module container.
52     */
53    public function __construct( Container $container ) {
54        $hiive                  = new HiiveConnection();
55        self::$entitlements_api = new EntitlementsApi( $hiive );
56        $this->container        = $container;
57        \add_action( 'rest_api_init', array( $this, 'init_entitilements_apis' ) );
58        \add_action( 'admin_menu', array( __CLASS__, 'add_plugins_solutions_menu_link' ) );
59        \add_action( 'admin_menu', array( __CLASS__, 'add_jetpack_menu_link' ) );
60        \add_action( 'admin_init', array( __CLASS__, 'check_jetpack_connection_redirect' ) );
61
62        \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'solutions_page_assets' ) );
63        \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'solutions_page_component_assets' ) );
64        \add_action( 'admin_enqueue_scripts', array( $this, 'addnew_plugins_solutions_assets' ) );
65
66        \add_filter( 'admin_body_class', array( __CLASS__, 'add_solutions_admin_body_class' ), 99 );
67
68        \add_filter( 'nfd_plugin_subnav', array( $this, 'add_nfd_subnav' ) );
69        \add_action( 'admin_menu', array( __CLASS__, 'add_dummy_solutions_menu_link' ) );
70        \add_action( 'admin_init', array( __CLASS__, 'handle_solutions_redirect' ) );
71        \add_filter( 'install_plugins_tabs', array( $this, 'addnew_brand_solutions_tab' ), 99 );
72        \add_filter( 'install_plugins_nfd_solutions', array( $this, 'render_nfd_solutions_tab' ) );
73
74        new I18nService( $container );
75        new SolutionsUpsell( $container );
76    }
77
78    /**
79     * Initialize the Entitilement API Controller.
80     */
81    public function init_entitilements_apis(): void {
82        self::$entitlements_api->register_routes();
83    }
84
85    /**
86     * Add "My Solution" sub-link to admin plugins menu.
87     */
88    public static function add_plugins_solutions_menu_link() {
89        \add_submenu_page(
90            'plugins.php',
91            'My Solution',
92            __( 'My Solution', 'wp-module-solutions' ),
93            'manage_options',
94            'plugin-install.php?tab=nfd_solutions',
95            null,
96            3,
97        );
98    }
99
100    /**
101     * Add to the Newfold subnav.
102     *
103     * @param array $subnav The nav array.
104     * @return array The filtered nav array
105     */
106    public static function add_nfd_subnav( $subnav ) {
107        $brand       = container()->get( 'plugin' )['id'];
108        $solutions = array(
109            'title'    => __( 'Solutions', 'wp-module-solutions' ),
110            'route'    => $brand . '#/commerce',
111            'priority' => 10,
112        );
113        array_push( $subnav, $solutions );
114        return $subnav;
115    }
116
117    /**
118     * Render "Solutions" page root
119     *
120     * @return void
121     */
122    public static function render_solutions_page() {
123        echo '<div id="nfd-solutions-app"></div>';
124    }
125
126    /**
127     * Register dummy solutions menu page for redirect purposes
128     */
129    public static function add_dummy_solutions_menu_link() {
130        add_submenu_page(
131            '', // Using empty string as parent, so it won't appear in any menu
132            'Old Solutions',
133            '',
134            'manage_options',
135            'solutions',
136            array( __CLASS__, 'old_solutions_redirect' )
137        );
138    }
139
140    /**
141     * Handle performance redirect from old URL.
142     * This runs on admin_init to catch the redirect before headers are sent.
143     *
144     * @return void
145     */
146    public static function handle_solutions_redirect() {
147        if (
148            is_admin() &&
149            isset( $_GET['page'] ) &&
150            'solutions' === $_GET['page']
151        ) {
152            $new_url = admin_url( 'admin.php?page=' . container()->plugin()->id . '#/commerce' );
153            wp_safe_redirect( $new_url );
154            exit;
155        }
156    }
157
158    /**
159     * Redirects the user to the new solutions page.
160     * This is the callback for the dummy menu page.
161     *
162     * @return void
163     */
164    public static function old_solutions_redirect() {
165        // Fallback: redirect using JavaScript if headers already sent
166        $new_url = admin_url( 'admin.php?page=' . container()->plugin()->id . '#/commerce' );
167        ?>
168        <script>
169            window.location.href = '<?php echo esc_js( $new_url ); ?>';
170        </script>
171        <p>Redirecting to new solutions page...</p>
172        <?php
173    }
174
175    /**
176     * Enqueue assets and set locals.
177     */
178    public static function solutions_page_assets() {
179        $asset_file = NFD_SOLUTIONS_DIR . '/build/solutions-page/bundle.asset.php';
180        if ( is_readable( $asset_file ) ) {
181            $asset = include_once $asset_file;
182        } else {
183            return;
184        }
185
186        \wp_register_script(
187            'solutions-page',
188            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/solutions-page/bundle.js',
189            array_merge(
190                $asset['dependencies'],
191                array( 'nfd-installer' ),
192            ),
193            $asset['version'],
194            true
195        );
196
197        \wp_register_style(
198            'solutions-page-style-common',
199            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/solutions-page/style-solutions-page.css',
200            null,
201            $asset['version']
202        );
203
204        \wp_register_style(
205            'solutions-page-style',
206            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/solutions-page/solutions-page.css',
207            array( 'nfd-installer', 'solutions-page-style-common' ),
208            $asset['version']
209        );
210
211        // Only enqueue on solutions page
212        $screen = \get_current_screen();
213        if ( isset( $screen->id ) && ( false !== strpos( $screen->id, 'solution' ) ) ) {
214            \wp_enqueue_script( 'solutions-page' );
215            \wp_enqueue_style( 'solutions-page-style' );
216            self::add_branding_primary_inline_style( 'solutions-page-style' );
217
218            \wp_localize_script(
219                'solutions-page',
220                'NewfoldSolutions',
221                self::get_newfold_solutions_payload()
222            );
223        }
224    }
225
226
227    /**
228     * Enqueue assets and set locals.
229     */
230    public static function solutions_page_component_assets() {
231        $asset_file = NFD_SOLUTIONS_DIR . '/build/solutions-page-component/index.asset.php';
232        if ( is_readable( $asset_file ) ) {
233            $asset = include_once $asset_file;
234        } else {
235            return;
236        }
237
238        \wp_register_script(
239            'solutions-page-component',
240            false,
241            null,
242            $asset['version']
243        );
244
245        \wp_register_style(
246            'solutions-page-component-style-common',
247            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/solutions-page-component/style-solutions-page-component.css',
248            null,
249            $asset['version']
250        );
251
252        \wp_register_style(
253            'solutions-page-component-style',
254            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/solutions-page-component/solutions-page-component.css',
255            array( 'nfd-installer', 'solutions-page-component-style-common' ),
256            $asset['version']
257        );
258
259        // Only enqueue on solutions page
260        $screen = \get_current_screen();
261        if ( isset( $screen->id ) && false !== strpos( $screen->id, container()->plugin()->id ) ) {
262            \wp_enqueue_style( 'solutions-page-component-style' );
263            self::add_branding_primary_inline_style( 'solutions-page-component-style' );
264
265            \wp_add_inline_script(
266                'solutions-page-component',
267                'window.NewfoldSolutions =' . wp_json_encode( self::get_newfold_solutions_payload() ) . ';',
268                'before'
269            );
270            \wp_enqueue_script( 'solutions-page-component' );
271        }
272    }
273
274    /**
275     * Add "Brand solution" tab to plugins install tabs.
276     *
277     * @param array $tabs Collection of tabs.
278     *
279     * @return array
280     */
281    public function addnew_brand_solutions_tab( $tabs ) {
282        $branding = SolutionsBranding::get_localization_branding( $this->container );
283        $name     = '';
284        if ( isset( $branding['brandDisplayName'] ) && is_string( $branding['brandDisplayName'] ) && '' !== trim( $branding['brandDisplayName'] ) ) {
285            $name = trim( $branding['brandDisplayName'] );
286        } else {
287            $name = ucfirst( (string) $this->container->plugin()->brand );
288        }
289        $solutions_tab = array( 'nfd_solutions' => $name . ' ' . __( 'Solutions', 'wp-module-solutions' ) );
290
291        // Append (not prepend) the Solutions tab so it is offered alongside the
292        // native tabs without becoming the default landing view. WordPress falls
293        // back to the first registered tab (`key( $tabs )` in
294        // WP_Plugin_Install_List_Table::prepare_items()) when no `tab` query arg
295        // is present, so prepending here replaced the native plugin
296        // search/install screen with the Solutions screen on every "Add New" click.
297        return array_merge( $tabs, $solutions_tab );
298    }
299
300    /**
301     * Render solutions section on "Add new" plugins section.
302     *
303     * @return void
304     */
305    public function render_nfd_solutions_tab() {
306        global $wp_list_table;
307        echo '<div id="nfd-add-new-app"></div>';
308
309        if ( isset( $wp_list_table ) ) {
310            ?>
311            <form id="plugin-filter" class="ndf-plugin-filter-section" method="post">
312                <?php $wp_list_table->display(); ?>
313            </form>
314            <?php
315        }
316    }
317
318    /**
319     * Enqueue assets and set locals for brand solutions on add plugins section.
320     *
321     * @param string $hook The current admin page.
322     */
323    public function addnew_plugins_solutions_assets( $hook ) {
324        $asset_file = NFD_SOLUTIONS_DIR . '/build/addnew/bundle.asset.php';
325        if ( is_readable( $asset_file ) ) {
326            $asset = include_once $asset_file;
327        } else {
328            return;
329        }
330
331        \wp_register_script(
332            'solutions-add-new-tools',
333            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/addnew/bundle.js',
334            array_merge(
335                $asset['dependencies'],
336                array( 'nfd-installer' ),
337            ),
338            $asset['version'],
339            true
340        );
341
342        \wp_register_style(
343            'solutions-add-new',
344            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/addnew/addnew.css',
345            null,
346            $asset['version']
347        );
348
349        \wp_register_style(
350            'solutions-add-new-style',
351            NFD_SOLUTIONS_PLUGIN_URL . 'vendor/newfold-labs/wp-module-solutions/build/addnew/style-addnew.css',
352            array( 'nfd-installer', 'solutions-add-new' ),
353            $asset['version']
354        );
355
356        // Only enqueue on plugin install page
357        if ( 'plugin-install.php' === $hook ) {
358            \wp_enqueue_script( 'solutions-add-new-tools' );
359            \wp_enqueue_style( 'solutions-add-new-style' );
360            self::add_branding_primary_inline_style( 'solutions-add-new-style' );
361
362            \wp_localize_script(
363                'solutions-add-new-tools',
364                'NewfoldSolutions',
365                array_merge(
366                    self::get_newfold_solutions_payload(),
367                    array(
368                        'siteUrl' => get_site_url(),
369                    )
370                )
371            );
372
373            \wp_add_inline_script(
374                'solutions-add-new-tools',
375                SolutionsBranding::get_plugin_install_tab_icon_inline_script( $this->container )
376            );
377        }
378    }
379
380    /**
381     * Entitlements merged with localization branding for `window.NewfoldSolutions`.
382     *
383     * @return array<string,mixed>
384     */
385    public static function get_newfold_solutions_payload(): array {
386        return array_merge(
387            self::get_enhanced_entitlment_data(),
388            array(
389                'branding' => SolutionsBranding::get_localization_branding( container() ),
390            )
391        );
392    }
393
394    /**
395     * Outputs inline CSS custom property for `--nfd-solutions-primary`.
396     *
397     * @param string $style_handle Style handle queued before calling.
398     * @return void
399     */
400    protected static function add_branding_primary_inline_style( $style_handle ) {
401        $hex = SolutionsBranding::get_primary_color_hex( container() );
402        $css = ':root{--nfd-solutions-primary:' . esc_attr( $hex ) . ';}';
403
404        wp_add_inline_style( $style_handle, $css );
405    }
406
407    /**
408     * Enhance the entitlements data with data regarding isActive on site.
409     *
410     * @return array The enhanced entitlements data.
411     */
412    public static function get_enhanced_entitlment_data() {
413        // get the entitlements data from the API (or from the transient if it exists)
414        $solutions_data = json_decode( \wp_json_encode( self::$entitlements_api->get_entitlements_data()->data ), true );
415
416        // validate response
417        if ( ! is_array( $solutions_data ) || empty( $solutions_data ) ) {
418            return EntitlementsApi::$default_response;
419        }
420        // check if entitlements data is present
421        if ( array_key_exists( 'entitlements', $solutions_data ) && is_array( $solutions_data['entitlements'] ) ) {
422            $solutions_data['entitlements'] = array_map(
423            // add isActive key to any entitlement that is active on the site
424                function ( $entitlement ) {
425                    $entitlement['isActive'] = is_plugin_active( $entitlement['basename'] );
426                    return $entitlement;
427                },
428                $solutions_data['entitlements']
429            );
430        }
431        return $solutions_data;
432    }
433
434    /**
435     * Checks Jetpack connection status and redirects the user accordingly.
436     *
437     * This function is triggered when visiting the admin page `admin.php?page=check-jetpack-connection`.
438     * It checks if Jetpack is connected using the Jetpack Connection Manager class.
439     * Based on the result, it redirects the user either to the Jetpack Forms responses page
440     * (if connected) or the Jetpack onboarding dashboard (if not connected).
441     *
442     * @return void
443     */
444    public static function check_jetpack_connection_redirect() {
445
446        if (
447            is_admin() &&
448            isset( $_GET['page'] ) &&
449            'check-jetpack-connection' === $_GET['page']
450        ) {
451            $connected = false;
452            // Use Jetpack's internal check if available
453            if ( class_exists( '\Automattic\Jetpack\Connection\Manager' ) ) {
454                $manager   = new \Automattic\Jetpack\Connection\Manager();
455                $connected = $manager->is_connected();
456            }
457            // Set your redirect destinations
458            $redirect_url_if_connected     = admin_url( 'admin.php?page=jetpack-forms-admin#/responses' ); // Feedback post type
459            $redirect_url_if_not_connected = admin_url( 'admin.php?page=jetpack#/dashboard' ); // Jetpack dashboard (onboarding)
460
461            wp_safe_redirect( $connected ? $redirect_url_if_connected : $redirect_url_if_not_connected );
462            exit;
463        }
464    }
465
466    /**
467     * Registers a hidden submenu page for checking Jetpack connection status.
468     */
469    public static function add_jetpack_menu_link() {
470
471        \add_submenu_page(
472            '', // Using empty string as parent, so it won't appear in any menu
473            'Jetpack Connection Check',
474            '',
475            'manage_options',
476            'check-jetpack-connection',
477            array( __CLASS__, 'check_jetpack_connection_redirect' ),
478        );
479    }
480}