From 1427503bb46a660dc08b3f783f56e54a8a006564 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Thu, 23 Oct 2025 11:33:56 +0800 Subject: [PATCH 01/13] init --- .config/webpack.config.dev.js | 1 + .config/webpack.config.prod.js | 1 + src/compatibility/cimo/index.js | 40 +++++++++++ src/compatibility/cimo/index.php | 113 +++++++++++++++++++++++++++++++ src/compatibility/index.php | 1 + 5 files changed, 156 insertions(+) create mode 100644 src/compatibility/cimo/index.js create mode 100644 src/compatibility/cimo/index.php diff --git a/.config/webpack.config.dev.js b/.config/webpack.config.dev.js index 52a32f8ad..0e312ed00 100644 --- a/.config/webpack.config.dev.js +++ b/.config/webpack.config.dev.js @@ -178,6 +178,7 @@ module.exports = [ 'frontend_block_horizontal_scroller': path.resolve( __dirname, '../src/block/horizontal-scroller/frontend-horizontal-scroller.js' ), 'frontend_block_tabs': path.resolve( __dirname, '../src/block/tabs/frontend-tabs.js' ), 'frontend_image_optimizer_polyfill': path.resolve( __dirname, '../src/block-components/image/image-optimizer-polyfill.js' ), + 'stk_cimo_notice': path.resolve( __dirname, '../src/compatibility/cimo/index.js' ), }, output: { diff --git a/.config/webpack.config.prod.js b/.config/webpack.config.prod.js index c26992821..ab4c42868 100644 --- a/.config/webpack.config.prod.js +++ b/.config/webpack.config.prod.js @@ -163,6 +163,7 @@ module.exports = [ 'frontend_block_horizontal_scroller': path.resolve( __dirname, '../src/block/horizontal-scroller/frontend-horizontal-scroller.js' ), 'frontend_block_tabs': path.resolve( __dirname, '../src/block/tabs/frontend-tabs.js' ), 'frontend_image_optimizer_polyfill': path.resolve( __dirname, '../src/block-components/image/image-optimizer-polyfill.js' ), + 'stk_cimo_notice': path.resolve( __dirname, '../src/compatibility/cimo/index.js' ), }, output: { diff --git a/src/compatibility/cimo/index.js b/src/compatibility/cimo/index.js new file mode 100644 index 000000000..c81f9cf67 --- /dev/null +++ b/src/compatibility/cimo/index.js @@ -0,0 +1,40 @@ +import domReady from '@wordpress/dom-ready' +class StackableCimoNotice { + init = () => { + // eslint-disable-next-line no-undef + if ( typeof stackable === undefined || ! stackable[ 'cimo-notice' ] || + typeof wp === 'undefined' || ! wp.media || ! wp.media.view || ! wp.media.view.Attachment || ! wp.media.view.Attachment.Details + ) { + return + } + + // eslint-disable-next-line no-undef + const cimoNotice = stackable[ 'cimo-notice' ] + + wp.media.view.Attachment.Details = wp.media.view.Attachment.Details.extend( { + template: function template( view ) { + const html = wp.media.template( 'attachment-details' )( view ) + const dom = document.createElement( 'div' ) + dom.innerHTML = html + + const details = dom.querySelector( '.attachment-info' ) + if ( details && cimoNotice.content ) { + const noticeDiv = document.createElement( 'div' ) + noticeDiv.className = 'stk-cimo-notice' + + const content = document.createElement( 'p' ) + content.innerHTML = cimoNotice.content + + noticeDiv.appendChild( content ) + details.appendChild( noticeDiv ) + } + + return dom.innerHTML + }, + } ) + } +} + +window.stackableCimoNotice = new StackableCimoNotice() + +domReady( window.stackableCimoNotice.init ) diff --git a/src/compatibility/cimo/index.php b/src/compatibility/cimo/index.php new file mode 100644 index 000000000..8d5eb6315 --- /dev/null +++ b/src/compatibility/cimo/index.php @@ -0,0 +1,113 @@ + 'install-plugin', + 'plugin' => self::$CIMO_SLUG, + ], + admin_url( 'update.php' ) + ), + 'install-plugin_' . self::$CIMO_SLUG + ); + } else if ( ! self::is_plugin_activated() ) { + $cimo_state = 'installed'; + $cimo_action = wp_nonce_url( + admin_url( 'plugins.php?action=activate&plugin=' . self::$CIMO_FULL_SLUG ), + 'activate-plugin_' . self::$CIMO_FULL_SLUG + ); + } + + if ( $cimo_state === 'activated' && ! $cimo_action ) { + return; + } + + $content = sprintf('%s %s.', + __( 'Optimize your images with Cimo Image Optimizer.', STACKABLE_I18N ), + $cimo_action, + ( $cimo_state === 'installed' ? __( 'Activate', STACKABLE_I18N ) : __( 'Install', STACKABLE_I18N ) ) + . ' ' . __( 'Cimo Image Optimizer', STACKABLE_I18N ) + ); + + $data = array( + 'state' => $cimo_state, + 'content' => $content, + 'action' => wp_json_encode( [ 'action' => $cimo_action ] ) + ); + + wp_enqueue_script( + 'stk-cimo-notice', + plugins_url( 'dist/stk_cimo_notice.js', STACKABLE_FILE ), + array(), + STACKABLE_VERSION, + true + ); + + add_filter( 'stackable_localize_script', function ( $args ) use( $data ) { + return $this->add_localize_script( $args, $data ); + } ); + + + } + + public function add_localize_script( $args, $data ) { + $args[ 'cimo-notice' ] = $data; + return $args; + } + + } + + new Stackable_Cimo_Notice(); +} diff --git a/src/compatibility/index.php b/src/compatibility/index.php index 6bd4f0e24..e7788ed97 100644 --- a/src/compatibility/index.php +++ b/src/compatibility/index.php @@ -9,3 +9,4 @@ require_once( plugin_dir_path( __FILE__ ) . './ewww.php' ); require_once( plugin_dir_path( __FILE__ ) . './woocommerce.php' ); require_once( plugin_dir_path( __FILE__ ) . './blocksy/index.php' ); +require_once( plugin_dir_path( __FILE__ ) . './cimo/index.php' ); From 7de0280c9587b66a020cd17ef1804f7b1a295629 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Thu, 30 Oct 2025 09:00:29 +0800 Subject: [PATCH 02/13] lazy load cimo notice, display notice in image control --- .config/webpack.config.dev.js | 1 - .config/webpack.config.prod.js | 1 - plugin.php | 4 + src/compatibility/cimo/index.js | 40 ------ src/compatibility/cimo/index.php | 113 ---------------- src/compatibility/index.php | 1 - src/components/image-control2/index.js | 43 ++++++- src/init.php | 1 + src/lazy-components/cimo/index.js | 170 ++++++++++++++++++++++++ src/lazy-components/cimo/index.php | 171 +++++++++++++++++++++++++ src/lazy-components/cimo/style.scss | 47 +++++++ 11 files changed, 432 insertions(+), 160 deletions(-) delete mode 100644 src/compatibility/cimo/index.js delete mode 100644 src/compatibility/cimo/index.php create mode 100644 src/lazy-components/cimo/index.js create mode 100644 src/lazy-components/cimo/index.php create mode 100644 src/lazy-components/cimo/style.scss diff --git a/.config/webpack.config.dev.js b/.config/webpack.config.dev.js index 0e312ed00..52a32f8ad 100644 --- a/.config/webpack.config.dev.js +++ b/.config/webpack.config.dev.js @@ -178,7 +178,6 @@ module.exports = [ 'frontend_block_horizontal_scroller': path.resolve( __dirname, '../src/block/horizontal-scroller/frontend-horizontal-scroller.js' ), 'frontend_block_tabs': path.resolve( __dirname, '../src/block/tabs/frontend-tabs.js' ), 'frontend_image_optimizer_polyfill': path.resolve( __dirname, '../src/block-components/image/image-optimizer-polyfill.js' ), - 'stk_cimo_notice': path.resolve( __dirname, '../src/compatibility/cimo/index.js' ), }, output: { diff --git a/.config/webpack.config.prod.js b/.config/webpack.config.prod.js index ab4c42868..c26992821 100644 --- a/.config/webpack.config.prod.js +++ b/.config/webpack.config.prod.js @@ -163,7 +163,6 @@ module.exports = [ 'frontend_block_horizontal_scroller': path.resolve( __dirname, '../src/block/horizontal-scroller/frontend-horizontal-scroller.js' ), 'frontend_block_tabs': path.resolve( __dirname, '../src/block/tabs/frontend-tabs.js' ), 'frontend_image_optimizer_polyfill': path.resolve( __dirname, '../src/block-components/image/image-optimizer-polyfill.js' ), - 'stk_cimo_notice': path.resolve( __dirname, '../src/compatibility/cimo/index.js' ), }, output: { diff --git a/plugin.php b/plugin.php index fb7813bbe..d2664b3cc 100644 --- a/plugin.php +++ b/plugin.php @@ -288,6 +288,10 @@ function is_frontend() { require_once( plugin_dir_path( __FILE__ ) . 'src/plugins/global-settings/block-styles/index.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/css-optimize.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/compatibility/index.php' ); + +// For cross-selling +require_once( plugin_dir_path( __FILE__ ) . 'src/lazy-components/cimo/index.php' ); + if ( ! is_admin() ) { require_once( plugin_dir_path( __FILE__ ) . 'src/lightbox/index.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/block/accordion/index.php' ); diff --git a/src/compatibility/cimo/index.js b/src/compatibility/cimo/index.js deleted file mode 100644 index c81f9cf67..000000000 --- a/src/compatibility/cimo/index.js +++ /dev/null @@ -1,40 +0,0 @@ -import domReady from '@wordpress/dom-ready' -class StackableCimoNotice { - init = () => { - // eslint-disable-next-line no-undef - if ( typeof stackable === undefined || ! stackable[ 'cimo-notice' ] || - typeof wp === 'undefined' || ! wp.media || ! wp.media.view || ! wp.media.view.Attachment || ! wp.media.view.Attachment.Details - ) { - return - } - - // eslint-disable-next-line no-undef - const cimoNotice = stackable[ 'cimo-notice' ] - - wp.media.view.Attachment.Details = wp.media.view.Attachment.Details.extend( { - template: function template( view ) { - const html = wp.media.template( 'attachment-details' )( view ) - const dom = document.createElement( 'div' ) - dom.innerHTML = html - - const details = dom.querySelector( '.attachment-info' ) - if ( details && cimoNotice.content ) { - const noticeDiv = document.createElement( 'div' ) - noticeDiv.className = 'stk-cimo-notice' - - const content = document.createElement( 'p' ) - content.innerHTML = cimoNotice.content - - noticeDiv.appendChild( content ) - details.appendChild( noticeDiv ) - } - - return dom.innerHTML - }, - } ) - } -} - -window.stackableCimoNotice = new StackableCimoNotice() - -domReady( window.stackableCimoNotice.init ) diff --git a/src/compatibility/cimo/index.php b/src/compatibility/cimo/index.php deleted file mode 100644 index 8d5eb6315..000000000 --- a/src/compatibility/cimo/index.php +++ /dev/null @@ -1,113 +0,0 @@ - 'install-plugin', - 'plugin' => self::$CIMO_SLUG, - ], - admin_url( 'update.php' ) - ), - 'install-plugin_' . self::$CIMO_SLUG - ); - } else if ( ! self::is_plugin_activated() ) { - $cimo_state = 'installed'; - $cimo_action = wp_nonce_url( - admin_url( 'plugins.php?action=activate&plugin=' . self::$CIMO_FULL_SLUG ), - 'activate-plugin_' . self::$CIMO_FULL_SLUG - ); - } - - if ( $cimo_state === 'activated' && ! $cimo_action ) { - return; - } - - $content = sprintf('%s %s.', - __( 'Optimize your images with Cimo Image Optimizer.', STACKABLE_I18N ), - $cimo_action, - ( $cimo_state === 'installed' ? __( 'Activate', STACKABLE_I18N ) : __( 'Install', STACKABLE_I18N ) ) - . ' ' . __( 'Cimo Image Optimizer', STACKABLE_I18N ) - ); - - $data = array( - 'state' => $cimo_state, - 'content' => $content, - 'action' => wp_json_encode( [ 'action' => $cimo_action ] ) - ); - - wp_enqueue_script( - 'stk-cimo-notice', - plugins_url( 'dist/stk_cimo_notice.js', STACKABLE_FILE ), - array(), - STACKABLE_VERSION, - true - ); - - add_filter( 'stackable_localize_script', function ( $args ) use( $data ) { - return $this->add_localize_script( $args, $data ); - } ); - - - } - - public function add_localize_script( $args, $data ) { - $args[ 'cimo-notice' ] = $data; - return $args; - } - - } - - new Stackable_Cimo_Notice(); -} diff --git a/src/compatibility/index.php b/src/compatibility/index.php index e7788ed97..6bd4f0e24 100644 --- a/src/compatibility/index.php +++ b/src/compatibility/index.php @@ -9,4 +9,3 @@ require_once( plugin_dir_path( __FILE__ ) . './ewww.php' ); require_once( plugin_dir_path( __FILE__ ) . './woocommerce.php' ); require_once( plugin_dir_path( __FILE__ ) . './blocksy/index.php' ); -require_once( plugin_dir_path( __FILE__ ) . './cimo/index.php' ); diff --git a/src/components/image-control2/index.js b/src/components/image-control2/index.js index 1203522c3..b2866c9c2 100644 --- a/src/components/image-control2/index.js +++ b/src/components/image-control2/index.js @@ -11,7 +11,7 @@ import Button from '../button' * External dependencies */ import classnames from 'classnames' -import { i18n } from 'stackable' +import { i18n, cimo } from 'stackable' import { useAttributeName, useBlockAttributesContext, useBlockSetAttributesContext, } from '~stackable/hooks' @@ -20,8 +20,11 @@ import { * WordPress dependencies */ import { __ } from '@wordpress/i18n' -import { Fragment, memo } from '@wordpress/element' +import { + Fragment, memo, useEffect, useState, +} from '@wordpress/element' import { MediaUpload } from '@wordpress/block-editor' +import { currentUserHasCapability } from '~stackable/util' const ImageControl = memo( props => { const attrNameId = useAttributeName( `${ props.attribute }Id`, props.responsive, props.hover ) @@ -81,7 +84,37 @@ const ImageControl = memo( props => { } ) } - return ( + const [ CimoDownloadNotice, setCimoDownloadNotice ] = useState( null ) + + useEffect( () => { + // Skip displaying the Cimo notice if the plugin is already activated or the user has chosen to hide the notice + if ( ! cimo || cimo.hideNotice || cimo.status === 'activated' ) { + return + } + + const userCanInstall = currentUserHasCapability( 'install_plugins' ) + const userCanActivate = currentUserHasCapability( 'activate_plugins' ) + // Show the Cimo notice only if the user has permissions to install or activate plugins + if ( ( cimo.status === 'not_installed' && userCanInstall ) || ( cimo.status === 'installed' && userCanActivate ) ) { + const loadNotice = async () => { + try { + // Import the Cimo notice component with explicit chunk naming + const { default: CimoNoticeComponent } = await import( + /* webpackChunkName: "cimo-download-notice" */ + /* webpackMode: "lazy" */ + '../../lazy-components/cimo' + ) + setCimoDownloadNotice( () => CimoNoticeComponent ) + } catch ( err ) { + // eslint-disable-next-line no-console + console.error( 'Failed to load Cimo download notice component:', err ) + } + } + loadNotice() + } + }, [] ) + + return ( <> { /> ) } { type === 'image' && ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions { hasPanelModifiedIndicator={ props.hasPanelModifiedIndicator } /> + { CimoDownloadNotice && setCimoDownloadNotice( null ) } /> } + ) } ) diff --git a/src/init.php b/src/init.php index ab886cb39..d1599c0b5 100644 --- a/src/init.php +++ b/src/init.php @@ -349,6 +349,7 @@ public function register_block_editor_assets() { 'version' => array_shift( $version_parts ), 'wpVersion' => ! empty( $wp_version ) ? preg_replace( '/-.*/', '', $wp_version ) : $wp_version, // Ensure semver, strip out after dash 'adminUrl' => admin_url(), + 'ajaxUrl' => admin_url('admin-ajax.php'), // Fonts. 'locale' => get_locale(), diff --git a/src/lazy-components/cimo/index.js b/src/lazy-components/cimo/index.js new file mode 100644 index 000000000..32705b3c2 --- /dev/null +++ b/src/lazy-components/cimo/index.js @@ -0,0 +1,170 @@ +import { + cimo, i18n, ajaxUrl, +} from 'stackable' +import { createRoot } from '~stackable/util' + +import { __ } from '@wordpress/i18n' +import { Dashicon } from '@wordpress/components' +import domReady from '@wordpress/dom-ready' +import { + useState, useRef, useEffect, +} from '@wordpress/element' +import { models } from '@wordpress/api' + +const CimoDownloadNotice = props => { + const [ data, setData ] = useState( { status: cimo?.status, action: cimo?.action } ) + const pollCountRef = useRef( 0 ) + + const onDismiss = () => { + const settings = new models.Settings( { stackable_hide_cimo_notice: true } ) // eslint-disable-line camelcase + settings.save() + props?.onDismiss?.() + } + + // Polls the Cimo plugin status to detect installation or activation state changes + const pollStatus = ( action, pollOnce = false ) => { + fetch( ajaxUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams( { + action: 'stackable_check_cimo_status', + // eslint-disable-next-line camelcase + user_action: action, + } ), + credentials: 'same-origin', + } ).then( res => res.json() ).then( _data => { + pollCountRef.current += 1 + + if ( data.status !== _data.status ) { + setData( _data ) + + // Update the global stackable.cimo status/action variables + // so new image block selections reflect the latest Cimo installation state + // eslint-disable-next-line no-undef + stackable.cimo.status = _data.status + // eslint-disable-next-line no-undef + stackable.cimo.action = _data.action + } + + // Stop polling if it has reached 3 attempts, or plugin status indicates installation/activation is complete + if ( pollOnce || pollCountRef.current >= 3 || + ( action === 'install' && ( _data.status === 'installed' || _data.status === 'activated' ) ) || + ( action === 'activate' && _data.status === 'activated' ) + ) { + return + } + + setTimeout( () => { + pollStatus( action ) + }, 3000 * pollCountRef.current ) + } ) + } + + useEffect( () => { + const _media = wp.media + const old = _media.view.MediaFrame.Select + + // When the media library closes, check and update the Cimo plugin status + // to ensure the UI reflects the latest installation or activation state. + _media.view.MediaFrame.Select = old.extend( { + initialize() { + old.prototype.initialize.apply( this, arguments ) + + this.on( 'close', () => { + pollCountRef.current = 0 + if ( data.status === 'activated' ) { + return + } + + if ( data.status === 'not_installed' ) { + pollStatus( 'install', true ) + return + } + + pollStatus( 'activate', true ) + } ) + }, + } ) + }, [] ) + + const onActionClick = async () => { + pollCountRef.current = 0 + + if ( data.status === 'not_installed' ) { + setData( { status: 'installing', action: '' } ) + setTimeout( () => { + pollStatus( 'install' ) + }, 3000 ) + + return + } + + setData( { status: 'activating', action: '' } ) + setTimeout( () => { + pollStatus( 'activate' ) + }, 3000 ) + } + + return ( <> + + { data.status === 'activated' + ?

+ { __( 'Cimo Image Optimizer has been activated. Please refresh this page to begin optimizing your images automatically.', i18n ) } +

+ :

{ __( 'Instantly optimize images as you upload them with Cimo Image Optimizer.', i18n ) } +   + { data.status === 'installing' + ? { __( 'Installing', i18n ) } + : ( data.status === 'activating' + ? { __( 'Activating', i18n ) } + : + { data.status === 'installed' ? __( 'Activate now', i18n ) : __( 'Install now', i18n ) } + + ) + } +

+ } + ) +} + +const CimoDownloadNoticeWrapper = props => { + return
+} + +export default CimoDownloadNoticeWrapper + +domReady( () => { + if ( ! cimo || cimo.status === 'activated' || cimo.hideNotice || typeof wp === 'undefined' || ! wp.media || ! wp.media.view || + ! wp.media.view.Attachment || ! wp.media.view.Attachment.Details + ) { + return + } + + const CurrentDetailsView = wp.media.view.Attachment.Details + + // Display the Cimo download notice in the media library + const CustomDetailsView = CurrentDetailsView.extend( { + render() { + const result = CurrentDetailsView.prototype.render.apply( this, arguments ) + + const details = this.el.querySelector( '.attachment-info .details' ) + if ( details && ! this.el.querySelector( '.stk-cimo-notice' ) ) { + const noticeDiv = document.createElement( 'div' ) + noticeDiv.className = 'stk-cimo-notice' + + const onDismiss = () => { + if ( noticeDiv && noticeDiv.parentNode ) { + noticeDiv.parentNode.removeChild( noticeDiv ) + } + } + + createRoot( noticeDiv ).render( ) + details.insertAdjacentElement( 'afterend', noticeDiv ) + } + + return result + }, + } ) + + wp.media.view.Attachment.Details = CustomDetailsView +} ) diff --git a/src/lazy-components/cimo/index.php b/src/lazy-components/cimo/index.php new file mode 100644 index 000000000..e9cdc404b --- /dev/null +++ b/src/lazy-components/cimo/index.php @@ -0,0 +1,171 @@ + 'boolean', + 'description' => __( 'Hides the Cimo download notice.', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => false, + ) + ); + } + + public static function is_plugin_installed() { + if ( ! function_exists( 'get_plugins' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + $all_plugins = get_plugins(); + if ( isset( $all_plugins[ self::$CIMO_FULL_SLUG ] ) ) { + return true; + } + + return false; + } + + public static function is_plugin_activated() { + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + if ( class_exists( 'Cimo_Script_Loader' ) || is_plugin_active( self::$CIMO_FULL_SLUG ) ) { + return true; + } + + return false; + + } + + // Determines the current status of the Cimo plugin (not installed, installed but inactive, or activated) + // and provides the appropriate action URL for installation or activation. + public function enqueue_script() { + $hide_cimo = get_option( 'stackable_hide_cimo_notice', false ); + + if ( $hide_cimo ) { + return; + } + + $cimo_status = 'activated'; + $cimo_action = ''; + + if ( ! self::is_plugin_installed() ) { + $cimo_status = 'not_installed'; + $cimo_action = wp_nonce_url( + add_query_arg( + [ + 'action' => 'install-plugin', + 'plugin' => self::$CIMO_SLUG, + ], + admin_url( 'update.php' ) + ), + 'install-plugin_' . self::$CIMO_SLUG + ); + } else if ( ! self::is_plugin_activated() ) { + $cimo_status = 'installed'; + $cimo_action = wp_nonce_url( + add_query_arg( + [ + 'action' => 'activate', + 'plugin' => self::$CIMO_FULL_SLUG, + ], + admin_url( 'plugins.php' ) + ), + 'activate-plugin_' . self::$CIMO_FULL_SLUG + ); + } + + $data = array( + 'status' => $cimo_status, + 'action' => html_entity_decode( $cimo_action ), + ); + + // Expose the Cimo plugin status and action URL for use in JS + add_filter( 'stackable_localize_script', function ( $args ) use( $data ) { + return $this->add_localize_script( $args, $data ); + }, 1 ); + } + + public function add_localize_script( $args, $data ) { + $args[ 'cimo' ] = $data; + return $args; + } + + // Adds the hide notice option for the Cimo plugin to the localized script arguments. + public function localize_hide_cimo_notice( $args ) { + $hide_cimo = get_option( 'stackable_hide_cimo_notice', false ); + if ( isset( $args['cimo'] ) ) { + $args['cimo']['hideNotice'] = $hide_cimo; + return $args; + } + + $args[ 'cimo' ] = array( 'hideNotice' => $hide_cimo ); + return $args; + } + + /** + * Checks the status of the Cimo plugin installation or activation. + * Returns JSON indicating if Cimo is installed, installing, activated, or activating, + * and provides the respective action URL if activation is needed. + * + * Used for polling Cimo plugin status changes via AJAX in the admin UI. + */ + function check_cimo_status() { + $action = sanitize_text_field( $_POST['user_action'] ); + $response = array( + 'status' => 'activated', + 'action' => '' + ); + + if ( $action === 'install' && ! self::is_plugin_installed() ) { + $response[ 'status' ] = 'installing'; + } else if ( ! self::is_plugin_activated() ) { + $response[ 'status' ] = $action === 'install' ? 'installed' : 'activating'; + $response[ 'action' ] = $action === 'install' ? html_entity_decode( wp_nonce_url( + add_query_arg( + [ + 'action' => 'activate', + 'plugin' => self::$CIMO_FULL_SLUG, + ], + admin_url( 'plugins.php' ) + ), + 'activate-plugin_' . self::$CIMO_FULL_SLUG + ) ) : ''; + } + + wp_send_json( $response ); + } + } + + new Stackable_Cimo_Notice(); +} diff --git a/src/lazy-components/cimo/style.scss b/src/lazy-components/cimo/style.scss new file mode 100644 index 000000000..185b3106b --- /dev/null +++ b/src/lazy-components/cimo/style.scss @@ -0,0 +1,47 @@ +.stk-cimo-notice { + clear: both; + padding: 16px; + border: 2px solid #16a249; + background: #fff; + margin: 12px 0; + border-radius: 4px; + box-shadow: 0 1px 4px #33533f70; + position: relative; + + button { + background: none; + border: none; + height: 14px; + width: 14px; + position: absolute; + right: 4px; + top: 4px; + cursor: pointer; + } + + .dashicon { + font-size: 14px; + height: 14px; + width: 14px; + &:hover { + color: var(--stk-skin-error, #f15449); + } + } + + p { + margin: 0; + font-size: 12px; + + a { + color: inherit; + font-weight: 700; + + &:hover { + color: #16a249; + } + } + span { + font-weight: 700; + } + } +} From df79ec59c24344e313694e28a07855590f2ac940 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Thu, 30 Oct 2025 10:40:09 +0800 Subject: [PATCH 03/13] code rabbit's qa fixes --- src/lazy-components/cimo/index.js | 64 +++++++++++++++++++++--------- src/lazy-components/cimo/index.php | 25 ++++++++++-- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/lazy-components/cimo/index.js b/src/lazy-components/cimo/index.js index 32705b3c2..563610a7f 100644 --- a/src/lazy-components/cimo/index.js +++ b/src/lazy-components/cimo/index.js @@ -18,11 +18,21 @@ const CimoDownloadNotice = props => { const onDismiss = () => { const settings = new models.Settings( { stackable_hide_cimo_notice: true } ) // eslint-disable-line camelcase settings.save() + + if ( cimo ) { + cimo.hideNotice = true + } + + // Update the global stackable.cimo hideNotice variable + if ( typeof window !== 'undefined' && window.stackable?.cimo ) { + window.stackable.cimo.hideNotice = true + } + props?.onDismiss?.() } // Polls the Cimo plugin status to detect installation or activation state changes - const pollStatus = ( action, pollOnce = false ) => { + const pollStatus = ( action, link, pollOnce = false ) => { fetch( ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -30,20 +40,35 @@ const CimoDownloadNotice = props => { action: 'stackable_check_cimo_status', // eslint-disable-next-line camelcase user_action: action, + nonce: cimo.nonce, } ), credentials: 'same-origin', - } ).then( res => res.json() ).then( _data => { + } ).then( res => res.json() ).then( res => { + if ( ! res.success ) { + setData( { status: 'error', action: '' } ) + + const errorMessage = res?.data?.message ? res.data.message : 'Server error' + + throw new Error( 'Stackable: ' + errorMessage ) + } + + if ( pollCountRef.current === 0 && link ) { + window.open( link, '_blank' ) + } + pollCountRef.current += 1 + const _data = res.data + if ( data.status !== _data.status ) { setData( _data ) // Update the global stackable.cimo status/action variables // so new image block selections reflect the latest Cimo installation state - // eslint-disable-next-line no-undef - stackable.cimo.status = _data.status - // eslint-disable-next-line no-undef - stackable.cimo.action = _data.action + if ( typeof window !== 'undefined' && window.stackable?.cimo ) { + window.stackable.cimo.status = _data.status + window.stackable.cimo.action = _data.action + } } // Stop polling if it has reached 3 attempts, or plugin status indicates installation/activation is complete @@ -57,6 +82,9 @@ const CimoDownloadNotice = props => { setTimeout( () => { pollStatus( action ) }, 3000 * pollCountRef.current ) + } ).catch( e => { + // eslint-disable-next-line no-console + console.error( e.message ) } ) } @@ -77,32 +105,28 @@ const CimoDownloadNotice = props => { } if ( data.status === 'not_installed' ) { - pollStatus( 'install', true ) + pollStatus( 'install', null, true ) return } - pollStatus( 'activate', true ) + pollStatus( 'activate', null, true ) } ) }, } ) }, [] ) - const onActionClick = async () => { + const onActionClick = e => { + e.preventDefault() pollCountRef.current = 0 if ( data.status === 'not_installed' ) { setData( { status: 'installing', action: '' } ) - setTimeout( () => { - pollStatus( 'install' ) - }, 3000 ) - + pollStatus( 'install', e.currentTarget.href ) return } setData( { status: 'activating', action: '' } ) - setTimeout( () => { - pollStatus( 'activate' ) - }, 3000 ) + pollStatus( 'activate', e.currentTarget.href ) } return ( <> @@ -134,8 +158,8 @@ const CimoDownloadNoticeWrapper = props => { export default CimoDownloadNoticeWrapper domReady( () => { - if ( ! cimo || cimo.status === 'activated' || cimo.hideNotice || typeof wp === 'undefined' || ! wp.media || ! wp.media.view || - ! wp.media.view.Attachment || ! wp.media.view.Attachment.Details + if ( ! cimo || cimo.status === 'activated' || cimo.hideNotice || + typeof wp === 'undefined' || ! wp?.media?.view?.Attachment?.Details ) { return } @@ -147,6 +171,10 @@ domReady( () => { render() { const result = CurrentDetailsView.prototype.render.apply( this, arguments ) + if ( cimo?.hideNotice ) { + return result + } + const details = this.el.querySelector( '.attachment-info .details' ) if ( details && ! this.el.querySelector( '.stk-cimo-notice' ) ) { const noticeDiv = document.createElement( 'div' ) diff --git a/src/lazy-components/cimo/index.php b/src/lazy-components/cimo/index.php index e9cdc404b..2facdfe2f 100644 --- a/src/lazy-components/cimo/index.php +++ b/src/lazy-components/cimo/index.php @@ -16,7 +16,6 @@ class Stackable_Cimo_Notice { function __construct() { add_action( 'admin_init', array( $this, 'register_settings' ) ); - add_action( 'rest_api_init', array( $this, 'register_settings' ) ); // For polling the status add_action('wp_ajax_stackable_check_cimo_status', array( $this, 'check_cimo_status' ) ); @@ -34,7 +33,7 @@ public function register_settings() { array( 'type' => 'boolean', 'description' => __( 'Hides the Cimo download notice.', STACKABLE_I18N ), - 'sanitize_callback' => 'sanitize_text_field', + 'sanitize_callback' => 'rest_sanitize_boolean', 'show_in_rest' => true, 'default' => false, ) @@ -108,6 +107,7 @@ public function enqueue_script() { $data = array( 'status' => $cimo_status, 'action' => html_entity_decode( $cimo_action ), + 'nonce' => wp_create_nonce( 'stackable_cimo_status' ) ); // Expose the Cimo plugin status and action URL for use in JS @@ -141,12 +141,29 @@ public function localize_hide_cimo_notice( $args ) { * Used for polling Cimo plugin status changes via AJAX in the admin UI. */ function check_cimo_status() { - $action = sanitize_text_field( $_POST['user_action'] ); + // Verify nonce + if ( ! check_ajax_referer( 'stackable_cimo_status', 'nonce', false ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); + return; + } + + $action = isset( $_POST['user_action'] ) ? sanitize_text_field( $_POST['user_action'] ) : ''; $response = array( 'status' => 'activated', 'action' => '' ); + if ( ! $action || ( $action !== 'install' && $action !== 'activate' ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid request action.' ), 400 ); + return; + } + + if ( ( $action === 'install' && ! current_user_can( 'install_plugins' ) ) || + ( $action === 'activate' && ! current_user_can( 'activate_plugins' ) ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Insufficient permissions.' ), 403 ); + return; + } + if ( $action === 'install' && ! self::is_plugin_installed() ) { $response[ 'status' ] = 'installing'; } else if ( ! self::is_plugin_activated() ) { @@ -163,7 +180,7 @@ function check_cimo_status() { ) ) : ''; } - wp_send_json( $response ); + wp_send_json_success( $response ); } } From 4e39c6f71cab94a825fb996fb752d92ff872e300 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Thu, 30 Oct 2025 10:53:27 +0800 Subject: [PATCH 04/13] fix to coderabbit's qa --- src/lazy-components/cimo/index.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lazy-components/cimo/index.php b/src/lazy-components/cimo/index.php index 2facdfe2f..3a58a2c4a 100644 --- a/src/lazy-components/cimo/index.php +++ b/src/lazy-components/cimo/index.php @@ -75,10 +75,19 @@ public function enqueue_script() { return; } + $is_installed = self::is_plugin_installed(); + + // Prevent exposing Cimo plugin status and action URLs to users lacking the necessary install or activate plugin capabilities. + if ( ( ! $is_installed && ! current_user_can( 'install_plugins' ) ) || + ( $is_installed && ! current_user_can( 'activate_plugins' ) ) + ) { + return; + } + $cimo_status = 'activated'; $cimo_action = ''; - if ( ! self::is_plugin_installed() ) { + if ( ! $is_installed ) { $cimo_status = 'not_installed'; $cimo_action = wp_nonce_url( add_query_arg( @@ -104,6 +113,11 @@ public function enqueue_script() { ); } + // No need to expose plugin status and action URL if it's activated. + if ( $cimo_status === 'activated' ) { + return; + } + $data = array( 'status' => $cimo_status, 'action' => html_entity_decode( $cimo_action ), From 59de0d4f8faaf82b38448e6acc875c22be27b9a2 Mon Sep 17 00:00:00 2001 From: Benjamin Intal Date: Tue, 4 Nov 2025 10:25:10 +0800 Subject: [PATCH 05/13] Apply suggestion from @bfintal --- plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.php b/plugin.php index d2664b3cc..4335ef3e1 100644 --- a/plugin.php +++ b/plugin.php @@ -289,7 +289,7 @@ function is_frontend() { require_once( plugin_dir_path( __FILE__ ) . 'src/css-optimize.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/compatibility/index.php' ); -// For cross-selling +// For cross-marketing require_once( plugin_dir_path( __FILE__ ) . 'src/lazy-components/cimo/index.php' ); if ( ! is_admin() ) { From 8dcef44fe588cf514fed373119eab6305bd01be0 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 12:08:37 +0800 Subject: [PATCH 06/13] add useful plugins --- plugin.php | 4 +- src/lazy-components/cimo/index.php | 202 -------------------- src/welcome/admin.js | 9 + src/welcome/admin.scss | 65 ++++++- src/welcome/getting-started.scss | 55 ------ src/welcome/index.php | 38 +++- src/welcome/useful-plugins.js | 118 ++++++++++++ src/welcome/useful-plugins.php | 294 +++++++++++++++++++++++++++++ src/welcome/useful-plugins.scss | 57 ++++++ 9 files changed, 578 insertions(+), 264 deletions(-) delete mode 100644 src/lazy-components/cimo/index.php create mode 100644 src/welcome/useful-plugins.js create mode 100644 src/welcome/useful-plugins.php create mode 100644 src/welcome/useful-plugins.scss diff --git a/plugin.php b/plugin.php index 62732e107..e3a2fc038 100644 --- a/plugin.php +++ b/plugin.php @@ -289,9 +289,6 @@ function is_frontend() { require_once( plugin_dir_path( __FILE__ ) . 'src/css-optimize.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/compatibility/index.php' ); -// For cross-marketing -require_once( plugin_dir_path( __FILE__ ) . 'src/lazy-components/cimo/index.php' ); - if ( ! is_admin() ) { require_once( plugin_dir_path( __FILE__ ) . 'src/lightbox/index.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/block/accordion/index.php' ); @@ -318,6 +315,7 @@ function is_frontend() { */ require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/getting-started.php' ); if ( is_admin() ) { + require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/useful-plugins.php' ); // For cross-marketing require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/index.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/news.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/welcome/freemius.php' ); diff --git a/src/lazy-components/cimo/index.php b/src/lazy-components/cimo/index.php deleted file mode 100644 index 3a58a2c4a..000000000 --- a/src/lazy-components/cimo/index.php +++ /dev/null @@ -1,202 +0,0 @@ - 'boolean', - 'description' => __( 'Hides the Cimo download notice.', STACKABLE_I18N ), - 'sanitize_callback' => 'rest_sanitize_boolean', - 'show_in_rest' => true, - 'default' => false, - ) - ); - } - - public static function is_plugin_installed() { - if ( ! function_exists( 'get_plugins' ) ) { - include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); - } - - $all_plugins = get_plugins(); - if ( isset( $all_plugins[ self::$CIMO_FULL_SLUG ] ) ) { - return true; - } - - return false; - } - - public static function is_plugin_activated() { - if ( ! function_exists( 'is_plugin_active' ) ) { - include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); - } - - if ( class_exists( 'Cimo_Script_Loader' ) || is_plugin_active( self::$CIMO_FULL_SLUG ) ) { - return true; - } - - return false; - - } - - // Determines the current status of the Cimo plugin (not installed, installed but inactive, or activated) - // and provides the appropriate action URL for installation or activation. - public function enqueue_script() { - $hide_cimo = get_option( 'stackable_hide_cimo_notice', false ); - - if ( $hide_cimo ) { - return; - } - - $is_installed = self::is_plugin_installed(); - - // Prevent exposing Cimo plugin status and action URLs to users lacking the necessary install or activate plugin capabilities. - if ( ( ! $is_installed && ! current_user_can( 'install_plugins' ) ) || - ( $is_installed && ! current_user_can( 'activate_plugins' ) ) - ) { - return; - } - - $cimo_status = 'activated'; - $cimo_action = ''; - - if ( ! $is_installed ) { - $cimo_status = 'not_installed'; - $cimo_action = wp_nonce_url( - add_query_arg( - [ - 'action' => 'install-plugin', - 'plugin' => self::$CIMO_SLUG, - ], - admin_url( 'update.php' ) - ), - 'install-plugin_' . self::$CIMO_SLUG - ); - } else if ( ! self::is_plugin_activated() ) { - $cimo_status = 'installed'; - $cimo_action = wp_nonce_url( - add_query_arg( - [ - 'action' => 'activate', - 'plugin' => self::$CIMO_FULL_SLUG, - ], - admin_url( 'plugins.php' ) - ), - 'activate-plugin_' . self::$CIMO_FULL_SLUG - ); - } - - // No need to expose plugin status and action URL if it's activated. - if ( $cimo_status === 'activated' ) { - return; - } - - $data = array( - 'status' => $cimo_status, - 'action' => html_entity_decode( $cimo_action ), - 'nonce' => wp_create_nonce( 'stackable_cimo_status' ) - ); - - // Expose the Cimo plugin status and action URL for use in JS - add_filter( 'stackable_localize_script', function ( $args ) use( $data ) { - return $this->add_localize_script( $args, $data ); - }, 1 ); - } - - public function add_localize_script( $args, $data ) { - $args[ 'cimo' ] = $data; - return $args; - } - - // Adds the hide notice option for the Cimo plugin to the localized script arguments. - public function localize_hide_cimo_notice( $args ) { - $hide_cimo = get_option( 'stackable_hide_cimo_notice', false ); - if ( isset( $args['cimo'] ) ) { - $args['cimo']['hideNotice'] = $hide_cimo; - return $args; - } - - $args[ 'cimo' ] = array( 'hideNotice' => $hide_cimo ); - return $args; - } - - /** - * Checks the status of the Cimo plugin installation or activation. - * Returns JSON indicating if Cimo is installed, installing, activated, or activating, - * and provides the respective action URL if activation is needed. - * - * Used for polling Cimo plugin status changes via AJAX in the admin UI. - */ - function check_cimo_status() { - // Verify nonce - if ( ! check_ajax_referer( 'stackable_cimo_status', 'nonce', false ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); - return; - } - - $action = isset( $_POST['user_action'] ) ? sanitize_text_field( $_POST['user_action'] ) : ''; - $response = array( - 'status' => 'activated', - 'action' => '' - ); - - if ( ! $action || ( $action !== 'install' && $action !== 'activate' ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid request action.' ), 400 ); - return; - } - - if ( ( $action === 'install' && ! current_user_can( 'install_plugins' ) ) || - ( $action === 'activate' && ! current_user_can( 'activate_plugins' ) ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Insufficient permissions.' ), 403 ); - return; - } - - if ( $action === 'install' && ! self::is_plugin_installed() ) { - $response[ 'status' ] = 'installing'; - } else if ( ! self::is_plugin_activated() ) { - $response[ 'status' ] = $action === 'install' ? 'installed' : 'activating'; - $response[ 'action' ] = $action === 'install' ? html_entity_decode( wp_nonce_url( - add_query_arg( - [ - 'action' => 'activate', - 'plugin' => self::$CIMO_FULL_SLUG, - ], - admin_url( 'plugins.php' ) - ), - 'activate-plugin_' . self::$CIMO_FULL_SLUG - ) ) : ''; - } - - wp_send_json_success( $response ); - } - } - - new Stackable_Cimo_Notice(); -} diff --git a/src/welcome/admin.js b/src/welcome/admin.js index 92dd69ee1..3d83ea5ee 100644 --- a/src/welcome/admin.js +++ b/src/welcome/admin.js @@ -42,6 +42,7 @@ import { GettingStarted } from './getting-started' import { BLOCK_STATE } from '~stackable/util/blocks' import { BlockToggler, OptimizationSettings } from '~stackable/deprecated/v2/welcome/admin' import blockData from '~stackable/deprecated/v2/welcome/blocks' +import { UsefulPlugins } from './useful-plugins' const [ FREE_BLOCKS, BLOCK_DEPENDENCIES ] = importBlocks( require.context( '../block', true, /block\.json$/ ) ) @@ -1658,4 +1659,12 @@ domReady( () => { ) } + + if ( document.getElementById( 's-useful-plugins' ) ) { + createRoot( + document.getElementById( 's-useful-plugins' ) + ).render( + + ) + } } ) diff --git a/src/welcome/admin.scss b/src/welcome/admin.scss index 1fc2f8494..30537f977 100644 --- a/src/welcome/admin.scss +++ b/src/welcome/admin.scss @@ -8,6 +8,15 @@ --stk-welcome-light-border: #d0d5dd; } +@mixin s-shadow { + box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; + transition: all 0.3s ease-in-out; + &:hover { + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; + } +} + + // Make scroll smooth with inline navigation. html { scroll-behavior: smooth; @@ -17,6 +26,9 @@ html { body[class*="page_stackable"], body[class*="page_stk-"] { + --wp-components-color-accent: var(--stk-welcome-primary); + --wp-components-color-accent-darker-20: var(--stk-welcome-secondary); + --wp-components-color-accent-inverted: var(--stk-welcome-secondary); #wpcontent { padding-left: 0; } @@ -154,6 +166,54 @@ body[class*="page_stk-"] { align-items: center; } + +.s-card { + padding: 1.5em; + display: flex; + flex-direction: column; + border-radius: 16px; + @include s-shadow; + background: #fff; + overflow: hidden; + + .s-card-title, + h3 { + margin: 0 0 0.5em; + } + .s-card-subtitle, + p { + margin: 0; + } + > *:last-child { + margin-bottom: 0; + } + + .s-video-wrapper { + align-items: center; + } + + .s-icon-wrapper { + border: 1px solid #eaecf0; + border-radius: 8px; + padding: 10px; + display: flex; + align-items: center; + max-width: fit-content; + margin-bottom: 20px; + } + + .s-bottom-icon-wrapper { + display: flex; + justify-content: flex-end; + } + + svg { + height: 24px; + width: 24px; + } + +} + .s-admin-notice-marker { display: none !important; } @@ -369,10 +429,8 @@ body.toplevel_page_stackable { padding-bottom: 0; } } -body.stackable_page_stackable-settings, body.toplevel_page_stackable, -body.stackable_page_stk-custom-fields, -body.stackable_page_stackable-go-premium { +body[class*="page_stackable"] { img { max-width: 100%; } @@ -939,3 +997,4 @@ body.stackable_page_stackable-go-premium { @import "news"; @import "getting-started"; @import "freemius"; +@import "useful-plugins"; diff --git a/src/welcome/getting-started.scss b/src/welcome/getting-started.scss index 1545a4a5a..f955d5e53 100644 --- a/src/welcome/getting-started.scss +++ b/src/welcome/getting-started.scss @@ -2,14 +2,6 @@ * Styles used by the Getting Started section. */ -@mixin s-shadow { - box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; - transition: all 0.3s ease-in-out; - &:hover { - box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; - } -} - .toplevel_page_stackable { .s-body { max-width: 1200px; @@ -154,53 +146,6 @@ } } - .s-card { - padding: 1.5em; - display: flex; - flex-direction: column; - border-radius: 16px; - @include s-shadow; - background: #fff; - overflow: hidden; - - .s-card-title, - h3 { - margin: 0 0 0.5em; - } - .s-card-subtitle, - p { - margin: 0; - } - > *:last-child { - margin-bottom: 0; - } - - .s-video-wrapper { - align-items: center; - } - - .s-icon-wrapper { - border: 1px solid #eaecf0; - border-radius: 8px; - padding: 10px; - display: flex; - align-items: center; - max-width: fit-content; - margin-bottom: 20px; - } - - .s-bottom-icon-wrapper { - display: flex; - justify-content: flex-end; - } - - svg { - height: 24px; - width: 24px; - } - - } - .s-divider { width: 150px; margin: 64px auto; diff --git a/src/welcome/index.php b/src/welcome/index.php index 94fcd6d5d..a3fed63e3 100644 --- a/src/welcome/index.php +++ b/src/welcome/index.php @@ -80,6 +80,16 @@ public function add_dashboard_page() { '__return_null', ); } + + // Our settings page. + add_submenu_page( + 'stackable', // Parent slug. + __( 'Useful Plugins', STACKABLE_I18N ), // Page title. + __( 'Useful Plugins', STACKABLE_I18N ), // Menu title. + 'manage_options', // Capability. + 'stackable-useful-plugins', // Menu slug. + function() { $this->stackable_content('s-useful-plugins'); } + ); } public function enqueue_dashboard_script( $hook ) { @@ -91,7 +101,7 @@ public function enqueue_dashboard_script( $hook ) { } // For the options page, load our options script. - if ( 'settings_page_stackable' === $hook || stripos( $hook, 'page_stackable-settings' ) !== false || 'toplevel_page_stackable' === $hook ) { + if ( 'settings_page_stackable' === $hook || stripos( $hook, 'page_stackable' ) !== false || 'toplevel_page_stackable' === $hook ) { wp_enqueue_script( 'wp-i18n' ); wp_enqueue_script( 'wp-element' ); @@ -195,6 +205,12 @@ public static function print_tabs() { + + + + + +
+
+ print_header() ?> + print_premium_button() ?> + print_tabs() ?> +
+

+
+
+
+
+ +
+
+
+ { + const [ status, setStatus ] = useState( usefulPlugins[ plugin.id ].status ) + const onClickAction = () => { + if ( status === PLUGIN_STATUS.ACTIVATED || + status === PLUGIN_STATUS.INSTALLING || + status === PLUGIN_STATUS.ACTIVATING + ) { + return + } + + const prevStatus = status // Remember previous status to revert on error + let successStatus = status // Will be set for next success state + const formData = new window.FormData() + setStatus( prev => { + let newStatus = prev + if ( prev === PLUGIN_STATUS.NOT_INSTALLED ) { + formData.append( 'action', 'stackable_useful_plugins_install' ) + formData.append( '_ajax_nonce', installerNonce ) + formData.append( 'slug', plugin.id ) + newStatus = PLUGIN_STATUS.INSTALLING + successStatus = PLUGIN_STATUS.INSTALLED + } else if ( prev === PLUGIN_STATUS.INSTALLED ) { + formData.append( 'action', 'stackable_useful_plugins_activate' ) + formData.append( 'nonce', activateNonce ) + formData.append( 'slug', plugin.id ) + formData.append( 'full_slug', usefulPlugins[ plugin.id ].fullSlug ) + newStatus = PLUGIN_STATUS.ACTIVATING + successStatus = PLUGIN_STATUS.ACTIVATED + } + return newStatus + } ) + + // Perform Ajax request to install or activate plugin + apiFetch( { + url: ajaxUrl, + method: 'POST', + body: formData, + } ).then( response => { + setTimeout( () => { + // Mark as succeeded if operation successful or folder already exists after install + if ( response.success || response.data.errorCode === 'folder_exists' ) { + usefulPlugins[ plugin.id ].status = successStatus + setStatus( successStatus ) + } else { + usefulPlugins[ plugin.id ].status = prevStatus + setStatus( prevStatus ) + } + }, 1000 ) // Add small delay to avoid race conditions with plugin activation/installation + } ).catch( e => { + // eslint-disable-next-line no-console + console.error( 'Stackable: ', e ) + usefulPlugins[ plugin.id ].status = prevStatus + setStatus( prevStatus ) + } ) + } + + return
+
+ { +

{ plugin.title }

+
+

{ plugin.description }

+ +
+} + +export const UsefulPlugins = () => { + return
+ { PLUGINS.map( ( plugin, i ) => { + return + } ) } +
+} diff --git a/src/welcome/useful-plugins.php b/src/welcome/useful-plugins.php new file mode 100644 index 000000000..f05b221c7 --- /dev/null +++ b/src/welcome/useful-plugins.php @@ -0,0 +1,294 @@ + array( + 'slug' => 'interactions', + 'full_slug' => 'interactions/interactions.php', + ), + 'cimo-image-optimizer' => array( + 'slug' => 'cimo-image-optimizer', + 'full_slug' => 'cimo-image-optimizer/cimo.php', + ), + ); + + function __construct() { + add_action( 'admin_init', array( $this, 'register_settings' ) ); + + // Register action on 'admin_menu' to ensure filters for the editor and admin settings + // are added early, before those scripts are enqueued and filters are applied. + add_action( 'admin_menu', array( $this, 'get_useful_plugins_info' ) ); + + // use WordPress ajax installer + // see Docs: https://developer.wordpress.org/reference/functions/wp_ajax_install_plugin/ + add_action('wp_ajax_stackable_useful_plugins_activate', array( $this, 'do_plugin_activate' ) ); + add_action('wp_ajax_stackable_useful_plugins_install', 'wp_ajax_install_plugin' ); + + // handler for polling the Cimo plugin's installation or activation status from the block editor + add_action('wp_ajax_stackable_check_cimo_status', array( $this, 'check_cimo_status' ) ); + + if ( is_admin() ) { + add_filter( 'stackable_localize_script', array( $this, 'localize_hide_cimo_notice' ) ); + } + } + + public function register_settings() { + register_setting( + 'stackable_editor_settings', + 'stackable_hide_cimo_notice', + array( + 'type' => 'boolean', + 'description' => __( 'Hides the Cimo download notice.', STACKABLE_I18N ), + 'sanitize_callback' => 'rest_sanitize_boolean', + 'show_in_rest' => true, + 'default' => false, + ) + ); + } + + public static function is_plugin_installed( $plugin_slug ) { + if ( ! function_exists( 'get_plugins' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + $all_plugins = get_plugins(); + if ( isset( $all_plugins[ $plugin_slug ] ) ) { + return true; + } + + return false; + } + + public static function is_plugin_activated( $plugin_slug ) { + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + if ( is_plugin_active( $plugin_slug ) ) { + return true; + } + + return false; + } + + + public function get_useful_plugins_info() { + $current_user_cap = current_user_can( 'install_plugins' ) ? 2 : ( + current_user_can( 'activate_plugins') ? 1 : 0 + ); + + if ( ! $current_user_cap ) { + return; + } + + if ( ! function_exists( 'plugins_api' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + } + if ( ! function_exists( 'get_plugins' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + $all_plugins = get_plugins(); + $data_to_localize = array(); + + foreach ( self::$PLUGINS as $key => $plugin ) { + $status = 'not_installed'; + $action_link = ''; + + if ( $current_user_cap === 2 ) { // user can install plugins + $action_link = wp_nonce_url( + add_query_arg( [ + 'action' => 'install-plugin', + 'plugin' => $plugin['slug'], + ], admin_url( 'update.php' ) ), + 'install-plugin_' . $plugin['slug'] + ); + } + + if ( isset( $all_plugins[ $plugin['full_slug'] ] ) ) { + $status = 'installed'; + $action_link = wp_nonce_url( + add_query_arg( [ + 'action' => 'activate', + 'plugin' => $plugin['full_slug'], + ], admin_url( 'plugins.php' ) ), + 'activate-plugin_' . $plugin['full_slug'] + ); + } + + if ( is_plugin_active( $plugin['full_slug'] ) ) { + $status = 'activated'; + $action_link = ''; // nothing to do + } + + $plugin_info = plugins_api( 'plugin_information', [ + 'slug' => $plugin['slug'], + 'fields' =>[ 'icons' => true, 'sections' => false ], + ] ); + + $icon_url = $plugin_info && isset( $plugin_info->icons ) + && is_array( $plugin_info->icons ) && ! empty( $plugin_info->icons ) + ? array_values( $plugin_info->icons )[0] : ''; + + $data_to_localize[ $key ] = array( + 'status' => $status, + 'icon' => $icon_url, + 'fullSlug' => $plugin[ 'full_slug' ], + ); + } + + // Make Cimo available in the block editor + add_filter( 'stackable_localize_script', function ( $args ) use( $data_to_localize, $action_link ) { + $cimo_data = $data_to_localize[ 'cimo-image-optimizer' ]; + $cimo_data[ 'action' ] = html_entity_decode( $action_link ); + $cimo_data['nonce'] = wp_create_nonce( 'stackable_cimo_status' ); + return $this->add_localize_script( $args, 'cimo', $cimo_data ); + }, 1 ); + + // Make all plugin data and the ajax url available in the admin settings + add_filter( 'stackable_localize_settings_script', function ( $args ) use( $data_to_localize ) { + $argsToAdd = array( + 'usefulPlugins' => $data_to_localize, + 'installerNonce' => wp_create_nonce( "updates" ), + 'activateNonce' => wp_create_nonce( "stk_activate_useful_plugin" ), + 'ajaxUrl' => admin_url('admin-ajax.php') + ); + return $this->add_localize_script( $args, '', $argsToAdd ); + } ); + } + + public function add_localize_script( $args, $arg_key, $data ) { + // If an argument key is provided, save data under that key and return + if ( $arg_key ) { + $args[ $arg_key ] = $data; + return $args; + } + + // Otherwise, add each key/value from $data to merge with $args + foreach ( $data as $key => $value ) { + $args[$key] = $value; + } + + return $args; + } + + // Adds the hide notice option for the Cimo plugin to the localized script arguments. + public function localize_hide_cimo_notice( $args ) { + $hide_cimo = get_option( 'stackable_hide_cimo_notice', false ); + if ( isset( $args['cimo'] ) ) { + $args['cimo']['hideNotice'] = $hide_cimo; + return $args; + } + + $args[ 'cimo' ] = array( 'hideNotice' => $hide_cimo ); + return $args; + } + + function do_plugin_activate() { + $slug = isset( $_POST['slug'] ) ? sanitize_text_field( $_POST['slug'] ) : ''; + $full_slug = isset( $_POST['full_slug'] ) ? sanitize_text_field( $_POST['full_slug'] ) : ''; + if ( ! $slug || ! $full_slug ) { + error_log( 'invalid slug' ); + wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid slug.' ), 400 ); + } + + if ( ! check_ajax_referer( 'stk_activate_useful_plugin', 'nonce', false ) ) { + error_log( 'security failed' ); + wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); + return; + } + + if ( ! current_user_can( 'activate_plugins' ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Insufficient permissions.' ), 403 ); + return; + } + + // Clear the plugins cache to ensure newly installed plugins are recognized (avoids activation errors due to outdated plugin cache) + wp_clean_plugins_cache(); + + if ( ! function_exists( 'activate_plugin' ) ) { + include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + $result = activate_plugin( $full_slug, '', false, true ); + + if ( is_wp_error( $result ) ) { + error_log( 'failed to activate ' . print_r( $result, true )); + wp_send_json_error( array( 'status' => 'error', 'message' => 'Failed to activate plugin.' ), 500 ); + return; + } + + wp_send_json_success( array( 'status' => 'success', 'message' => 'Successfully activated plugin.' ), 200 ); + } + + + /** + * Checks the status of the Cimo plugin installation or activation. + * Returns JSON indicating if Cimo is installed, installing, activated, or activating, + * and provides the respective action URL if activation is needed. + * + * Used for polling Cimo plugin status changes via AJAX in the admin UI. + */ + function check_cimo_status() { + $slug = 'cimo-image-optimizer'; + // Verify nonce + if ( ! check_ajax_referer( 'stackable_cimo_status', 'nonce', false ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); + return; + } + + $action = isset( $_POST['user_action'] ) ? sanitize_text_field( $_POST['user_action'] ) : ''; + $response = array( + 'status' => 'activated', + 'action' => '' + ); + + if ( ! $action || ( $action !== 'install' && $action !== 'activate' ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid request action.' ), 400 ); + return; + } + + if ( ( $action === 'install' && ! current_user_can( 'install_plugins' ) ) || + ( $action === 'activate' && ! current_user_can( 'activate_plugins' ) ) ) { + wp_send_json_error( array( 'status' => 'error', 'message' => 'Insufficient permissions.' ), 403 ); + return; + } + + $full_slug = self::$PLUGINS[ $slug ][ 'full_slug' ]; + + if ( $action === 'install' && ! self::is_plugin_installed( $full_slug ) ) { + $response[ 'status' ] = 'installing'; + } else if ( ! self::is_plugin_activated( $full_slug ) ) { + $response[ 'status' ] = $action === 'install' ? 'installed' : 'activating'; + $response[ 'action' ] = $action === 'install' ? html_entity_decode( wp_nonce_url( + add_query_arg( + [ + 'action' => 'activate', + 'plugin' => $full_slug, + ], + admin_url( 'plugins.php' ) + ), + 'activate-plugin_' . $full_slug + ) ) : ''; + } + + wp_send_json_success( $response ); + } + } + + new Stackable_Useful_Plugins(); +} diff --git a/src/welcome/useful-plugins.scss b/src/welcome/useful-plugins.scss new file mode 100644 index 000000000..5f0649a0a --- /dev/null +++ b/src/welcome/useful-plugins.scss @@ -0,0 +1,57 @@ +#s-useful-plugins { + max-width: 1200px; + margin: 60px auto; +} + +.s-useful-plugin-list { + display: grid; + grid-template-columns: repeat(auto-fit, 350px); + gap: 32px; + justify-content: center; + + .s-card { + .s-plugin-title { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + .s-card-title { + margin: 0; + } + .s-plugin-icon { + width: 48px; + height: 48px; + } + } + + .s-button.s-button--ghost { + margin: 24px 0 0; + padding: 0 20px; + color: var(--stk-welcome-primary); + box-shadow: none; + border: 1px solid var(--stk-welcome-primary); + display: inline-flex; + align-items: center; + justify-content: center; + text-transform: none; + + &:hover:not(:disabled) { + border-color: #b300be; + color: #b300be; + } + &:disabled { + cursor: not-allowed; + } + &.pending:disabled { + cursor: progress; + } + } + + .s-spinner svg { + height: 12px; + width: 12px; + margin-top: 0; + } + } + +} From d75f1dba3a9898bf17fc035cdbeb92036df24541 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 12:30:05 +0800 Subject: [PATCH 07/13] remove error logs, add guard if pluginData is not available --- src/welcome/useful-plugins.js | 17 +++++++++++------ src/welcome/useful-plugins.php | 3 --- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/welcome/useful-plugins.js b/src/welcome/useful-plugins.js index 77ed7b171..5815e7cd7 100644 --- a/src/welcome/useful-plugins.js +++ b/src/welcome/useful-plugins.js @@ -35,7 +35,12 @@ const BUTTON_LABELS = { } const PluginCard = ( { plugin } ) => { - const [ status, setStatus ] = useState( usefulPlugins[ plugin.id ].status ) + const pluginData = usefulPlugins?.[ plugin.id ] ?? null + const [ status, setStatus ] = useState( pluginData?.status ?? PLUGIN_STATUS.ACTIVATED ) + + if ( ! pluginData ) { + return null + } const onClickAction = () => { if ( status === PLUGIN_STATUS.ACTIVATED || status === PLUGIN_STATUS.INSTALLING || @@ -59,7 +64,7 @@ const PluginCard = ( { plugin } ) => { formData.append( 'action', 'stackable_useful_plugins_activate' ) formData.append( 'nonce', activateNonce ) formData.append( 'slug', plugin.id ) - formData.append( 'full_slug', usefulPlugins[ plugin.id ].fullSlug ) + formData.append( 'full_slug', pluginData.fullSlug ) newStatus = PLUGIN_STATUS.ACTIVATING successStatus = PLUGIN_STATUS.ACTIVATED } @@ -74,8 +79,8 @@ const PluginCard = ( { plugin } ) => { } ).then( response => { setTimeout( () => { // Mark as succeeded if operation successful or folder already exists after install - if ( response.success || response.data.errorCode === 'folder_exists' ) { - usefulPlugins[ plugin.id ].status = successStatus + if ( response.success || response.data?.errorCode === 'folder_exists' ) { + pluginData.status = successStatus setStatus( successStatus ) } else { usefulPlugins[ plugin.id ].status = prevStatus @@ -85,14 +90,14 @@ const PluginCard = ( { plugin } ) => { } ).catch( e => { // eslint-disable-next-line no-console console.error( 'Stackable: ', e ) - usefulPlugins[ plugin.id ].status = prevStatus + pluginData.status = prevStatus setStatus( prevStatus ) } ) } return
- { + {

{ plugin.title }

{ plugin.description }

diff --git a/src/welcome/useful-plugins.php b/src/welcome/useful-plugins.php index f05b221c7..3c1ed4ef5 100644 --- a/src/welcome/useful-plugins.php +++ b/src/welcome/useful-plugins.php @@ -202,12 +202,10 @@ function do_plugin_activate() { $slug = isset( $_POST['slug'] ) ? sanitize_text_field( $_POST['slug'] ) : ''; $full_slug = isset( $_POST['full_slug'] ) ? sanitize_text_field( $_POST['full_slug'] ) : ''; if ( ! $slug || ! $full_slug ) { - error_log( 'invalid slug' ); wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid slug.' ), 400 ); } if ( ! check_ajax_referer( 'stk_activate_useful_plugin', 'nonce', false ) ) { - error_log( 'security failed' ); wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); return; } @@ -227,7 +225,6 @@ function do_plugin_activate() { $result = activate_plugin( $full_slug, '', false, true ); if ( is_wp_error( $result ) ) { - error_log( 'failed to activate ' . print_r( $result, true )); wp_send_json_error( array( 'status' => 'error', 'message' => 'Failed to activate plugin.' ), 500 ); return; } From d5caa3b8890cf53d888a3e2da20b2343585f6ef7 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 12:55:09 +0800 Subject: [PATCH 08/13] fix functions to localize script --- src/welcome/useful-plugins.php | 79 +++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/welcome/useful-plugins.php b/src/welcome/useful-plugins.php index 3c1ed4ef5..da635f5a6 100644 --- a/src/welcome/useful-plugins.php +++ b/src/welcome/useful-plugins.php @@ -107,32 +107,13 @@ public function get_useful_plugins_info() { foreach ( self::$PLUGINS as $key => $plugin ) { $status = 'not_installed'; - $action_link = ''; - - if ( $current_user_cap === 2 ) { // user can install plugins - $action_link = wp_nonce_url( - add_query_arg( [ - 'action' => 'install-plugin', - 'plugin' => $plugin['slug'], - ], admin_url( 'update.php' ) ), - 'install-plugin_' . $plugin['slug'] - ); - } if ( isset( $all_plugins[ $plugin['full_slug'] ] ) ) { $status = 'installed'; - $action_link = wp_nonce_url( - add_query_arg( [ - 'action' => 'activate', - 'plugin' => $plugin['full_slug'], - ], admin_url( 'plugins.php' ) ), - 'activate-plugin_' . $plugin['full_slug'] - ); } if ( is_plugin_active( $plugin['full_slug'] ) ) { $status = 'activated'; - $action_link = ''; // nothing to do } $plugin_info = plugins_api( 'plugin_information', [ @@ -152,21 +133,57 @@ public function get_useful_plugins_info() { } // Make Cimo available in the block editor - add_filter( 'stackable_localize_script', function ( $args ) use( $data_to_localize, $action_link ) { - $cimo_data = $data_to_localize[ 'cimo-image-optimizer' ]; - $cimo_data[ 'action' ] = html_entity_decode( $action_link ); - $cimo_data['nonce'] = wp_create_nonce( 'stackable_cimo_status' ); + $this->add_cimo_args_to_localize_editor( $data_to_localize, $current_user_cap ); + // Make all plugin data and the ajax url available in the admin settings + $this->add_args_to_localize_admin( $data_to_localize ); + } + + public function add_cimo_args_to_localize_editor( $data_to_localize, $current_user_cap ) { + $slug = 'cimo-image-optimizer'; + $full_slug = self::$PLUGINS[ $slug ][ 'full_slug' ]; + + $cimo_data = $data_to_localize[ $slug ]; + $cimo_data['nonce'] = wp_create_nonce( 'stackable_cimo_status' ); + $action_link = ''; + + if ( $current_user_cap === 2 && $cimo_data[ 'status' ] === 'not_installed' ) { + $action_link = wp_nonce_url( + add_query_arg( + [ + 'action' => 'install-plugin', + 'plugin' => $slug, + ], + admin_url( 'update.php' ) + ), + 'install-plugin_' . $slug + ); + } else if ( $current_user_cap >= 1 && $cimo_data[ 'status' ] === 'installed' ) { + $action_link = wp_nonce_url( + add_query_arg( [ + 'action' => 'activate', + 'plugin' => $full_slug, + ], admin_url( 'plugins.php' ) ), + 'activate-plugin_' . $full_slug + ); + } + + $cimo_data[ 'action' ] = $action_link; + + add_filter( 'stackable_localize_script', function ( $args ) use( $cimo_data ) { return $this->add_localize_script( $args, 'cimo', $cimo_data ); }, 1 ); - // Make all plugin data and the ajax url available in the admin settings - add_filter( 'stackable_localize_settings_script', function ( $args ) use( $data_to_localize ) { - $argsToAdd = array( - 'usefulPlugins' => $data_to_localize, - 'installerNonce' => wp_create_nonce( "updates" ), - 'activateNonce' => wp_create_nonce( "stk_activate_useful_plugin" ), - 'ajaxUrl' => admin_url('admin-ajax.php') - ); + } + + public function add_args_to_localize_admin( $data_to_localize ) { + $argsToAdd = array( + 'usefulPlugins' => $data_to_localize, + 'installerNonce' => wp_create_nonce( "updates" ), + 'activateNonce' => wp_create_nonce( "stk_activate_useful_plugin" ), + 'ajaxUrl' => admin_url('admin-ajax.php') + ); + + add_filter( 'stackable_localize_settings_script', function ( $args ) use( $argsToAdd ) { return $this->add_localize_script( $args, '', $argsToAdd ); } ); } From 032a901335e635ef6fe628c34c5e132c94b180dc Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 12:58:33 +0800 Subject: [PATCH 09/13] check for wp error --- src/welcome/useful-plugins.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/welcome/useful-plugins.php b/src/welcome/useful-plugins.php index da635f5a6..50061a576 100644 --- a/src/welcome/useful-plugins.php +++ b/src/welcome/useful-plugins.php @@ -121,9 +121,11 @@ public function get_useful_plugins_info() { 'fields' =>[ 'icons' => true, 'sections' => false ], ] ); - $icon_url = $plugin_info && isset( $plugin_info->icons ) - && is_array( $plugin_info->icons ) && ! empty( $plugin_info->icons ) - ? array_values( $plugin_info->icons )[0] : ''; + $icon_url = ''; + if ( ! is_wp_error( $plugin_info ) && isset( $plugin_info->icons ) + && is_array( $plugin_info->icons ) && ! empty( $plugin_info->icons ) ) { + $icon_url = array_values( $plugin_info->icons )[0]; + } $data_to_localize[ $key ] = array( 'status' => $status, From b62a7e448c8a47deaa5471b35fffb933374601ec Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 13:08:57 +0800 Subject: [PATCH 10/13] fix to coderabbit's qa --- src/welcome/useful-plugins.js | 5 +++++ src/welcome/useful-plugins.php | 14 +++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/welcome/useful-plugins.js b/src/welcome/useful-plugins.js index 5815e7cd7..afcc4b01d 100644 --- a/src/welcome/useful-plugins.js +++ b/src/welcome/useful-plugins.js @@ -71,6 +71,11 @@ const PluginCard = ( { plugin } ) => { return newStatus } ) + // formData is empty + if ( formData.entries().next().done ) { + return + } + // Perform Ajax request to install or activate plugin apiFetch( { url: ajaxUrl, diff --git a/src/welcome/useful-plugins.php b/src/welcome/useful-plugins.php index 50061a576..05524adee 100644 --- a/src/welcome/useful-plugins.php +++ b/src/welcome/useful-plugins.php @@ -94,11 +94,7 @@ public function get_useful_plugins_info() { if ( ! function_exists( 'plugins_api' ) ) { include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); } - if ( ! function_exists( 'get_plugins' ) ) { - include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); - } - - if ( ! function_exists( 'is_plugin_active' ) ) { + if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'is_plugin_active' ) ) { include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); } @@ -123,7 +119,8 @@ public function get_useful_plugins_info() { $icon_url = ''; if ( ! is_wp_error( $plugin_info ) && isset( $plugin_info->icons ) - && is_array( $plugin_info->icons ) && ! empty( $plugin_info->icons ) ) { + && is_array( $plugin_info->icons ) && ! empty( $plugin_info->icons ) + ) { $icon_url = array_values( $plugin_info->icons )[0]; } @@ -169,7 +166,7 @@ public function add_cimo_args_to_localize_editor( $data_to_localize, $current_us ); } - $cimo_data[ 'action' ] = $action_link; + $cimo_data[ 'action' ] = html_entity_decode( $action_link ); add_filter( 'stackable_localize_script', function ( $args ) use( $cimo_data ) { return $this->add_localize_script( $args, 'cimo', $cimo_data ); @@ -286,6 +283,9 @@ function check_cimo_status() { $full_slug = self::$PLUGINS[ $slug ][ 'full_slug' ]; + // Clear plugin cache to ensure we get the most current status ++ wp_clean_plugins_cache(); + if ( $action === 'install' && ! self::is_plugin_installed( $full_slug ) ) { $response[ 'status' ] = 'installing'; } else if ( ! self::is_plugin_activated( $full_slug ) ) { From 8a68b33d35e4c3499f556ec505754ce27b1d7f7a Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 13:16:42 +0800 Subject: [PATCH 11/13] minor fix --- src/welcome/useful-plugins.js | 2 +- src/welcome/useful-plugins.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/welcome/useful-plugins.js b/src/welcome/useful-plugins.js index afcc4b01d..8f736fade 100644 --- a/src/welcome/useful-plugins.js +++ b/src/welcome/useful-plugins.js @@ -88,7 +88,7 @@ const PluginCard = ( { plugin } ) => { pluginData.status = successStatus setStatus( successStatus ) } else { - usefulPlugins[ plugin.id ].status = prevStatus + pluginData.status = prevStatus setStatus( prevStatus ) } }, 1000 ) // Add small delay to avoid race conditions with plugin activation/installation diff --git a/src/welcome/useful-plugins.php b/src/welcome/useful-plugins.php index 05524adee..984923896 100644 --- a/src/welcome/useful-plugins.php +++ b/src/welcome/useful-plugins.php @@ -218,16 +218,16 @@ function do_plugin_activate() { $slug = isset( $_POST['slug'] ) ? sanitize_text_field( $_POST['slug'] ) : ''; $full_slug = isset( $_POST['full_slug'] ) ? sanitize_text_field( $_POST['full_slug'] ) : ''; if ( ! $slug || ! $full_slug ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid slug.' ), 400 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Invalid slug.', STACKABLE_I18N ) ), 400 ); } if ( ! check_ajax_referer( 'stk_activate_useful_plugin', 'nonce', false ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Security check failed.', STACKABLE_I18N ) ), 403 ); return; } if ( ! current_user_can( 'activate_plugins' ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Insufficient permissions.' ), 403 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Insufficient permissions.', STACKABLE_I18N ) ), 403 ); return; } @@ -241,11 +241,11 @@ function do_plugin_activate() { $result = activate_plugin( $full_slug, '', false, true ); if ( is_wp_error( $result ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Failed to activate plugin.' ), 500 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Failed to activate plugin.', STACKABLE_I18N ) ), 500 ); return; } - wp_send_json_success( array( 'status' => 'success', 'message' => 'Successfully activated plugin.' ), 200 ); + wp_send_json_success( array( 'status' => 'success', 'message' => __( 'Successfully activated plugin.', STACKABLE_I18N ) ), 200 ); } @@ -260,7 +260,7 @@ function check_cimo_status() { $slug = 'cimo-image-optimizer'; // Verify nonce if ( ! check_ajax_referer( 'stackable_cimo_status', 'nonce', false ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Security check failed.' ), 403 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Security check failed.', STACKABLE_I18N ) ), 403 ); return; } @@ -271,20 +271,20 @@ function check_cimo_status() { ); if ( ! $action || ( $action !== 'install' && $action !== 'activate' ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Invalid request action.' ), 400 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Invalid request action.', STACKABLE_I18N ) ), 400 ); return; } if ( ( $action === 'install' && ! current_user_can( 'install_plugins' ) ) || ( $action === 'activate' && ! current_user_can( 'activate_plugins' ) ) ) { - wp_send_json_error( array( 'status' => 'error', 'message' => 'Insufficient permissions.' ), 403 ); + wp_send_json_error( array( 'status' => 'error', 'message' => __( 'Insufficient permissions.', STACKABLE_I18N ) ), 403 ); return; } $full_slug = self::$PLUGINS[ $slug ][ 'full_slug' ]; // Clear plugin cache to ensure we get the most current status -+ wp_clean_plugins_cache(); + wp_clean_plugins_cache(); if ( $action === 'install' && ! self::is_plugin_installed( $full_slug ) ) { $response[ 'status' ] = 'installing'; From 18da2233434e8fe79f09e9b0ace8538b77989f12 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 5 Nov 2025 13:24:49 +0800 Subject: [PATCH 12/13] reset status --- src/welcome/useful-plugins.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/welcome/useful-plugins.js b/src/welcome/useful-plugins.js index 8f736fade..e0282cef4 100644 --- a/src/welcome/useful-plugins.js +++ b/src/welcome/useful-plugins.js @@ -73,6 +73,7 @@ const PluginCard = ( { plugin } ) => { // formData is empty if ( formData.entries().next().done ) { + setStatus( prevStatus ) return } From 8a6b8676188ee6ff57dcc081c662ff74c329dbf9 Mon Sep 17 00:00:00 2001 From: "bfintal@gmail.com" <> Date: Mon, 10 Nov 2025 17:10:55 +0800 Subject: [PATCH 13/13] tweaked css --- src/welcome/useful-plugins.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/welcome/useful-plugins.scss b/src/welcome/useful-plugins.scss index 5f0649a0a..020e4cfe1 100644 --- a/src/welcome/useful-plugins.scss +++ b/src/welcome/useful-plugins.scss @@ -34,6 +34,7 @@ align-items: center; justify-content: center; text-transform: none; + align-self: flex-start; &:hover:not(:disabled) { border-color: #b300be;