8889841cPK&[** readme.txtnu[=== Extendify — Gutenberg Patterns and Templates === Contributors: extendify, richtabor, kbat82, clubkert, arturgrabo Tags: templates, patterns, layouts, blocks, gutenberg, layout, template, editor, library, page builder, gutenberg blocks, wordpress blocks Requires at least: 5.4 Tested up to: 6.1 Stable tag: 1.2.4 Requires PHP: 5.6 License: GPLv2 License URI: https://www.gnu.org/licenses/gpl-2.0.html The best WordPress templates, pattern, and layout library with 1,000+ designs built for the Gutenberg block editor. == Description == Extendify is the platform of site design and creation tools for people that want to build a beautiful WordPress website with a library of patterns and full page layouts for the Gutenberg block editor. Make a beautiful WordPress website easier and faster than ever before with Extendify's library of Gutenberg block patterns, templates, and page layouts for Gutenberg. = A library of Gutenberg block patterns, templates, and layouts = Our library of reusable website block patterns, templates, and full page layouts can be assembled to rapidly build beautiful websites or add to existing ones. These well-designed templates enable you to drag, drop and publish in WordPress, without a single line of code. = Core Gutenberg blocks = Get beautiful patterns and layouts without the added maintenance of third-party blocks or block collections, as our Gutenberg block patterns are made with core WordPress blocks whenever possible. = Built for your theme = Extendify is a theme-agnostic design experience platform that works with your Gutenberg-friendly WordPress theme or Block Theme — instantly leveling-up your editing and publishing flow today. If you change your theme, colors, typography, or any other element, your Extendify Gutenberg block patterns and full page layouts will adapt to the new design. Below is a partial list of themes that are fully supported and work great with Extendify: * GeneratePress * Neve * Genesis * Kadence * Storefront * Blocksy * Blockbase * Tove * Armando * Hansen * Ono * Bricksy * Naledi * Clove * Twenty Twenty-Two * Twenty Twenty-One * Twenty Twenty * Twenty Nineteen = Site Types = Extendify has tons of Gutenberg block patterns for all sorts of site types. Whether you’re publishing a site for a yoga studio, restaurant, dentist, or lawyer, select your site type in the library and see a curated selection of Gutenberg block patterns and full page layouts — with industry-specific images and copy. = Block Pattern Types = Choose from Gutenberg block pattern types for every part of your website, including: * About page patterns * Call to action patterns * Feature patterns * Gallery patterns * Headline patterns * Hero patterns * Team patterns * Text patterns = Layouts = Layouts are a full collection of Gutenberg block patterns that are put together in effective and compelling page templates. By selecting the “Layouts” tab at the top of the library, you’ll see the beautiful full page templates that you can then add to your site. = Free Imports = Users gets 5 free imports per site each calendar month. Free imports can be used with base Gutenberg block patterns, templates, and layouts. You can publish and use these elements on your site without a subscription to Extendify. = Extendify Pro = Upgrade to [Extendify Pro](https://extendify.com/pricing/) for unlimited access to the full library of beautiful Gutenberg block patterns, templates, and layouts. Extendify Pro includes access to our "Pro" patterns and layouts. Users can import any Gutenberg block patterns, templates, and layouts they wish and continue using them on their site forever. A subscription is not required for your Gutenberg block patterns, templates, and layouts to continue to work perfectly on your site. = Like Extendify? = - Follow us on [Twitter](https://www.twitter.com/extendifyinc). - Rate us on [WordPress](https://wordpress.org/support/plugin/extendify/reviews/?filter=5/#new-post) :) = Privacy = Extendify is a SaaS (software as a service) connector plugin that uses a custom API to fetch block patterns and page layouts from our servers. API requests are only made when a user clicks on the Library button. In order to provide and improve this service, Extendify passes site data along with an API request, including: * Browser * Referring site * Category selection * WP language * Active theme * Active plugins * Anonymized UUID * Anonymized IP address By activating the Extendify plugin and accessing the library, you agree to our [privacy policy](https://extendify.com/privacy-policy) and [terms of service](https://extendify.com/terms-of-service). == Installation == 1. Install using the WordPress plugin installer, or extract the zip file and drop the contents into the `wp-content/plugins/` directory of your WordPress site. 2. Activate the plugin through the 'Plugins' menu in WordPress. 3. Edit or create a new page on your site 4. Press the 'Library' button at the top left of the editor header 5. Browse and import beautiful patterns and full page layouts; click one to add it directly to the page. For documentation and tutorials, read our [guides](https://extendify.com/guides/?utm_source=wp-repo&utm_medium=link&utm_campaign=readme). == Frequently Asked Questions == **Can I use Extendify with any theme?** Yes! You can create a beautiful site in just a few clicks with just about any Gutenberg-friendly WordPress theme. **Can I use Extendify with WooCommerce?** Yes! Extendify is compatible with WooCommerce. You’ll need to install and configure WooCommerce separately to set up eCommerce functionality. **Is Extendify free?** Extendify is a free plugin available through the WordPress.org directory that allows users to extend the power of the Gutenberg Block Editor. Each user receives a limited number of imports completely free. We offer a paid subscription for folks who want unlimited access to all of our beautiful patterns and page layouts. **What is Extendify Pro?** Extendify Pro gives you unlimited access to the entire library of our patterns and page layouts, without restrictions. **Will Extendify slow down my website?** Nope! Extendify imports lightweight block-based content that is served directly from your WordPress site. Our templates use the latest WordPress technologies, leveraging the best that Gutenberg has to offer, without any of the bloat of traditional page builders. == Screenshots == 1. Select a site type/industry to use patterns with images and copy relevant to you 2. Quickly open the Extendify library with the Pattern Library block 3. The Extendify library, as seen with the Twenty Twenty Two block theme == Changelog == = 1.2.4 - 2023-01-19 = - Various bug fixes and improvements = 1.2.3 - 2022-12-14 = - Fixed a bug where comparing active plugins would crash the UI = 1.2.2 - 2022-12-02 = - Add automated testing - Various bug fixes and improvements = 1.2.1 - 2022-11-17 = - Various bug fixes and improvements - Update translations and comments = 1.2.0 - 2022-11-10 = - Improve transition/animation processing - Fix modal page jumps - Add automated testing = 1.1.0 - 2022-11-02 = - Update site type sort ordering - Optimize preview loading - Various bug fixes = 1.0.1 - 2022-10-20 = - Fixes a bug where page data was ignored = 1.0.0 - 2022-10-20 = - v1 release is here! 🚀 - Improved loading of patterns in live previews - More templates and more site types to chose from - Improved compatibility with Extendable theme - Fixed issue where som users would see API errors randomly - Better handling of page redirects = 0.11.1 - 2022-10-08 = - Fix a bug where page data wasn't loading = 0.11.0 - 2022-10-06 = - Fix issue with request headers - Update logic for admin page routing - Various bug fixes = 0.10.2 - 2022-09-20 = - Update side nav to show relevant menu pages - Various bug fixes = 0.10.1 - 2022-09-07 = - Add check for https in urls - Minor bug fixes = 0.10.0 - 2022-09-06 = - Add aspect ratio utility classes - Various bug fixes and optimizations = 0.9.5 - 2022-08-25 = - Various bug fixes and performance improvements - Update menu page logic - Update network request effeciency = 0.9.4 - 2022-08-12 = - Fix server error issues = 0.9.3 - 2022-08-11 = - Various bug fixes and performance improvements = 0.9.2 - 2022-07-29 = - Various bug fixes and performance improvements = 0.9.1 - 2022-07-19 = - Fix bug where library data isn't loading = 0.9.0 - 2022-07-19 = - Update UI/UX components - Add shared site type experience across users - Better handle asset and data loading - Various bug fixes and updates = 0.8.3 - 2022-06-09 = - Add API language support - Various bug fixes and updates = 0.8.3 - 2022-06-27 = - Minor translation updates - Various bug fixes and updates = 0.8.2 - 2022-05-26 = - Various bug fixes and updates = 0.8.1 - 2022-05-11 = - Remove forced full width on older themes - Added title tooltips to site type selector - Remove notices from admin page - Various bug fixes and performance improvements = 0.8.0 - 2022-04-26 = - Optimize live preview rendering - Add faster server backend - Add Extendify welcome page - Remove content idle timer - Update sidebar handle accessability styling - Updated wp tested up to value - Implement UI improvements to encourage discoverability = 0.7.0 - 2022-03-21 = - Add support for the Full Site Editor experience - Update SiteType UX component - Optimize layout rendering idle checker - Improve server error refresh - Fix library button on mobile = 0.6.0 - 2022-03-08 = - Add new design categories - Add ability to open library via search param - Tweak get_plugins call to not cache results - Improve toolbar on mobile = 0.5.0 - 2022-02-22 = - Add support for inverted style patterns - Improve library loading time - Add pattern library variants to open the library = 0.4.0 - 2022-02-08 = - Enhance layout view with autoscroll - Improve modal layout design consistency - Remove NeedsRegistrationModal - Add various performance improvements = 0.3.1 - 2022-01-26 = - Add singular value when import count is 1 - Remove destructuring within block filters - Fix typo in setTimeout function name = 0.3.0 - 2022-01-25 = - Improve layout rendering performance - Improve utility styles to better support WordPress 5.9 - Add fallback for blurred background in Firefox - Add refresh screen when user is idle after 10 minutes - Fix missing property check = 0.2.0 - 2022-01-13 = - Add initial support for pro patterns - Improve pattern loading speed - Improve library header toolbar button - Improve multi-site support - Fix excess blockGap on full/wide blocks with WP 5.9 = 0.1.0 - 2022-01-06 = * Add null check on import position * Add support for importing patterns to a specific location * Add `/extendify` slash command to open the library * Add preview optimizations * Add check for live preview visibility * Fix pattern display bug with TT1 CSS Grid galleries PK&[aaapp/Assist/AdminPage.phpnu[
'post_title']); $pages = array_filter($pages, function ($page) { $meta = \metadata_exists( 'post', $page->ID, 'made_with_extendify_launch' ); return $meta === true; }); $data = array_map(function ($page) { $page->permalink = \get_the_permalink($page->ID); $path = \wp_parse_url($page->permalink)['path']; $parseSiteUrl = \wp_parse_url(\get_site_url()); $page->url = $parseSiteUrl['scheme'] . '://' . $parseSiteUrl['host'] . $path; $page->madeWithLaunch = true; return $page; }, $pages); return new \WP_REST_Response([ 'success' => true, 'data' => array_values($data), ]); } } PK&[;00)app/Assist/Controllers/TourController.phpnu[get_param('data'), true); update_option('extendify_assist_tour_progress', $data); return new \WP_REST_Response($data); } } PK&BB2app/Assist/Controllers/UserSelectionController.phpnu[get_param('data'), true); update_option('extendify_user_selections', $data); return new \WP_REST_Response($data); } } PK&nTT4app/Assist/Controllers/RecommendationsController.phpnu[get_json_params(); \update_option($params['option'], $params['value']); return new \WP_REST_Response(['success' => true]); } /** * Get a setting from the options table * * @param \WP_REST_Request $request - The request. * @return \WP_REST_Response */ public static function getOption($request) { $value = \get_option($request->get_param('option'), null); return new \WP_REST_Response([ 'success' => true, 'data' => $value, ]); } /** * Get the list of active plugins slugs * * @return \WP_REST_Response */ public static function getActivePlugins() { $value = \get_option('active_plugins', null); $slugs = []; foreach ($value as $plugin) { $slugs[] = explode('/', $plugin)[0]; } return new \WP_REST_Response([ 'success' => true, 'data' => $slugs, ]); } } PK&y 99/app/Assist/Controllers/QuickLinksController.phpnu[get_param('data'), true); update_option('extendify_assist_tasks', $data); return new \WP_REST_Response($data); } /** * Returns remaining incomplete tasks. * * @return int */ public function getRemainingCount() { $tasks = get_option('extendify_assist_tasks', []); if (!isset($tasks['state']['seenTasks'])) { return 0; } $seenTasks = count($tasks['state']['seenTasks']); $completedTasks = count($tasks['state']['completedTasks']); return max(($seenTasks - $completedTasks), 0); } /** * Returns whether the task dependency was completed. * * @param \WP_REST_Request $request - The request. * @return Boolean */ public static function dependencyCompleted($request) { $task = $request->get_param('taskName'); // If no depedency then consider it not yet completed. // The user will complete them manually by other means. $completed = false; if ($task === 'setup-givewp') { $give = \get_option('give_onboarding', false); $completed = isset($give['form_id']) && $give['form_id'] > 0; } if ($task === 'setup-woocommerce-store') { $woo = \get_option('woocommerce_onboarding_profile', false); $completed = isset($woo['completed']) && $woo['completed'] === true; } return new \WP_REST_Response(['data' => $completed]); } } PK&[|Y66,app/Assist/Controllers/GlobalsController.phpnu[get_param('data'), true); update_option('extendify_assist_globals', $data); return new \WP_REST_Response($data); } } PK&app/Assist/Admin.phpnu[loadScripts(); add_action('after_setup_theme', function () { // phpcs:ignore WordPress.Security.NonceVerification if (isset($_GET['extendify-disable-admin-bar'])) { show_admin_bar(false); } }); } /** * Adds scripts to the admin * * @return void */ public function loadScripts() { \add_action( 'init', function () { if (!current_user_can(Config::$requiredCapability)) { return; } if (!Config::$showAssist) { return; } // Don't show on Launch pages. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (isset($_GET['page']) && $_GET['page'] === 'extendify-launch') { return; } $partnerData = $this->checkPartnerDataSources(); $logo = isset($partnerData['logo']) ? $partnerData['logo'] : null; $name = isset($partnerData['name']) ? $partnerData['name'] : \__('Partner logo', 'extendify'); $version = Config::$environment === 'PRODUCTION' ? Config::$version : uniqid(); $this->enqueueGutenbergAssets(); $assistState = get_option('extendify_assist_globals'); $dismissed = isset($assistState['state']['dismissedNotices']) ? $assistState['state']['dismissedNotices'] : []; \wp_add_inline_script( Config::$slug . '-assist-scripts', 'window.extAssistData = ' . wp_json_encode([ 'devbuild' => \esc_attr(Config::$environment === 'DEVELOPMENT'), 'insightsId' => \get_option('extendify_site_id', ''), // Only send insights if they have opted in explicitly. 'insightsEnabled' => defined('EXTENDIFY_INSIGHTS_URL'), 'root' => \esc_url_raw(\rest_url(Config::$slug . '/' . Config::$apiVersion)), 'nonce' => \wp_create_nonce('wp_rest'), 'adminUrl' => \esc_url_raw(\admin_url()), 'home' => \esc_url_raw(\get_home_url()), 'asset_path' => \esc_url(EXTENDIFY_URL . 'public/assets'), 'launchCompleted' => Config::$launchCompleted, 'dismissedNotices' => $dismissed, 'partnerLogo' => $logo, 'partnerName' => $name, ]), 'before' ); \wp_set_script_translations(Config::$slug . '-assist-scripts', 'extendify'); \wp_enqueue_style( Config::$slug . '-assist-styles', EXTENDIFY_BASE_URL . 'public/build/extendify-assist.css', [], $version, 'all' ); if (isset($partnerData['bgColor']) && isset($partnerData['fgColor'])) { \wp_add_inline_style(Config::$slug . '-assist-styles', ":root { --ext-partner-theme-primary-bg: {$partnerData['bgColor']}; --ext-partner-theme-primary-text: {$partnerData['fgColor']}; }"); } } ); } /** * Enqueues Gutenberg stuff on a non-Gutenberg page. * * @return void */ public function enqueueGutenbergAssets() { $version = Config::$environment === 'PRODUCTION' ? Config::$version : uniqid(); $scriptAssetPath = EXTENDIFY_PATH . 'public/build/extendify-assist.asset.php'; $fallback = [ 'dependencies' => [], 'version' => $version, ]; $scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback; wp_enqueue_media(); foreach ($scriptAsset['dependencies'] as $style) { wp_enqueue_style($style); } \wp_enqueue_script( Config::$slug . '-assist-scripts', EXTENDIFY_BASE_URL . 'public/build/extendify-assist.js', $scriptAsset['dependencies'], $scriptAsset['version'], true ); } /** * Check if partner data is available. * * @return array */ public function checkPartnerDataSources() { $return = []; try { if (defined('EXTENDIFY_ONBOARDING_BG')) { $return['bgColor'] = constant('EXTENDIFY_ONBOARDING_BG'); $return['fgColor'] = constant('EXTENDIFY_ONBOARDING_TXT'); $return['logo'] = constant('EXTENDIFY_PARTNER_LOGO'); } $data = get_option('extendify_partner_data'); if ($data) { $return['bgColor'] = $data['backgroundColor']; $return['fgColor'] = $data['foregroundColor']; // Need this check to avoid errors if no partner logo is set in Airtable. $return['logo'] = $data['logo'] ? $data['logo'][0]['thumbnails']['large']['url'] : null; $return['name'] = isset($data['name']) ? $data['name'] : ''; } } catch (\Exception $e) { // Do nothing here, set variables below. Coding Standards require something to be in the catch. $e; }//end try return $return; } } PK&[*RRapp/Config.phpnu[showOnboarding(); self::$showAssist = self::$launchCompleted || self::$showOnboarding; // Add the config. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $config = file_get_contents(EXTENDIFY_PATH . 'config.json'); self::$config = json_decode($config, true); } /** * Conditionally load Extendify Launch. * * @return boolean */ private function showOnboarding() { // Always show it for dev mode. if (self::$environment === 'DEVELOPMENT') { return true; } // Currently we require a flag to be set. if (!defined('EXTENDIFY_SHOW_ONBOARDING')) { return false; } // Check if they disabled it and respect that. if (constant('EXTENDIFY_SHOW_ONBOARDING') === false) { return false; } return true; } } PK&y'app/Onboarding/AdminPage.phpnu[
get_param('siteType'); $styles = $request->get_param('styles'); $response = Http::get('/styles', [ 'siteType' => $siteType, 'styles' => $styles, ]); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Get styles with code template. * * @param \WP_REST_Request $request - The request. * @return \WP_REST_Response */ public static function getTemplate($request) { $response = Http::get('/templates', $request->get_params()); if (\is_wp_error($response)) { // TODO: Maybe handle errors better here, or higher up in the Http class. wp_send_json_error(['message' => $response->get_error_message()], 400); } return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Get Site type information. * * @return \WP_REST_Response */ public static function getLayoutTypes() { $response = Http::get('/layout-types'); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Get Goals information. * * @return \WP_REST_Response */ public static function getGoals() { $response = Http::get('/goals'); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Get Goals information. * * @return \WP_REST_Response */ public static function getSuggestedPlugins() { $response = Http::get('/suggested-plugins'); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Fetch exit questions * * @return \WP_REST_Response */ public static function exitQuestions() { $response = Http::get('/exit-questions'); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Just here to check for 200 (vs server rate limting) * * @return \WP_REST_Response */ public static function ping() { return new \WP_REST_Response(true, 200); } } PK&U+app/Onboarding/Controllers/WPController.phpnu[get_param('themeJson')) { return new \WP_Error('invalid_theme_json', __('Invalid Theme.json file', 'extendify')); } $themeJson = new \WP_Theme_JSON(json_decode($request->get_param('themeJson'), true), ''); return new \WP_REST_Response([ 'success' => true, 'styles' => $themeJson->get_stylesheet(), ]); } /** * Persist the data * * @param \WP_REST_Request $request - The request. * @return \WP_REST_Response */ public static function updateOption($request) { $params = $request->get_json_params(); \update_option($params['option'], $params['value']); return new \WP_REST_Response(['success' => true]); } /** * Get a setting from the options table * * @param \WP_REST_Request $request - The request. * @return \WP_REST_Response */ public static function getOption($request) { $value = \get_option($request->get_param('option'), null); return new \WP_REST_Response([ 'success' => true, 'data' => $value, ]); } /** * Get the list of active plugins slugs * * @return \WP_REST_Response */ public static function getActivePlugins() { return new \WP_REST_Response([ 'success' => true, 'data' => \get_option('active_plugins', null), ]); } } PK&app/Onboarding/Admin.phpnu[loadScripts(); $this->redirectOnce(); $this->addMetaField(); } /** * Adds a meta field so we can indicate a page was made with launch * * @return void */ public function addMetaField() { \add_action( 'init', function () { register_post_meta( 'page', 'made_with_extendify_launch', [ 'single' => true, 'type' => 'boolean', 'show_in_rest' => true, ] ); } ); } /** * Adds scripts to the admin * * @return void */ public function loadScripts() { \add_action( 'admin_enqueue_scripts', function () { if (!current_user_can(Config::$requiredCapability)) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!isset($_GET['page']) || $_GET['page'] !== 'extendify-launch') { return; } $this->addScopedScriptsAndStyles(); } ); } /** * Redirect once to Launch, only once (at least once) when * the email matches the entry in WP Admin > Settings > General. * * @return void */ public function redirectOnce() { \add_action('admin_init', function () { if (\get_option('extendify_launch_loaded', 0) // These are here for legacy reasons. || \get_option('extendify_onboarding_skipped', 0) || Config::$launchCompleted ) { return; } // Only redirect if we aren't already on the page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (isset($_GET['page']) && $_GET['page'] === 'extendify-launch') { return; } $user = \wp_get_current_user(); if ($user // Check the main admin email, and they have an admin role. // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps && \get_option('admin_email') === $user->user_email && in_array('administrator', $user->roles, true) ) { \wp_safe_redirect(\admin_url() . 'admin.php?page=extendify-launch'); } }); } /** * Check if partner data is available. * * @return array */ public function checkPartnerDataSources() { $return = []; try { if (defined('EXTENDIFY_ONBOARDING_BG')) { $return['bgColor'] = constant('EXTENDIFY_ONBOARDING_BG'); $return['fgColor'] = constant('EXTENDIFY_ONBOARDING_TXT'); $return['logo'] = constant('EXTENDIFY_PARTNER_LOGO'); } $data = get_option('extendify_partner_data'); if ($data) { $return['bgColor'] = $data['backgroundColor']; $return['fgColor'] = $data['foregroundColor']; // Need this check to avoid errors if no partner logo is set in Airtable. $return['logo'] = $data['logo'] ? $data['logo'][0]['thumbnails']['large']['url'] : null; } } catch (\Exception $e) { // Do nothing here, set variables below. Coding Standards require something to be in the catch. $e; }//end try return $return; } /** * Adds various JS scripts * * @return void */ public function addScopedScriptsAndStyles() { $partnerData = $this->checkPartnerDataSources(); $bgColor = isset($partnerData['bgColor']) ? $partnerData['bgColor'] : '#2c39bd'; $fgColor = isset($partnerData['fgColor']) ? $partnerData['fgColor'] : '#ffffff'; $logo = isset($partnerData['logo']) ? $partnerData['logo'] : null; $version = Config::$environment === 'PRODUCTION' ? Config::$version : uniqid(); $scriptAssetPath = EXTENDIFY_PATH . 'public/build/extendify-onboarding.asset.php'; $fallback = [ 'dependencies' => [], 'version' => $version, ]; $scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback; foreach ($scriptAsset['dependencies'] as $style) { wp_enqueue_style($style); } \wp_enqueue_script( Config::$slug . '-onboarding-scripts', EXTENDIFY_BASE_URL . 'public/build/extendify-onboarding.js', $scriptAsset['dependencies'], $scriptAsset['version'], true ); \wp_add_inline_script( Config::$slug . '-onboarding-scripts', 'window.extOnbData = ' . \wp_json_encode([ 'globalStylesPostID' => \WP_Theme_JSON_Resolver::get_user_global_styles_post_id(), 'editorStyles' => \get_block_editor_settings([], null), 'site' => \esc_url_raw(\get_site_url()), 'adminUrl' => \esc_url_raw(\admin_url()), 'pluginUrl' => \esc_url_raw(EXTENDIFY_BASE_URL), 'home' => \esc_url_raw(\get_home_url()), 'root' => \esc_url_raw(\rest_url(Config::$slug . '/' . Config::$apiVersion)), 'config' => Config::$config, 'wpRoot' => \esc_url_raw(\rest_url()), 'nonce' => \wp_create_nonce('wp_rest'), 'partnerLogo' => $logo, 'partnerName' => \esc_attr(Config::$sdkPartner), 'partnerSkipSteps' => defined('EXTENDIFY_SKIP_STEPS') ? constant('EXTENDIFY_SKIP_STEPS') : [], 'devbuild' => \esc_attr(Config::$environment === 'DEVELOPMENT'), 'version' => Config::$version, 'insightsId' => \get_option('extendify_site_id', ''), // Only send insights if they have opted in explicitly. 'insightsEnabled' => defined('EXTENDIFY_INSIGHTS_URL'), 'activeTests' => \get_option('extendify_active_tests', []), 'wpLanguage' => \get_locale(), 'siteCreatedAt' => get_user_option('user_registered', 1), ]), 'before' ); \wp_set_script_translations(Config::$slug . '-onboarding-scripts', 'extendify'); \wp_enqueue_style( Config::$slug . '-onboarding-styles', EXTENDIFY_BASE_URL . 'public/build/extendify-onboarding.css', [], $version ); \wp_add_inline_style(Config::$slug . '-onboarding-styles', ":root { --ext-partner-theme-primary-bg: {$bgColor}; --ext-partner-theme-primary-text: {$fgColor}; }"); } } PK&[K5&5&app/Library/AdminPage.phpnu[slug) { \remove_all_actions('admin_notices'); \remove_all_actions('all_admin_notices'); } }, 1000 ); \add_action( 'admin_enqueue_scripts', function () { // phpcs:ignore WordPress.Security.NonceVerification if (isset($_GET['page']) && $_GET['page'] === $this->slug) { \wp_enqueue_style( 'extendify-welcome', EXTENDIFY_URL . 'public/admin-page/welcome.css', [], Config::$environment === 'PRODUCTION' ? Config::$version : uniqid() ); } } ); } /** * Settings page output * * @since 1.0.0 * * @return void */ public function pageContent() { ?>

                        <?php
                            echo \wp_sprintf(
                                /* translators: %s: The name of the plugin, Extendify */
                                esc_html__('%s Banner', 'extendify'),
                                'Extendify'
                            );
                        ?>


' . esc_html__('clicking here', 'extendify') . ''); ?>



loadScripts(); } /** * Adds scripts and styles to every page is enabled * * @return void */ public function loadScripts() { \add_action( 'wp_enqueue_scripts', function () { // TODO: Determine a way to conditionally load assets (https://github.com/extendify/company-product/issues/72). $this->addStylesheets(); } ); } /** * Adds stylesheets as needed * * @return void */ public function addStylesheets() { } } PK&[222.app/Library/Controllers/TaxonomyController.phpnu[get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Send data about a specific template * * @param \WP_REST_Request $request - The request. * @return WP_REST_Response|WP_Error */ public static function ping($request) { $response = Http::post('/templates', $request->get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } PK&?YY*app/Library/Controllers/AuthController.phpnu[get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Handle registration - It will return the API key. * * @param \WP_REST_Request $request - The request. * @return WP_REST_Response|WP_Error */ public static function register($request) { $response = Http::post('/register', $request->get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } PK&[3[*app/Library/Controllers/PingController.phpnu[get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } PK&[?*app/Library/Controllers/UserController.phpnu[get_param('key'))); return new \WP_REST_Response(User::data($key)); } /** * Persist the data * * @param \WP_REST_Request $request - The request. * @return array */ public static function store($request) { $userData = json_decode($request->get_param('data'), true); // Keep this key for historical reasons. \update_user_meta(\get_current_user_id(), 'extendifysdk_user_data', $userData); return new \WP_REST_Response(User::state()); } /** * Delete the data * * @return array */ public static function delete() { \delete_user_meta(\get_current_user_id(), 'extendifysdk_user_data'); return new \WP_REST_Response(User::state()); } /** * Sign up the user to the mailing list. * * @param \WP_REST_Request $request - The request. * @return WP_REST_Response|WP_Error */ public static function mailingList($request) { $response = Http::post('/register-mailing-list', $request->get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } /** * Get the max imports * * @return WP_REST_Response|WP_Error */ public static function maxImports() { $response = Http::get('/max-free-imports'); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } PK&[ۢR,app/Library/Controllers/PluginController.phpnu[get_param('plugins'), true); foreach ($requiredPlugins as $plugin) { $status = Plugin::install_and_activate_plugin($plugin); if (\is_wp_error($status)) { // Return first error encountered. return $status; } } return true; } } PK&[%*ѓ*app/Library/Controllers/MetaController.phpnu[get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } PK&2app/Library/Controllers/SiteSettingsController.phpnu[get_param('data'), true); \update_option(SiteSettings::key(), $settingsData, true); return new \WP_REST_Response(SiteSettings::data()); } /** * Persist the data * * @param \WP_REST_Request $request - The request. * @return \WP_REST_Response */ public static function updateOption($request) { $params = $request->get_json_params(); \update_option($params['option'], $params['value']); return new \WP_REST_Response(['success' => true], 200); } } PK&[:/0app/Library/SiteSettings.phpnu[key, $this->default); } /** * Returns Setting Key * Use it like Setting::key() * * @return string - Setting key */ private function keyHandler() { return $this->key; } /** * Use it like Setting::method() e.g. Setting::data() * * @param string $name - The name of the method to call. * @param array $arguments - The arguments to pass in. * * @return mixed */ public static function __callStatic($name, array $arguments) { $name = "{$name}Handler"; self::$instance = new static(); $r = self::$instance; return $r->$name(...$arguments); } } PK&[*L%%app/Library/Plugin.phpnu[install($zip_url); if (is_wp_error($result)) { return $result; } $plugin = self::get_plugin_id_by_slug($slug); $error_code = 'install_error'; if (! $plugin) { $error = __('There was an error installing your plugin', 'jetpack'); } if (! $result) { $error_code = $upgrader->skin->get_main_error_code(); $message = $upgrader->skin->get_main_error_message(); $error = $message ? $message : __('An unknown error occurred during installation', 'jetpack'); } if (! empty($error)) { if ('download_failed' === $error_code) { // For backwards compatibility: versions prior to 3.9 would return no_package instead of download_failed. $error_code = 'no_package'; } return new \WP_Error($error_code, $error, 400); } return (array) $upgrader->skin->get_upgrade_messages(); } /** * Get WordPress.org zip download link from a plugin slug * * @param string $plugin_slug Plugin slug. */ protected static function generate_wordpress_org_plugin_download_link($plugin_slug) { return "https://downloads.wordpress.org/plugin/$plugin_slug.latest-stable.zip"; } /** * Get the plugin ID (composed of the plugin slug and the name of the main plugin file) from a plugin slug. * * @param string $slug Plugin slug. */ public static function get_plugin_id_by_slug($slug) { // Check if get_plugins() function exists. This is required on the front end of the // site, since it is in a file that is normally only loaded in the admin. if (! function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ $plugins = apply_filters('all_plugins', get_plugins()); if (! is_array($plugins)) { return false; } foreach ($plugins as $plugin_file => $plugin_data) { if (self::get_slug_from_file_path($plugin_file) === $slug) { return $plugin_file; } } return false; } /** * Get the plugin slug from the plugin ID (composed of the plugin slug and the name of the main plugin file) * * @param string $plugin_file Plugin file (ID -- e.g. hello-dolly/hello.php). */ protected static function get_slug_from_file_path($plugin_file) { // Similar to get_plugin_slug() method. $slug = dirname($plugin_file); if ('.' === $slug) { $slug = preg_replace('/(.+)\.php$/', '$1', $plugin_file); } return $slug; } /** * Get the activation status for a plugin. * * @since 8.9.0 * * @param string $plugin_file The plugin file to check. * @return string Either 'network-active', 'active' or 'inactive'. */ public static function get_plugin_status($plugin_file) { if (is_plugin_active_for_network($plugin_file)) { return 'network-active'; } if (is_plugin_active($plugin_file)) { return 'active'; } return 'inactive'; } /** * Returns a list of all plugins in the site. * * @since 8.9.0 * @uses get_plugins() * * @return array */ public static function get_plugins() { if (! function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ $plugins = apply_filters('all_plugins', get_plugins()); if (is_array($plugins) && ! empty($plugins)) { foreach ($plugins as $plugin_slug => $plugin_data) { $plugins[ $plugin_slug ]['active'] = in_array( self::get_plugin_status($plugin_slug), array( 'active', 'network-active' ), true ); } return $plugins; } return array(); } } include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/file.php'; /** * Allows us to capture that the site doesn't have proper file system access. * In order to update the plugin. */ class PluginUpgraderSkin extends \Automatic_Upgrader_Skin { /** * Stores the last error key; **/ protected $main_error_code = 'install_error'; /** * Stores the last error message. **/ protected $main_error_message = 'An unknown error occurred during installation'; /** * Overwrites the set_upgrader to be able to tell if we e ven have the ability to write to the files. * * @param WP_Upgrader $upgrader * */ public function set_upgrader(&$upgrader) { parent::set_upgrader($upgrader); // Check if we even have permission to. $result = $upgrader->fs_connect(array( WP_CONTENT_DIR, WP_PLUGIN_DIR )); if (! $result) { // set the string here since they are not available just yet $upgrader->generic_strings(); $this->feedback('fs_unavailable'); } } /** * Overwrites the error function */ public function error($error) { if (is_wp_error($error)) { $this->feedback($error); } } private function set_main_error_code($code) { // Don't set the process_failed as code since it is not that helpful unless we don't have one already set. $this->main_error_code = ($code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $code); } private function set_main_error_message($message, $code) { // Don't set the process_failed as message since it is not that helpful unless we don't have one already set. $this->main_error_message = ($code === 'process_failed' && $this->main_error_code ? $this->main_error_code : $message); } public function get_main_error_code() { return $this->main_error_code; } public function get_main_error_message() { return $this->main_error_message; } /** * Overwrites the feedback function * * @param string|array|WP_Error $data Data. * @param mixed ...$args Optional text replacements. */ public function feedback($data, ...$args) { $current_error = null; if (is_wp_error($data)) { $this->set_main_error_code($data->get_error_code()); $string = $data->get_error_message(); } elseif (is_array($data)) { return; } else { $string = $data; } if (! empty($this->upgrader->strings[$string])) { $this->set_main_error_code($string); $current_error = $string; $string = $this->upgrader->strings[$string]; } if (strpos($string, '%') !== false) { if (! empty($args)) { $string = vsprintf($string, $args); } } $string = trim($string); $string = wp_kses( $string, array( 'a' => array( 'href' => true ), 'br' => true, 'em' => true, 'strong' => true, ) ); $this->set_main_error_message($string, $current_error); $this->messages[] = $string; } } PK&yh&&app/Library/Shared.phpnu[themeCompatInlineStyles(); } ); \add_action( 'admin_init', function () { $this->themeCompatInlineStyles(); } ); } /** * Inline styles to be applied for compatible themes * * @return void */ // phpcs:ignore public function themeCompatInlineStyles() { $css = ''; $theme = get_option('template'); if ($theme === 'kadence') { $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: var(--global-palette8); --wp--preset--color--foreground: var(--global-palette4); --wp--preset--color--primary: var(--global-palette1); --wp--preset--color--secondary: var(--global-palette2); --wp--preset--color--tertiary: var(--global-palette7); --wp--custom--spacing--large: clamp(var(--global-sm-spacing), 5vw, var(--global-xxl-spacing)); --wp--preset--font-size--large: var(--h2FontSize); --wp--preset--font-size--huge: var(--h1FontSize); }'; } if ($theme === 'neve') { $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: var(--nv-site-bg); --wp--preset--color--foreground: var(--nv-text-color); --wp--preset--color--primary: var(--nv-primary-accent); --wp--preset--color--secondary: var(--nv-secondary-accent); --wp--preset--color--tertiary: var(--nv-light-bg); --wp--custom--spacing--large: clamp(15px, 5vw, 80px); --wp--preset--font-size--large: var(--h2FontSize); --wp--preset--font-size--huge: var(--h1FontSize); }'; } if ($theme === 'blocksy') { $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: var(--paletteColor7); --wp--preset--color--foreground: var(--color); --wp--preset--color--primary: var(--paletteColor1); --wp--preset--color--secondary: var(--paletteColor4); }'; } if ($theme === 'go') { $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: var(--go--color--background); --wp--preset--color--foreground: var(--go--color--text); }'; } if ($theme === 'astra') { $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: #ffffff; --wp--preset--color--foreground: var(--ast-global-color-2); --wp--preset--color--primary: var(--ast-global-color-0); --wp--preset--color--secondary: var(--ast-global-color-2); }'; } if ($theme === 'oceanwp') { $background = get_theme_mod('ocean_background_color', '#ffffff'); $primary = get_theme_mod('ocean_primary_color', '#13aff0'); $secondary = get_theme_mod('ocean_hover_primary_color', '#0b7cac'); $gap = get_theme_mod('ocean_separate_content_padding', '30px'); $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: ' . $background . '; --wp--preset--color--foreground: #1B1B1B; --wp--preset--color--primary: ' . $primary . '; --wp--preset--color--secondary: ' . $secondary . '; --wp--style--block-gap: ' . $gap . '; --wp--custom--spacing--large: clamp(2rem, 7vw, 8rem); }'; } if ($theme === 'generatepress') { $settings = (array) get_option('generate_settings', []); if (! array_key_exists('background_color', $settings)) { $background = '#f7f8f9'; } else { $background = $settings['background_color']; } if (! array_key_exists('text_color', $settings)) { $foreground = '#222222'; } else { $foreground = $settings['text_color']; } if (! array_key_exists('link_color', $settings)) { $primary = '#1e73be'; } else { $primary = $settings['link_color']; } if (! array_key_exists('link_color', $settings)) { $primary = '#1e73be'; } else { $primary = $settings['link_color']; } $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: ' . $background . '; --wp--preset--color--foreground: ' . $foreground . '; --wp--preset--color--primary: ' . $primary . '; --wp--preset--color--secondary: #636363; --wp--style--block-gap: 3rem; --wp--custom--spacing--large: clamp(2rem, 7vw, 8rem); --responsive--alignwide-width: 1120px; }'; }//end if if ($theme === 'twentytwentytwo') { $css = 'body, .editor-styles-wrapper { --extendify--spacing--large: clamp(2rem,8vw,8rem); }'; } if ($theme === 'twentytwentyone') { $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: var(--global--color-background); --wp--preset--color--foreground: var(--global--color-primary); --wp--preset--color--primary: var(--global--color-gray); --wp--preset--color--secondary: #464b56; --wp--preset--color--tertiary: var(--global--color-light-gray); --wp--style--block-gap: var(--global--spacing-unit); --wp--preset--font-size--large: 2.5rem; --wp--preset--font-size--huge: var(--global--font-size-xxl); } .has-foreground-background-color, .has-primary-background-color, .has-secondary-background-color { --local--color-primary: var(--wp--preset--color--background); --local--color-background: var(--wp--preset--color--primary); }'; } if ($theme === 'twentytwenty') { $background = sanitize_hex_color_no_hash(get_theme_mod('background_color', 'f5efe0')); $primary = get_theme_mod( 'accent_accessible_colors', [ 'content' => [ 'accent' => '#cd2653' ], ] ); $primary = $primary['content']['accent']; $css = 'body, .editor-styles-wrapper { --wp--preset--color--background: #' . $background . '; --wp--preset--color--foreground: #000; --wp--preset--color--primary: ' . $primary . '; --wp--preset--color--secondary: #69603e; --wp--style--block-gap: 3rem; --wp--custom--spacing--large: clamp(2rem, 7vw, 8rem); --responsive--alignwide-width: 120rem; }'; }//end if if ($theme === 'twentynineteen') { /** * Use the color from Twenty Nineteen's customizer value. */ $primary = 199; if (get_theme_mod('primary_color', 'default') !== 'default') { $primary = absint(get_theme_mod('primary_color_hue', 199)); } /** * Filters Twenty Nineteen default saturation level. * * @since Twenty Nineteen 1.0 * * @param int $saturation Color saturation level. */ // phpcs:ignore $saturation = apply_filters('twentynineteen_custom_colors_saturation', 100); $saturation = absint($saturation) . '%'; /** * Filters Twenty Nineteen default lightness level. * * @since Twenty Nineteen 1.0 * * @param int $lightness Color lightness level. */ // phpcs:ignore $lightness = apply_filters('twentynineteen_custom_colors_lightness', 33); $lightness = absint($lightness) . '%'; $css = 'body, .editor-styles-wrapper { --wp--preset--color--foreground: #111; --wp--preset--color--primary: hsl( ' . $primary . ', ' . $saturation . ', ' . $lightness . ' ); --wp--preset--color--secondary: #767676; --wp--preset--color--tertiary: #f7f7f7; }'; }//end if // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents(EXTENDIFY_PATH . 'public/extendify-utilities.css'); $version = Config::$environment === 'PRODUCTION' ? Config::$version : uniqid(); \wp_register_style(Config::$slug . '-utilities', false, [], $version); \wp_enqueue_style(Config::$slug . '-utilities'); \wp_add_inline_style(Config::$slug . '-utilities', $content . $css); // Adds inline to the live preview. \wp_add_inline_style('wp-components', $content . $css); } } PK&[gqqapp/Library/Admin.phpnu[loadScripts(); \add_filter('plugin_action_links_' . EXTENDIFY_PLUGIN_BASENAME, [ $this, 'pluginActionLinks' ]); } /** * Adds action links to the plugin list table * * @param array $links An array of plugin action links. * @return array An array of plugin action links. */ public function pluginActionLinks($links) { if (defined('EXTENDIFY_SITE_LICENSE')) { return $links; } $theme = get_option('template'); $label = esc_html__('Upgrade', 'extendify'); $links['upgrade'] = sprintf( '%2$s', "https://extendify.com/pricing?utm_source=extendify-plugin&utm_medium=wp-dash&utm_campaign=action-link&utm_content=$label&utm_term=$theme", $label ); return $links; } /** * Adds scripts to the admin * * @return void */ public function loadScripts() { \add_action( 'admin_enqueue_scripts', function ($hook) { if (!current_user_can(Config::$requiredCapability)) { return; } if (!$this->checkItsGutenbergPost($hook)) { return; } if (!$this->isLibraryEnabled()) { return; } $this->addScopedScriptsAndStyles(); } ); } /** * Makes sure we are on the correct page * * @param string $hook - An optional hook provided by WP to identify the page. * @return boolean */ public function checkItsGutenbergPost($hook = '') { // Check for the post type, or on the FSE page. $type = isset($GLOBALS['typenow']) ? $GLOBALS['typenow'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (!$type && isset($_GET['postType'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $type = sanitize_text_field(wp_unslash($_GET['postType'])); } if (\use_block_editor_for_post_type($type)) { return $hook && in_array($hook, ['post.php', 'post-new.php', 'appearance_page_gutenberg-edit-site'], true); } return false; } /** * Adds various JS scripts * * @return void */ public function addScopedScriptsAndStyles() { $user = json_decode(User::data('extendifysdk_user_data'), true); $openOnNewPage = isset($user['state']['openOnNewPage']) ? $user['state']['openOnNewPage'] : Config::$launchCompleted; $version = Config::$environment === 'PRODUCTION' ? Config::$version : uniqid(); $scriptAssetPath = EXTENDIFY_PATH . 'public/build/extendify-asset.php'; $fallback = [ 'dependencies' => [], 'version' => $version, ]; $scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback; foreach ($scriptAsset['dependencies'] as $style) { wp_enqueue_style($style); } \wp_register_script( Config::$slug . '-scripts', EXTENDIFY_BASE_URL . 'public/build/extendify.js', $scriptAsset['dependencies'], $scriptAsset['version'], true ); \wp_localize_script( Config::$slug . '-scripts', 'extendifyData', [ 'root' => \esc_url_raw(rest_url(Config::$slug . '/' . Config::$apiVersion)), 'nonce' => \wp_create_nonce('wp_rest'), 'user' => $user, 'openOnNewPage' => $openOnNewPage, 'sitesettings' => json_decode(SiteSettings::data()), 'sdk_partner' => \esc_attr(Config::$sdkPartner), 'asset_path' => \esc_url(EXTENDIFY_URL . 'public/assets'), 'standalone' => \esc_attr(Config::$standalone), 'devbuild' => \esc_attr(Config::$environment === 'DEVELOPMENT'), 'insightsId' => \get_option('extendify_site_id', ''), ] ); \wp_enqueue_script(Config::$slug . '-scripts'); \wp_set_script_translations(Config::$slug . '-scripts', 'extendify'); // Inline the library styles to keep them out of the iframe live preview. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $css = file_get_contents(EXTENDIFY_PATH . 'public/build/extendify.css'); \wp_register_style(Config::$slug, false, [], $version); \wp_enqueue_style(Config::$slug); \wp_add_inline_style(Config::$slug, $css); } /** * Check if current user is Admin * * @return Boolean */ private function isAdmin() { if (\is_multisite()) { return \is_super_admin(); } return in_array('administrator', \wp_get_current_user()->roles, true); } /** * Check if scripts should add * * @return Boolean */ public function isLibraryEnabled() { $settings = json_decode(SiteSettings::data()); // If it's disabled, only show it for admins. if (isset($settings->state) && (isset($settings->state->enabled)) && !$settings->state->enabled) { return $this->isAdmin(); } return true; } } PK&[ ֊ app/User.phpnu[user = $user; } /** * Return the user ID * * @return void */ private function setupUuid() { $uuid = \get_user_meta($this->user->ID, $this->key . 'uuid', true); if (!$uuid) { $id = \wp_hash(\wp_json_encode($this->user)); \update_user_meta($this->user->ID, $this->key . 'uuid', $id); } $this->uuid = $uuid; } /** * Returns data about the user * Use it like User::data('ID') to get the user id * * @param string $arguments - Right now a string of arguments, like ID. * @return mixed - Data about the user. */ private function dataHandler($arguments) { // Right now assume a single argument, but could expand to multiple. if (isset($this->user->$arguments)) { return $this->user->$arguments; } return \get_user_meta($this->user->ID, $this->key . $arguments, true); } /** * Returns application state for the current user * Use it like User::data('ID') to get the user id * * @return string - JSON representation of the current state */ private function stateHandler() { $state = \get_user_meta($this->user->ID, $this->key . 'user_data'); // Add some state boilerplate code for the first load. if (!isset($state[0])) { $state[0] = '{}'; } $userData = json_decode($state[0], true); if (!isset($userData['version'])) { $userData['version'] = 0; } // This will reset the allowed max imports to 0 once a week which will force the library to re-check. if (!get_transient('extendify_import_max_check_' . $this->user->ID)) { set_transient('extendify_import_max_check_' . $this->user->ID, time(), strtotime('1 week', 0)); $userData['state']['allowedImports'] = 0; } // Similar to above, this will give the user free imports once a month just for logging in. if (!get_transient('extendify_free_extra_imports_check_' . $this->user->ID)) { set_transient('extendify_free_extra_imports_check_' . $this->user->ID, time(), strtotime('first day of next month', 0)); $userData['state']['runningImports'] = 0; } if (!isset($userData['state']['sdkPartner']) || !$userData['state']['sdkPartner']) { $userData['state']['sdkPartner'] = Config::$sdkPartner; } $userData['state']['uuid'] = self::data('uuid'); $userData['state']['canInstallPlugins'] = \current_user_can('install_plugins'); $userData['state']['canActivatePlugins'] = \current_user_can('activate_plugins'); $userData['state']['isAdmin'] = \current_user_can('create_users'); // If the license key is set on the server, force use it. if (defined('EXTENDIFY_SITE_LICENSE')) { $userData['state']['apiKey'] = constant('EXTENDIFY_SITE_LICENSE'); } // This probably shouldn't have been wrapped in wp_json_encode, // but needs to remain until we can safely log pro users out, // as changing this now would erase all user data. return \wp_json_encode($userData); } /** * Allows to dynamically setup the user with uuid * Use it like User::data('ID') to get the user id * * @param string $name - The name of the method to call. * @param array $arguments - The arguments to pass in. * * @return mixed */ public static function __callStatic($name, array $arguments) { $name = "{$name}Handler"; if (is_null(self::$instance)) { require_once ABSPATH . 'wp-includes/pluggable.php'; self::$instance = new static(\wp_get_current_user()); $r = self::$instance; $r->setupUuid(); } $r = self::$instance; return $r->$name(...$arguments); } } PK&  app/Insights.phpnu[ [ 'A', 'B', ], 'launch-site-vs-next' => [ 'A', 'B', ], ]; /** * Process the readme file to get version and name * * @return void */ public function __construct() { // If there isn't a siteId, then create one. if (!\get_option('extendify_site_id', false)) { \update_option('extendify_site_id', \wp_generate_uuid4()); if (defined('EXTENDIFY_INSIGHTS_URL') && class_exists('ExtendifyInsights')) { // If we are generating an ID, then trigger the job here too. // This only runs if they have opted in. add_action('init', function () { wp_schedule_single_event(time(), 'extendify_insights'); spawn_cron(); }); } } $this->setUpActiveTests(); $this->filterExternalInsights(); } /** * Returns the active tests for the user, and sets up tests as needed. * * @return void */ public function setUpActiveTests() { // Make sure that the active tests are set. $currentTests = \get_option('extendify_active_tests', []); $newTests = array_map(function ($test) { // Pick from value randomly. return $test[array_rand($test)]; }, array_diff_key($this->activeTests, $currentTests)); $testsCombined = array_merge($currentTests, $newTests); if ($newTests) { \update_option('extendify_active_tests', $testsCombined); } } /** * Add additional data to the opt-in insights * * @return void */ public function filterExternalInsights() { add_filter('extendify_insights_data', function ($data) { $insights = array_merge($data, [ 'launch' => defined('EXTENDIFY_SHOW_ONBOARDING') && constant('EXTENDIFY_SHOW_ONBOARDING'), 'partner' => defined('EXTENDIFY_PARTNER_ID') ? constant('EXTENDIFY_PARTNER_ID') : null, 'siteCreatedAt' => get_user_option('user_registered', 1), ]); return $insights; }); } } PK&Q~~app/AdminPageRouter.phpnu[addSubMenu('Assist', $assist->slug, $cb); } // Always load the Library page. $library = new LibraryAdminPage(); $cb = [$library, 'pageContent']; $this->addSubMenu('Library', $library->slug, $cb); // Show the Launch menu for dev users. if ((Config::$showOnboarding && !Config::$launchCompleted) || Config::$environment === 'DEVELOPMENT') { $onboarding = new OnboardingAdminPage(); $cb = [$onboarding, 'pageContent']; $this->addSubMenu('Launch', $onboarding->slug, $cb); } }); // Hide the menu items unless in dev mode. if (Config::$environment === 'PRODUCTION') { add_action('admin_head', function () { echo ''; }); } // If the user is redirected to this while visiting our url, intercept it. \add_filter('wp_redirect', function ($url) { // Check for extendify-launch-success as other plugins will not override // this as they intercept the request. // Special treatment for Yoast to disable their redirect when installing. if ($url === \admin_url() . 'admin.php?page=wpseo_installation_successful_free') { return \admin_url() . 'admin.php?page=extendify-assist'; } // phpcs:ignore WordPress.Security.NonceVerification if (isset($_GET['extendify-launch-success'])) { return \admin_url() . $this->getRoute(); } return $url; }, 9999); // Intercept requests and redirect as needed. // phpcs:ignore WordPress.Security.NonceVerification if (isset($_GET['page']) && $_GET['page'] === 'extendify-admin-page') { header('Location: ' . \admin_url() . $this->getRoute(), true, 302); exit; } } /** * A helper for handling sub menus * * @param string $name - The menu name. * @param string $slug - The menu slug. * @param callable $callback - The callback to render the page. * * @return void */ public function addSubMenu($name, $slug, $callback = '') { \add_submenu_page( 'extendify-admin-page', $name, $name, Config::$requiredCapability, $slug, $callback ); } /** * Adds Extendify top menu * * @return void */ public function addAdminMenu() { $tasksController = new AssistTasksController(); $remainingTasks = $tasksController->getRemainingCount(); $badgeCount = $remainingTasks > 9 ? '9' : strval($remainingTasks); $menuLabel = Config::$launchCompleted ? __('Site Assistant', 'extendify') : __('Site Launcher', 'extendify'); $menuLabel = Config::$showOnboarding ? $menuLabel : 'Extendify'; $menuLabel = sprintf('%1$s ', $menuLabel); \add_menu_page( 'Extendify', $menuLabel, Config::$requiredCapability, 'extendify-admin-page', '__return_null', // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 'data:image/svg+xml;base64,' . base64_encode(' '), Config::$showOnboarding ? 2 : null ); } /** * Routes pages accordingly * * @return string */ public function getRoute() { // If dev, redirect to assist always. if (Config::$environment === 'DEVELOPMENT') { return 'admin.php?page=extendify-assist'; } // If Launch/Assist isn't enabled, show the Library page. if (!Config::$showOnboarding) { return 'admin.php?page=extendify-welcome'; } // If they've yet to complete launch, send them back to Launch. if (!Config::$launchCompleted) { return 'admin.php?page=extendify-launch'; } // If they made it this far, they can go to Assist. return 'admin.php?page=extendify-assist'; } } PK& app/ApiRouter.phpnu[capability = Config::$requiredCapability; add_filter( 'rest_request_before_callbacks', // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed function ($response, $handler, $request) { // Add the request to our helper class. if ($request->get_header('x_extendify')) { Http::init($request); } return $response; }, 10, 3 ); } /** * Check the authorization of the request * * @return boolean */ public function checkPermission() { // Check for the nonce on the server (used by WP REST). if (isset($_SERVER['HTTP_X_WP_NONCE']) && \wp_verify_nonce(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_NONCE'])), 'wp_rest')) { return \current_user_can($this->capability); } return false; } /** * Register dynamic routes * * @param string $namespace - The api name space. * @param string $endpoint - The endpoint. * @param function $callback - The callback to run. * * @return void */ public function getHandler($namespace, $endpoint, $callback) { \register_rest_route( $namespace, $endpoint, [ 'methods' => 'GET', 'callback' => $callback, 'permission_callback' => [ $this, 'checkPermission', ], ] ); } /** * The post handler * * @param string $namespace - The api name space. * @param string $endpoint - The endpoint. * @param string $callback - The callback to run. * * @return void */ public function postHandler($namespace, $endpoint, $callback) { \register_rest_route( $namespace, $endpoint, [ 'methods' => 'POST', 'callback' => $callback, 'permission_callback' => [ $this, 'checkPermission', ], ] ); } /** * The caller * * @param string $name - The name of the method to call. * @param array $arguments - The arguments to pass in. * * @return mixed */ public static function __callStatic($name, array $arguments) { $name = "{$name}Handler"; if (is_null(self::$instance)) { self::$instance = new static(); } $r = self::$instance; return $r->$name(Config::$slug . '/' . Config::$apiVersion, ...$arguments); } } PK&833 app/Http.phpnu[get_header('x_wp_nonce'))), 'wp_rest')) { return; } // Some special cases for library development. $this->baseUrl = $this->getBaseUrl($request); $this->data = [ 'wp_language' => \get_locale(), 'wp_theme' => \get_option('template'), 'mode' => Config::$environment, 'uuid' => User::data('uuid'), 'library_version' => Config::$version, 'wp_active_plugins' => $request->get_method() === 'POST' ? \get_option('active_plugins') : [], 'sdk_partner' => Config::$sdkPartner, ]; if ($request->get_header('x_extendify_dev_mode') === 'true') { $this->data['devmode'] = true; } $this->headers = [ 'Accept' => 'application/json', 'referer' => $request->get_header('referer'), 'user_agent' => $request->get_header('user_agent'), ]; } /** * Register dynamic routes * * @param string $endpoint - The endpoint. * @param array $data - The data to include. * @param array $headers - The headers to include. * * @return array */ public function getHandler($endpoint, $data = [], $headers = []) { $url = \esc_url_raw( \add_query_arg( \urlencode_deep(\urldecode_deep(array_merge($this->data, $data))), $this->baseUrl . $endpoint ) ); $response = \wp_remote_get( $url, [ 'headers' => array_merge($this->headers, $headers), ] ); if (\is_wp_error($response)) { return $response; } $responseBody = \wp_remote_retrieve_body($response); return json_decode($responseBody, true); } /** * Register dynamic routes * * @param string $endpoint - The endpoint. * @param array $data - The arguments to include. * @param array $headers - The headers to include. * * @return array */ public function postHandler($endpoint, $data = [], $headers = []) { $response = \wp_remote_post( $this->baseUrl . $endpoint, [ 'headers' => array_merge($this->headers, $headers), 'body' => array_merge($this->data, $data), ] ); if (\is_wp_error($response)) { return $response; } $responseBody = \wp_remote_retrieve_body($response); return json_decode($responseBody, true); } /** * The caller * * @param string $name - The name of the method to call. * @param array $arguments - The arguments to pass in. * * @return mixed */ public static function __callStatic($name, array $arguments) { if ($name === 'init') { self::$instance = new static($arguments[0]); return; } $name = "{$name}Handler"; $r = self::$instance; return $r->$name(...$arguments); } /** * Figure out the base URL to use. * * @param \WP_REST_Request $request - The request. * * @return string */ public function getBaseUrl($request) { // Library Dev request. if ($request->get_header('x_extendify_dev_mode') === 'true') { return Config::$config['api']['dev']; } // Library Local request. if ($request->get_header('x_extendify_local_mode') === 'true') { return Config::$config['api']['local']; } // Onboarding dev request. if ($request->get_header('x_extendify_onboarding_dev_mode') === 'true') { return Config::$config['api']['onboarding-dev']; } // Onborarding local request. if ($request->get_header('x_extendify_onboarding_local_mode') === 'true') { return Config::$config['api']['onboarding-local']; } // Onboarding request. if ($request->get_header('x_extendify_onboarding') === 'true') { return Config::$config['api']['onboarding']; } // Assist dev request. if ($request->get_header('x_extendify_assist_dev_mode') === 'true') { return Config::$config['api']['assist-dev']; } // Assist local request. if ($request->get_header('x_extendify_assist_local_mode') === 'true') { return Config::$config['api']['assist-local']; } // Assist request. if ($request->get_header('x_extendify_assist') === 'true') { return Config::$config['api']['assist']; } // Normal Library request. return Config::$config['api']['live']; } } PK&_*'' loader.phpnu[ $data) { if ($data['TextDomain'] === $name) { return $plugin; } } return false; } }//end if $extendifyPluginName = extendifyCheckPluginInstalled('extendify'); if ($extendifyPluginName) { // Exit if the library is installed and active. // Remember, this file is only loaded by partner plugins. if (is_plugin_active($extendifyPluginName)) { // If the SDK is active then ignore the partner plugins. $GLOBALS['extendify_sdk_partner'] = 'standalone'; return false; } } // Next is first come, first serve. The later class is left in for historical reasons. if (class_exists('Extendify') || class_exists('ExtendifySdk')) { return false; } require_once plugin_dir_path(__FILE__) . 'extendify.php'; PK&E config.jsonnu[{ "api": { "onboarding": "https://dashboard.extendify.com/api/onboarding", "onboarding-dev": "https://testing.extendify.com/api/onboarding", "onboarding-local": "http://templates.test/api/onboarding", "assist": "https://dashboard.extendify.com/api/assist", "assist-dev": "https://testing.extendify.com/api/assist", "assist-local": "http://templates.test/api/assist", "live": "https://dashboard.extendify.com/api", "dev": "https://testing.extendify.com/api", "local": "http://templates.test/api" } } PK&ҏvendor/autoload.phpnu[ * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier * @author Jordi Boggiano * @see http://www.php-fig.org/psr/psr-0/ * @see http://www.php-fig.org/psr/psr-4/ */ class ClassLoader { // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); private $fallbackDirsPsr4 = array(); // PSR-0 private $prefixesPsr0 = array(); private $fallbackDirsPsr0 = array(); private $useIncludePath = false; private $classMap = array(); private $classMapAuthoritative = false; private $missingClasses = array(); private $apcuPrefix; public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } public function getFallbackDirs() { return $this->fallbackDirsPsr0; } public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } public function getClassMap() { return $this->classMap; } /** * @param array $classMap Class to filename map */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 base directories */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); } /** * Unregisters this instance as an autoloader. */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); } /** * Loads the given class or interface. * * @param string $class The name of the class * @return bool|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. */ function includeFile($file) { include $file; } PK&!ו'vendor/composer/autoload_namespaces.phpnu[= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitde6056382b54124f6f68c9fe4214de3b::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); return $loader; } } PK&%vendor/composer/autoload_classmap.phpnu[ array($baseDir . '/app'), ); PK&g^33#vendor/composer/autoload_static.phpnu[ array ( 'Extendify\\' => 10, ), ); public static $prefixDirsPsr4 = array ( 'Extendify\\' => array ( 0 => __DIR__ . '/../..' . '/app', ), ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInitde6056382b54124f6f68c9fe4214de3b::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInitde6056382b54124f6f68c9fe4214de3b::$prefixDirsPsr4; }, null, ClassLoader::class); } } PK&vsrc/Assist/AssistLandingPage.jsnu[import { SWRConfig } from 'swr' import { useRouter } from '@assist/hooks/useRouter' import { WelcomeNotice } from '@assist/notices/WelcomeNotice' import { Header } from '@assist/pages/parts/Header' const Page = () => { const { CurrentPage } = useRouter() return ( <>
) } export const AssistLandingPage = () => ( { if (error.status === 404) return if (error?.data?.status === 403) { // if they are logged out, we can't recover window.location.reload() return } // Retry after 5 seconds. setTimeout(() => revalidate({ retryCount }), 5000) }, }}> ) PK&{99src/Assist/api/axios.jsnu[import axios from 'axios' const Axios = axios.create({ baseURL: window.extAssistData.root, headers: { 'X-WP-Nonce': window.extAssistData.nonce, 'X-Requested-With': 'XMLHttpRequest', 'X-Extendify-Assist': true, 'X-Extendify': true, }, }) Axios.interceptors.request.use( (request) => checkDevMode(request), (error) => error, ) Axios.interceptors.response.use((response) => Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response, ) const checkDevMode = (request) => { request.headers['X-Extendify-Assist-Dev-Mode'] = window.location.search.indexOf('DEVMODE') > -1 request.headers['X-Extendify-Assist-Local-Mode'] = window.location.search.indexOf('LOCALMODE') > -1 return request } export { Axios } PK&[}czsrc/Assist/api/Data.jsnu[import { Axios as api } from './axios' export const getTasks = () => api.get('assist/tasks') export const getTaskData = () => api.get('assist/task-data') export const saveTaskData = (data) => api.post('assist/task-data', { data }) export const completedDependency = (taskName) => api.get('assist/tasks/dependency-completed', { params: { taskName }, }) export const getTourData = () => api.get('assist/tour-data') export const saveTourData = (data) => api.post('assist/tour-data', { data }) export const getGlobalData = () => api.get('assist/global-data') export const saveGlobalData = (data) => api.post('assist/global-data', { data }) export const getUserSelectionData = () => api.get('assist/user-selection-data') export const saveUserSelectionData = (data) => api.post('assist/user-selection-data', { data }) export const getQuickLinks = () => api.get('assist/quicklinks') export const getRecommendations = () => api.get('assist/recommendations') PK&9src/Assist/api/WPApi.jsnu[import { Axios as api } from './axios' export const getLaunchPages = () => api.get('assist/launch-pages') export const updateOption = (option, value) => api.post('assist/options', { option, value }) export const getOption = async (option) => { const { data } = await api.get('assist/options', { params: { option }, }) return data } export const getActivePlugins = () => api.get('assist/active-plugins') PK&bsrc/Assist/svg/index.jsnu[export { default as Confetti } from './Confetti' export { default as LogoIcon } from './LogoIcon' PK&[$ FGsrc/Assist/svg/Confetti.jsnu[export default function Confetti(props) { return ( ) } PK&[2src/Assist/svg/LogoIcon.jsnu[export default function LogoIcon(props) { return ( ) } PK&w))src/Assist/state/Taskbar.jsnu[import create from 'zustand' import { devtools } from 'zustand/middleware' const state = (set) => ({ open: false, toggleOpen: () => set((state) => ({ open: !state.open })), }) export const useTaskbarStore = create( devtools(state, { name: 'Extendify Assist Taskbar' }), state, ) PK&[ ( src/Assist/state/Global.jsnu[import { useEffect, useState } from '@wordpress/element' import create from 'zustand' import { devtools, persist } from 'zustand/middleware' import { getGlobalData, saveGlobalData } from '../api/Data' const state = (set, get) => ({ dismissedNotices: [], modals: [], pushModal(modal) { set((state) => ({ modals: [modal, ...state.modals] })) }, popModal() { set((state) => ({ modals: state.modals.slice(1) })) }, clearModals() { set({ modals: [] }) }, isDismissed(id) { return get().dismissedNotices.some((notice) => notice.id === id) }, dismissNotice(id) { if (get().isDismissed(id)) return const notice = { id, dismissedAt: new Date().toISOString() } set((state) => ({ dismissedNotices: [...state.dismissedNotices, notice], })) }, }) const storage = { getItem: async () => JSON.stringify(await getGlobalData()), setItem: async (_, value) => await saveGlobalData(value), removeItem: () => undefined, } export const useGlobalStore = create( persist(devtools(state, { name: 'Extendify Assist Globals' }), { name: 'extendify-assist-globals', getStorage: () => storage, partialize: (state) => { delete state.modals return state }, }), state, ) /* Hook useful for when you need to wait on the async state to hydrate */ export const useGlobalStoreReady = () => { const [hydrated, setHydrated] = useState(useGlobalStore.persist.hasHydrated) useEffect(() => { const unsubFinishHydration = useGlobalStore.persist.onFinishHydration( () => setHydrated(true), ) return () => { unsubFinishHydration() } }, []) return hydrated } PK&[ɺJJsrc/Assist/state/Tours.jsnu[import { useEffect, useState } from '@wordpress/element' import create from 'zustand' import { devtools, persist } from 'zustand/middleware' import { getTourData, saveTourData } from '../api/Data' const state = (set, get) => ({ currentTour: null, currentStep: 0, progress: [], startTour(tourData) { set({ currentTour: tourData }) // See if the tour already was opened const tour = get().trackTourProgress(tourData.id) // Increment the opened count get().updateProgress(tour.id, { openedCount: tour.openedCount + 1, lastAction: 'started', }) }, completeCurrentTour() { if (!get().currentTour) return const tour = get().findTourProgress(get().currentTour.id) // if already completed, dont update the completedAt if (!get().isCompleted(tour.id)) { get().updateProgress(tour.id, { completedAt: new Date().toISOString(), lastAction: 'completed', }) } // Track how many times it was completed get().updateProgress(tour.id, { completedCount: tour.completedCount + 1, lastAction: 'completed', }) set({ currentTour: null, currentStep: 0 }) }, closeForRedirect() { if (!get().currentTour) return const tour = get().findTourProgress(get().currentTour.id) // update last action get().updateProgress(tour?.id ?? get().currentTour, { lastAction: 'redirected', }) set({ currentTour: null, currentStep: 0 }) }, closeCurrentTourManually() { const tour = get().findTourProgress(get().currentTour.id) // Track how many times it was closed early get().updateProgress(tour.id, { closedManuallyCount: tour.closedManuallyCount + 1, lastAction: 'closed-manually', }) set({ currentTour: null, currentStep: 0 }) }, closeCurrentTourFromError() { if (!get().currentTour) return console.error('No tour found') const tour = get().findTourProgress(get().currentTour.id) if (!tour) return console.error('No tour found') get().updateProgress(tour.id, { errored: true, lastAction: 'closed-by-caught-error', }) set({ currentTour: null, currentStep: 0 }) }, findTourProgress(tourId) { return get().progress.find((tour) => tour.id === tourId) }, isCompleted(tourId) { return get().findTourProgress(tourId)?.completedAt }, isSeen(tourId) { return get().findTourProgress(tourId)?.firstSeenAt }, trackTourProgress(tourId) { // If we are already tracking it, return that if (get().findTourProgress(tourId)) { return get().findTourProgress(tourId) } set((state) => ({ progress: [ ...state.progress, { id: tourId, firstSeenAt: new Date().toISOString(), updatedAt: new Date().toISOString(), completedAt: null, lastAction: 'init', currentStep: 0, openedCount: 0, closedManuallyCount: 0, completedCount: 0, errored: false, }, ], })) return get().findTourProgress(tourId) }, updateProgress(tourId, update) { const lastAction = update?.lastAction ?? 'unknown' set((state) => { const progress = state.progress.map((tour) => { if (tour.id === tourId) { return { ...tour, ...update, lastAction, updatedAt: new Date().toISOString(), } } return tour }) return { progress } }) }, hasNextStep() { if (!get().currentTour) return false return get().currentStep < get().currentTour.steps.length - 1 }, nextStep() { if (!get().hasNextStep()) { get().closeCurrentTourFromError() return } const tour = get().currentTour const next = get().currentStep + 1 set(() => ({ currentStep: next })) get().updateProgress(tour.id, { currentStep: next, lastAction: 'next', }) }, hasPreviousStep() { if (!get().currentTour) return false return get().currentStep > 0 }, prevStep() { if (!get().hasPreviousStep()) { get().closeCurrentTourFromError() return } const tour = get().currentTour const prev = get().currentStep - 1 set(() => ({ currentStep: prev })) get().updateProgress(tour.id, { currentStep: prev, lastAction: 'prev', }) }, goToStep(step) { const tour = get().currentTour // Check that the step is valid if (step < 0 || step > tour.steps.length - 1) return get().updateProgress(tour.id, { currentStep: step, lastAction: `go-to-step-${step}`, }) set(() => ({ currentStep: step })) }, }) const storage = { getItem: async () => JSON.stringify(await getTourData()), setItem: async (_, value) => await saveTourData(value), removeItem: () => undefined, } export const useTourStore = create( persist(devtools(state, { name: 'Extendify Assist Tour Progress' }), { name: 'extendify-assist-tour-progress', getStorage: () => storage, partialize: (state) => { // return without currentTour or currentStep // eslint-disable-next-line no-unused-vars const { currentTour, currentStep, ...newState } = state return newState }, }), state, ) /* Hook useful for when you need to wait on the async state to hydrate */ export const useTourStoreReady = () => { const [hydrated, setHydrated] = useState(useTourStore.persist.hasHydrated) useEffect(() => { const unsubFinishHydration = useTourStore.persist.onFinishHydration( () => setHydrated(true), ) return () => { unsubFinishHydration() } }, []) return hydrated } PK&[>vsrc/Assist/state/Tasks.jsnu[import { useEffect, useState } from '@wordpress/element' import create from 'zustand' import { devtools, persist } from 'zustand/middleware' import { getTaskData, saveTaskData } from '../api/Data' const state = (set, get) => ({ // These are tests the user is in progress of completing. // Not to be confused with tasks that are in progress. // ! This should have probably been in Global or elsewhere? activeTests: [], // These are tasks that the user has seen. When added, // they will look like [{ key, firstSeenAt }] seenTasks: [], // These are tasks the user has already completed // [{ key, completedAt }] but it used to just be [key] // so use ?.completedAt to check if it's completed with the (.?) completedTasks: [], inProgressTasks: [], // Available tasks that are actually shown to the user // Each tasks is responsible for checking if it's available // Use this for keeping a total count of available tasks, // and not for showing the task itself availableTasks: [], isCompleted(taskId) { return get().completedTasks.some((task) => task?.id === taskId) }, completeTask(taskId) { if (get().isCompleted(taskId)) { return } set((state) => ({ completedTasks: [ ...state.completedTasks, { id: taskId, completedAt: new Date().toISOString(), }, ], })) }, // Marks the task as dismissed: true dismissTask(taskId) { get().completeTask(taskId) set((state) => { const { completedTasks } = state const task = completedTasks.find((task) => task.id === taskId) return { completedTasks: [ ...completedTasks, { ...task, dismissed: true }, ], } }) }, isSeen(taskId) { return get().seenTasks.some((task) => task?.id === taskId) }, seeTask(taskId) { if (get().isSeen(taskId)) { return } const task = { id: taskId, firstSeenAt: new Date().toISOString(), } set((state) => ({ seenTasks: [...state.seenTasks, task], })) }, uncompleteTask(taskId) { set((state) => ({ completedTasks: state.completedTasks.filter( (task) => task.id !== taskId, ), })) }, toggleCompleted(taskId) { if (get().isCompleted(taskId)) { get().uncompleteTask(taskId) return } get().completeTask(taskId) }, setAvailable(taskId) { if (get().isAvailable(taskId)) return set((state) => ({ availableTasks: [...state.availableTasks, taskId], })) }, isAvailable(taskId) { return get().availableTasks.some((task) => task === taskId) }, }) const storage = { getItem: async () => JSON.stringify(await getTaskData()), setItem: async (_, value) => await saveTaskData(value), removeItem: () => undefined, } export const useTasksStore = create( persist(devtools(state, { name: 'Extendify Assist Tasks' }), { name: 'extendify-assist-tasks', getStorage: () => storage, partialize: (state) => { // return without availableTasks // eslint-disable-next-line no-unused-vars const { availableTasks, ...newState } = state return newState }, }), state, ) /* Hook useful for when you need to wait on the async state to hydrate */ export const useTasksStoreReady = () => { const [hydrated, setHydrated] = useState(useTasksStore.persist.hasHydrated) useEffect(() => { const unsubFinishHydration = useTasksStore.persist.onFinishHydration( () => setHydrated(true), ) return () => { unsubFinishHydration() } }, []) return hydrated } PK&[ީsrc/Assist/state/GlobalSync.jsnu[import create from 'zustand' import { devtools, persist } from 'zustand/middleware' // Similiar to Global.js but syncronous ("faster") const state = (set) => ({ designColors: {}, queuedTour: null, setDesignColors(designColors) { set({ designColors }) }, queueTourForRedirect(tour) { set({ queuedTour: tour }) }, clearQueuedTour() { set({ queuedTour: null }) }, }) export const useGlobalSyncStore = create( persist(devtools(state, { name: 'Extendify Assist Globals Sync' }), { name: 'extendify-assist-globals-sync', getStorage: () => localStorage, }), state, ) PK&+ src/Assist/state/Selections.jsnu[import { useEffect, useState } from '@wordpress/element' import create from 'zustand' import { devtools, persist } from 'zustand/middleware' import { getUserSelectionData, saveUserSelectionData } from '@assist/api/Data' const state = () => ({ siteType: {}, siteInformation: { title: undefined, }, feedbackMissingSiteType: '', feedbackMissingGoal: '', exitFeedback: undefined, siteTypeSearch: [], style: null, pages: [], plugins: [], goals: [], }) // This checks the local storage cache for the user selections set in Launch, if any const cachedSelections = localStorage.getItem('extendify-site-selection') const storage = { getItem: cachedSelections ? () => { localStorage.removeItem('extendify-site-selection') return cachedSelections } : async () => JSON.stringify(await getUserSelectionData()), setItem: async (_, value) => await saveUserSelectionData(value), removeItem: () => undefined, } export const useSelectionStore = create( persist(devtools(state, { name: 'Extendify User Selections' }), { name: 'extendify-site-selection', getStorage: () => storage, }), state, ) /* Hook useful for when you need to wait on the async state to hydrate */ export const useSelectionStoreReady = () => { const [hydrated, setHydrated] = useState( useSelectionStore.persist.hasHydrated, ) useEffect(() => { const unsubFinishHydration = useSelectionStore.persist.onFinishHydration(() => setHydrated(true)) return () => { unsubFinishHydration() } }, []) return hydrated } PK&ZX"src/Assist/components/TaskBadge.jsnu[import { useTasksStoreReady, useTasksStore } from '@assist/state/Tasks' export const TaskBadge = (props) => { const { availableTasks, isCompleted } = useTasksStore() const ready = useTasksStoreReady() if (!ready) return null const taskCount = availableTasks?.filter((t) => !isCompleted(t)).length ?? 0 if (taskCount === 0) return null return ( {taskCount > 9 ? '9' : taskCount} ) } PK&[s s (src/Assist/components/Recommendations.jsnu[import { Spinner } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { useRecommendations } from '@assist/hooks/useRecommendations' export const Recommendations = () => { const { recommendations, loading, error } = useRecommendations() if (loading || error) { return (
) } if (recommendations.length === 0) { return (
{__('No recommendations found...', 'extendify')}
) } return (
{recommendations.map( ({ slug, title, description, linkType, externalLink, internalLink, buttonText, }) => (

{title}

{description}

{buttonText}
), )}
) } PK&Z""$src/Assist/components/TaskbarBody.jsnu[import { Icon } from '@wordpress/components' import { useLayoutEffect, useState, useEffect } from '@wordpress/element' import { __, sprintf } from '@wordpress/i18n' import classNames from 'classnames' import { motion, AnimatePresence } from 'framer-motion' import { useDesignColors } from '@assist/hooks/useDesignColors' import { useEditorHeightAdjust } from '@assist/hooks/useEditorHeightAdjust' import { useTasks } from '@assist/hooks/useTasks' import { useTaskbarStore } from '@assist/state/Taskbar' import { useTasksStore } from '@assist/state/Tasks' import { LogoIcon } from '@assist/svg' import { TaskHead } from './task-items/TaskHead' import { TaskItem } from './task-items/TaskItem' export const TaskbarBody = () => { const { open } = useTaskbarStore() const [offset, setOffset] = useState(0) const [transitioning, setTransitioning] = useState(true) const { darkColor } = useDesignColors() const { availableTasks, isCompleted } = useTasksStore() const { tasks } = useTasks() const taskCount = availableTasks?.filter((t) => !isCompleted(t))?.length ?? 0 const [current, setCurrent] = useState(0) const notCompleted = tasks?.filter((t) => !isCompleted(t.slug)) useEditorHeightAdjust({ open, leaveDelay: 300 }) const handleNext = () => { setTransitioning(true) setCurrent((cur) => (cur + 1 === taskCount ? cur : cur + 1)) } const handlePrev = () => { setTransitioning(true) setCurrent((cur) => (cur === 0 ? cur : cur - 1)) } useLayoutEffect(() => { const handle = () => { // get the height of the admin bar setOffset(document.getElementById('wpadminbar')?.offsetHeight ?? 0) } handle() addEventListener('resize', handle) return () => removeEventListener('resize', handle) }, [open]) useEffect(() => { if (!taskCount) return // if the tasks count changes (they complete/add one) // and they are out of bound, reset the current to the end if (current > taskCount - 1) { setCurrent(taskCount - 1) } // If they dismiss/complete the current task, it's moving setTransitioning(true) }, [taskCount, current]) return ( <>
) } const TasksList = ({ tasks, current, onTransitionEnd }) => { if (!tasks) { return (
{__('Loading tasks...', 'extendify')}
) } if (!tasks?.length) { return (

{__('All caught up!', 'extendify')}

{__( 'Take a break or explore some recommendations to improve site even further!', 'extendify', )}

{/* {__('See recommendations', 'extendify')} */}
) } if (!tasks[current]) return null return ( ) } const ButtonControl = ({ current, totalCount, onNext, onPrev }) => { if (totalCount === 0) return null return ( ) } PK&[" "src/Assist/components/PagesList.jsnu[import { Spinner } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { usePagesList } from '@assist/hooks/usePagesList' import { maybeHttps } from '@assist/util/util' export const PagesList = () => { const { pages, loading, error } = usePagesList() if (loading || error) { return (
) } if (pages.length === 0) { return (
{__('No pages found...', 'extendify')}
) } return (

{__('Pages', 'extendify')}:

{pages.map((page) => (
{page.post_title || __('Untitled', 'extendify')}
))}
) } PK&[o+src/Assist/components/buttons/TourButton.jsnu[import { useTasksStore } from '@assist/state/Tasks' import { useTourStore } from '@assist/state/Tours' import welcomeTour from '@assist/tours/welcome.js' export const TourButton = ({ task, className }) => { const { startTour } = useTourStore() const { isCompleted } = useTasksStore() return ( ) } PK& dd,src/Assist/components/buttons/ModalButton.jsnu[import { useGlobalStore } from '@assist/state/Global' import { useTasksStore } from '@assist/state/Tasks' import { UpdateLogo } from '@assist/tasks/UpdateLogo' import { UpdateSiteDescription } from '@assist/tasks/UpdateSiteDescription' import { UpdateSiteIcon } from '@assist/tasks/UpdateSiteIcon' export const ModalButton = ({ task, className }) => { const { pushModal } = useGlobalStore() const { isCompleted } = useTasksStore() const Components = { UpdateLogo, UpdateSiteDescription, UpdateSiteIcon, } if (!Components[task.modalFunction]) return null return ( ) } PK&[!u%%3src/Assist/components/buttons/InternalLinkButton.jsnu[import { useEffect, useState } from '@wordpress/element' import { getOption } from '@assist/api/WPApi' import { useTasksStore } from '@assist/state/Tasks' export const InternalLinkButton = ({ task, className }) => { const { completeTask } = useTasksStore() const [link, setLink] = useState(null) const handleClick = () => { // If no dependency then complete the task !task.doneDependencies && completeTask(task.slug) } useEffect(() => { if (task.slug === 'edit-homepage') { const split = task.internalLink.split('$') getOption('page_on_front').then((id) => { setLink(split[0] + id + split[1]) }) return } setLink(task.internalLink) }, [task]) if (!link) return null return ( {task.buttonTextToDo} ) } PK&?hhsrc/Assist/components/Modal.jsnu[import { Button } from '@wordpress/components' import { useState, useEffect } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { Icon, close } from '@wordpress/icons' import { Dialog } from '@headlessui/react' import { useGlobalStore } from '@assist/state/Global' export const Modal = () => { const { modals, popModal } = useGlobalStore() const ModalContent = modals[0] const [title, setTitle] = useState('') useEffect(() => { if (!modals[0]) setTitle('') }, [modals]) return ( 0} onClose={popModal}>
) } PK&戗 ,src/Assist/components/task-items/TaskItem.jsnu[import { Dropdown, Icon } from '@wordpress/components' import { useEffect } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { InternalLinkButton } from '@assist/components/buttons/InternalLinkButton' import { ModalButton } from '@assist/components/buttons/ModalButton' import { TourButton } from '@assist/components/buttons/TourButton' import { TaskHead } from '@assist/components/task-items/TaskHead' import { useTasksStore } from '@assist/state/Tasks' export const TaskItem = ({ task, className }) => { const actions = { modal: ModalButton, tour: TourButton, 'internal link': InternalLinkButton, } const Action = task?.taskType ? actions[task.taskType] : null const { dismissTask, seeTask } = useTasksStore() useEffect(() => { task.slug && seeTask(task.slug) }, [seeTask, task?.slug]) return (

{task.title}

{Action && ( )} ( )} renderToggle={({ onToggle }) => ( )} />
) } PK&[&',src/Assist/components/task-items/TaskHead.jsnu[import { useEffect } from '@wordpress/element' import { completedDependency } from '@assist/api/Data' import { useTaskDependencies } from '@assist/hooks/useFetch' import { useTasksStore } from '@assist/state/Tasks' export const TaskHead = ({ task, children }) => { const { isCompleted, isAvailable, completeTask, setAvailable } = useTasksStore() const { slug, doneDependencies } = task // Some tasks have a dependency we look for before showing them // in case we want to pre-emptively mark it as completed const { data, loading } = useTaskDependencies( `tasks${slug}}`, () => { return doneDependencies ? completedDependency(slug) : // If no dependencies, return false to NOT mark it as completed { data: false, loading: false } }, !isCompleted(slug), // refresh when not completed ) useEffect(() => { if (loading) return if (data) completeTask(slug) // All tasks are available by default currently, // but some we may want to mark as completed before showing setAvailable(slug) }, [data, loading, completeTask, slug, setAvailable, task]) if (!isAvailable(slug)) return null return children ?? null } PK&B1gg/src/Assist/components/task-items/TaskItemOld.jsnu[import { Dropdown } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { Icon, moreVertical } from '@wordpress/icons' import { InternalLinkButton } from '@assist/components/buttons/InternalLinkButton' import { ModalButton } from '@assist/components/buttons/ModalButton' import { TourButton } from '@assist/components/buttons/TourButton' import { useTasksStore } from '@assist/state/Tasks' import { TaskHead } from './TaskHead' // TODO: If the plan is to make this not a pressable checkbox, // then we should merge this component in with the TaskItem component // and make them more "headless" with styles applied externally or via class props export const TaskItemOld = ({ task }) => { const { isCompleted, dismissTask } = useTasksStore() const { slug } = task const actions = { modal: ModalButton, tour: TourButton, 'internal link': InternalLinkButton, } const Action = task?.taskType ? actions[task.taskType] : null return (
{isCompleted(slug) ? __('Completed', 'extendify') : __('Not completed', 'extendify')}
{task.title}
{Action && ( )} {isCompleted(slug) ? (
) : ( ( )} renderToggle={({ onToggle }) => ( )} /> )}
) } PK&[99#src/Assist/components/GuidedTour.jsnu[import { Button } from '@wordpress/components' import { useRef, useEffect, useLayoutEffect, useState, } from '@wordpress/element' import { sprintf, __ } from '@wordpress/i18n' import { Icon, close } from '@wordpress/icons' import { Dialog } from '@headlessui/react' import { motion, AnimatePresence } from 'framer-motion' import useSWRImmutable from 'swr/immutable' import { useDesignColors } from '@assist/hooks/useDesignColors' import { useGlobalSyncStore } from '@assist/state/GlobalSync' import { useTasksStore } from '@assist/state/Tasks' import { useTourStore } from '@assist/state/Tours' import welcomeTour from '@assist/tours/welcome.js' import { copyNodeStyle } from '@assist/util/element' const useClonedElement = ({ elementSelector, key, options }) => { const { data: clonedNode } = useSWRImmutable(key, () => { const currentElement = document.querySelector(elementSelector) if (!currentElement) return null // Clone currentElement then place the new element fixed in the same position // as the original element. This allows us to use the original element as a // reference for positioning the tour modal. const clonedElement = currentElement?.cloneNode(true) // copy over computed styles, walking down - mutates the node directly copyNodeStyle(currentElement, clonedElement, options) // add to the DOM with max z-index clonedElement.style.position = 'fixed' clonedElement.style.zIndex = 999999 return clonedElement }) return clonedNode } const availableTours = { [welcomeTour.id]: welcomeTour, } export const GuidedTour = () => { const tourModalRef = useRef() const { currentTour, currentStep, startTour, closeCurrentTourManually, closeCurrentTourFromError, closeForRedirect, } = useTourStore() const { steps, settings } = currentTour || {} const { image, title, text, attachTo, events, cloneOptions } = steps?.[currentStep] ?? {} const { queueTourForRedirect, queuedTour, clearQueuedTour } = useGlobalSyncStore() const [redirecting, setRedirecting] = useState(false) const { onAttach, onDetach, beforeAttach } = events || {} const { element: elementSelector, offset, position, hook } = attachTo || {} const [attachToElement, setAttachToElement] = useState(null) const initialFocus = useRef() const [x, setX] = useState() const [y, setY] = useState() // x is 20 on mobile, so exclude the offset here const placement = x === 20 ? { x, y } : { x, y, ...offset } const clonedElement = useClonedElement({ elementSelector, key: { elementSelector, x }, options: cloneOptions, }) useEffect(() => { if (redirecting) return const tour = queuedTour if (!tour || !availableTours[tour]) return clearQueuedTour() return () => requestAnimationFrame(() => startTour(availableTours[tour])) }, [startTour, queuedTour, redirecting, clearQueuedTour]) useLayoutEffect(() => { // if the tour has a start from url, redirect there if (!settings?.startFrom) return if (window.location.href === settings.startFrom) return if ( // if only hash changed, update the url only window.location.href.split('#')[0] === settings.startFrom.split('#')[0] ) { window.location.assign(settings?.startFrom) return } setRedirecting(true) queueTourForRedirect(currentTour.id) closeForRedirect() window.location.assign(settings?.startFrom) }, [ settings?.startFrom, currentTour, queueTourForRedirect, closeForRedirect, ]) useLayoutEffect(() => { if (!currentTour || redirecting) return const currentElement = document.querySelector(elementSelector) if (!currentElement) { // TODO: error message? snackbar? closeCurrentTourFromError() return } beforeAttach && beforeAttach(clonedElement) setAttachToElement(currentElement) // Cache so we can access it in render callback const previouslyCloned = clonedElement const model = tourModalRef.current const offset = () => { const hooks = hook?.split(' ') || [] return { x: hooks.includes('right') ? model?.offsetWidth : 0, y: hooks.includes('bottom') ? model?.offsetHeight : 0, } } const reset = () => { if (onDetach && previouslyCloned) onDetach(previouslyCloned) if (previouslyCloned) document.body.removeChild(previouslyCloned) setAttachToElement(null) removeEventListener('resize', measure) setX(null) setY(null) } // Measure the position of the element and set the position of the modal nearby const measure = () => { const currentElementRect = currentElement?.getBoundingClientRect() const windowSize = window.innerWidth // Just put near the top if on smaller screens // (960 is when the admin bar collapses) if (windowSize <= 960) { setX(20) setY(20) const id = requestAnimationFrame(() => { if (clonedElement?.parentNode) { // set opacity to 0 clonedElement.style.opacity = 0 } }) return () => { cancelAnimationFrame(id) reset() } } // Happy path - animate to the position setX(currentElementRect?.[position.x] - offset().x) setY(currentElementRect?.[position.y] - offset().y) if (!clonedElement) return // Position the clone right above the source clonedElement.style.top = `${currentElementRect?.top}px` clonedElement.style.left = `${currentElementRect?.left}px` clonedElement.style.opacity = 1 } measure() if (!clonedElement) return reset if (onAttach) onAttach(clonedElement) document.body.appendChild(clonedElement) addEventListener('resize', measure) return reset }, [ closeCurrentTourFromError, clonedElement, currentStep, currentTour, elementSelector, hook, position, beforeAttach, onAttach, onDetach, redirecting, ]) useLayoutEffect(() => { if (!settings?.allowOverflow) return // TODO: Should this be an option? We may need to scroll document.documentElement.classList.add('ext-force-overflow-auto') return () => { document.documentElement.classList.remove('ext-force-overflow-auto') } }, [settings]) if (!attachToElement) return null return ( <> {Boolean(currentTour) && ( undefined}>
{currentTour?.title ?? __('Tour', 'extendify')}
{image && ( {title} )}
{title && (

{title}

)} {text &&

{text}

}
)}
{Boolean(currentTour) && ( ) } const BottomNav = ({ initialFocus }) => { const { goToStep, completeCurrentTour, currentStep, nextStep, hasNextStep, hasPreviousStep, prevStep, } = useTourStore() const { currentTour } = useTourStore() const { id, steps } = currentTour || {} const { mainColor } = useDesignColors() const { completeTask } = useTasksStore() return (
{hasPreviousStep() ? ( ) : null}
{steps?.length > 2 ? ( ) : null}
{hasNextStep() ? ( ) : ( )}
) } PK&[9&src/Assist/components/ImageUploader.jsnu[import { isBlobURL } from '@wordpress/blob' import { DropZone, Button, Spinner, ResponsiveWrapper, } from '@wordpress/components' import { store as coreStore } from '@wordpress/core-data' import { useSelect } from '@wordpress/data' import { useEffect } from '@wordpress/element' import { useState } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { MediaUpload, uploadMedia } from '@wordpress/media-utils' import { getOption, updateOption } from '@assist/api/WPApi' import { getMediaDetails } from '../lib/media' export const ImageUploader = ({ type, onUpdate, title, actionLabel }) => { const [isLoading, setIsLoading] = useState(false) const [imageId, setImageId] = useState(0) const media = useSelect( (select) => select(coreStore).getMedia(imageId), [imageId], ) const { mediaWidth, mediaHeight, mediaSourceUrl } = getMediaDetails(media) useEffect(() => { getOption(type).then((id) => setImageId(Number(id))) }, [type]) const onUpdateImage = (image) => { setImageId(image.id) updateOption(type, image.id) onUpdate() } const onRemoveImage = () => { setImageId(0) updateOption(type, 0) } const onDropFiles = (filesList) => { uploadMedia({ allowedTypes: ['image'], filesList, onFileChange([image]) { if (isBlobURL(image?.url)) { setIsLoading(true) return } onUpdateImage(image) setIsLoading(false) }, onError(message) { console.error({ message }) }, }) } return (
(
)} />
{Boolean(imageId) && (
{imageId && ( ( )} /> )}
)}
) } const MediaUploadCheck = ({ fallback = null, children }) => { const { checkingPermissions, hasUploadPermissions } = useSelect( (select) => { const core = select('core') return { hasUploadPermissions: core.canUser('read', 'media'), checkingPermissions: !core.hasFinishedResolution('canUser', [ 'read', 'media', ]), } }, ) return ( <> {checkingPermissions && } {!checkingPermissions && hasUploadPermissions ? children : fallback} ) } PK&[^3PP2src/Assist/components/dashboard/Recommendations.jsnu[import { Spinner } from '@wordpress/components' import { sprintf, __ } from '@wordpress/i18n' import { Icon, chevronRightSmall } from '@wordpress/icons' import { useRecommendations } from '@assist/hooks/useRecommendations' export const Recommendations = () => { const { recommendations, loading, error } = useRecommendations() if (loading || error) { return (
{error &&

{error}

}
) } if (recommendations?.length === 0) { return (
{__('No recommendations found...', 'extendify')}
) } return (

{__('Recommendatons', 'extendify')}

{recommendations .slice(0, 5) .map( ({ slug, title, linkType, externalLink, internalLink, buttonText, }) => ( ), )}
) } PK&[N))-src/Assist/components/dashboard/QuickLinks.jsnu[import { Spinner } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { Icon, chevronRightSmall } from '@wordpress/icons' import { useQuickLinks } from '@assist/hooks/useQuickLinks' export const QuickLinks = () => { const { quickLinks, loading, error } = useQuickLinks() if (loading || error) { return (
) } if (quickLinks.length === 0) { return (
{__('No quick links found...', 'extendify')}
) } return (

{__('Quick links', 'extendify')}

{quickLinks.map((link) => ( {link.name} ))}
) } PK&U_ _ ,src/Assist/components/dashboard/TasksList.jsnu[import { Spinner } from '@wordpress/components' import { useEffect } from '@wordpress/element' import { sprintf, __ } from '@wordpress/i18n' import { Icon, chevronRightSmall } from '@wordpress/icons' import { TaskItemOld } from '@assist/components/task-items/TaskItemOld' import { useTasks } from '@assist/hooks/useTasks' import { useSelectionStoreReady } from '@assist/state/Selections' import { useTasksStoreReady, useTasksStore } from '@assist/state/Tasks' import { Confetti } from '@assist/svg' export const TasksList = () => { const { seeTask, isCompleted } = useTasksStore() const { tasks, loading, error } = useTasks() const readyTasks = useTasksStoreReady() const readyPlugins = useSelectionStoreReady() // Now filter all tasks that are not completed yet const notCompleted = tasks?.filter((task) => !isCompleted(task.slug)) useEffect(() => { if (!notCompleted?.length || !readyTasks) return // Mark all tasks as seen. If always seen they will not update. notCompleted.forEach((task) => seeTask(task.slug)) }, [notCompleted, seeTask, readyTasks]) if (loading || !readyTasks || !readyPlugins || error) { return (
) } if (tasks?.length === 0) { return (
{__('No tasks found...', 'extendify')}
) } return (

{__('Tasks', 'extendify')}

{notCompleted.length === 0 ? (

{__('All caught up!', 'extendify')}

{__( 'Congratulations! Take a moment to celebrate.', 'extendify', )}

) : ( notCompleted .slice(0, 5) .map((task) => ( )) )}
) } const TaskItemWrapper = ({ task, Action }) => (
) PK&OT("src/Assist/components/TasksList.jsnu[import { Spinner } from '@wordpress/components' import { useEffect, useState } from '@wordpress/element' import { __, sprintf } from '@wordpress/i18n' import { motion, AnimatePresence } from 'framer-motion' import { TaskItemOld } from '@assist/components/task-items/TaskItemOld' import { useTasks } from '@assist/hooks/useTasks' import { useSelectionStoreReady } from '@assist/state/Selections' import { useTasksStoreReady, useTasksStore } from '@assist/state/Tasks' import { Confetti } from '@assist/svg' export const TasksList = () => { const { seeTask, isCompleted } = useTasksStore() const { tasks, loading, error } = useTasks() const [showCompleted, setShowCompleted] = useState(false) const readyTasks = useTasksStoreReady() const readyPlugins = useSelectionStoreReady() // Now filter all tasks that are marked as completed const completed = tasks?.filter((task) => isCompleted(task.slug)) // Now filter all tasks that are not completed yet const notCompleted = tasks?.filter((task) => !isCompleted(task.slug)) // Toggle show/hide completed tasks const toggleCompletedTasks = () => { showCompleted ? setShowCompleted(false) : setShowCompleted(true) } useEffect(() => { if (!tasks?.length || !readyTasks) return // Mark all tasks as seen. If always seen they will not update. tasks.forEach((task) => seeTask(task.slug)) }, [tasks, seeTask, readyTasks]) if (loading || !readyTasks || !readyPlugins || error) { return (
) } if (tasks?.length === 0 || tasks?.length === 0) { return (
{__('No tasks found...', 'extendify')}
) } return (

{__('Personalized tasks for your site', 'extendify')}

{sprintf( // translators: %s is the number of tasks __('%s completed', 'extendify'), completed.length, )} {completed.length > 0 && ( <> · )}
{showCompleted ? ( notCompleted.map((task) => ( )) ) : notCompleted.length === 0 ? (

{__('All caught up!', 'extendify')}

{__( 'Congratulations! Take a moment to celebrate.', 'extendify', )}

) : ( {notCompleted.map((task) => ( ))} )}
{showCompleted && (
{completed.map((task) => ( ))}
)}
) } const TaskItemWrapper = ({ task, Action }) => (
) PK&[5(src/Assist/util/element.jsnu[import { kebabCase } from 'lodash' // Without this there are just too many to process. // Some of these are required though to render properly. const vendorAllowList = [ '-webkit-mask-image', '-webkit-mask-size', '-webkit-mask-repeat', '-webkit-mask-position', '-webkit-mask-origin', '-webkit-mask-clip', ] export const copyNodeStyle = ( currentElement, clonedElement, options, depth = 0, ) => { const computedStyle = window.getComputedStyle(currentElement) Array.from(computedStyle).forEach((key) => { // Ignore some properties prefixed with vendor prefixes if (key.startsWith('-') && !vendorAllowList.includes(key)) return clonedElement.style.setProperty( key, computedStyle.getPropertyValue(key), computedStyle.getPropertyPriority(key), ) }) // Remove pointer events clonedElement.style.pointerEvents = 'none' // This is "expensive" so keep it off by default if (options?.includePsuedoDepth > depth) { // This copies ::before to child elements const identifier = 'id' + Math.round(performance.now()) // Create a stylesheet with all the before values const style = document.createElement('style') // Add a class of the identifier clonedElement.classList.add(identifier) style.innerHTML = `.${identifier}::before { ${Object.entries( window.getComputedStyle(currentElement, 'before'), ) // filter out items with numeric keys .filter(([key]) => !Number.isInteger(Number(key))) // Normalize the key names .map(([key, value]) => `${kebabCase(key)}: ${value};`) .join(' ')} }` document.getElementsByTagName('head')[0].appendChild(style) } // Recursively do the same for all children // TODO: if there's a performance hit on an item, use depth (like above) const children = currentElement?.querySelectorAll('*') const clonedChildren = clonedElement?.querySelectorAll('*') children.forEach((_, i) => { copyNodeStyle(children[i], clonedChildren[i], options, depth + 1) }) } PK&g src/Assist/util/colors.jsnu[import { colord } from 'colord' export const adminBgColor = () => { const menu = document?.querySelector('a.wp-has-current-submenu') if (!menu) return '#1e1e1e' return window.getComputedStyle(menu)?.['background-color'] || '#1e1e1e' } export const adminTextColor = () => { const menu = document?.querySelector('a.wp-has-current-submenu') if (!menu) return '#fff' return window.getComputedStyle(menu)?.['color'] || '#fff' } export const assistAdminBarBgColor = () => { const adminBar = document?.querySelector('#wpadminbar') if (!adminBar) return '#1e1e1e' const computed = window.getComputedStyle(adminBar)?.['background-color'] if (!computed) return '#1e1e1e' return colord(computed).isDark() ? adminBgColor() : computed } PK&src/Assist/util/util.jsnu[export const maybeHttps = (url) => { try { const transformed = new URL(url) if (window.location.protocol === 'https:') { transformed.protocol = 'https:' } return transformed.toString() } catch (e) { return url } } PK&[}8  #src/Assist/pages/Recommendations.jsnu[import { Recommendations as RecommendationsComponent } from '@assist/components/Recommendations' import { Full } from './layouts/Full' export const Recommendations = () => { return ( ) } PK&[= src/Assist/pages/layouts/Full.jsnu[export const Full = ({ children }) => (
{children}
) PK&[S  src/Assist/pages/Dashboard.jsnu[import { QuickLinks } from '@assist/components/dashboard/QuickLinks' import { Recommendations } from '@assist/components/dashboard/Recommendations' import { TasksList } from '@assist/components/dashboard/TasksList' import { Full } from './layouts/Full' export const Dashboard = () => { return (
) } PK&Vsrc/Assist/pages/Tours.jsnu[import { Full } from './layouts/Full' export const Tours = () => { return (
tours
) } PK&[~src/Assist/pages/Tasks.jsnu[import { TasksList } from '@assist/components/TasksList' import { Full } from './layouts/Full' export const Tasks = () => { return ( ) } PK&[src/Assist/pages/HelpCenter.jsnu[import { Full } from './layouts/Full' export const HelpCenter = () => { return (
help center
) } PK&Z src/Assist/pages/parts/Header.jsnu[import { ExternalLink } from '@wordpress/components' import { __ } from '@wordpress/i18n' import { Logo } from '@onboarding/svg' import { useDesignColors } from '@assist/hooks/useDesignColors' import { useRouter } from '@assist/hooks/useRouter' import { Nav } from '@assist/pages/parts/Nav' export const Header = () => { const { pages, navigateTo, current } = useRouter() useDesignColors() return (
{window.extAssistData?.partnerLogo && (
{window.extAssistData.partnerName}
)} {!window.extAssistData?.partnerLogo && ( )} {__('View site', 'extendify')}
) } PK&OOsrc/Assist/pages/parts/Nav.jsnu[import { __ } from '@wordpress/i18n' import { Icon } from '@wordpress/icons' import classNames from 'classnames' export const Nav = ({ pages, activePage, setActivePage }) => ( ) PK&[ src/Assist/AssistTaskbar.jsnu[import { createPortal } from '@wordpress/element' import { __ } from '@wordpress/i18n' import classNames from 'classnames' import { TaskbarBody } from '@assist/components/TaskbarBody' import { useDesignColors } from '@assist/hooks/useDesignColors' import { useTaskbarStore } from '@assist/state/Taskbar' import { LogoIcon } from '@assist/svg' import { TaskBadge } from './components/TaskBadge' export const AssistTaskbar = () => { const { open, toggleOpen } = useTaskbarStore() const { darkColor } = useDesignColors() return ( <>
) } document.body.prepend( Object.assign(document.createElement('div'), { id: 'extendify-assist-taskbar-portal', className: 'extendify-assist', }), ) const TaskbarPortal = () => { return createPortal( , document.getElementById('extendify-assist-taskbar-portal'), ) } PK&[t ddsrc/Assist/tours/welcome.jsnu[import { __ } from '@wordpress/i18n' import { adminBgColor, adminTextColor, assistAdminBarBgColor, } from '../util/colors' export default { id: 'welcome-tour', title: __('Welcome Tour', 'extendify'), description: __('The Welcome Tour', 'extendify'), settings: { allowOverflow: true, startFrom: window.extAssistData.adminUrl + 'admin.php?page=extendify-assist#dashboard', }, steps: [ { title: __('Pages', 'extendify'), text: __( 'Use the pages menu to add, delete, or edit the pages on your site.', 'extendify', ), image: 'https://assets.extendify.com/tours/welcome/add-pages.gif', attachTo: { element: '#menu-pages', offset: { marginTop: 0, marginLeft: 15, }, position: { x: 'right', y: 'top', }, hook: 'top left', }, events: { onAttach: (el) => { el.style.setProperty('background-color', adminBgColor()) Array.from(el.querySelectorAll('*')).forEach((child) => { child.style.setProperty('color', adminTextColor()) }) }, }, }, { title: __('Blog Posts', 'extendify'), text: __( 'Use the posts menu to add, delete, or edit blog posts on your site.', 'extendify', ), image: 'https://assets.extendify.com/tours/welcome/blog-posts.gif', attachTo: { element: '#menu-posts', offset: { marginTop: 0, marginLeft: 15, }, position: { x: 'right', y: 'top', }, hook: 'top left', }, events: { onAttach: (el) => { el.style.setProperty('background-color', adminBgColor()) Array.from(el.querySelectorAll('*')).forEach((child) => { child.style.setProperty('color', adminTextColor()) }) }, }, }, { title: __('View Site', 'extendify'), text: __( "At any time, you can view your site (from a visitor's perspective) from the top admin bar under your site's name.", 'extendify', ), image: 'https://assets.extendify.com/tours/welcome/view-site.gif', attachTo: { element: '#wp-admin-bar-view-site', offset: { marginTop: 0, marginLeft: 10, }, position: { x: 'right', y: 'top', }, hook: 'top left', }, cloneOptions: { includePsuedoDepth: 2, }, events: { beforeAttach: () => { document .querySelector('#wp-admin-bar-site-name') ?.classList?.add('hover') }, onAttach: (el) => { el?.querySelectorAll('#wp-admin-bar-view-site *').forEach( (child) => { child?.style?.setProperty( 'background-color', assistAdminBarBgColor(), 'important', ) child?.style?.setProperty( 'color', adminTextColor(), 'important', ) }, ) }, onDetach: () => { document .querySelector('#wp-admin-bar-site-name') ?.classList?.remove('hover') }, }, }, { title: __('Site Assistant', 'extendify'), text: __( 'Your site assistant will give you personalized recommendations for your site and help guide you through what is needed to achieve your goals. You can access the assistant from the top admin bar or in the left side menu.', 'extendify', ), image: 'https://assets.extendify.com/tours/welcome/site-assistant.gif', attachTo: { element: '#wp-admin-bar-extendify-assist-link', offset: { marginTop: 15, marginLeft: 0, }, position: { x: 'right', y: 'bottom', }, hook: 'top right', }, events: { onAttach: (el) => { el.querySelector('.ab-item')?.style?.setProperty( 'background-color', assistAdminBarBgColor(), 'important', ) }, }, }, ], } PK&G)src/Assist/hooks/useEditorHeightAdjust.jsnu[import { useEffect, useRef } from '@wordpress/element' /** Adjusts the editor height to fit the taskbar drop */ export const useEditorHeightAdjust = ({ open, leaveDelay = 0 }) => { const timerId = useRef(0) const previous = useRef(null) useEffect(() => { const editor = document.querySelector('.edit-post-layout') if (!open) return if (!editor) return clearTimeout(timerId.current) // Return if in full screen mode if (document.body.classList.contains('is-fullscreen-mode')) return // In case something is there, save it for when we close if (open) previous.current = editor.style const previousStyles = previous.current // Position it so it plays nicely with the expanding taskbar editor.style.position = 'absolute' editor.style.left = '0' editor.style.top = '0' editor.style.right = '0' return () => { const editor = document.querySelector('.edit-post-layout') timerId.current = setTimeout(() => { editor.style = previousStyles }, leaveDelay) } }, [open, leaveDelay]) } PK&[[  src/Assist/hooks/usePagesList.jsnu[import useSWRImmutable from 'swr/immutable' import { getLaunchPages } from '@assist/api/WPApi' export const usePagesList = () => { const { data: pages, error } = useSWRImmutable('pages-list', async () => { const response = await getLaunchPages() if (!response?.data || !Array.isArray(response.data)) { console.error(response) throw new Error('Bad data') } return response.data }) return { pages, error, loading: !pages && !error } } PK&77!src/Assist/hooks/useQuickLinks.jsnu[import useSWRImmutable from 'swr/immutable' import { getQuickLinks } from '@assist/api/Data' export const useQuickLinks = () => { const { data: quickLinks, error } = useSWRImmutable( 'quicklinks', async () => { const response = await getQuickLinks() if (!response?.data || !Array.isArray(response.data)) { console.error(response) throw new Error('Bad data') } return response.data }, ) return { quickLinks, error, loading: !quickLinks && !error } } PK& ,src/Assist/hooks/useTasks.jsnu[import useSWRImmutable from 'swr/immutable' import { getTasks } from '@assist/api/Data' import { getActivePlugins } from '@assist/api/WPApi' export const useTasks = () => { const { data: tasks, error } = useSWRImmutable('tasks', async () => { const { data: activePlugins } = await getActivePlugins() const response = await getTasks() if (!response?.data || !Array.isArray(response.data)) { console.error(response) throw new Error('Bad data') } return response.data?.filter((task) => { // If no plugins, show the task if (!task?.plugins?.length) return true // Check if task.plugins intersect with activePlugins return task?.plugins?.some((plugin) => activePlugins?.includes(plugin), ) }) }) // Filter out tasks that have plugin dependencies that don't match the user's plugins return { tasks, error, loading: !tasks && !error } } PK&ssrc/Assist/hooks/useRouter.jsnu[import { useEffect, useLayoutEffect } from '@wordpress/element' import { useState } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { home, tool, tip } from '@wordpress/icons' import { Dashboard } from '@assist/pages/Dashboard' import { Recommendations } from '@assist/pages/Recommendations' import { Tasks } from '@assist/pages/Tasks' const pages = [ { slug: 'dashboard', name: __('Dashboard', 'extendify'), icon: home, component: Dashboard, }, { slug: 'tasks', name: __('Tasks', 'extendify'), icon: tool, component: Tasks, }, { slug: 'recommendations', name: __('Recommendations', 'extendify'), icon: tip, component: Recommendations, }, ] export const useRouter = () => { const [current, setCurrent] = useState() const Component = current?.component ?? (() => null) const navigateTo = (slug) => { window.location.hash = `#${slug}` } useLayoutEffect(() => { // if no hash is present use previous or add #dashboard if (!window.location.hash) { window.location.hash = `#${current?.slug ?? 'dashboard'}` } }, [current]) useEffect(() => { // watch url changes for #dashboard, etc const handle = () => { const hash = window.location.hash.replace('#', '') const page = pages.find((page) => page.slug === hash) if (!page) { navigateTo(current?.slug ?? 'dashboard') return } setCurrent(page) // Update title to match the page document.title = `${page.name} | Extendify Assist` } window.addEventListener('hashchange', handle) handle() return () => { window.removeEventListener('hashchange', handle) } }, [current]) return { current, CurrentPage: () => (
{/* Announce to SR on change */}

{current?.name}

), pages, navigateTo, } } PK&[坦@#src/Assist/hooks/useDesignColors.jsnu[import { useEffect } from '@wordpress/element' import { colord } from 'colord' import useSWRImmutable from 'swr/immutable' import { useGlobalSyncStore } from '@assist/state/GlobalSync' export const useDesignColors = () => { const { designColors: globalDesignColors, setDesignColors } = useGlobalSyncStore() const { data: designColors } = useSWRImmutable('designColors', () => { // If we have partner colors, use those const documentStyles = window.getComputedStyle(document.documentElement) const partnerBg = documentStyles?.getPropertyValue( '--ext-partner-theme-primary-bg', ) if (partnerBg) { return { mainColor: partnerBg, darkColor: colord(partnerBg).darken(0.1).toHex(), textColor: documentStyles?.getPropertyValue( '--ext-partner-theme-primary-text', ) ?? '#fff', } } // Otherwise, use the admin colors const menu = document?.querySelector( 'a.wp-has-current-submenu, li.current > a.current', ) if (!menu) { return globalDesignColors } const adminColor = window.getComputedStyle(menu)?.['background-color'] return { mainColor: adminColor, darkColor: colord(adminColor).darken(0.1).toHex(), textColor: '#fff', } }) useEffect(() => { if (designColors?.mainColor) { document.documentElement.style.setProperty( '--ext-design-main', designColors.mainColor, ) } if (designColors?.darkColor) { document.documentElement.style.setProperty( '--ext-design-dark', designColors.darkColor, ) } if (designColors?.textColor) { document.documentElement.style.setProperty( '--ext-design-text', designColors.textColor, ) } setDesignColors(designColors) }, [designColors, setDesignColors]) return designColors || {} } PK&[@$"MM$src/Assist/hooks/useActivePlugins.jsnu[import useSWRImmutable from 'swr/immutable' import { getActivePlugins } from '@assist/api/WPApi' export const useActivePlugins = () => { const { data: activePlugins, error } = useSWRImmutable( 'active-plugins', async () => { const response = await getActivePlugins() if (!response?.data || !Array.isArray(response.data)) { console.error(response) throw new Error('Bad data') } return response.data }, ) return { activePlugins, error, loading: !activePlugins && !error } } PK&#%ZZ&src/Assist/hooks/useRecommendations.jsnu[import useSWRImmutable from 'swr/immutable' import { getRecommendations } from '@assist/api/Data' export const useRecommendations = () => { const { data: recommendations, error } = useSWRImmutable( 'recommendations', async () => { const response = await getRecommendations() if (!response?.data || !Array.isArray(response.data)) { console.error(response) throw new Error('Bad data') } return response.data }, ) return { recommendations, error, loading: !recommendations && !error } } PK&gsrc/Assist/hooks/useFetch.jsnu[import useSWR from 'swr' export const useTaskDependencies = (params, fetcher, refresh) => useFetch(params, fetcher, { refreshInterval: refresh ? 5000 : 0, dedupingInterval: refresh ? 5000 : 60_000, }) const useFetch = (params, fetcher, options = {}) => { const { data: fetchedData, error } = useSWR( params, async (key) => { const response = await fetcher(key) if (response?.data === undefined || !response) { console.error(response) // This is here in response to CloudFlare intercepting // and redirecting responses throw new Error('No data returned') } return response }, { dedupingInterval: 60_000, refreshInterval: 0, ...options, }, ) const data = fetchedData?.data return { data, loading: data === undefined && !error, error } } PK&[,=src/Assist/Assist.jsnu[import { GuidedTour } from '@assist/components/GuidedTour' import { Modal } from '@assist/components/Modal' export const Assist = () => { return ( <> ) } PK&[rrsrc/Assist/notices/Example.jsnu[import { useGlobalStore } from '../state/Global' const noticeKey = 'next-steps' export const NextSteps = () => { const { isDismissed, dismissNotice } = useGlobalStore() // To avoid content flash, we load in this partial piece of state early via php const dismissed = window.extAssistData.dismissedNotices.find( (notice) => notice.id === noticeKey, ) if (!dismissed || isDismissed(noticeKey)) return null return (
Notice
) } PK&j #src/Assist/notices/WelcomeNotice.jsnu[import { useState, useEffect } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { useGlobalStore, useGlobalStoreReady } from '@assist/state/Global' import { useTourStore } from '@assist/state/Tours' import welcomeTour from '@assist/tours/welcome.js' const noticeKey = 'welcome-message' export const WelcomeNotice = () => { const { isDismissed, dismissNotice } = useGlobalStore() const ready = useGlobalStoreReady() // To avoid content flash, we load in this partial piece of state early via php const dismissed = window.extAssistData.dismissedNotices.find( (notice) => notice.id === noticeKey, ) const [enabled, setEnabled] = useState(false) const { startTour } = useTourStore() useEffect(() => { if (dismissed || isDismissed(noticeKey)) { return } setEnabled(true) }, [dismissed, isDismissed, dismissNotice]) useEffect(() => { if (!enabled || !ready) return // For this notice, we only want to show it once dismissNotice(noticeKey) }, [dismissNotice, enabled, ready]) if (!enabled) return null return (

{__('Congratulations!', 'extendify')}

{__('Your site is ready.', 'extendify')}

{__("What's Next?", 'extendify')}

{__( 'The Extendify Assistant is your go-to dashboard to help you get the most out of your site. Take a quick tour!', 'extendify', )}

) } PK&[!>!src/Assist/notices/AdminNotice.jsnu[import { Fragment } from '@wordpress/element' import { __ } from '@wordpress/i18n' import classNames from 'classnames' import { useDesignColors } from '@assist/hooks/useDesignColors' import { useGlobalStore } from '../state/Global' const steps = { 'site-type': { step: __('Site Industry', 'extendify'), title: __("Let's Start Building Your Website", 'extendify'), description: __( 'Create a super-fast, beautiful, and fully customized site in minutes with the Launch in the core WordPress editor.', 'extendify', ), buttonText: __('Select Site Industry', 'extendify'), }, goals: { step: __('Goals', 'extendify'), title: __('Continue Building Your Website', 'extendify'), description: __( 'Create a super-fast, beautiful, and fully customized site in minutes with the Launch in the core WordPress editor.', 'extendify', ), buttonText: __('Select Site Goals', 'extendify'), }, style: { step: __('Design', 'extendify'), title: __('Continue Building Your Website', 'extendify'), description: __( 'Create a super-fast, beautiful, and fully customized site in minutes with the Launch in the core WordPress editor.', 'extendify', ), buttonText: __('Select Site Design', 'extendify'), }, pages: { step: __('Pages', 'extendify'), title: __('Continue Building Your Website', 'extendify'), description: __( 'Create a super-fast, beautiful, and fully customized site in minutes with the Launch in the core WordPress editor.', 'extendify', ), buttonText: __('Select Site Pages', 'extendify'), }, confirmation: { step: __('Launch', 'extendify'), title: __("Let's Launch Your Site", 'extendify'), description: __( "You're one step away from creating a super-fast, beautiful, and fully customizable site with the Launch in the core WordPress editor.", 'extendify', ), buttonText: __('Launch The Site', 'extendify'), }, } export const AdminNotice = () => { const noticeKey = 'extendify-launch' const { isDismissed, dismissNotice } = useGlobalStore() const { mainColor: bgColor, darkColor: bgDarker } = useDesignColors() // To avoid content flash, we load in this partial piece of state early via php const dismissed = window.extAssistData.dismissedNotices.find( (notice) => notice.id === noticeKey, ) if (dismissed || isDismissed(noticeKey)) return null const pageData = JSON.parse(localStorage.getItem('extendify-pages') ?? null) if (!pageData) return null // Filter out pages that don't match step keys const pages = pageData?.state?.availablePages.filter((p) => Object.keys(steps).includes(p), ) // If their last step doesn't exist in our options, just use step 1 const lastStep = pageData?.state?.currentPageSlug const currentStep = Object.keys(steps).includes(lastStep) ? lastStep : 'site-type' let reached = false return (
{pages.map((item, index) => { if (item === currentStep) reached = true return ( {index !== pages.length - 1 && (
)} ) })}

{steps[currentStep]?.title}

{steps[currentStep]?.description}

{steps[currentStep]?.buttonText}
) } const StepCircle = ({ reached, step, current, stepName, bgColor }) => (
{reached ? step : } {stepName}
) PK& )src/Assist/tasks/UpdateSiteDescription.jsnu[import { useEffect, useState, useRef } from '@wordpress/element' import { __ } from '@wordpress/i18n' import classNames from 'classnames' import { getOption, updateOption } from '@assist/api/WPApi' import { useDesignColors } from '@assist/hooks/useDesignColors' import { useTasksStore } from '@assist/state/Tasks' export const UpdateSiteDescription = ({ popModal, setModalTitle }) => { const [siteDescription, setSiteDescription] = useState(undefined) const [initialValue, setInitialValue] = useState(undefined) const inputRef = useRef() const { completeTask } = useTasksStore() const { mainColor } = useDesignColors() useEffect(() => { setModalTitle(__('Add site description', 'extendify')) }, [setModalTitle]) useEffect(() => { getOption('blogdescription').then((text) => { setSiteDescription(text) setInitialValue(text) }) }, [setSiteDescription]) useEffect(() => { inputRef?.current?.focus() }, [initialValue]) if (typeof siteDescription === 'undefined') { return
{__('Loading...', 'extendify')}
} return (
e.preventDefault()}>
{ setSiteDescription(e.target.value) }} value={siteDescription} placeholder={__('Enter a site description...', 'extendify')} />
) } PK&src/Assist/tasks/UpdateLogo.jsnu[import { useEffect } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { ImageUploader } from '@assist/components/ImageUploader' import { useTasksStore } from '@assist/state/Tasks' export const UpdateLogo = ({ setModalTitle }) => { const { completeTask } = useTasksStore() const updateTask = () => { completeTask('logo') } useEffect(() => { setModalTitle(__('Upload site logo', 'extendify')) }, [setModalTitle]) return ( ) } PK&z"src/Assist/tasks/UpdateSiteIcon.jsnu[import { useEffect } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { ImageUploader } from '@assist/components/ImageUploader' import { useTasksStore } from '@assist/state/Tasks' export const UpdateSiteIcon = ({ setModalTitle }) => { const { completeTask } = useTasksStore() const updateTask = () => { completeTask('site-icon') } useEffect(() => { setModalTitle(__('Upload site icon', 'extendify')) }, [setModalTitle]) return ( ) } PK&4wwsrc/Assist/app.cssnu[/* Adding CSS classes should be done with consideration and rarely */ @tailwind base; @tailwind components; @tailwind utilities; .extendify-assist { --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/); --tw-ring-offset-width: 0px; --tw-ring-offset-color: transparent; --tw-ring-color: var(--ext-design-main, #2c39bd); } .extendify-assist *, .extendify-assist *:after, .extendify-assist *:before { box-sizing: border-box; border: 0 solid #e5e7eb; } .hide-checkmark::before { content: none; } .extendify-assist-upload-logo .components-responsive-wrapper > span { display: block; padding-bottom: 192px !important; } body[class*="_page_extendify-assist"] { @apply bg-white; } body[class*="_page_extendify-assist"] #wpcontent { @apply p-0; } .ext-force-overflow-auto { @apply overflow-auto; } .extendify-assist *:not(.dashicons) { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; } .extendify-assist .input-focus { @apply outline-none text-sm ring-design-main focus:shadow-none focus:ring-wp; } PK& " " src/Assist/app.jsnu[import { render } from '@wordpress/element' import { __ } from '@wordpress/i18n' import { Assist } from '@assist/Assist' import { AssistLandingPage } from '@assist/AssistLandingPage' import { AssistTaskbar } from '@assist/AssistTaskbar' import { TaskBadge } from '@assist/components/TaskBadge' import { AdminNotice } from '@assist/notices/AdminNotice' import './app.css' // Disable Assist while Launch is running const q = new URLSearchParams(window.location.search) const launchActive = ['page'].includes(q.get('extendify-launch')) const launchCompleted = window.extAssistData.launchCompleted const devbuild = window.extAssistData.devbuild const assistPage = document.getElementById('extendify-assist-landing-page') const dashboard = document.getElementById('dashboard-widgets-wrap') // Assist landing page if (!launchActive && assistPage) { // append skip link to get here document .querySelector('.screen-reader-shortcut') .insertAdjacentHTML( 'afterend', `${__( 'Skip to Assist', 'extendify', )}`, ) render(, assistPage) } // Check if they dismissed the notiec already const dismissed = window.extAssistData.dismissedNotices.find( (notice) => notice.id === 'extendify-launch', ) // Launch admin notice - show on devmode too if (dashboard && !dismissed && (!launchCompleted || devbuild)) { const launchAdminNotice = Object.assign(document.createElement('div'), { className: 'extendify-assist', }) // Hide the welcome notice if we are going to display ours document.getElementById('welcome-panel')?.remove() dashboard.before(launchAdminNotice) render(, launchAdminNotice) } if (!launchActive) { const assist = Object.assign(document.createElement('div'), { className: 'extendify-assist', }) document.body.append(assist) render(, assist) } if (!launchActive) { document .querySelector( '#toplevel_page_extendify-admin-page.wp-has-current-submenu', ) ?.classList.add('current') document .querySelectorAll('.extendify-assist-badge-count') ?.forEach((el) => render(, el)) } if (!launchActive) { const taskbar = Object.assign(document.createElement('li'), { id: 'wp-admin-bar-extendify-assist-link', }) document.querySelector('#wp-admin-bar-my-account')?.after(taskbar) render(, taskbar) } PK&[ ? src/Assist/lib/media.jsnu[import { applyFilters } from '@wordpress/hooks' export const getMediaDetails = (media) => { if (!media) return {} const defaultSize = applyFilters( 'editor.PostFeaturedImage.imageSize', 'large', media.id, ) if (defaultSize in (media?.media_details?.sizes ?? {})) { return { mediaWidth: media.media_details.sizes[defaultSize].width, mediaHeight: media.media_details.sizes[defaultSize].height, mediaSourceUrl: media.media_details.sizes[defaultSize].source_url, } } // Use fallbackSize when defaultSize is not available. const fallbackSize = applyFilters( 'editor.PostFeaturedImage.imageSize', 'thumbnail', media.id, ) if (fallbackSize in (media?.media_details?.sizes ?? {})) { return { mediaWidth: media.media_details.sizes[fallbackSize].width, mediaHeight: media.media_details.sizes[fallbackSize].height, mediaSourceUrl: media.media_details.sizes[fallbackSize].source_url, } } // Use full image size when fallbackSize and defaultSize are not available. return { mediaWidth: media.media_details.width, mediaHeight: media.media_details.height, mediaSourceUrl: media.source_url, } } PK&ձsrc/Onboarding/api/axios.jsnu[import axios from 'axios' const Axios = axios.create({ baseURL: window.extOnbData.root, headers: { 'X-WP-Nonce': window.extOnbData.nonce, 'X-Requested-With': 'XMLHttpRequest', 'X-Extendify-Onboarding': true, 'X-Extendify': true, }, }) Axios.interceptors.request.use( (request) => checkDevMode(request), (error) => error, ) Axios.interceptors.response.use( (response) => findResponse(response), (error) => handleErrors(error), ) const findResponse = (response) => { return Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response } const handleErrors = (error) => { if (!error.response) { return } console.error(error.response) // if 4XX, return the error object if (error.response.status >= 400 && error.response.status < 500) { return Promise.reject(error.response) } return Promise.reject(findResponse(error.response)) } const checkDevMode = (request) => { request.headers['X-Extendify-Onboarding-Dev-Mode'] = window.location.search.indexOf('DEVMODE') > -1 request.headers['X-Extendify-Onboarding-Local-Mode'] = window.location.search.indexOf('LOCALMODE') > -1 return request } export { Axios } PK&src/Onboarding/api/WPApi.jsnu[import { __, sprintf } from '@wordpress/i18n' import { Axios as api } from './axios' export const parseThemeJson = (themeJson) => api.post('onboarding/parse-theme-json', { themeJson }) export const updateOption = (option, value) => api.post('onboarding/options', { option, value }) export const getOption = async (option) => { const { data } = await api.get('onboarding/options', { params: { option }, }) return data } export const createPage = (pageData) => api.post(`${window.extOnbData.wpRoot}wp/v2/pages`, pageData) export const trashPost = (postId, postType) => api.delete(`${window.extOnbData.wpRoot}wp/v2/${postType}s/${postId}`) export const getPost = (postSlug, type = 'post') => api.get(`${window.extOnbData.wpRoot}wp/v2/${type}s?slug=${postSlug}`) export const getPageById = (pageId) => api.get(`${window.extOnbData.wpRoot}wp/v2/pages/${pageId}`) export const installPlugin = async (plugin) => { // Fail silently if no slug is provided if (!plugin?.wordpressSlug) return try { // Install plugin and try to activate it. const response = await api.post( `${window.extOnbData.wpRoot}wp/v2/plugins`, { slug: plugin.wordpressSlug, status: 'active', }, ) if (!response.ok) return response } catch (e) { // Fail gracefully for now } try { // Try and activate it if the above fails return await activatePlugin(plugin) } catch (e) { // Fail gracefully for now } } export const activatePlugin = async (plugin) => { const endpoint = `${window.extOnbData.wpRoot}wp/v2/plugins` const response = await api.get(`${endpoint}?search=${plugin.wordpressSlug}`) const pluginSlug = response?.[0]?.plugin if (!pluginSlug) { throw new Error('Plugin not found') } // Attempt to activate the plugin with the slug we found return await api.post(`${endpoint}/${pluginSlug}`, { status: 'active' }) } export const updateTemplatePart = (part, content) => api.post(`${window.extOnbData.wpRoot}wp/v2/template-parts/${part}`, { slug: `${part}`, theme: 'extendable', type: 'wp_template_part', status: 'publish', description: sprintf( // translators: %s is the name of the product, Extendify Launch __('Added by %s', 'extendify'), 'Extendify Launch', ), content, }) export const getHeadersAndFooters = async () => { let patterns = await getTemplateParts() patterns = patterns?.filter((p) => p.theme === 'extendable') const headers = patterns?.filter((p) => p?.slug?.includes('header')) const footers = patterns?.filter((p) => p?.slug?.includes('footer')) return { headers, footers } } const getTemplateParts = () => api.get(window.extOnbData.wpRoot + 'wp/v2/template-parts') export const getThemeVariations = async () => { const variations = await api.get( window.extOnbData.wpRoot + 'wp/v2/global-styles/themes/extendable/variations', ) if (!Array.isArray(variations)) { throw new Error('Could not get theme variations') } return { data: variations } } export const updateThemeVariation = (id, variation) => api.post(`${window.extOnbData.wpRoot}wp/v2/global-styles/${id}`, { id, settings: variation.settings, styles: variation.styles, }) export const addLaunchPagesToNav = ( pages, pageIds, rawCode, replace = null, ) => { if (!replace) replace = '' const pageListItems = pages .map((page) => { return pageIds[page.slug]?.id || page.id ? `` : `` }) .join('') return rawCode.replace(replace, pageListItems) } export const getActivePlugins = () => api.get('onboarding/active-plugins') PK&K src/Onboarding/api/LibraryApi.jsnu[import { Axios as api } from './axios' /* Updates the site type in the Library */ export const updateSiteType = (data) => api.post('library/site-type', data) PK&src/Onboarding/api/DataApi.jsnu[import { getHeadersAndFooters } from './WPApi' import { Axios as api } from './axios' export const getSiteTypes = () => api.get('onboarding/site-types') export const getStylesList = () => api.get('onboarding/styles-list') export const getStyles = async (data) => { // First get the header and footer code const styles = await api.get('onboarding/styles', { params: data }) const { headers, footers } = await getHeadersAndFooters() if (!styles?.data?.length) { throw new Error('Could not get styles') } return { data: styles.data.map((style) => { const header = headers?.find( (h) => h?.slug === style?.headerSlug ?? 'header', ) const footer = footers?.find( (f) => f?.slug === style?.footerSlug ?? 'footer', ) return { ...style, headerCode: header?.content?.raw?.trim() ?? '', footerCode: footer?.content?.raw?.trim() ?? '', } }), } } export const getGoals = () => api.get('onboarding/goals') export const getSuggestedPlugins = () => api.get('onboarding/suggested-plugins') export const getLayoutTypes = () => api.get('onboarding/layout-types') export const getTemplate = (data) => api.get('onboarding/template', { params: data }) export const getExitQuestions = () => api.get('onboarding/exit-questions', { timeout: 1500, }) export const pingServer = () => api.get('onboarding/ping') PK&src/Onboarding/svg/index.jsnu[import BarChart from './BarChart' import Checkmark from './Checkmark' import Design from './Design' import Donate from './Donate' import LeftArrowIcon from './LeftArrowIcon' import Logo from './Logo' import OpenEnvelope from './OpenEnvelope' import Pencil from './Pencil' import Planner from './Planner' import PriceTag from './PriceTag' import Radio from './Radio' import RefreshIcon from './RefreshIcon' import RightArrowIcon from './RightArrowIcon' import School from './School' import SearchIcon from './SearchIcon' import Shop from './Shop' import Speech from './Speech' import Spinner from './Spinner' import SpinnerIcon from './SpinnerIcon' import Ticket from './Ticket' export { BarChart, Checkmark, Design, Donate, LeftArrowIcon, Logo, OpenEnvelope, Pencil, Planner, PriceTag, Radio, School, RefreshIcon, RightArrowIcon, SearchIcon, Shop, Speech, Spinner, SpinnerIcon, Ticket, } PK&[Ԁpff"src/Onboarding/svg/SpinnerIcon.jsxnu[import { memo } from '@wordpress/element' const SpinnerIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(SpinnerIcon) PK&[{r-QQsrc/Onboarding/svg/PriceTag.jsxnu[import { memo } from '@wordpress/element' const PriceTag = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(PriceTag) PK&߃src/Onboarding/svg/Pencil.jsxnu[import { memo } from '@wordpress/element' const Pencil = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Pencil) PK&[0լsrc/Onboarding/svg/Donate.jsxnu[import { memo } from '@wordpress/element' const Donate = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Donate) PK&Lx<< src/Onboarding/svg/Checkmark.jsxnu[import { memo } from '@wordpress/element' const Checkmark = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Checkmark) PK&[ z#src/Onboarding/svg/OpenEnvelope.jsxnu[import { memo } from '@wordpress/element' const OpenEnvelope = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(OpenEnvelope) PK&[`͛~~src/Onboarding/svg/Design.jsxnu[import { memo } from '@wordpress/element' const Design = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Design) PK&[src/Onboarding/svg/Shop.jsxnu[import { memo } from '@wordpress/element' const Shop = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Shop) PK&[$DDsrc/Onboarding/svg/Planner.jsxnu[import { memo } from '@wordpress/element' const Planner = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Planner) PK&[!src/Onboarding/svg/BarChart.jsxnu[import { memo } from '@wordpress/element' const BarChart = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(BarChart) PK&xZ;src/Onboarding/svg/Spinner.jsxnu[import { memo } from '@wordpress/element' const Spinner = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Spinner) PK&zFF$src/Onboarding/svg/LeftArrowIcon.jsxnu[import { memo } from '@wordpress/element' const LeftArrowIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(LeftArrowIcon) PK&[psrc/Onboarding/svg/School.jsxnu[import { memo } from '@wordpress/element' const School = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(School) PK&[FHH%src/Onboarding/svg/RightArrowIcon.jsxnu[import { memo } from '@wordpress/element' const RightArrowIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(RightArrowIcon) PK&3"src/Onboarding/svg/RefreshIcon.jsxnu[import { memo } from '@wordpress/element' const RefreshIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(RefreshIcon) PK&1Xsrc/Onboarding/svg/Logo.jsxnu[import { memo } from '@wordpress/element' const Logo = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Logo) PK&WW!src/Onboarding/svg/SearchIcon.jsxnu[import { memo } from '@wordpress/element' const SearchIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(SearchIcon) PK&[src/Onboarding/svg/Radio.jsxnu[import { memo } from '@wordpress/element' const Radio = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Radio) PK&[ src/Onboarding/svg/Ticket.jsxnu[import { memo } from '@wordpress/element' const Ticket = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Ticket) PK&src/Onboarding/svg/Speech.jsxnu[import { memo } from '@wordpress/element' const Speech = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Speech) PK&k $src/Onboarding/layouts/PageLayout.jsnu[import { __ } from '@wordpress/i18n' import { CompletedTasks } from '@onboarding/components/CompletedTasks' import { PageControl } from '@onboarding/components/PageControl' import { Logo } from '@onboarding/svg' export const PageLayout = ({ children, includeNav = true }) => { return (
{window.extOnbData?.partnerLogo && (
{window.extOnbData?.partnerName
)} {children[0]}
{__('Powered by', 'extendify')} Launch
{includeNav ? (
) : null}
{children[1]}
) } PK&!src/Onboarding/state/factory.jsnu[import create from 'zustand' import { devtools } from 'zustand/middleware' export const pageState = (name, dataCb) => create(devtools(dataCb, { name: `Extendify Launch ${name}` })) PK&src/Onboarding/state/Pages.jsnu[import create from 'zustand' import { persist, devtools } from 'zustand/middleware' import { pages } from '@onboarding/lib/pages' const store = (set, get) => ({ pages: new Map(pages), currentPageIndex: 0, count() { return get().pages.size }, getPageOrder() { return Array.from(get().pages.keys()) }, getCurrentPageData() { return get().pages.get(get().getCurrentPageSlug()) }, getCurrentPageSlug() { const page = get().getPageOrder()[get().currentPageIndex] if (!page) { get().setPage(0) return get().getPageOrder()[0] } return page }, getNextPageData() { const nextIndex = get().currentPageIndex + 1 if (nextIndex > get().count() - 1) return {} return get().pages.get(get().getPageOrder()[nextIndex]) }, setPage(page) { // If page is a string, get the index if (typeof page === 'string') { page = get().getPageOrder().indexOf(page) } if (page > get().count() - 1) return if (page < 0) return set({ currentPageIndex: page }) }, nextPage() { get().setPage(get().currentPageIndex + 1) }, previousPage() { get().setPage(get().currentPageIndex - 1) }, }) const withDevtools = devtools(store, { name: 'Extendify Launch Pages', serialize: true, }) const withPersist = persist(withDevtools, { name: 'extendify-pages', getStorage: () => localStorage, partialize: (state) => ({ currentPageIndex: state?.currentPageIndex ?? 0, currentPageSlug: state?.getCurrentPageSlug() ?? null, availablePages: state?.getPageOrder() ?? [], }), }) export const usePagesStore = create(withPersist) PK&߈src/Onboarding/state/Global.jsnu[import create from 'zustand' import { devtools } from 'zustand/middleware' const store = (set) => ({ generating: false, exitModalOpen: false, closeExitModal: () => set({ exitModalOpen: false }), openExitModal: () => set({ exitModalOpen: true }), hoveredOverExitButton: false, setExitButtonHovered: () => set({ hoveredOverExitButton: true }), }) const withDevtools = devtools(store, { name: 'Extendify Launch Globals' }) export const useGlobalStore = create(withDevtools) PK&[W[&src/Onboarding/state/UserSelections.jsnu[import create from 'zustand' import { persist, devtools } from 'zustand/middleware' const initialState = { siteType: {}, siteInformation: { title: undefined, }, feedbackMissingSiteType: '', feedbackMissingGoal: '', exitFeedback: undefined, siteTypeSearch: [], style: null, variation: null, pages: [], plugins: [], goals: [], } const state = (set, get) => ({ ...initialState, setSiteType(siteType) { set({ siteType }) }, setSiteInformation(name, value) { const siteInformation = { ...get().siteInformation, [name]: value } set({ siteInformation }) }, setFeedbackMissingSiteType: (feedbackMissingSiteType) => set({ feedbackMissingSiteType }), setFeedbackMissingGoal: (feedbackMissingGoal) => set({ feedbackMissingGoal }), setExitFeedback: (exitFeedback) => set({ exitFeedback }), has(type, item) { if (!item?.id) return false return get()[type].some((t) => t.id === item.id) }, add(type, item) { if (get().has(type, item)) return set({ [type]: [...get()[type], item] }) }, remove(type, item) { set({ [type]: get()[type]?.filter((t) => t.id !== item.id) }) }, reset(type) { set({ [type]: [] }) }, toggle(type, item) { if (get().has(type, item)) { get().remove(type, item) return } get().add(type, item) }, setStyle(style) { set({ style }) }, canLaunch() { // The user can launch if they have a complete selection return ( Object.keys(get()?.siteType ?? {})?.length > 0 && Object.keys(get()?.style ?? {})?.length > 0 && get()?.pages?.length > 0 ) }, resetState() { set(initialState) }, }) export const useUserSelectionStore = create( persist(devtools(state, { name: 'Extendify User Selection' }), { name: 'extendify-site-selection', getStorage: () => localStorage, }), state, ) PK&[> -src/Onboarding/components/SuggestedPlugins.jsnu[import { useCallback, useEffect, useMemo } from '@wordpress/element' import { getSuggestedPlugins } from '@onboarding/api/DataApi' import { CheckboxInput } from '@onboarding/components/CheckboxInput' import { useFetch } from '@onboarding/hooks/useFetch' import { useUserSelectionStore } from '@onboarding/state/UserSelections' export const fetcher = () => getSuggestedPlugins() export const fetchData = () => ({ key: 'plugins' }) export const SuggestedPlugins = () => { const { data: suggestedPlugins } = useFetch(fetchData, fetcher) const { goals, add, toggle, remove } = useUserSelectionStore() const nothingToRecommend = useMemo(() => { if (!goals?.length) return true // If no suggested plugins match any of the goals, return false return !goals?.find((goal) => { return suggestedPlugins?.some((plugin) => plugin?.goals?.includes(goal?.slug), ) }) }, [goals, suggestedPlugins]) const hasGoal = useCallback( (plugin) => { // True if we have no recommendations if (nothingToRecommend) return true // Otherwise check the goal/suggestion overlap const goalSlugs = goals.map((goal) => goal.slug) return plugin?.goals.find((goalSlug) => goalSlugs.includes(goalSlug), ) }, [goals, nothingToRecommend], ) useEffect(() => { // Clean up first in case they updated their choices suggestedPlugins?.forEach((plugin) => remove('plugins', plugin)) // If nothing to recommend, don't autoselect anything if (nothingToRecommend) return // Select all plugins that match goals on mount suggestedPlugins ?.filter(hasGoal) ?.forEach((plugin) => add('plugins', plugin)) }, [suggestedPlugins, add, nothingToRecommend, hasGoal, remove]) return (
{suggestedPlugins?.filter(hasGoal)?.map((plugin) => (
toggle('plugins', plugin)} />
))}
) } PK&//.src/Onboarding/components/CheckboxInputCard.jsnu[export const CheckboxInputCard = ({ label, slug, description, checked, onChange, Icon, }) => { return ( ) } PK&[ (src/Onboarding/components/PagePreview.jsnu[import { useMemo } from '@wordpress/element' import { __, sprintf } from '@wordpress/i18n' import classNames from 'classnames' import { getTemplate } from '@onboarding/api/DataApi' import { StylePreview } from '@onboarding/components/StyledPreview' import { useFetch } from '@onboarding/hooks/useFetch' import { findTheCode } from '@onboarding/lib/util' import { useUserSelectionStore } from '@onboarding/state/UserSelections' import { Checkmark } from '@onboarding/svg' export const fetcher = (data) => getTemplate(data) export const PagePreview = ({ page, blockHeight, required = false, displayOnly = false, title = '', }) => { const { siteType, style, toggle, has } = useUserSelectionStore() const isHome = page?.slug === 'home' const { data: pageData } = useFetch( { siteType: siteType.slug, layoutType: page.slug, baseLayout: isHome ? siteType.slug.startsWith('blog') ? style?.blogBaseLayout : style?.homeBaseLayout : null, kit: page.slug !== 'home' ? style?.kit : null, }, fetcher, ) if (displayOnly) { return (
{title && (
{title}
)}
) } return (
required || toggle('pages', page)} title={ required && title ? sprintf( // translators: %s is the name of a page (e.g. Home, Blog, About) __('%s page is required', 'extendify'), title, ) : sprintf( // translators: %s is the name of a page (e.g. Home, Blog, About) __('Toggle %s page', 'extendify'), title, ) } onKeyDown={(e) => { if (['Enter', 'Space', ' '].includes(e.key)) { if (!required) toggle('pages', page) } }}>
{title && (
{title} {required && ( )}
)} {has('pages', page) ? (
) : (
)}
) } const StylePreviewWrapper = ({ page, style, measure = true, blockHeight = false, }) => { const context = useMemo( () => ({ type: 'page', detail: page.slug, measure, }), [page, measure], ) return ( ) } PK&[}^G$G$&src/Onboarding/components/ExitModal.jsnu[import { Button } from '@wordpress/components' import { useRef, useState, useEffect } from '@wordpress/element' import { __, sprintf } from '@wordpress/i18n' import { Icon, close } from '@wordpress/icons' import { Dialog } from '@headlessui/react' import classNames from 'classnames' import { shuffle } from 'lodash' import { getExitQuestions } from '@onboarding/api/DataApi' import { updateOption } from '@onboarding/api/WPApi' import { useGlobalStore } from '@onboarding/state/Global' import { useUserSelectionStore } from '@onboarding/state/UserSelections' import { Checkmark } from '@onboarding/svg' export const ExitModal = () => { const { exitModalOpen, closeExitModal, hoveredOverExitButton } = useGlobalStore() const { setExitFeedback } = useUserSelectionStore() const [value, setValue] = useState(null) const [customOther, setCustomOther] = useState('') const [options, setOptions] = useState([]) const initialFocus = useRef() const submitExitSurvey = () => { setExitFeedback( value === __('Other', 'extendify') ? `Other: ${customOther}` : value, ) skipLaunch() } const skipLaunch = async () => { // Store when Launch is skipped. await updateOption( 'extendify_onboarding_skipped', new Date().toISOString(), ) location.href = window.extOnbData.adminUrl } useEffect(() => { if (!hoveredOverExitButton) return // Intentionally not using SWR so we only try once. getExitQuestions() .then(({ data }) => { if (!Array.isArray(data) || !data[0]?.key) { throw new Error('Invalid data') } setOptions([ ...shuffle(data.filter((d) => d.key !== 'Other')), { key: 'Other', label: __('Other', 'extendify') }, ]) }) .catch(() => { const backupQuestions = [ { key: 'I still want it, just disabling temporary', label: __( 'I still want it, just disabling temporary', 'extendify', ), }, { key: 'I plan on using my own theme or builder', label: __( 'I plan on using my own theme or builder', 'extendify', ), }, { key: "The theme designs don't look great", label: __( "The theme designs don't look great", 'extendify', ), }, ] setOptions([ ...shuffle(backupQuestions), { key: 'Other', label: __('Other', 'extendify') }, ]) }) .finally(() => { initialFocus.current?.focus() }) }, [hoveredOverExitButton]) return (
) } const LabeledCheckbox = ({ label, slug, setValue, value, customOther, setCustomOther, initialFocus, }) => { const needsInput = slug === 'Other' const checked = value === slug const id = slug .toLowerCase() .replace(/ /g, '-') .replace(/[^\w-]+/g, '') return ( <> setValue(slug)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { setValue(slug) } }} role="radio" aria-labelledby={id} aria-checked={checked} data-value={slug} // Will focus on the first element in the list ref={initialFocus} tabIndex={0} className="w-5 h-5 relative mr-2"> setValue(slug)} id={id}> {label} {needsInput && checked ? (