+);
export default Loading;
diff --git a/assets/apps/dashboard/src/Components/ModuleCard.js b/assets/apps/dashboard/src/Components/ModuleCard.js
index a39dda77d1..8db6cdc2a2 100644
--- a/assets/apps/dashboard/src/Components/ModuleCard.js
+++ b/assets/apps/dashboard/src/Components/ModuleCard.js
@@ -229,7 +229,7 @@ const ModuleCard = ({
)}
- {0 < options.length &&
+ {0 < options?.length &&
true === getModuleStatus(slug) &&
-1 < tier && (
diff --git a/assets/apps/dashboard/src/Components/Plugin/InstallActivate.js b/assets/apps/dashboard/src/Components/Plugin/InstallActivate.js
index 92bf801052..680c83b9d2 100644
--- a/assets/apps/dashboard/src/Components/Plugin/InstallActivate.js
+++ b/assets/apps/dashboard/src/Components/Plugin/InstallActivate.js
@@ -1,7 +1,6 @@
/* global neveDash */
import { useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
-import { LucideLoaderCircle } from 'lucide-react';
import { get } from '../../utils/rest';
import Button from '../Common/Button';
import Notice from '../Common/Notice';
diff --git a/assets/apps/dashboard/src/Components/PluginsCard.js b/assets/apps/dashboard/src/Components/PluginsCard.js
index b0faf4784a..9192aa9528 100644
--- a/assets/apps/dashboard/src/Components/PluginsCard.js
+++ b/assets/apps/dashboard/src/Components/PluginsCard.js
@@ -1,12 +1,129 @@
+/* global neveDash */
+import usePluginActions from '../Hooks/usePluginActions';
+import Card from '../Layout/Card';
+import { NEVE_HIDE_PLUGINS, NEVE_PLUGIN_ICON_MAP } from '../utils/constants';
+
+import { useSelect } from '@wordpress/data';
+import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+import cn from 'classnames';
+import { LoaderCircle, LucidePuzzle } from 'lucide-react';
+import Pill from './Common/Pill';
+import TransitionInOut from './Common/TransitionInOut';
+import Toast from './Toast';
-import { NEVE_HIDE_PLUGINS } from '../utils/constants';
-import Card from '../Layout/Card';
+const PluginCard = ({ slug, data }) => {
+ const ICON = NEVE_PLUGIN_ICON_MAP[slug] || LucidePuzzle;
+
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+
+ const { title, description } = data;
+
+ const { doPluginAction, loading, buttonText } = usePluginActions(
+ slug,
+ true
+ );
+
+ const isPluginActive = useSelect((select) => {
+ const { getPlugins } = select('neve-dashboard');
+
+ const plugins = getPlugins();
+
+ return plugins[slug].cta === 'deactivate';
+ });
+
+ if (isPluginActive && !success) {
+ return null;
+ }
+
+ const handleClick = async () => {
+ setError(null);
+
+ const result = await doPluginAction();
+
+ if (result.success) {
+ setSuccess(true);
+
+ return;
+ }
+
+ if (!result.success) {
+ setError(result.error);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ {!success && (
+
+ )}
+ {success && (
+
+
+
+ {__('Active', 'neve')}
+
+
+
+ )}
+
+
+ {description}
+
+
+ {error && (
+
+
+
+ )}
+
+
+ );
+};
+
+const PluginsCard = () => {
+ const { plugins } = neveDash;
-const PluginsCard = () => {};
+ if (NEVE_HIDE_PLUGINS || plugins.length < 1) {
+ return null;
+ }
-const PluginCard = ({ slug, title, icon }) => {
-
;
+ return (
+
+ {Object.entries(plugins).map(([slug, args]) => (
+
+ ))}
+
+ );
};
export default PluginsCard;
diff --git a/assets/apps/dashboard/src/Components/SupportCard.js b/assets/apps/dashboard/src/Components/SupportCard.js
index 6f451a1d1f..75eb890b0e 100644
--- a/assets/apps/dashboard/src/Components/SupportCard.js
+++ b/assets/apps/dashboard/src/Components/SupportCard.js
@@ -1,7 +1,6 @@
-import { useSelect, withSelect } from '@wordpress/data';
-import { Button } from '@wordpress/components';
-import Link from './Common/Link';
+import { useSelect } from '@wordpress/data';
import { NEVE_STORE } from '../utils/constants';
+import Link from './Common/Link';
const SupportCard = () => {
const { license } = useSelect((select) => {
diff --git a/assets/apps/dashboard/src/Hooks/usePluginActions.js b/assets/apps/dashboard/src/Hooks/usePluginActions.js
new file mode 100644
index 0000000000..551547b5c2
--- /dev/null
+++ b/assets/apps/dashboard/src/Hooks/usePluginActions.js
@@ -0,0 +1,191 @@
+/* global neveDash */
+
+import { useState } from '@wordpress/element';
+import { useDispatch } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { useEffect } from 'react';
+
+const usePluginActions = (slug, activateAfterInstall = false) => {
+ const buttonLabelsMap = {
+ install: {
+ static: activateAfterInstall
+ ? __('Install & Activate', 'neve')
+ : __('Install', 'neve'),
+ loading: __('Installing', 'neve') + '...',
+ },
+ activate: {
+ static: __('Activate', 'neve'),
+ loading: __('Activating', 'neve') + '...',
+ },
+ deactivate: {
+ static: __('Deactivate', 'neve'),
+ loading: __('Deactivating', 'neve') + '...',
+ },
+ };
+
+ const activateURL = neveDash.plugins[slug].activate;
+ const deactivateURL = neveDash.plugins[slug].deactivate;
+
+ const [loading, setLoading] = useState(false);
+ const [currentCTA, setCurrentCTA] = useState(neveDash.plugins[slug].cta);
+ const [buttonText, setButtonText] = useState(
+ buttonLabelsMap[currentCTA].static
+ );
+
+ const { setPluginState } = useDispatch('neve-dashboard');
+
+ useEffect(() => {
+ setButtonText(
+ loading
+ ? buttonLabelsMap[currentCTA].loading
+ : buttonLabelsMap[currentCTA].static
+ );
+ }, [loading, currentCTA]);
+
+ const installPlugin = () => {
+ return new Promise((resolve) => {
+ wp.updates.ajax('install-plugin', {
+ slug,
+ success: () => resolve({ success: true }),
+ error: (error) =>
+ resolve({
+ success: false,
+ error:
+ error.errorMessage ||
+ __('Could not install plugin.', 'neve'),
+ }),
+ });
+ });
+ };
+
+ const activatePlugin = async () => {
+ try {
+ const response = await window.fetch(activateURL, {
+ headers: {
+ 'X-WP-Nonce': neveDash.nonce,
+ },
+ });
+
+ if (!response.ok) {
+ return {
+ success: false,
+ error: __('Could not activate plugin.', 'neve'),
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error:
+ error.message || __('Could not activate plugin.', 'neve'),
+ };
+ }
+ };
+
+ const deactivatePlugin = async () => {
+ try {
+ const response = await window.fetch(deactivateURL, {
+ headers: {
+ 'X-WP-Nonce': neveDash.nonce,
+ },
+ });
+
+ if (!response.ok) {
+ return {
+ success: false,
+ error: __('Could not deactivate plugin.', 'neve'),
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ };
+
+ /**
+ * Do plugin action.
+ *
+ * @param {'activate'|'deactivate'|'install'|null} type - Action to perform. If null, it will use the currentCTA.
+ * @return {Promise<{success: boolean, error: Error}>} - Result of the action.
+ */
+ const doPluginAction = async (type = null) => {
+ return await handlePluginAction(type || currentCTA);
+ };
+
+ /**
+ * Handle plugin action.
+ *
+ * @param {'activate'|'deactivate'|'install'} action - Action to perform.
+ *
+ * @return {Promise<{success: boolean, error: Error}>} - Result of the action ().
+ *
+ */
+ const handlePluginAction = async (action) => {
+ setLoading(true);
+
+ try {
+ let result;
+
+ switch (action) {
+ case 'install':
+ setLoading(true);
+ result = await installPlugin();
+ if (result.success) {
+ setPluginState(slug, 'activate');
+ setCurrentCTA('activate');
+
+ if (activateAfterInstall) {
+ return await handlePluginAction('activate');
+ }
+ }
+ break;
+
+ case 'activate':
+ result = await activatePlugin();
+ if (result.success) {
+ setPluginState(slug, 'deactivate');
+ setCurrentCTA('deactivate');
+
+ if (slug === 'templates-patterns-collection') {
+ window.location.href =
+ neveDash.tpcAdminURL +
+ (neveDash.canInstallPlugins
+ ? '&onboarding=yes'
+ : '');
+ }
+ }
+ break;
+
+ case 'deactivate':
+ result = await deactivatePlugin(slug);
+
+ if (result.success) {
+ setPluginState(slug, 'activate');
+ setCurrentCTA('activate');
+ }
+
+ break;
+
+ default:
+ result = {
+ success: false,
+ error: __('Invalid action', 'neve'),
+ };
+ }
+
+ return result;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return {
+ loading,
+ buttonText,
+ doPluginAction,
+ };
+};
+
+export default usePluginActions;
diff --git a/assets/apps/dashboard/src/style.css b/assets/apps/dashboard/src/style.css
index 7b6162d3a0..eacf688872 100644
--- a/assets/apps/dashboard/src/style.css
+++ b/assets/apps/dashboard/src/style.css
@@ -5,7 +5,7 @@
@tailwind utilities;
#wpcontent {
- @apply pl-0 font-sans;
+ @apply pl-0 font-sans min-h-[100vh];
}
#wpbody-content > .notice, #wpbody-content > .error {
diff --git a/assets/apps/dashboard/src/utils/common.js b/assets/apps/dashboard/src/utils/common.js
index 62656a4c98..3732eb75b0 100644
--- a/assets/apps/dashboard/src/utils/common.js
+++ b/assets/apps/dashboard/src/utils/common.js
@@ -4,11 +4,11 @@ import compareVersions from 'compare-versions';
import StarterSitesUnavailable from '../Components/Content/StarterSitesUnavailable';
import Welcome from '../Components/Content/Welcome';
import Pro from '../Components/Content/Pro';
-import Plugins from '../Components/Content/Plugins';
-import Help from '../Components/Content/Help';
+
import Changelog from '../Components/Content/Changelog';
import FreePro from '../Components/Content/FreePro';
import { __ } from '@wordpress/i18n';
+import { NEVE_HAS_VALID_PRO } from './constants';
const addUrlHash = (hash) => {
window.location.hash = hash;
@@ -43,9 +43,9 @@ const tabs = {
label: __('Free vs Pro', 'neve'),
render: () =>
,
},
- plugins: {
- label: __('Plugins', 'neve'),
- render: () =>
,
+ settings: {
+ label: __('Settings', 'neve'),
+ render: () =>
,
},
changelog: {
render: () =>
,
@@ -64,7 +64,7 @@ const properTPC =
) === 1;
if (activeTPC && properTPC) {
- delete tabs['starter-sites']['render'];
+ delete tabs['starter-sites'].render;
tabs['starter-sites'].url = neveDash.tpcAdminURL;
}
@@ -78,8 +78,8 @@ if (
}
if (neveDash.pro || neveDash.hasOldPro) {
- tabs.pro = {
- label: neveDash.strings.proTabTitle,
+ tabs.settings = {
+ label: __('Settings', 'neve'),
render: () =>
,
};
delete tabs['free-pro'];
diff --git a/assets/apps/dashboard/src/utils/constants.js b/assets/apps/dashboard/src/utils/constants.js
index 102a46abba..2bdfe80d34 100644
--- a/assets/apps/dashboard/src/utils/constants.js
+++ b/assets/apps/dashboard/src/utils/constants.js
@@ -16,6 +16,12 @@ import {
LucideShield,
LucideShoppingCart,
LucideTypeOutline,
+ LucideToyBrick,
+ LucideLayoutTemplate,
+ LucideCreditCard,
+ LucideImage,
+ LucideTimer,
+ LucideRss,
} from 'lucide-react';
export const NEVE_STORE = 'neve-dashboard';
@@ -47,3 +53,14 @@ export const NEVE_MODULE_ICON_MAP = {
custom_sidebars: LucidePanelRightDashed,
access_restriction: LucideShield,
};
+
+export const NEVE_PLUGIN_ICON_MAP = {
+ 'otter-blocks': LucideToyBrick,
+ 'templates-patterns-collection': LucideLayoutTemplate,
+ 'wp-full-stripe-free': LucideCreditCard,
+ 'optimole-wp': LucideImage,
+ 'wp-cloudflare-page-cache': LucideTimer,
+ 'feedzy-rss-feeds': LucideRss,
+ // 'hyve'
+ // 'sparks'
+};
diff --git a/inc/admin/dashboard/main.php b/inc/admin/dashboard/main.php
index b9efc5fb84..e54fdf73af 100755
--- a/inc/admin/dashboard/main.php
+++ b/inc/admin/dashboard/main.php
@@ -59,14 +59,14 @@ class Main {
*
* @var string
*/
- private $plugins_cache_key = 'neve_dash_useful_plugins';
+ private $plugins_cache_key = 'neve_dash_useful_plugins_v2';
/**
* Plugins Cache Hash key.
*
* @var string
*/
- private $plugins_cache_hash_key = 'neve_dash_useful_plugins_hash';
+ private $plugins_cache_hash_key = 'neve_dash_useful_plugins_hash_v2';
/**
* Main constructor.
@@ -334,7 +334,7 @@ private function get_localization() {
'notifications' => $this->get_notifications(),
'customizerShortcuts' => $this->get_customizer_shortcuts(),
'plugins' => $this->get_useful_plugins(),
- 'recommended_plugins' => $this->get_recommended_plugins(),
+ 'plugins' => $this->get_recommended_plugins(),
'modules' => $this->get_modules(),
'featureData' => $this->get_free_pro_features(),
'showFeedbackNotice' => $this->should_show_feedback_notice(),
@@ -697,15 +697,64 @@ private function get_modules() {
*/
private function get_recommended_plugins() {
$plugins = [
- 'otter-blocks' => 'wp',
- 'templates-patterns-collection' => 'wp',
- 'optimole-wp' => 'wp',
- 'wp-cloudflare-page-cache' => 'wp',
- 'feedzy-rss-feeds' => 'wp',
- 'hyve' => 'ti',
- 'sparks' => 'ti',
+ 'otter-blocks' => [
+ 'title' => __( 'Otter Blocks', 'neve' ),
+ 'description' => __( 'Advanced blocks for modern WordPress editing', 'neve' ),
+ ],
+ 'templates-patterns-collection' => [
+ 'title' => __( 'Starter Sites', 'neve' ),
+ 'description' => __( 'Import ready-made websites with a single click', 'neve' ),
+ ],
+ 'wp-full-stripe-free' => [
+ 'title' => __( 'WP Full Pay', 'neve' ),
+ 'description' => __( 'Simple ecommerce solution with Stripe integration', 'neve' ),
+ ],
+ 'optimole-wp' => [
+ 'title' => __( 'Optimole', 'neve' ),
+ 'description' => __( 'Smart image optimization and CDN', 'neve' ),
+ ],
+ 'wp-cloudflare-page-cache' => [
+ 'title' => __( 'Super Page Cache', 'neve' ),
+ 'description' => __( 'Lightning-fast caching made simple', 'neve' ),
+ ],
+ 'feedzy-rss-feeds' => [
+ 'title' => __( 'Feedzy', 'neve' ),
+ 'description' => __( 'RSS feeds aggregator and content curator', 'neve' ),
+ ],
+ // External ones.
+ // 'hyve' => [
+ // 'title' => __('Hyve', 'neve'),
+ // 'description' => __('AI chatbot for your website', 'neve')
+ // ],
+ // 'sparks' => [
+ // 'title' => __('Sparks', 'neve'),
+ // 'description' => __('WooCommerce enhancements', 'neve')
+ // ],
];
+ foreach ( $plugins as $slug => $args ) {
+
+ $action = $this->plugin_helper->get_plugin_state( $slug );
+
+ if ( $action === 'deactivate' ) {
+ unset( $plugins[ $slug ] );
+
+ continue;
+ }
+
+ $plugins[ $slug ] = array_merge(
+ [
+ 'cta' => $action,
+ 'path' => $this->plugin_helper->get_plugin_path( $slug ),
+ 'activate' => $this->plugin_helper->get_plugin_action_link( $slug ),
+ 'deactivate' => $this->plugin_helper->get_plugin_action_link( $slug, 'deactivate' ),
+ 'network' => $this->plugin_helper->get_is_network_wide( $slug ),
+ 'version' => $this->plugin_helper->get_plugin_version( $slug, '0.0.0' ),
+ ],
+ $args
+ );
+ }
+
return $plugins;
}
diff --git a/inc/admin/dashboard/plugin_helper.php b/inc/admin/dashboard/plugin_helper.php
index 6a142415e8..58d50abc18 100644
--- a/inc/admin/dashboard/plugin_helper.php
+++ b/inc/admin/dashboard/plugin_helper.php
@@ -54,6 +54,8 @@ public function get_plugin_path( $slug ) {
return $slug . '/feedzy-rss-feed.php';
case 'wp-cloudflare-page-cache':
return $slug . '/wp-cloudflare-super-page-cache.php';
+ case 'wp-full-stripe-free':
+ return $slug . '/wp-full-stripe.php';
default:
return $slug . '/' . $slug . '.php';
}