Pro handoff. * * PHP hoists top-level function declarations at COMPILE time, so a runtime * early-return cannot prevent a "cannot redeclare" fatal when the free and Pro * builds are both momentarily active. Keeping the whole bootstrap inside the * else branch means the declarations are skipped entirely for the duplicate * copy. */ if ( function_exists( 'addon_boss_fs' ) ) { if ( addon_boss_fs() ) { addon_boss_fs()->set_basename( false, __FILE__ ); } else { add_action( 'admin_notices', static function () { echo '

'; echo esc_html__( 'Two copies of AddOnBoss are active (the free version and AddOnBoss Premium). Please keep only one active on the Plugins screen. Your settings are safe.', 'addon-boss' ); echo '

'; } ); } } else { define( 'ADDON_BOSS_VERSION', '2.10.1' ); define( 'ADDON_BOSS_PATH', plugin_dir_path( __FILE__ ) ); define( 'ADDON_BOSS_URL', plugin_dir_url( __FILE__ ) ); define( 'ADDON_BOSS_FILE', __FILE__ ); // The options-engine version this build ships (see AddOn_Boss_Engine). define( 'ADDON_BOSS_ENGINE_VERSION', '0.4.0' ); /** * Cache-busting asset version: the plugin version in production, the file's * mtime under WP_DEBUG so dev edits are picked up without a version bump. */ function addon_boss_asset_version( string $relative_path ): string { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { $file = ADDON_BOSS_PATH . $relative_path; $mtime = file_exists( $file ) ? filemtime( $file ) : 0; if ( $mtime ) { return ADDON_BOSS_VERSION . '.' . $mtime; } } return ADDON_BOSS_VERSION; } /** * Stand-down guard: does the active BundleBoss still provide the standalone * product-options surface itself? * * BundleBoss 4.15.0 through 4.18.x ships this same surface under its own * bundle_boss_* names. Booting the AddOnBoss engine next to such a build * would double everything: two rendered option panels, two charges, two * admin tabs. bundle_boss_render_standalone_options() exists exactly when * BundleBoss provides that surface (its Pro slot-options module declares * it from bundle_boss_init on plugins_loaded priority 20); a post-split * BundleBoss no longer declares it. The probe deliberately references the * OTHER plugin's function name. */ function addon_boss_bundleboss_provides_surface(): bool { return function_exists( 'bundle_boss_render_standalone_options' ); } /** * Boot the options engine, or stand down while BundleBoss still provides * the surface. Runs on plugins_loaded priority 25: after every plugin's * own init (BundleBoss declares its surface at priority 20), and before * any of the engine's admin or storefront hooks can fire, so the decision * is made once, at the only safe moment, for the whole request. */ function addon_boss_boot_engine(): void { if ( addon_boss_bundleboss_provides_surface() ) { add_action( 'admin_notices', 'addon_boss_stand_down_notice' ); return; } require_once ADDON_BOSS_PATH . 'shared/options-engine/engine.php'; } add_action( 'plugins_loaded', 'addon_boss_boot_engine', 25 ); /** * One dismissible notice while standing down, pointing at the fix. */ function addon_boss_stand_down_notice(): void { echo '

'; echo esc_html__( 'AddOnBoss is waiting: your BundleBoss version still provides product options itself. Update BundleBoss to hand these features to AddOnBoss.', 'addon-boss' ); echo '

'; } /** * Leave a marker at the moment of activation so the next admin screens can * point at the AddOnBoss landing (see includes/onboarding.php). add_option, * never update: once the pointer was dismissed or has done its job, a * deactivate and reactivate must not resurrect it. No redirect is involved * anywhere; the merchant stays exactly where WordPress put them. */ function addon_boss_note_activation(): void { add_option( 'addon_boss_activation_notice', '1', '', false ); } register_activation_hook( __FILE__, 'addon_boss_note_activation' ); /** * Our own translation file wins for every locale it carries. * * WordPress looks in wp-content/languages/plugins before a plugin's own * folder, and a wordpress.org language pack holds only the free build's * strings. Once a pack exists for a locale, every Pro string in it would * fall back to English while our file, one folder away, translates it. So * for each locale we ship, the domain reads our languages folder; any other * locale is left to the pack, the only translation there is. Hooked at file * scope so it is in place before anything can load the domain. * * @param string|false $path The folder WordPress chose for this domain and locale. * @param string $domain Text domain. * @param string $locale Locale. * @return string|false */ function addon_boss_own_language_dir( $path, $domain, $locale ) { if ( 'addon-boss' !== $domain || ! is_string( $locale ) || '' === $locale ) { return $path; } $own = ADDON_BOSS_PATH . 'languages/'; if ( file_exists( $own . 'addon-boss-' . $locale . '.mo' ) || file_exists( $own . 'addon-boss-' . $locale . '.l10n.php' ) ) { return $own; } return $path; } add_filter( 'lang_dir_for_domain', 'addon_boss_own_language_dir', 10, 3 ); // Licensing (and the Freemius SDK behind it) initialises at include time. require ADDON_BOSS_PATH . 'includes/licensing.php'; addon_boss_fs(); // Declare WooCommerce compatibility. add_action( 'before_woocommerce_init', function () { if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); } } ); // Feature init AFTER the engine boot at priority 25. add_action( 'plugins_loaded', 'addon_boss_init', 30 ); /** * Without WooCommerce: what is missing and the one step that fixes it, for * someone who can take it. Installed but off: Activate WooCommerce. Not * installed: Install WooCommerce. Never on the front of the shop. */ function addon_boss_needs_woocommerce_notice() { if ( ! current_user_can( 'activate_plugins' ) ) { return; } if ( isset( $_GET['page'] ) && 'addon-boss' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which screen this is, nothing acted on. return; // The page below says it already. } echo '
' . addon_boss_needs_woocommerce_html() . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped where built. } /** * AddOnBoss's own address still answers without WooCommerce. Freemius's * "Complete activation" link, a bookmark or an old tab all lead to * admin.php?page=addon-boss; with no page there WordPress said "Sorry, you * are not allowed to access this page". A page outside the menu now says * what is missing instead. */ function addon_boss_needs_woocommerce_menu() { $hook = add_submenu_page( '', 'AddOnBoss', 'AddOnBoss', 'activate_plugins', 'addon-boss', function () { echo '

AddOnBoss

' . addon_boss_needs_woocommerce_html() . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped where built. } ); // A page outside the menu gets no title of its own: the tab read "‹ Site". add_action( 'load-' . $hook, function () { $GLOBALS['title'] = 'AddOnBoss'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- the admin header reads it. } ); } /** * The sentence and its button, escaped. * * @return string */ function addon_boss_needs_woocommerce_html() { $file = 'woocommerce/woocommerce.php'; $link = ''; if ( file_exists( WP_PLUGIN_DIR . '/' . $file ) ) { $link = sprintf( '%s', esc_url( wp_nonce_url( self_admin_url( 'plugins.php?action=activate&plugin=' . rawurlencode( $file ) ), 'activate-plugin_' . $file ) ), esc_html__( 'Activate WooCommerce', 'addon-boss' ) ); } elseif ( current_user_can( 'install_plugins' ) ) { $link = sprintf( '%s', esc_url( wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=woocommerce' ), 'install-plugin_woocommerce' ) ), esc_html__( 'Install WooCommerce', 'addon-boss' ) ); } return '

' . esc_html__( 'AddOnBoss works inside WooCommerce, so it needs WooCommerce installed and active.', 'addon-boss' ) . '

' . ( '' !== $link ? '

' . $link . '

' : '' ); } function addon_boss_init() { if ( ! class_exists( 'WooCommerce' ) ) { add_action( 'admin_notices', 'addon_boss_needs_woocommerce_notice' ); add_action( 'admin_menu', 'addon_boss_needs_woocommerce_menu' ); return; } // Translations: the files under /languages carry every string, Pro ones // included, and addon_boss_own_language_dir() puts them ahead of a // wp.org language pack for each locale they cover. A pack (free strings // only, in WP_LANG_DIR) still serves any other locale. This explicit // registration is what makes the folder known at all. Registered before // the stand-down check below, because a locale is not a feature. // On init, not earlier: WP 6.7 flags any translation loaded before init. add_action( 'init', function () { load_plugin_textdomain( 'addon-boss', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); } ); // The engine stood down because BundleBoss still provides the surface // (see addon_boss_boot_engine). Every AddOnBoss feature lives on the // engine, so there is nothing to init; the stand-down notice is already // queued and BundleBoss keeps working unchanged. if ( ! class_exists( 'AddOn_Boss_Engine', false ) ) { return; } require ADDON_BOSS_PATH . 'includes/admin-menu.php'; require ADDON_BOSS_PATH . 'includes/frontend.php'; require ADDON_BOSS_PATH . 'includes/onboarding.php'; // Loaded here, inside the engine-present branch, because the review // clock reads the engine's product-options meta constant. Its paid-build // silencer is a separate module that Freemius strips from the free // build, so this asks whether the file is there, never what the plan is. require ADDON_BOSS_PATH . 'includes/review-prompt.php'; if ( file_exists( ADDON_BOSS_PATH . 'includes/review-prompt__premium_only.php' ) ) { require ADDON_BOSS_PATH . 'includes/review-prompt__premium_only.php'; } } }