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