Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 363
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
WP_Admin
0.00% covered (danger)
0.00%
0 / 363
0.00% covered (danger)
0.00%
0 / 16
3906
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 load_php_textdomain
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 register_page
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 page_title
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 is_loading
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 register_assets
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
20
 enqueue_block_assets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 exit_to_dashboard
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 initialize
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 mark_sitegen_generated_themes
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
 set_onboarding_restart_option
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 can_restart_onboarding
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
12
 hide_onboarding_restart_card
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_site_editor_assets
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 enqueue_preview_fonts
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
650
1<?php
2namespace NewfoldLabs\WP\Module\Onboarding;
3
4use NewfoldLabs\WP\Module\Onboarding\Data\Data;
5use NewfoldLabs\WP\Module\Onboarding\Data\Options;
6use NewfoldLabs\WP\Module\Onboarding\Services\PluginService;
7use NewfoldLabs\WP\Module\Onboarding\Services\ThemeService;
8use NewfoldLabs\WP\Module\Onboarding\Data\Services\FlowService;
9use NewfoldLabs\WP\Module\Onboarding\Data\Services\SiteGenService;
10use NewfoldLabs\WP\Module\Onboarding\Data\Themes;
11use NewfoldLabs\WP\Module\Onboarding\Services\I18nService;
12use NewfoldLabs\WP\Module\Onboarding\Services\ReduxStateService;
13use NewfoldLabs\WP\Module\Onboarding\Services\StatusService;
14
15/**
16 * Register Admin Page, Assets & Admin functionality with WordPress.
17 */
18final class WP_Admin {
19
20    /**
21     * Identifier for page and assets.
22     *
23     * @var string
24     */
25    public static $slug = 'nfd-onboarding';
26
27    /**
28     * Array of allowed referrers
29     *
30     * @var array
31     */
32    protected static $allowed_referrers = array(
33        'nfd-onboarding',
34        'nfd-plugin',
35    );
36
37    /**
38     * Tap WordPress Hooks
39     *
40     * @return void
41     */
42    public function __construct() {
43        \add_action( 'init', array( __CLASS__, 'load_php_textdomain' ) );
44        \add_action( 'admin_menu', array( __CLASS__, 'register_page' ) );
45        \add_action( 'load-dashboard_page_' . self::$slug, array( __CLASS__, 'page_title' ), 9, 1 );
46        \add_action( 'load-dashboard_page_' . self::$slug, array( __CLASS__, 'initialize' ) );
47        /**
48         * We're disabling the restart onboarding feature for now.
49         */
50        \add_action( 'load-toplevel_page_bluehost', array( __CLASS__, 'hide_onboarding_restart_card' ) );
51        // \add_action( 'load-themes.php', array( __CLASS__, 'can_restart_onboarding' ) );
52        \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_site_editor_assets' ) );
53        if ( 'sitegen' === Data::current_flow() ) {
54            \add_action( 'load-themes.php', array( __CLASS__, 'mark_sitegen_generated_themes' ) );
55            SiteGenService::pre_set_filter_wonder_blocks_transients();
56            SiteGenService::instantiate_sitegen_hooks();
57        }
58    }
59
60    /**
61     * Loads the textdomain for the module. This applies only to PHP strings.
62     *
63     * @return boolean
64     */
65    public static function load_php_textdomain() {
66        return I18nService::load_php_translations(
67            'wp-module-onboarding',
68            NFD_ONBOARDING_PLUGIN_DIRNAME . '/vendor/newfold-labs/wp-module-onboarding/languages'
69        );
70    }
71
72    /**
73     * Register WordPress Admin Page.
74     *
75     * By passing null to add_submenu_page, the page isn't displayed in the left Admin Menu,
76     * but is available from /wp-admin/index.php?page=nfd-onboarding.
77     *
78     * @return void
79     */
80    public static function register_page() {
81        \add_submenu_page(
82            '',
83            \__( 'Onboarding', 'wp-module-onboarding' ),
84            \__( 'Onboarding', 'wp-module-onboarding' ),
85            Permissions::ADMIN,
86            self::$slug,
87            array( __CLASS__, 'render' ),
88            100
89        );
90    }
91
92    /**
93     * Set the page title for the Onboarding page.
94     *
95     * @return void
96     * */
97    public static function page_title() {
98        if ( isset( $_GET['page'] ) && \sanitize_text_field( wp_unslash( $_GET['page'] ) ) === self::$slug ) {
99            global $title;
100            $title = \__( 'Onboarding', 'wp-module-onboarding' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
101        }
102    }
103
104    /**
105     * Render a loading screen.
106     *
107     * @return string
108     */
109    public static function is_loading() {
110        ob_start();
111        ?>
112        <style>
113            body.wp-admin {
114                overflow: hidden !important;
115            }
116            .nfd-onboarding-loading-app__skeleton {
117                position: relative;
118                overflow: hidden;
119                background-color: #DDE7F0;
120            }
121            .nfd-onboarding-loading-app__skeleton::after {
122                position: absolute;
123                top: 0;
124                right: 0;
125                bottom: 0;
126                left: 0;
127                transform: translateX(-100%);
128                background-image: linear-gradient(
129                    90deg,
130                    rgba(255, 255, 255, 0) 0,
131                    rgba(255, 255, 255, 0.2) 20%,
132                    rgba(255, 255, 255, 0.5) 60%,
133                    rgba(255, 255, 255, 0)
134                );
135                animation: nfd-skeleton-animation 2.5s infinite;
136                content: "";
137            }
138            @keyframes nfd-skeleton-animation {
139                100% {
140                    transform: translateX(100%);
141                }
142            }
143            #nfd-onboarding {
144                z-index: 100000;
145            }
146            .nfd-onboarding-loading-app {
147                background-color: #F1F5F9;
148                width: 100%;
149                opacity: 1;
150                transition: opacity 0.3s ease-in-out;
151            }
152            .nfd-onboarding-loading-app.fade-out {
153                opacity: 0;
154            }
155            .nfd-onboarding-loading-app__header {
156                background-color: #FFF;
157                border-bottom: 1px solid #ECEEFE;
158                display: flex;
159                justify-content: space-between;
160                align-items: center;
161                padding: 0 1.5rem;
162                min-height: 4rem;
163            }
164            .nfd-onboarding-loading-app__header__logo {
165                width: 135px;
166                height: 24px;
167                border-radius: 6px;
168            }
169            .nfd-onboarding-loading-app__header__exit {
170                width: 24px;
171                height: 24px;
172                border-radius: 6px;
173            }
174            .nfd-onboarding-loading-app__body {
175                position: absolute;
176                top: 0;
177                left: 0;
178                right: 0;
179                bottom: 0;
180                display: flex;
181                justify-content: center;
182                align-items: center;
183            }
184            .nfd-onboarding-loading-app__body__spinner {
185                width: 100px;
186                height: 100px;
187                margin-top: -65px;
188                border-radius: 12px;
189                opacity: 0.8;
190            }
191        </style>
192        <div class="nfd-onboarding-loading-app">
193            <header class="nfd-onboarding-loading-app__header">
194                <div class="nfd-onboarding-loading-app__header__logo nfd-onboarding-loading-app__skeleton"></div>
195                <div class="nfd-onboarding-loading-app__header__exit nfd-onboarding-loading-app__skeleton"></div>
196            </header>
197            <div class="nfd-onboarding-loading-app__body">
198                <div class="nfd-onboarding-loading-app__body__spinner nfd-onboarding-loading-app__skeleton"></div>
199            </div>
200        </div>
201        <?php
202        return ob_get_clean();
203    }
204
205    /**
206     * Render DOM element for React SPA mount.
207     *
208     * @return void
209     */
210    public static function render() {
211        echo PHP_EOL;
212        echo '<!-- NFD:ONBOARDING -->';
213        echo PHP_EOL;
214        echo '<div id="nfd-onboarding" class="nfd-onboarding-container">' . self::is_loading() . '</div>';
215        echo PHP_EOL;
216        echo '<!-- /NFD:ONBOARDING -->';
217        echo PHP_EOL;
218    }
219
220    /**
221     * Register built assets with WP_Dependency system.
222     *
223     * @return void
224     */
225    public static function register_assets() {
226        global $current_screen;
227
228        $current_screen->is_block_editor( true );
229
230        $asset_file = NFD_ONBOARDING_BUILD_DIR . '/onboarding.asset.php';
231
232        if ( is_readable( $asset_file ) ) {
233            $asset = include_once $asset_file;
234
235            \wp_register_script(
236                self::$slug,
237                NFD_ONBOARDING_BUILD_URL . '/onboarding.js',
238                array_merge( $asset['dependencies'], array() ),
239                $asset['version'],
240                true
241            );
242
243            I18nService::load_js_translations(
244                'wp-module-onboarding',
245                self::$slug,
246                NFD_ONBOARDING_DIR . '/languages'
247            );
248
249            $nfdOnboardingData = array(
250                'runtime' => Data::runtime(),
251                'input'   => ReduxStateService::get( 'input' ),
252                'sitegen' => ReduxStateService::get( 'sitegen' ),
253            );
254            \wp_add_inline_script(
255                self::$slug,
256                'var nfdOnboarding =' . wp_json_encode( $nfdOnboardingData ) . ';',
257                'before'
258            );
259
260            \wp_register_style(
261                self::$slug,
262                NFD_ONBOARDING_BUILD_URL . '/onboarding.css',
263                array( 'wp-components', 'wp-editor', 'wp-edit-blocks' ),
264                $asset['version']
265            );
266
267            wp_add_inline_script(
268                'wp-blocks',
269                'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
270            );
271
272            // Preload server-registered block bindings sources.
273            $registered_sources = get_all_registered_block_bindings_sources();
274            if ( ! empty( $registered_sources ) ) {
275                $filtered_sources = array();
276                foreach ( $registered_sources as $source ) {
277                    $filtered_sources[] = array(
278                        'name'        => $source->name,
279                        'label'       => $source->label,
280                        'usesContext' => $source->uses_context,
281                    );
282                }
283                $script = sprintf( 'for ( const source of %s ) { wp.blocks.registerBlockBindingsSource( source ); }', wp_json_encode( $filtered_sources ) );
284                wp_add_inline_script(
285                    'wp-blocks',
286                    $script
287                );
288            }
289
290            wp_add_inline_script(
291                'wp-blocks',
292                sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( get_block_categories( $block_editor_context ) ) ),
293                'after'
294            );
295
296            \wp_enqueue_script( self::$slug );
297            \wp_enqueue_style( self::$slug );
298
299            \add_action( 'enqueue_block_assets', array( __CLASS__, 'enqueue_block_assets' ) );
300        }
301    }
302
303    /**
304     * Enqueues the block assets for the live previews.
305     *
306     * @return void
307     */
308    public static function enqueue_block_assets() {
309        // This hook exists in the patterns module.
310        \do_action( 'enqueue_nfd_wonder_blocks_utilities' );
311    }
312
313    /**
314     * Redirects to the brand plugin page or the WordPress admin dashboard.
315     *
316     * @return void
317     */
318    public static function exit_to_dashboard(): bool {
319        $runtime_data              = Data::runtime();
320        $brand_plugin_url          = '';
321        $dashboard_redirect_params = 'referrer=' . self::$slug;
322
323        // Get the brand plugin page URL from the runtime data.
324        if (
325            isset( $runtime_data['currentBrand'], $runtime_data['currentBrand']['pluginDashboardPage'] ) &&
326            is_string( $runtime_data['currentBrand']['pluginDashboardPage'] )
327            ) {
328                // Set the brand plugin page URL.
329                $brand_plugin_url = $runtime_data['currentBrand']['pluginDashboardPage'];
330        }
331
332        // If the brand plugin page URL is not found in the runtime, redirect to the WordPress admin.
333        if ( empty( $brand_plugin_url ) ) {
334            wp_redirect( admin_url() . '?' . $dashboard_redirect_params );
335            exit;
336        }
337
338        // If the brand plugin page URL is found in the runtime, redirect to the brand plugin page.
339        wp_redirect( $brand_plugin_url . '&' . $dashboard_redirect_params );
340        exit;
341    }
342
343    /**
344     * Initialize Plugins and Themes if necessary.
345     *
346     * @return void
347     */
348    public static function initialize() {
349        if ( ! empty( $_GET['nfd_plugins'] ) && 'true' === sanitize_text_field( $_GET['nfd_plugins'] ) ) {
350            PluginService::initialize();
351        }
352
353        // Install and activate the default theme.
354        $default_theme_installation_result = ThemeService::initialize();
355        if ( ! $default_theme_installation_result ) {
356            self::exit_to_dashboard();
357        }
358
359        self::register_assets();
360
361        self::set_onboarding_restart_option();
362    }
363
364    /**
365     * Enqueue scripts that mark Sitegen flow generated themes.
366     *
367     * @return void
368     */
369    public static function mark_sitegen_generated_themes() {
370        $flow_data = get_option( Options::get_option_name( 'flow' ), false );
371
372        if ( ! $flow_data || ! isset( $flow_data['sitegen']['homepages'] ) ) {
373            return;
374        }
375
376        \wp_register_script(
377            'sitegen-theme-marker',
378            NFD_ONBOARDING_BUILD_URL . '/sitegen-theme-marker.js',
379            array(),
380            '1.0.0',
381            true
382        );
383
384        \wp_add_inline_script(
385            'sitegen-theme-marker',
386            'var nfdOnboarding =' . wp_json_encode(
387                array(
388                    'homepages' => $flow_data['sitegen']['homepages'],
389                    'active'    => Themes::get_active_theme(),
390                )
391            ) . ';',
392            'before'
393        );
394
395        \wp_register_style(
396            'sitegen-theme-marker',
397            NFD_ONBOARDING_BUILD_URL . '/sitegen-theme-marker.css.css',
398            array(),
399            '1.0.0',
400            'all'
401        );
402
403        \wp_enqueue_script( 'sitegen-theme-marker' );
404        \wp_enqueue_style( 'sitegen-theme-marker' );
405    }
406
407    /**
408     * Sets the option in DB for the Initial Load of Onboarding
409     *
410     * @return void
411     */
412    public static function set_onboarding_restart_option(): void {
413        // Check if the customer is eligible for onboarding restart
414        if ( StatusService::is_onboarding_restart_eligible() ) {
415            // Get the option name for 'can_restart'
416            $option_name = Options::get_option_name( 'can_restart' );
417
418            // Check if the option doesn't exist before adding it
419            if ( ! get_option( $option_name ) ) {
420                // Add the option if it doesn't exist
421                add_option( $option_name, true );
422            }
423        } else {
424            // Get the option name for 'can_restart'
425            $option_name = Options::get_option_name( 'can_restart' );
426
427            // Add the option if it doesn't exist
428            update_option( $option_name, false );
429        }
430    }
431
432    /**
433     * Enqueue scripts that adds a new button to Restart Onboarding in themes.php
434     *
435     * @return void
436     */
437    public static function can_restart_onboarding(): void {
438        $can_restart = get_option( Options::get_option_name( 'can_restart' ), false );
439
440        // If the customer in ineligible for restart don't enqueue scripts
441        if ( ! $can_restart || ! StatusService::is_onboarding_restart_eligible() ) {
442            return;
443        }
444
445        \wp_register_script(
446            'onboarding-restart-button',
447            NFD_ONBOARDING_BUILD_URL . '/onboarding-restart-button.js',
448            array(),
449            '1.0.0',
450            true
451        );
452
453        \wp_add_inline_script(
454            'onboarding-restart-button',
455            'var nfdOnboardingRestartMeta =' . wp_json_encode(
456                array(
457                    'buttonText' => \__( 'Build with AI', 'wp-module-onboarding' ),
458                    'buttonHref' => \admin_url( 'index.php?page=' . self::$slug ),
459                )
460            ) . ';',
461            'before'
462        );
463
464        \wp_register_style(
465            'onboarding-restart-button',
466            NFD_ONBOARDING_BUILD_URL . '/onboarding-restart-button.css.css',
467            array(),
468            '1.0.0',
469            'all'
470        );
471
472        /**
473         * Temporary: hide the build with ai button
474         */
475        wp_add_inline_style(
476            'onboarding-restart-button',
477            '.themes .theme.build-with-ai {
478                display: none !important;
479            }'
480        );
481
482        \wp_enqueue_script( 'onboarding-restart-button' );
483        \wp_enqueue_style( 'onboarding-restart-button' );
484    }
485
486    /**
487     * Temporary: Enqueue scripts that hides the build with ai button
488     *
489     * @return void
490     */
491    public static function hide_onboarding_restart_card(): void {
492        \wp_register_style(
493            'hide-onboarding-restart-card',
494            false,
495        );
496
497        \wp_add_inline_style(
498            'hide-onboarding-restart-card',
499            'div[data-testid="restartOnboarding"] {
500                display: none !important;
501            }'
502        );
503
504        \wp_enqueue_style( 'hide-onboarding-restart-card' );
505    }
506
507    /*
508    Enqueue site editor specific assets when coming from onboarding.
509     *
510     * @return void
511     */
512    public static function enqueue_site_editor_assets() {
513        global $pagenow;
514
515        // Only proceed if we're on site-editor.php and have the right referrer
516        if ( 'site-editor.php' === $pagenow &&
517            isset( $_GET['referrer'] ) &&
518            in_array( $_GET['referrer'], self::$allowed_referrers, true )
519        ) {
520
521            $asset_file = NFD_ONBOARDING_BUILD_DIR . '/onboarding-design-studio.asset.php';
522
523            if ( is_readable( $asset_file ) ) {
524                $asset = include_once $asset_file;
525
526                \wp_register_script(
527                    'nfd-design-studio',
528                    NFD_ONBOARDING_BUILD_URL . '/onboarding-design-studio.js',
529                    $asset['dependencies'] ?? array( 'wp-editor', 'wp-blocks', 'wp-components' ),
530                    $asset['version'] ?? '1.0.0',
531                    true
532                );
533
534                \wp_register_style(
535                    'nfd-design-studio',
536                    NFD_ONBOARDING_BUILD_URL . '/onboarding-design-studio.css.css',
537                    array( 'wp-components' ),
538                    $asset['version'] ?? '1.0.0'
539                );
540
541                \wp_enqueue_script( 'nfd-design-studio' );
542                \wp_enqueue_style( 'nfd-design-studio' );
543
544                // Enqueue fonts for preview
545                self::enqueue_preview_fonts();
546            }
547        }
548    }
549
550    /**
551     * Enqueue all available fonts for preview in design studio
552     *
553     * @return void
554     */
555    private static function enqueue_preview_fonts() {
556
557        $hiive_api_base = defined( 'NFD_DATA_WB_DEV_MODE' ) && NFD_DATA_WB_DEV_MODE
558            ? 'http://patterns-platform.test/api/v1'
559            : 'https://paterns.hiive.cloud/api/v1';
560
561        $font_pairs   = get_option( 'nfd_module_onboarding_editor_fontpair' );
562        $unique_fonts = array();
563
564        if ( $font_pairs && is_array( $font_pairs ) ) {
565            foreach ( $font_pairs as $font_pair ) {
566                if ( isset( $font_pair['font_heading_name'] ) ) {
567                    $unique_fonts[] = $font_pair['font_heading_name'];
568                }
569                if ( isset( $font_pair['font_content_name'] ) ) {
570                    $unique_fonts[] = $font_pair['font_content_name'];
571                }
572            }
573        }
574
575        $unique_fonts = array_unique( $unique_fonts );
576
577        foreach ( $unique_fonts as $font_id ) {
578            if ( empty( $font_id ) ) {
579                continue;
580            }
581
582            // Check if font data is already cached
583            $cache_key = 'nfd_font_data_' . sanitize_key( $font_id );
584            $font_data = get_transient( $cache_key );
585
586            if ( false === $font_data ) {
587                // Fetch font data from Hiive API
588                $response = wp_remote_get(
589                    $hiive_api_base . '/fonts/' . urlencode( sanitize_title( $font_id ) ),
590                    array(
591                        'timeout' => 10,
592                        'headers' => array(
593                            'Accept' => 'application/json',
594                        ),
595                    )
596                );
597
598                if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
599                    $body      = wp_remote_retrieve_body( $response );
600                    $font_data = json_decode( $body, true );
601
602                    set_transient( $cache_key, $font_data, HOUR_IN_SECONDS );
603                }
604            }
605
606            // Enqueue the font if we have valid data
607            if ( $font_data &&
608                isset( $font_data['wp_json']['fontFace'][0]['src'][0] ) &&
609                isset( $font_data['wp_json']['name'] )
610            ) {
611                $font_url    = $font_data['wp_json']['fontFace'][0]['src'][0];
612                $font_name   = $font_data['wp_json']['name'];
613                $font_handle = 'nfd-font-' . sanitize_title( $font_id );
614
615                // Create CSS for the font-face declaration
616                $font_css = '';
617                if ( isset( $font_data['wp_json']['fontFace'] ) && is_array( $font_data['wp_json']['fontFace'] ) ) {
618                    foreach ( $font_data['wp_json']['fontFace'] as $font_face ) {
619                        $font_css .= '@font-face {';
620                        $font_css .= 'font-family: "' . esc_attr( $font_face['fontFamily'] ) . '";';
621
622                        if ( isset( $font_face['fontWeight'] ) ) {
623                            $font_css .= 'font-weight: ' . esc_attr( $font_face['fontWeight'] ) . ';';
624                        }
625
626                        if ( isset( $font_face['fontStyle'] ) ) {
627                            $font_css .= 'font-style: ' . esc_attr( $font_face['fontStyle'] ) . ';';
628                        }
629
630                        if ( isset( $font_face['fontStretch'] ) ) {
631                            $font_css .= 'font-stretch: ' . esc_attr( $font_face['fontStretch'] ) . ';';
632                        }
633
634                        if ( isset( $font_face['src'] ) && is_array( $font_face['src'] ) ) {
635                            $src_declarations = array();
636                            foreach ( $font_face['src'] as $src ) {
637                                $src_declarations[] = 'url("' . esc_url( $src ) . '") format("woff2")';
638                            }
639                            $font_css .= 'src: ' . implode( ', ', $src_declarations ) . ';';
640                        }
641
642                        $font_css .= 'font-display: swap;';
643                        $font_css .= '}';
644                    }
645                }
646
647                // Register and enqueue the font style
648                \wp_register_style( $font_handle, false );
649                \wp_enqueue_style( $font_handle );
650                \wp_add_inline_style( $font_handle, $font_css );
651            }
652        }
653    }
654} // END /NewfoldLabs/WP/Module/Onboarding/Admin()