8889841creadme.txt000064400000025264150534626470006567 0ustar00=== 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 app/Assist/AdminPage.php000064400000002141150534626470011142 0ustar00
'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), ]); } } app/Assist/Controllers/TourController.php000064400000001460150534626470014623 0ustar00get_param('data'), true); update_option('extendify_assist_tour_progress', $data); return new \WP_REST_Response($data); } } app/Assist/Controllers/UserSelectionController.php000064400000001502150534626470016453 0ustar00get_param('data'), true); update_option('extendify_user_selections', $data); return new \WP_REST_Response($data); } } app/Assist/Controllers/RecommendationsController.php000064400000001124150534626470017016 0ustar00get_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, ]); } } app/Assist/Controllers/QuickLinksController.php000064400000001071150534626470015745 0ustar00get_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]); } } app/Assist/Controllers/GlobalsController.php000064400000001466150534626470015263 0ustar00get_param('data'), true); update_option('extendify_assist_globals', $data); return new \WP_REST_Response($data); } } app/Assist/Admin.php000064400000013670150534626470010356 0ustar00loadScripts(); 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; } } app/Config.php000064400000010122150534626470007252 0ustar00showOnboarding(); 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; } } app/Onboarding/AdminPage.php000064400000001330150534626470011755 0ustar00
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); } } app/Onboarding/Controllers/WPController.php000064400000003525150534626470015040 0ustar00get_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), ]); } } app/Onboarding/Admin.php000064400000016662150534626470011176 0ustar00loadScripts(); $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}; }"); } } app/Library/AdminPage.php000064400000023065150534626470011310 0ustar00slug) { \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() { } } app/Library/Controllers/TaxonomyController.php000064400000001062150534626470015644 0ustar00get_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) ); } } app/Library/Controllers/AuthController.php000064400000002131150534626470014725 0ustar00get_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) ); } } app/Library/Controllers/PingController.php000064400000001215150534626470014723 0ustar00get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } app/Library/Controllers/UserController.php000064400000004271150534626470014751 0ustar00get_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) ); } } app/Library/Controllers/PluginController.php000064400000002636150534626470015274 0ustar00get_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; } } app/Library/Controllers/MetaController.php000064400000001223150534626470014713 0ustar00get_params()); return new \WP_REST_Response( $response, wp_remote_retrieve_response_code($response) ); } } app/Library/Controllers/SiteSettingsController.php000064400000002742150534626470016461 0ustar00get_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); } } app/Library/SiteSettings.php000064400000002650150534626470012105 0ustar00key, $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); } } app/Library/Plugin.php000064400000022611150534626470010715 0ustar00install($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; } } app/Library/Shared.php000064400000023034150534626470010665 0ustar00themeCompatInlineStyles(); } ); \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); } } app/Library/Admin.php000064400000013561150534626470010513 0ustar00loadScripts(); \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; } } app/User.php000064400000011224150534626470006767 0ustar00user = $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); } } app/Insights.php000064400000005037150534626470007646 0ustar00 [ '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; }); } } app/AdminPageRouter.php000064400000016176150534626470011112 0ustar00addSubMenu('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'; } } app/ApiRouter.php000064400000006331150534626470007766 0ustar00capability = 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); } } app/Http.php000064400000013063150534626470006773 0ustar00get_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']; } } loader.php000064400000003047150534626470006543 0ustar00 $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'; config.json000064400000001105150534626470006715 0ustar00{ "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" } } vendor/autoload.php000064400000000262150534626470010376 0ustar00 * 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; } vendor/composer/autoload_namespaces.php000064400000000225150534626470014423 0ustar00= 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; } } vendor/composer/autoload_classmap.php000064400000000223150534626470014105 0ustar00 array($baseDir . '/app'), ); vendor/composer/autoload_static.php000064400000001463150534626470013600 0ustar00 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); } } src/Assist/AssistLandingPage.js000064400000001706150534626470012517 0ustar00import { 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) }, }}> ) src/Assist/api/axios.js000064400000001471150534626470011052 0ustar00import 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 } src/Assist/api/Data.js000064400000001717150534626470010603 0ustar00import { 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') src/Assist/api/WPApi.js000064400000000657150534626470010714 0ustar00import { 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') src/Assist/svg/index.js000064400000000142150534626470011056 0ustar00export { default as Confetti } from './Confetti' export { default as LogoIcon } from './LogoIcon' src/Assist/svg/Confetti.js000064400000014366150534626470011537 0ustar00export default function Confetti(props) { return ( ) } src/Assist/svg/LogoIcon.js000064400000002671150534626470011471 0ustar00export default function LogoIcon(props) { return ( ) } src/Assist/state/Taskbar.js000064400000000451150534626470011662 0ustar00import 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, ) src/Assist/state/Global.js000064400000003402150534626470011472 0ustar00import { 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 } src/Assist/state/Tours.js000064400000014512150534626470011412 0ustar00import { 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 } src/Assist/state/Tasks.js000064400000007717150534626470011374 0ustar00import { 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 } src/Assist/state/GlobalSync.js000064400000001206150534626470012327 0ustar00import 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, ) src/Assist/state/Selections.js000064400000003225150534626470012405 0ustar00import { 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 } src/Assist/components/TaskBadge.js000064400000000752150534626470013171 0ustar00import { 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} ) } src/Assist/components/Recommendations.js000064400000004563150534626470014477 0ustar00import { 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}
), )}
) } src/Assist/components/TaskbarBody.js000064400000021253150534626470013550 0ustar00import { 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 ( ) } src/Assist/components/PagesList.js000064400000004712150534626470013237 0ustar00import { 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')}
))}
) } src/Assist/components/buttons/TourButton.js000064400000001036150534626470015163 0ustar00import { 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 ( ) } src/Assist/components/buttons/ModalButton.js000064400000001544150534626470015272 0ustar00import { 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 ( ) } src/Assist/components/buttons/InternalLinkButton.js000064400000002045150534626470016625 0ustar00import { 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} ) } src/Assist/components/Modal.js000064400000004150150534626470012374 0ustar00import { 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}>
) } src/Assist/components/task-items/TaskItem.js000064400000005226150534626470015147 0ustar00import { 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 }) => ( )} />
) } src/Assist/components/task-items/TaskHead.js000064400000002407150534626470015110 0ustar00import { 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 } src/Assist/components/task-items/TaskItemOld.js000064400000011147150534626470015605 0ustar00import { 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 }) => ( )} /> )}
) } src/Assist/components/GuidedTour.js000064400000034772150534626470013430 0ustar00import { 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() ? ( ) : ( )}
) } src/Assist/components/ImageUploader.js000064400000014346150534626470014066 0ustar00import { 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} ) } src/Assist/components/dashboard/Recommendations.js000064400000007120150534626470016416 0ustar00import { 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, }) => ( ), )}
) } src/Assist/components/dashboard/QuickLinks.js000064400000003451150534626470015347 0ustar00import { 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} ))}
) } src/Assist/components/dashboard/TasksList.js000064400000006537150534626470015223 0ustar00import { 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 }) => (
) src/Assist/components/TasksList.js000064400000013642150534626470013267 0ustar00import { 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 }) => (
) src/Assist/util/element.js000064400000004214150534626470011562 0ustar00import { 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) }) } src/Assist/util/colors.js000064400000001401150534626470011425 0ustar00import { 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 } src/Assist/util/util.js000064400000000426150534626470011107 0ustar00export const maybeHttps = (url) => { try { const transformed = new URL(url) if (window.location.protocol === 'https:') { transformed.protocol = 'https:' } return transformed.toString() } catch (e) { return url } } src/Assist/pages/Recommendations.js000064400000000414150534626470013400 0ustar00import { Recommendations as RecommendationsComponent } from '@assist/components/Recommendations' import { Full } from './layouts/Full' export const Recommendations = () => { return ( ) } src/Assist/pages/layouts/Full.js000064400000000271150534626470012654 0ustar00export const Full = ({ children }) => (
{children}
) src/Assist/pages/Dashboard.js000064400000001040150534626470012134 0ustar00import { 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 (
) } src/Assist/pages/Tours.js000064400000000225150534626470011365 0ustar00import { Full } from './layouts/Full' export const Tours = () => { return (
tours
) } src/Assist/pages/Tasks.js000064400000000313150534626470011334 0ustar00import { TasksList } from '@assist/components/TasksList' import { Full } from './layouts/Full' export const Tasks = () => { return ( ) } src/Assist/pages/HelpCenter.js000064400000000240150534626470012277 0ustar00import { Full } from './layouts/Full' export const HelpCenter = () => { return (
help center
) } src/Assist/pages/parts/Header.js000064400000004216150534626470012576 0ustar00import { 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')}
) } src/Assist/pages/parts/Nav.js000064400000003117150534626470012131 0ustar00import { __ } from '@wordpress/i18n' import { Icon } from '@wordpress/icons' import classNames from 'classnames' export const Nav = ({ pages, activePage, setActivePage }) => ( ) src/Assist/AssistTaskbar.js000064400000004701150534626470011733 0ustar00import { 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'), ) } src/Assist/tours/welcome.js000064400000012544150534626470011770 0ustar00import { __ } 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', ) }, }, }, ], } src/Assist/hooks/useEditorHeightAdjust.js000064400000002222150534626470014543 0ustar00import { 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]) } src/Assist/hooks/usePagesList.js000064400000000766150534626470012717 0ustar00import 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 } } src/Assist/hooks/useQuickLinks.js000064400000001067150534626470013074 0ustar00import 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 } } src/Assist/hooks/useTasks.js000064400000001745150534626470012107 0ustar00import 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 } } src/Assist/hooks/useRouter.js000064400000004272150534626470012300 0ustar00import { 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, } } src/Assist/hooks/useDesignColors.js000064400000004211150534626470013404 0ustar00import { 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 || {} } src/Assist/hooks/useActivePlugins.js000064400000001115150534626470013566 0ustar00import 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 } } src/Assist/hooks/useRecommendations.js000064400000001132150534626470014137 0ustar00import 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 } } src/Assist/hooks/useFetch.js000064400000001705150534626470012047 0ustar00import 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 } } src/Assist/Assist.js000064400000000350150534626470010417 0ustar00import { GuidedTour } from '@assist/components/GuidedTour' import { Modal } from '@assist/components/Modal' export const Assist = () => { return ( <> ) } src/Assist/notices/Example.js000064400000001162150534626470012212 0ustar00import { 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
) } src/Assist/notices/WelcomeNotice.js000064400000006211150534626470013354 0ustar00import { 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', )}

) } src/Assist/notices/AdminNotice.js000064400000016256150534626470013023 0ustar00import { 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}
) src/Assist/tasks/UpdateSiteDescription.js000064400000005650150534626470014561 0ustar00import { 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')} />
) } src/Assist/tasks/UpdateLogo.js000064400000001271150534626470012344 0ustar00import { 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 ( ) } src/Assist/tasks/UpdateSiteIcon.js000064400000001302150534626470013154 0ustar00import { 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 ( ) } src/Assist/app.css000064400000002167150534626470010115 0ustar00/* 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; } src/Assist/app.js000064400000005042150534626470007734 0ustar00import { 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) } src/Assist/lib/media.js000064400000002437150534626470011006 0ustar00import { 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, } } src/Onboarding/api/axios.js000064400000002400150534626470011657 0ustar00import 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 } src/Onboarding/api/WPApi.js000064400000011017150534626470011520 0ustar00import { __, 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') src/Onboarding/api/LibraryApi.js000064400000000237150534626470012600 0ustar00import { Axios as api } from './axios' /* Updates the site type in the Library */ export const updateSiteType = (data) => api.post('library/site-type', data) src/Onboarding/api/DataApi.js000064400000002755150534626470012054 0ustar00import { 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') src/Onboarding/svg/index.js000064400000001711150534626470011675 0ustar00import 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, } src/Onboarding/svg/SpinnerIcon.jsx000064400000002546150534626470013214 0ustar00import { memo } from '@wordpress/element' const SpinnerIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(SpinnerIcon) src/Onboarding/svg/PriceTag.jsx000064400000002521150534626470012454 0ustar00import { memo } from '@wordpress/element' const PriceTag = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(PriceTag) src/Onboarding/svg/Pencil.jsx000064400000002203150534626470012165 0ustar00import { memo } from '@wordpress/element' const Pencil = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Pencil) src/Onboarding/svg/Donate.jsx000064400000004327150534626470012176 0ustar00import { memo } from '@wordpress/element' const Donate = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Donate) src/Onboarding/svg/Checkmark.jsx000064400000001074150534626470012650 0ustar00import { memo } from '@wordpress/element' const Checkmark = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Checkmark) src/Onboarding/svg/OpenEnvelope.jsx000064400000001654150534626470013363 0ustar00import { memo } from '@wordpress/element' const OpenEnvelope = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(OpenEnvelope) src/Onboarding/svg/Design.jsx000064400000002576150534626470012201 0ustar00import { memo } from '@wordpress/element' const Design = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Design) src/Onboarding/svg/Shop.jsx000064400000001636150534626470011675 0ustar00import { memo } from '@wordpress/element' const Shop = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Shop) src/Onboarding/svg/Planner.jsx000064400000001504150534626470012355 0ustar00import { memo } from '@wordpress/element' const Planner = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Planner) src/Onboarding/svg/BarChart.jsx000064400000001765150534626470012455 0ustar00import { memo } from '@wordpress/element' const BarChart = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(BarChart) src/Onboarding/svg/Spinner.jsx000064400000001236150534626470012376 0ustar00import { memo } from '@wordpress/element' const Spinner = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Spinner) src/Onboarding/svg/LeftArrowIcon.jsx000064400000001106150534626470013472 0ustar00import { memo } from '@wordpress/element' const LeftArrowIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(LeftArrowIcon) src/Onboarding/svg/School.jsx000064400000001355150534626470012211 0ustar00import { memo } from '@wordpress/element' const School = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(School) src/Onboarding/svg/RightArrowIcon.jsx000064400000001110150534626470013650 0ustar00import { memo } from '@wordpress/element' const RightArrowIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(RightArrowIcon) src/Onboarding/svg/RefreshIcon.jsx000064400000001320150534626470013161 0ustar00import { memo } from '@wordpress/element' const RefreshIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(RefreshIcon) src/Onboarding/svg/Logo.jsx000064400000015701150534626470011662 0ustar00import { memo } from '@wordpress/element' const Logo = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Logo) src/Onboarding/svg/SearchIcon.jsx000064400000001527150534626470013001 0ustar00import { memo } from '@wordpress/element' const SearchIcon = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(SearchIcon) src/Onboarding/svg/Radio.jsx000064400000000775150534626470012025 0ustar00import { memo } from '@wordpress/element' const Radio = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Radio) src/Onboarding/svg/Ticket.jsx000064400000002002150534626470012173 0ustar00import { memo } from '@wordpress/element' const Ticket = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Ticket) src/Onboarding/svg/Speech.jsx000064400000001707150534626470012172 0ustar00import { memo } from '@wordpress/element' const Speech = (props) => { const { className, ...otherProps } = props return ( ) } export default memo(Speech) src/Onboarding/layouts/PageLayout.js000064400000004607150534626470013550 0ustar00import { __ } 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]}
) } src/Onboarding/state/factory.js000064400000000272150534626470012557 0ustar00import create from 'zustand' import { devtools } from 'zustand/middleware' export const pageState = (name, dataCb) => create(devtools(dataCb, { name: `Extendify Launch ${name}` })) src/Onboarding/state/Pages.js000064400000003355150534626470012154 0ustar00import 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) src/Onboarding/state/Global.js000064400000000760150534626470012312 0ustar00import 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) src/Onboarding/state/UserSelections.js000064400000004024150534626470014056 0ustar00import 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, ) src/Onboarding/components/SuggestedPlugins.js000064400000004666150534626470015464 0ustar00import { 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)} />
))}
) } src/Onboarding/components/CheckboxInputCard.js000064400000003457150534626470015525 0ustar00export const CheckboxInputCard = ({ label, slug, description, checked, onChange, Icon, }) => { return ( ) } src/Onboarding/components/PagePreview.js000064400000012213150534626470014371 0ustar00import { 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 ( ) } src/Onboarding/components/ExitModal.js000064400000022107150534626470014044 0ustar00import { 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 ? (