/.php` is * all it takes to make a new addon discoverable. */ public static function shouldLoad(string $addonId): bool { if (!self::isEnabled($addonId)) return false; if (!self::userCanAccess($addonId)) return false; $addon = AddonRegistry::find($addonId); if (!$addon) return false; if (empty($addon['pro_class'])) return false; if (!defined('EASY_INVOICE_PRO_PLUGIN_DIR'))return false; // class_exists triggers the composer autoloader; if the class // file is missing or unmapped this returns false. return class_exists($addon['pro_class']); } /** * Enable an addon. Refuses if license tier is insufficient. * * @return array{success:bool, code:string, message:string, required_plan?:string} */ public static function enable(string $addonId): array { $addon = AddonRegistry::find($addonId); if (!$addon) { return [ 'success' => false, 'code' => 'unknown_addon', 'message' => __('That addon does not exist.', 'easy-invoice'), ]; } if (!self::userCanAccess($addonId)) { return [ 'success' => false, 'code' => 'upgrade_required', 'message' => sprintf( /* translators: %s = required plan name */ __('This addon requires the %s plan.', 'easy-invoice'), LicensePlanResolver::getPlanLabel($addon['plan']) ), 'required_plan' => $addon['plan'], ]; } update_option(self::optionKey($addonId), 1); do_action('easy_invoice_addon_enabled', $addonId, $addon); return [ 'success' => true, 'code' => 'enabled', 'message' => __('Addon enabled.', 'easy-invoice'), ]; } /** * Disable an addon. Always allowed — disabling is never blocked. * * @return array{success:bool, code:string, message:string} */ public static function disable(string $addonId): array { $addon = AddonRegistry::find($addonId); if (!$addon) { return [ 'success' => false, 'code' => 'unknown_addon', 'message' => __('That addon does not exist.', 'easy-invoice'), ]; } update_option(self::optionKey($addonId), 0); do_action('easy_invoice_addon_disabled', $addonId, $addon); return [ 'success' => true, 'code' => 'disabled', 'message' => __('Addon disabled.', 'easy-invoice'), ]; } /** * Symbolic status for one addon — drives the card button state in the UI. * * @return string one of: 'active' | 'available' | 'locked' */ public static function statusFor(string $addonId): string { if (!self::userCanAccess($addonId)) { return 'locked'; } return self::isEnabled($addonId) ? 'active' : 'available'; } /** * Pro plugin entrypoint: bootstrap every addon that passes * shouldLoad(). Drops a `/.php` into `addons/` with * the right namespace and PSR-4 + AddonManager take it from * there — no require_once, no addon.php, no composer.json change. * * Convention: the entry class exposes a static `bootstrap()` that * registers hooks / instantiates sub-controllers. Classes without * a bootstrap method are still considered "loaded" (the autoload * itself is enough — useful for marker-only addons). */ public static function loadEnabledAddons(): void { foreach (AddonRegistry::all() as $addon) { if (!self::shouldLoad($addon['id'])) { continue; } $class = $addon['pro_class']; if (method_exists($class, 'bootstrap')) { call_user_func([$class, 'bootstrap']); } do_action('easy_invoice_addon_loaded', $addon['id'], $addon); } } }