class name. Each is required + instantiated + registered
// independently, so one bad/missing file never blocks the rest.
$conditions = [
'class-woocommerce-stock-condition.php' => 'WooCommerce_Stock_Condition',
'class-product-category-condition.php' => 'Product_Category_Condition',
'class-product-tag-condition.php' => 'Product_Tag_Condition',
'class-product-price-condition.php' => 'Product_Price_Condition',
'class-featured-product-condition.php' => 'Featured_Product_Condition',
];
foreach ( $conditions as $file => $class_name ) {
try {
require_once ATOMORA_PATH . 'includes/conditions/' . $file;
$fqcn = '\\Atomora\\Conditions\\' . $class_name;
if ( ! class_exists( $fqcn ) ) {
continue;
}
$instance = new $fqcn( [ new \ElementorPro\Core\Isolation\Wordpress_Adapter() ] );
$conditions_manager->register_condition_instance( $instance );
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( 'Display condition: ' . $class_name, $e );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Atomora — failed to register display condition ' . $class_name . ': ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
}
}
/**
* @param \Elementor\Elements_Manager $elements_manager
*/
/**
* Moves the Atomora panel sections directly below "Atomic Form"
* (above Custom Widgets and the rest). Elementor promotes Custom Widgets
* after our categories are registered, so this runs once widgets register.
*/
public function reorder_widget_categories() {
try {
$manager = \Elementor\Plugin::$instance->elements_manager;
$categories = $manager->get_categories();
$ours = [ 'atomora', 'atomora-woocommerce', 'atomora-review' ];
$anchor = null;
foreach ( [ 'atomic-form', 'v4-elements' ] as $candidate ) {
if ( isset( $categories[ $candidate ] ) ) {
$anchor = $candidate;
break;
}
}
if ( null === $anchor ) {
return;
}
$moved = [];
foreach ( $ours as $key ) {
if ( isset( $categories[ $key ] ) ) {
$moved[ $key ] = $categories[ $key ];
unset( $categories[ $key ] );
}
}
$result = [];
foreach ( $categories as $key => $value ) {
$result[ $key ] = $value;
if ( $key === $anchor ) {
$result += $moved;
}
}
$property = new \ReflectionProperty( $manager, 'categories' );
if ( PHP_VERSION_ID < 80100 ) {
$property->setAccessible( true );
}
$property->setValue( $manager, $result );
} catch ( \Throwable $e ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Atomora — category reorder failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
}
public function register_widget_category( $elements_manager ) {
$elements_manager->add_category(
'atomora',
[
'title' => __( 'Atomora', 'atomora' ),
'icon' => 'eicon-atomic',
]
);
// Dedicated section for WooCommerce action widgets (Add to Cart,
// Buy Now, Quantity, Variations) — kept separate from the general
// 'atomora' category (Product Card, Gallery Slider) so they
// group together in the panel. Elementor's category system is a
// flat list (verified — no true nested subsections exist), so
// "Variation" grouping within this is done via a shared
// "Variation: " title prefix rather than a real sub-menu.
$elements_manager->add_category(
'atomora-woocommerce',
[
'title' => __( 'Atomora WooCom', 'atomora' ),
'icon' => 'eicon-woocommerce',
]
);
$elements_manager->add_category(
'atomora-review',
[
'title' => __( 'Atomora Review', 'atomora' ),
'icon' => 'eicon-product-rating',
]
);
}
/**
* Admin warning: the Reviews Form defaults use Elementor Pro's native form elements.
*/
public function pro_form_notice() {
if ( defined( 'ELEMENTOR_PRO_VERSION' ) || ! current_user_can( 'manage_options' ) || ! Compatibility::is_atomic_editor_available() ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || ( 'plugins' !== $screen->id && false === strpos( (string) $screen->id, 'elementor' ) ) ) {
return;
}
echo '
Atomora: '
. esc_html__( 'The Reviews Form uses Elementor Pro\'s native form elements (label, textarea, submit button) by default. Elementor Pro is not active, so the form falls back to Atomora\'s own fields and a standard Button.', 'atomora' )
. '
';
}
/**
* Loads the Reviews elements (container, list, form, item).
*/
private function require_review_elements(): void {
require_once ATOMORA_PATH . 'includes/elements/class-review-item.php';
require_once ATOMORA_PATH . 'includes/elements/class-reviews-list.php';
require_once ATOMORA_PATH . 'includes/elements/class-reviews-form.php';
require_once ATOMORA_PATH . 'includes/elements/class-reviews-container.php';
}
/**
* admin-ajax: Product Card quick view. Widgets register lazily, so the
* widget's own hooks would not exist yet in an admin-ajax request.
*/
public function ajax_quick_view() {
try {
if ( Compatibility::is_atomic_editor_available() && class_exists( '\WooCommerce' ) ) {
$widget = \Elementor\Plugin::$instance->widgets_manager->get_widget_types( 'e-atomora-product-card' );
if ( $widget && method_exists( $widget, 'ajax_quick_view' ) ) {
$widget->ajax_quick_view();
}
}
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( 'Quick view ajax', $e );
}
wp_send_json_error( null, 500 );
}
/**
* admin-ajax: "load more" / numbered pagination of the Reviews List.
*/
public function ajax_load_reviews() {
try {
if ( Compatibility::is_atomic_editor_available() && class_exists( '\WooCommerce' ) ) {
\Elementor\Plugin::$instance->elements_manager->get_element_types();
$this->require_review_elements();
Elements\Reviews_List::ajax_load_reviews();
}
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( 'Reviews list ajax', $e );
}
wp_send_json_error( null, 500 );
}
/**
* Registers Atomora's own nestable atomic elements (containers).
* Widgets (leaf parts) go through register_widgets() instead.
*
* @param \Elementor\Elements_Manager $elements_manager
*/
public function register_elements( $elements_manager ) {
try {
if ( ! Compatibility::is_atomic_editor_available() ) {
return;
}
$this->require_review_elements();
foreach ( [ 'Reviews_Container', 'Reviews_List', 'Reviews_Form', 'Review_Item' ] as $class_name ) {
$fqcn = '\\Atomora\\Elements\\' . $class_name;
if ( class_exists( $fqcn ) ) {
$elements_manager->register_element_type( new $fqcn() );
}
}
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( 'Reviews container registration', $e );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Atomora — Reviews container failed to register: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
}
/**
* @param \Elementor\Widgets_Manager $widgets_manager
*/
public function register_widgets( $widgets_manager ) {
try {
$available = Compatibility::is_atomic_editor_available();
} catch ( \Throwable $e ) {
$available = false;
}
if ( ! $available ) {
return;
}
// Everything below — including the require_once itself — is wrapped
// in one try/catch. Loading a widget file declares a class that
// extends Elementor's Atomic_Widget_Base; if that base class (or
// anything it in turn extends) is broken in this Elementor build,
// PHP throws a catchable \Error the moment the file is parsed, not
// just when we later instantiate it — so the require_once has to
// be inside the try, not just the `new`.
try {
require_once ATOMORA_PATH . 'includes/widgets/class-gallery-slider.php';
if ( class_exists( '\Atomora\Widgets\Gallery_Slider' ) ) {
$widgets_manager->register( new Widgets\Gallery_Slider() );
}
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( 'Gallery Slider registration', $e );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Atomora — Gallery Slider failed to register: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
add_action( 'admin_notices', function () use ( $e ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return;
}
echo 'Atomora: ' .
esc_html__( 'The Gallery Slider widget could not be registered because Elementor\'s beta Atomic Widgets API returned something unexpected. Nothing else on your site was affected.', 'atomora' ) .
'
' . esc_html( $e->getMessage() ) . '
';
} );
}
// Separate try/catch from Gallery Slider above — a problem loading
// one widget should never take the other down with it.
try {
require_once ATOMORA_PATH . 'includes/widgets/class-product-card.php';
if ( class_exists( '\Atomora\Widgets\Product_Card' ) ) {
$widgets_manager->register( new Widgets\Product_Card() );
}
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( 'Product Card registration', $e );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Atomora — Product Card failed to register: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
add_action( 'admin_notices', function () use ( $e ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return;
}
echo 'Atomora: ' .
esc_html__( 'The Product Card widget could not be registered because Elementor\'s beta Atomic Widgets API returned something unexpected. Nothing else on your site was affected.', 'atomora' ) .
'
' . esc_html( $e->getMessage() ) . '
';
} );
}
// Atomora WooCom widgets — 8 fully independent widgets (no
// shared container), each guarded separately so one failing
// never blocks the others.
$atc_widgets = [
'class-atc-quantity-input.php' => 'Atc_Quantity_Input',
'class-atc-quantity-plus.php' => 'Atc_Quantity_Plus',
'class-atc-quantity-minus.php' => 'Atc_Quantity_Minus',
'class-atc-variations.php' => 'Atc_Variations',
'class-atc-variation-buttons.php' => 'Atc_Variation_Buttons',
'class-atc-add-to-cart-button.php' => 'Atc_Add_To_Cart_Button',
'class-atc-buy-now-button.php' => 'Atc_Buy_Now_Button',
'class-atc-product-price.php' => 'Atc_Product_Price',
'class-reviews-summary.php' => 'Reviews_Summary',
'class-review-item-part.php' => 'Review_Item_Part',
'class-review-form-field.php' => 'Review_Form_Field',
'class-review-average.php' => 'Review_Average',
'class-review-stars.php' => 'Review_Stars',
'class-review-count.php' => 'Review_Count',
'class-review-breakdown.php' => 'Review_Breakdown',
];
foreach ( $atc_widgets as $file => $class_name ) {
try {
require_once ATOMORA_PATH . 'includes/widgets/' . $file;
$fqcn = '\\Atomora\\Widgets\\' . $class_name;
if ( class_exists( $fqcn ) ) {
$widgets_manager->register( new $fqcn() );
}
} catch ( \Throwable $e ) {
Admin\Crash_Reporter::instance()->report( $class_name . ' registration', $e );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'Atomora — ' . $class_name . ' failed to register: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
add_action( 'admin_notices', function () use ( $e, $class_name ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return;
}
echo 'Atomora: ' .
esc_html( sprintf(
/* translators: %s: widget class name */
__( 'The %s widget could not be registered because Elementor\'s beta Atomic Widgets API returned something unexpected. Nothing else on your site was affected.', 'atomora' ),
$class_name
) ) .
'
' . esc_html( $e->getMessage() ) . '
';
} );
}
}
}
public static function on_activation() {
// Off by default, per plugin requirements: never delete user data
// unless the site owner explicitly opts in from Settings.
if ( false === get_option( self::OPTION_DELETE_DATA, false ) ) {
add_option( self::OPTION_DELETE_DATA, 'no' );
}
}
public static function on_deactivation() {
// Deactivation never deletes data by itself. Deletion (if the
// admin opted in) only happens on actual uninstall (see
// uninstall.php), which is the correct, WordPress-standard place
// for destructive cleanup.
}
}