| 1 |
<?php |
| 2 |
|
| 3 |
namespace YayMailScoped\YayCommerce\AdminShell\Registry; |
| 4 |
|
| 5 |
use YayMailScoped\YayCommerce\AdminShell\License\License; |
| 6 |
use YayMailScoped\YayCommerce\AdminShell\License\RestAPI; |
| 7 |
use YayMailScoped\YayCommerce\AdminShell\Support\Slug; |
| 8 |
/** |
| 9 |
* Reads a plugin-specific addon licensing filter and bridges entries |
| 10 |
* into the LicenseRegistry. Renders license cards for addon entries. |
| 11 |
* |
| 12 |
* Similar to LegacyBridge but for plugin-specific addon filters |
| 13 |
* (e.g., yaymail_available_licensing_plugins) rather than the global |
| 14 |
* yaycommerce_licensing_plugins filter. |
| 15 |
* |
| 16 |
* Pattern mirrors LegacyBridge — derive_status() logic intentionally |
| 17 |
* duplicated (only two consumers; extract if a third appears). |
| 18 |
*/ |
| 19 |
class AddonBridge |
| 20 |
{ |
| 21 |
private string $filter_name; |
| 22 |
private LicenseRegistry $registry; |
| 23 |
/** @var string[] Slugs this bridge registered (for rendering). */ |
| 24 |
private array $addon_slugs = []; |
| 25 |
/** @var array<string, array> Raw addon data keyed by slug (for rendering). */ |
| 26 |
private array $addon_data = []; |
| 27 |
public function __construct(string $filter_name, LicenseRegistry $registry) |
| 28 |
{ |
| 29 |
$this->filter_name = $filter_name; |
| 30 |
$this->registry = $registry; |
| 31 |
} |
| 32 |
/** |
| 33 |
* Hook into plugins_loaded at priority 9999 (same as LegacyBridge) |
| 34 |
* and yaycommerce_licenses_page at priority 100 for card rendering. |
| 35 |
*/ |
| 36 |
public function init() : void |
| 37 |
{ |
| 38 |
\add_action('plugins_loaded', [$this, 'load_addon_plugins'], 9999); |
| 39 |
if (\is_admin()) { |
| 40 |
\add_action('yaycommerce_licenses_page', [$this, 'render_addon_cards'], 100); |
| 41 |
\add_action('admin_enqueue_scripts', [$this, 'enqueue_addon_scripts']); |
| 42 |
} |
| 43 |
} |
| 44 |
/** |
| 45 |
* Read addon filter and convert entries to PluginLicenseInfo stubs. |
| 46 |
* Addon format matches yaycommerce_licensing_plugins: |
| 47 |
* [ 'slug', 'name', 'basename', 'file', 'url', 'item_id' ] |
| 48 |
*/ |
| 49 |
public function load_addon_plugins() : void |
| 50 |
{ |
| 51 |
$addons = \apply_filters($this->filter_name, []); |
| 52 |
foreach ($addons as $plugin) { |
| 53 |
$slug = $plugin['slug'] ?? ''; |
| 54 |
if (empty($slug) || $this->registry->has($slug)) { |
| 55 |
continue; |
| 56 |
} |
| 57 |
$info = $this->build_addon_info($plugin); |
| 58 |
$this->registry->register($info); |
| 59 |
$this->addon_slugs[] = $slug; |
| 60 |
$this->addon_data[$slug] = $plugin; |
| 61 |
// Register REST routes so license card buttons (activate/update/deactivate) work. |
| 62 |
new RestAPI($this->build_addon_adapter($plugin)); |
| 63 |
} |
| 64 |
} |
| 65 |
/** |
| 66 |
* Build a PluginLicenseInfo from an addon filter entry. |
| 67 |
* Reads {slug}_license_info from wp_options for current status. |
| 68 |
* Pattern mirrors LegacyBridge::build_legacy_info(). |
| 69 |
*/ |
| 70 |
private function build_addon_info(array $plugin) : PluginLicenseInfo |
| 71 |
{ |
| 72 |
$slug = $plugin['slug'] ?? ''; |
| 73 |
$raw_info = \get_option($slug . '_license_info', []); |
| 74 |
if (\is_string($raw_info)) { |
| 75 |
$raw_info = \json_decode($raw_info, \true) ?: []; |
| 76 |
} |
| 77 |
$license_key = (string) \get_option($slug . '_license_key', ''); |
| 78 |
$expires_raw = $raw_info['expires'] ?? null; |
| 79 |
$status = $this->derive_status($license_key, $raw_info); |
| 80 |
$info = new PluginLicenseInfo(); |
| 81 |
$info->slug = $slug; |
| 82 |
$info->name = $plugin['name'] ?? $slug; |
| 83 |
$info->version = ''; |
| 84 |
$info->basename = $plugin['basename'] ?? ''; |
| 85 |
$info->item_id = (int) ($plugin['item_id'] ?? 0); |
| 86 |
$info->store_link = $plugin['url'] ?? ''; |
| 87 |
$info->license_key = $license_key; |
| 88 |
$info->status = $status; |
| 89 |
$info->expires_at = $expires_raw && 'Not updated' !== $expires_raw ? $expires_raw : null; |
| 90 |
$info->activations_used = (int) ($raw_info['site_count'] ?? 0); |
| 91 |
$info->activations_limit = (int) ($raw_info['license_limit'] ?? 0); |
| 92 |
$info->is_legacy = \true; |
| 93 |
$info->raw_info = $raw_info; |
| 94 |
return $info; |
| 95 |
} |
| 96 |
/** |
| 97 |
* Derive normalized status from wp_options data. |
| 98 |
* Mirrors LegacyBridge::derive_status(). |
| 99 |
*/ |
| 100 |
private function derive_status(string $license_key, array $raw_info) : string |
| 101 |
{ |
| 102 |
if (empty($license_key)) { |
| 103 |
return 'inactive'; |
| 104 |
} |
| 105 |
$expires = $raw_info['expires'] ?? null; |
| 106 |
if ($expires && 'lifetime' !== $expires && 'Not updated' !== $expires) { |
| 107 |
if (\strtotime($expires) < \time()) { |
| 108 |
return 'expired'; |
| 109 |
} |
| 110 |
} |
| 111 |
return 'valid'; |
| 112 |
} |
| 113 |
/** |
| 114 |
* Render license cards for addon entries this bridge registered. |
| 115 |
* Builds a minimal LicenseConfigAdapter per addon so the License |
| 116 |
* class and card templates work without modification. |
| 117 |
*/ |
| 118 |
public function render_addon_cards() : void |
| 119 |
{ |
| 120 |
foreach ($this->addon_slugs as $slug) { |
| 121 |
$plugin_data = $this->addon_data[$slug] ?? []; |
| 122 |
if (empty($plugin_data)) { |
| 123 |
continue; |
| 124 |
} |
| 125 |
$adapter = $this->build_addon_adapter($plugin_data); |
| 126 |
$license = new License($adapter); |
| 127 |
$_plugin = ['slug' => $slug, 'name' => $plugin_data['name'] ?? $slug]; |
| 128 |
if ($license->is_active()) { |
| 129 |
include __DIR__ . '/../../views/license-card.php'; |
| 130 |
} else { |
| 131 |
include __DIR__ . '/../../views/license-activate-card.php'; |
| 132 |
} |
| 133 |
} |
| 134 |
} |
| 135 |
/** |
| 136 |
* Enqueue license.js and localize data for each addon slug. |
| 137 |
* Mirrors LicenseHandler::enqueue_license_scripts(). |
| 138 |
* |
| 139 |
* NOTE: Addon must register REST routes at {slug}/v1/license/* |
| 140 |
* for the activate/update/deactivate buttons to work. |
| 141 |
*/ |
| 142 |
public function enqueue_addon_scripts() : void |
| 143 |
{ |
| 144 |
if (!isset($_GET['page']) || 'yaycommerce-licenses' !== $_GET['page']) { |
| 145 |
// phpcs:ignore WordPress.Security.NonceVerification |
| 146 |
return; |
| 147 |
} |
| 148 |
if (empty($this->addon_slugs)) { |
| 149 |
return; |
| 150 |
} |
| 151 |
$assets_url = \plugin_dir_url(__FILE__) . '../../assets/'; |
| 152 |
\wp_enqueue_script('yaycommerce-license', $assets_url . 'js/license.js', ['jquery'], '1.0', \true); |
| 153 |
foreach ($this->addon_slugs as $slug) { |
| 154 |
\wp_localize_script('yaycommerce-license', Slug::to_var_name($slug) . 'LicenseData', ['slug' => $slug, 'apiSettings' => ['restNonce' => \wp_create_nonce('wp_rest'), 'restUrl' => \esc_url_raw(\rest_url(Slug::to_var_name($slug) . '/v1')), 'adminUrl' => \admin_url()]]); |
| 155 |
} |
| 156 |
} |
| 157 |
/** |
| 158 |
* Build a minimal LicenseConfigAdapter from addon filter data. |
| 159 |
* Used by License constructor + card templates. |
| 160 |
*/ |
| 161 |
private function build_addon_adapter(array $plugin_data) : AddonLicenseAdapter |
| 162 |
{ |
| 163 |
return new AddonLicenseAdapter($plugin_data); |
| 164 |
} |
| 165 |
} |
| 166 |
|