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';
}
}
}