diff --git a/assets/apps/dashboard/src/Components/Content/AvailableModule.js b/assets/apps/dashboard/src/Components/Content/AvailableModule.js new file mode 100644 index 0000000000..1b633e6617 --- /dev/null +++ b/assets/apps/dashboard/src/Components/Content/AvailableModule.js @@ -0,0 +1,212 @@ +/* global neveDash */ +import { __, sprintf } from '@wordpress/i18n'; +import { + NEVE_AVAILABLE_MODULES_ICON_MAP, + NEVE_STORE, +} from '../../utils/constants'; +import { ArrowRight, LoaderCircle, LucideSettings } from 'lucide-react'; +import Card from '../../Layout/Card'; +import { useState } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import Toggle from '../Common/Toggle'; +import { send } from '../../utils/rest'; + +const Toast = ({ message }) => { + return ( +
+ {message} +
+ ); +}; + +const ModuleToggle = ({ + slug, + moduleData, + setMessage, + isActive, + setIsActive, + isInstalled, + setIsInstalled, +}) => { + const [loading, setLoading] = useState(false); + const { changeModuleStatus, setObfxModuleStatus, setToast } = + useDispatch(NEVE_STORE); + const { moduleStatus } = useSelect((select) => { + const { getObfxModuleStatus } = select(NEVE_STORE); + + return { + moduleStatus: getObfxModuleStatus(slug) || false, + }; + }); + + const { api } = neveDash; + const { title } = moduleData; + const toastMessage = { + //translators: %s - Plugin name + installing: sprintf(__('Installing %s', 'neve'), 'Orbit Fox Plugin'), + //translators: %s - Plugin name + activating: sprintf(__('Activating %s', 'neve'), 'Orbit Fox Plugin'), + }; + + const handleToggle = async (value) => { + try { + setLoading(true); + changeModuleStatus(slug, value); + + let isPluginActive = true; + // Handle plugin installation or activation + if (!isInstalled) { + setMessage(toastMessage.installing); + isPluginActive = false; + } else if (!isActive) { + setMessage(toastMessage.activating); + isPluginActive = false; + } + + if (!isPluginActive) { + await send(api + 'activate-plugin', { + slug: 'themeisle-companion', + }).then((res) => { + if (res.success) { + setIsInstalled(true); + setIsActive(true); + } + }); + } + + // Fire the send method after install/activate or immediately if both are done + const response = await send(api + 'activate-module', { + slug, + value, + }); + + setObfxModuleStatus(slug, response.success ? value : !value); + setToast( + response.success + ? (value + ? __('Module Activated', 'neve') + : __('Module Deactivated.', 'neve')) + ` (${title})` + : response.data + ); + } catch (error) { + setToast( + __('Something went wrong while activating the module.', 'neve') + ); + } finally { + setLoading(false); + setMessage(''); + } + }; + + return ( +
+ +
+ ); +}; + +const AvailableModuleCard = ({ + moduleData, + slug, + setMessage, + isActive, + setIsActive, + isInstalled, + setIsInstalled, +}) => { + const { title, description } = moduleData; + const CardIcon = NEVE_AVAILABLE_MODULES_ICON_MAP[slug] || LucideSettings; + + return ( + } + title={title} + className="bg-white p-6 rounded-lg shadow-sm" + afterTitle={ + + } + id={`module-${slug}`} + > +

+ {description} +

+ {!isActive ? ( +

+ {__( + 'This feature is part of OrbitFox plugin, built by the Neve team. Enabling the toggle will automatically install and activate the plugin.', + 'neve' + )} +

+ ) : ( + + {__('Go to Settings to Edit', 'neve')} + + + )} +
+ ); +}; + +export default () => { + const [message, setMessage] = useState(''); + const [isInstalled, setIsInstalled] = useState( + neveDash.orbitFox.isInstalled + ); + const [isActive, setIsActive] = useState(neveDash.orbitFox.isActive); + + return ( + <> +
+
+

+ {__('Available Modules', 'neve')} +

+
+
+ {Object.entries(neveDash.availableModules).map( + ([slug, moduleData]) => ( + + ) + )} +
+
+ {message && ( + + + {message} + + } + /> + )} + + ); +}; diff --git a/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js index 8a077600b0..8248c15ff1 100644 --- a/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js +++ b/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js @@ -1,5 +1,11 @@ +import AvailableModule from '../AvailableModule'; import ModuleGrid from '../ModuleGrid'; export default () => { - return ; + return ( + <> + + + + ); }; diff --git a/assets/apps/dashboard/src/store/actions.js b/assets/apps/dashboard/src/store/actions.js index ab6f81dc71..802451a46e 100644 --- a/assets/apps/dashboard/src/store/actions.js +++ b/assets/apps/dashboard/src/store/actions.js @@ -50,4 +50,10 @@ export default { payload: loggerStatus, }; }, + setObfxModuleStatus(slug, value) { + return { + type: 'SET_OBFX_MODULE_STATUS', + payload: { slug, value }, + }; + }, }; diff --git a/assets/apps/dashboard/src/store/reducer.js b/assets/apps/dashboard/src/store/reducer.js index d2cd911b7c..e9f5ec628e 100644 --- a/assets/apps/dashboard/src/store/reducer.js +++ b/assets/apps/dashboard/src/store/reducer.js @@ -8,6 +8,7 @@ const initialState = { currentTab: 'start', license: neveDash.pro ? neveDash.license : {}, notifications: neveDash.notifications || {}, + obfxModuleStatus: neveDash.orbitFox?.data?.module_status || {}, }; const hash = getTabHash(); @@ -73,6 +74,16 @@ const reducer = (state = initialState, action) => { neve_logger_flag: action.payload, }, }; + case 'SET_OBFX_MODULE_STATUS': + return { + ...state, + obfxModuleStatus: { + ...state.obfxModuleStatus, + [action.payload.slug]: { + active: action.payload.value, + }, + }, + }; } return state; }; diff --git a/assets/apps/dashboard/src/store/selectors.js b/assets/apps/dashboard/src/store/selectors.js index cb576148e4..c49ad370d5 100644 --- a/assets/apps/dashboard/src/store/selectors.js +++ b/assets/apps/dashboard/src/store/selectors.js @@ -25,4 +25,15 @@ export default { return shownNotifications; }, + getObfxModuleStatus: (state, slug) => { + if (!state.obfxModuleStatus) { + return false; + } + + if (state.obfxModuleStatus[slug]) { + return state.obfxModuleStatus[slug]?.active || false; + } + + return false; + }, }; diff --git a/assets/apps/dashboard/src/utils/constants.js b/assets/apps/dashboard/src/utils/constants.js index f9ca5aa4f4..3e695cfed1 100644 --- a/assets/apps/dashboard/src/utils/constants.js +++ b/assets/apps/dashboard/src/utils/constants.js @@ -10,9 +10,11 @@ import { LucideGraduationCap, LucideImage, LucideLayoutTemplate, + LucideLock, LucideNewspaper, LucidePalette, LucidePanelRightDashed, + LucidePanelsTopLeft, LucidePanelTopDashed, LucidePin, LucideRss, @@ -22,6 +24,7 @@ import { LucideShoppingCart, LucideTimer, LucideToyBrick, + LucideType, LucideTypeOutline, } from 'lucide-react'; @@ -67,3 +70,10 @@ export const NEVE_PLUGIN_ICON_MAP = { 'hyve-lite': LucideBotMessageSquare, // 'sparks' }; + +export const NEVE_AVAILABLE_MODULES_ICON_MAP = { + 'login-customizer': LucideLock, + 'custom-fonts': LucideType, + 'policy-notice': LucideShield, + 'post-duplicator': LucidePanelsTopLeft, +}; diff --git a/inc/admin/dashboard/main.php b/inc/admin/dashboard/main.php index 2924fc5e30..dfa9f41aa8 100755 --- a/inc/admin/dashboard/main.php +++ b/inc/admin/dashboard/main.php @@ -364,6 +364,14 @@ private function get_localization() { 'canActivatePlugins' => current_user_can( 'activate_plugins' ), 'rootUrl' => get_site_url(), 'sparksActive' => defined( 'SPARKS_WC_VERSION' ) ? 'yes' : 'no', + 'api' => esc_url( rest_url( '/nv/v1/dashboard/' ) ), + 'availableModules' => $this->get_available_modules(), + 'orbitFox' => array( + 'isInstalled' => file_exists( WP_PLUGIN_DIR . '/themeisle-companion/themeisle-companion.php' ), + 'isActive' => class_exists( 'Orbit_Fox' ), + 'activationUrl' => $this->plugin_helper->get_plugin_action_link( 'themeisle-companion' ), + 'data' => class_exists( 'Orbit_Fox' ) ? get_option( 'obfx_data' ) : array(), + ), ]; if ( defined( 'NEVE_PRO_PATH' ) ) { @@ -824,6 +832,34 @@ private function get_external_plugins_data() { return $plugins; } + /** + * Get available modules. + * + * @return array + */ + private function get_available_modules() { + $modules = array( + 'login-customizer' => array( + 'title' => __( 'Login Customizer', 'neve' ), + 'description' => __( 'Customize your WordPress login page with branding and styling options.', 'neve' ), + ), + 'custom-fonts' => array( + 'title' => __( 'Custom Fonts/Scripts', 'neve' ), + 'description' => __( 'Add custom fonts and scripts to your website easily.', 'neve' ), + ), + 'policy-notice' => array( + 'title' => __( 'Cookie Notice', 'neve' ), + 'description' => __( 'Display a customizable cookie consent notice for GDPR compliance.', 'neve' ), + ), + 'post-duplicator' => array( + 'title' => __( 'Duplicate Page', 'neve' ), + 'description' => __( 'Quickly duplicate posts, pages, and custom post types.', 'neve' ), + ), + ); + + return apply_filters( 'neve_available_modules', $modules ); + } + /** * Renders the custom layout header section in the admin dashboard for Custom Layouts * diff --git a/inc/core/admin.php b/inc/core/admin.php index dca7974ea0..f6238e32c2 100644 --- a/inc/core/admin.php +++ b/inc/core/admin.php @@ -303,6 +303,47 @@ public function register_rest_routes() { ], ] ); + + register_rest_route( + 'nv/v1/dashboard', + '/activate-plugin', + array( + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'activate_plugin' ], + 'permission_callback' => function() { + return ( current_user_can( 'install_plugins' ) && current_user_can( 'activate_plugins' ) ); + }, + 'args' => array( + 'slug' => array( + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ), + ), + ) + ); + + register_rest_route( + 'nv/v1/dashboard', + '/activate-module', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'activate_module' ], + 'permission_callback' => function() { + return current_user_can( 'manage_options' ); + }, + 'args' => array( + 'slug' => array( + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ), + 'value' => array( + 'required' => true, + 'sanitize_callback' => 'rest_sanitize_boolean', + 'validate_callback' => 'rest_validate_request_arg', + ), + ), + ] + ); } /** @@ -324,6 +365,110 @@ public function get_plugin_state( \WP_REST_Request $request ) { ); } + /** + * Activate a plugin via REST API. + * + * @param \WP_REST_Request> $request Request details. + * + * @return void + */ + public function activate_plugin( $request ) { + $slug = $request->get_param( 'slug' ); + + if ( empty( $slug ) ) { + return; + } + + $plugin_helper = new Plugin_Helper(); + $path = $plugin_helper->get_plugin_path( $slug ); + + if ( ! file_exists( WP_PLUGIN_DIR . '/' . $path ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + + /** @var object|\WP_Error $api */ + $api = plugins_api( + 'plugin_information', + array( + + 'slug' => $slug, + 'fields' => array( 'sections' => false ), + ) + ); + + if ( is_wp_error( $api ) ) { + wp_send_json_error( array( 'message' => $api->get_error_message() ) ); + } + + if ( ! isset( $api->download_link ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid plugin information.', 'neve' ) ) ); + } + + $skin = new \WP_Ajax_Upgrader_Skin(); + $upgrader = new \Plugin_Upgrader( $skin ); + $result = $upgrader->install( $api->download_link ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + if ( $skin->get_errors()->has_errors() && is_wp_error( $skin->result ) ) { + if ( 'folder_exists' !== $skin->result->get_error_code() ) { + wp_send_json_error( array( 'message' => $skin->result->get_error_message() ) ); + } + } + + if ( ! $result ) { + global $wp_filesystem; + + $status = [ + 'message' => __( 'Invalid plugin information.', 'neve' ), + ]; + + if ( $wp_filesystem instanceof \WP_Filesystem_Base && $wp_filesystem->errors->has_errors() ) { + $status['message'] = esc_html( $wp_filesystem->errors->get_error_message() ); + } + + wp_send_json_error( $status ); + } + } + + $result = activate_plugin( $path ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( array( 'message' => __( 'Plugin activated successfully.', 'neve' ) ) ); + } + + /** + * Activate Orbit Fox module. + * + * @param \WP_REST_Request> $request Request details. + * @return void + */ + public function activate_module( $request ) { + $module_slug = $request->get_param( 'slug' ); + $module_value = $request->get_param( 'value' ); + + if ( ! class_exists( 'Orbit_Fox_Global_Settings' ) ) { + wp_send_json_error( __( 'Orbit Fox is not installed or activated.', 'neve' ) ); + } + + $settings = new \Orbit_Fox_Global_Settings(); + $modules = $settings::$instance->module_objects; + + if ( ! isset( $modules[ $module_slug ] ) ) { + wp_send_json_error( __( 'Invalid module slug.', 'neve' ) ); + } + + $response = $modules[ $module_slug ]->set_status( 'active', $module_value ); + + wp_send_json_success( __( 'Module status changed.', 'neve' ) ); + } + /** * Drop `Background` submenu item. */ diff --git a/phpstan.neon b/phpstan.neon index 07b949acdf..d150008b33 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -39,6 +39,11 @@ parameters: - '#^Function apply_filters(_ref_array)? invoked with [34567] parameters, 2 required\.$#' - '#Parameter \#2 \$default of function get_theme_mod expects#' - '#Parameter \#2 \$args of method WP_Customize_Manager::add_setting\(\) expects#' + - + identifiers: + - includeOnce.fileNotFound + - requireOnce.fileNotFound + includes: - %currentWorkingDirectory%/vendor/szepeviktor/phpstan-wordpress/extension.neon - %currentWorkingDirectory%/phpstan-baseline.neon