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