Phase 11D: Blueprint license activation, phone-home, and tier gating

- LicenseService.php: activate/validate/deactivate + 7-day grace period
- ValidateLicense.php: mvc:validate Artisan command (daily cron)
- Updated controller.php: license activation/deactivation in update handler
- Updated view.blade.php: order ID input, status indicator (green/yellow/red),
  grace/expired banners, dynamic pro-tier field gating, update-available card

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude (Chronicler #83 - The Compiler)
2026-04-12 20:36:55 -05:00
parent 7a2a1d8dbd
commit 8872f67727
6 changed files with 444 additions and 37 deletions

View File

@@ -0,0 +1,35 @@
# Architectural Response
**Re:** Phase 11B/C Deployment Complete
**Date:** 2026-04-12
**From:** Chronicler #84 — The Meridian
---
## Status: DEPLOYED ✅
### Discord Role Created
- Name: **ModpackChecker Customer**
- Color: Frost teal (#1A9ECF)
- ID: `1493061127423262870`
### Env Var Added
```
MVC_CUSTOMER_ROLE_ID=1493061127423262870
```
### Code Deployed
- `src/discord/verifymvc.js`
- `src/discord/events.js`
- `src/index.js`
### Verification
- Health: `{"status":"online","bot":"The Arbiter#6636"}`
- Logs: `✅ Successfully reloaded application (/) commands`
- `/verify-mvc` is live in Discord
---
Proceed to Phase 11D.
*— Chronicler #84, The Meridian*

View File

@@ -1,19 +1,19 @@
# Code Status Update # Code Status Update
**Last Updated:** 2026-04-12 20:45 CDT **Last Updated:** 2026-04-12 21:15 CDT
## Current Focus ## Current Focus
Phase 11B/C complete — /verify-mvc slash command written. Ready for Chronicler deployment + role creation. Phase 11D complete — Blueprint extension licensing integration written. Ready for testing.
## Recently Completed ## Recently Completed
- Phase 11A: DEPLOYED ✅ — migration ran, API routes live, health check passed - Phase 11A: DEPLOYED ✅ — DB + API routes live on Command Center
- Phase 11B/C: Created `src/discord/verifymvc.js` — /verify-mvc slash command - Phase 11B/C: DEPLOYED ✅ — /verify-mvc live, customer role created (1493061127423262870)
- Phase 11B/C: Wired into events.js handler + index.js command registration - Phase 11D: Created LicenseService.php — activate, validate, deactivate, grace period, tier check
- Command assigns MVC_CUSTOMER_ROLE_ID role on successful verification - Phase 11D: Created ValidateLicense.php — `mvc:validate` Artisan command (daily phone-home)
- Phase 11D: Updated admin controller.php — license activation/deactivation in update handler
- Phase 11D: Updated admin view.blade.php — order ID input, status indicator, grace/expired banners, dynamic tier gating
## Next Steps Pending ## Next Steps Pending
- **DEPLOY: Chronicler restarts Arbiter + creates ModpackChecker Customer role on Discord** - Phase 11D testing: build .blueprint, install, verify license flow end-to-end
- **Chronicler needs to:** set MVC_CUSTOMER_ROLE_ID in .env after creating role
- Phase 11D: Blueprint extension — license activation UI, phone-home cron, tier gating
- Phase 11E: GitBook knowledge base migration - Phase 11E: GitBook knowledge base migration
- Phase 11F: BuiltByBit listing creation (Standard $14.99, Professional $24.99) - Phase 11F: BuiltByBit listing creation (Standard $14.99, Professional $24.99)
- Phase 11G: Business hours & support boundaries - Phase 11G: Business hours & support boundaries

View File

@@ -7,6 +7,7 @@ use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary; use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest; use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
use Pterodactyl\Services\LicenseService;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
class modpackcheckerExtensionController extends Controller class modpackcheckerExtensionController extends Controller
@@ -14,54 +15,70 @@ class modpackcheckerExtensionController extends Controller
public function __construct( public function __construct(
private ViewFactory $view, private ViewFactory $view,
private BlueprintExtensionLibrary $blueprint, private BlueprintExtensionLibrary $blueprint,
private LicenseService $licenseService,
) {} ) {}
public function index(): View public function index(): View
{ {
// Get current settings
$curseforge_api_key = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key'); $curseforge_api_key = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
$discord_webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url'); $discord_webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url');
$check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval'); $check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval');
$tier = $this->blueprint->dbGet('modpackchecker', 'tier');
// Set defaults if empty
if ($check_interval == '') { if ($check_interval == '') {
$this->blueprint->dbSet('modpackchecker', 'check_interval', 'daily'); $this->blueprint->dbSet('modpackchecker', 'check_interval', 'daily');
$check_interval = 'daily'; $check_interval = 'daily';
} }
if ($tier == '') {
$this->blueprint->dbSet('modpackchecker', 'tier', 'standard'); $license = $this->licenseService->getState();
$tier = 'standard';
}
return $this->view->make( return $this->view->make(
'admin.extensions.modpackchecker.index', [ 'admin.extensions.modpackchecker.index', [
'curseforge_api_key' => $curseforge_api_key, 'curseforge_api_key' => $curseforge_api_key,
'discord_webhook_url' => $discord_webhook_url, 'discord_webhook_url' => $discord_webhook_url,
'check_interval' => $check_interval, 'check_interval' => $check_interval,
'tier' => $tier, 'license' => $license,
'root' => '/admin/extensions/modpackchecker', 'root' => '/admin/extensions/modpackchecker',
'blueprint' => $this->blueprint, 'blueprint' => $this->blueprint,
] ]
); );
} }
/**
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse
{ {
// Handle license activation
$orderId = $request->input('order_id');
if (!empty($orderId)) {
$currentOrderId = $this->blueprint->dbGet('modpackchecker', 'order_id');
if ($orderId !== $currentOrderId) {
$result = $this->licenseService->activate($orderId);
if (!$result['success']) {
return redirect()
->route('admin.extensions.modpackchecker.index')
->with('error', 'License activation failed: ' . ($result['error'] ?? 'Unknown error'));
}
}
}
// Handle license deactivation
if ($request->input('deactivate_license') === '1') {
$this->licenseService->deactivate();
return redirect()
->route('admin.extensions.modpackchecker.index')
->with('success', 'License deactivated.');
}
// Save standard settings
$this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? ''); $this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? '');
// Only save PRO-tier fields if the user is on the pro tier // Only save PRO-tier fields if licensed as professional
$tier = $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard'; if ($this->licenseService->isProFeatureAllowed()) {
if ($tier === 'pro') {
$this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? ''); $this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? '');
$this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily'); $this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily');
} }
return redirect()->route('admin.extensions.modpackchecker.index')->with('success', 'Settings saved successfully.'); return redirect()
->route('admin.extensions.modpackchecker.index')
->with('success', 'Settings saved successfully.');
} }
} }
@@ -70,6 +87,8 @@ class modpackcheckerSettingsFormRequest extends AdminFormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'order_id' => 'nullable|string|max:64',
'deactivate_license' => 'nullable|string',
'curseforge_api_key' => 'nullable|string|max:500', 'curseforge_api_key' => 'nullable|string|max:500',
'discord_webhook_url' => 'nullable|url|max:500', 'discord_webhook_url' => 'nullable|url|max:500',
'check_interval' => 'nullable|in:daily,12h,6h', 'check_interval' => 'nullable|in:daily,12h,6h',
@@ -79,6 +98,7 @@ class modpackcheckerSettingsFormRequest extends AdminFormRequest
public function attributes(): array public function attributes(): array
{ {
return [ return [
'order_id' => 'Order ID',
'curseforge_api_key' => 'CurseForge API Key', 'curseforge_api_key' => 'CurseForge API Key',
'discord_webhook_url' => 'Discord Webhook URL', 'discord_webhook_url' => 'Discord Webhook URL',
'check_interval' => 'Check Interval', 'check_interval' => 'Check Interval',

View File

@@ -61,7 +61,81 @@
</div> </div>
</div> </div>
<!-- License Status Banner -->
@if($license['status'] === 'expired')
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-12">
<div style="background: #4a1a1a; border-left: 4px solid #e74c3c; border-radius: 4px; padding: 15px;">
<h4 style="margin: 0 0 5px 0; color: #e74c3c;"><i class="fa fa-exclamation-triangle"></i> License Expired</h4>
<p style="margin: 0; color: #ccc;">Your license could not be validated. Pro features are disabled. Please check your order ID or contact support.</p>
</div>
</div>
</div>
@elseif($license['status'] === 'grace')
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-12">
<div style="background: #4a3a1a; border-left: 4px solid #f39c12; border-radius: 4px; padding: 15px;">
<h4 style="margin: 0 0 5px 0; color: #f39c12;"><i class="fa fa-clock-o"></i> License Validation Warning</h4>
<p style="margin: 0; color: #ccc;">Could not reach license server. Grace period expires: <strong>{{ $license['grace_expires'] }}</strong></p>
</div>
</div>
</div>
@endif
<!-- License Activation -->
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-6">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-id-card"></i> License
@if($license['status'] === 'active')
<span class="label label-success" style="margin-left: 10px;">Active</span>
@elseif($license['status'] === 'grace')
<span class="label label-warning" style="margin-left: 10px;">Grace Period</span>
@elseif($license['status'] === 'expired')
<span class="label label-danger" style="margin-left: 10px;">Expired</span>
@else
<span class="label label-default" style="margin-left: 10px;">Not Activated</span>
@endif
</h3>
</div>
<div class="box-body">
<div class="form-group">
<label class="control-label">BuiltByBit Order ID</label>
<input
type="text"
name="order_id"
id="order_id"
value="{{ $license['order_id'] }}"
placeholder="Enter your order ID..."
class="form-control"
/>
<p class="text-muted small" style="margin-top: 8px;">
Enter the order ID from your BuiltByBit purchase to activate.
</p>
</div>
@if($license['status'] === 'active' || $license['status'] === 'grace')
<div style="margin-top: 8px; padding: 10px; background: #2a2a3e; border-radius: 4px;">
<p style="margin: 0; color: #aaa; font-size: 12px;">
<strong>Tier:</strong> {{ ucfirst($license['tier']) }}
&nbsp;|&nbsp;
<strong>Last Validated:</strong> {{ $license['last_validated'] ?: 'Never' }}
</p>
</div>
@endif
@if(!empty($license['order_id']))
<div style="margin-top: 10px;">
<button type="submit" name="deactivate_license" value="1" class="btn btn-danger btn-xs"
onclick="return confirm('Are you sure you want to deactivate? This frees the activation slot.')">
Deactivate License
</button>
</div>
@endif
</div>
</div>
</div>
<!-- CurseForge API Key --> <!-- CurseForge API Key -->
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<div class="box box-primary"> <div class="box box-primary">
@@ -91,7 +165,9 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="row">
<!-- Check Interval (PRO TIER) --> <!-- Check Interval (PRO TIER) -->
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<div class="box box-info"> <div class="box box-info">
@@ -104,22 +180,25 @@
<div class="box-body"> <div class="box-body">
<div class="form-group"> <div class="form-group">
<label class="control-label">Automatic Check Frequency</label> <label class="control-label">Automatic Check Frequency</label>
<select class="form-control" name="check_interval" id="check_interval" disabled> <select class="form-control" name="check_interval" id="check_interval"
{{ $license['tier'] !== 'professional' || $license['status'] === 'expired' ? 'disabled' : '' }}>
<option value="daily" {{ $check_interval === 'daily' ? 'selected' : '' }}>Daily (24 Hours)</option> <option value="daily" {{ $check_interval === 'daily' ? 'selected' : '' }}>Daily (24 Hours)</option>
<option value="12h" {{ $check_interval === '12h' ? 'selected' : '' }}>Every 12 Hours</option> <option value="12h" {{ $check_interval === '12h' ? 'selected' : '' }}>Every 12 Hours</option>
<option value="6h" {{ $check_interval === '6h' ? 'selected' : '' }}>Every 6 Hours</option> <option value="6h" {{ $check_interval === '6h' ? 'selected' : '' }}>Every 6 Hours</option>
</select> </select>
<p class="text-muted small" style="margin-top: 8px;"> <p class="text-muted small" style="margin-top: 8px;">
Standard tier is locked to daily cron checks. @if($license['tier'] === 'professional' && $license['status'] !== 'expired')
Upgrade to Professional for more frequent automated checks. Professional tier custom check intervals enabled.
@else
Standard tier is locked to daily checks.
Upgrade to Professional for more frequent automated checks.
@endif
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="row">
<!-- Discord Webhook (PRO TIER) --> <!-- Discord Webhook (PRO TIER) -->
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<div class="box box-success"> <div class="box box-success">
@@ -139,16 +218,22 @@
value="{{ $discord_webhook_url }}" value="{{ $discord_webhook_url }}"
placeholder="https://discord.com/api/webhooks/..." placeholder="https://discord.com/api/webhooks/..."
class="form-control" class="form-control"
disabled {{ $license['tier'] !== 'professional' || $license['status'] === 'expired' ? 'disabled' : '' }}
/> />
<p class="text-muted small" style="margin-top: 8px;"> <p class="text-muted small" style="margin-top: 8px;">
Upgrade to Professional to receive automated update alerts in your Discord server. @if($license['tier'] === 'professional' && $license['status'] !== 'expired')
Professional tier Discord webhook alerts enabled.
@else
Upgrade to Professional to receive automated update alerts in your Discord server.
@endif
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="row">
<!-- Supported Platforms --> <!-- Supported Platforms -->
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<div class="box box-default"> <div class="box box-default">
@@ -177,12 +262,37 @@
<li style="padding: 8px 0;"> <li style="padding: 8px 0;">
<i class="fa fa-cube" style="color: #e04e39; width: 20px;"></i> <i class="fa fa-cube" style="color: #e04e39; width: 20px;"></i>
<strong>FTB (modpacks.ch)</strong> <strong>FTB (modpacks.ch)</strong>
<span class="text-muted small">(No key required)</span> <span class="text-muted small">
@if($license['tier'] === 'professional')
(Enabled Professional)
@else
(PRO TIER only)
@endif
</span>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<!-- Update Available -->
@if(!empty($license['latest_version']) && $license['latest_version'] !== '1.0.0')
<div class="col-xs-12 col-md-6">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-download"></i> Update Available
</h3>
</div>
<div class="box-body">
<p>A new version (<strong>{{ $license['latest_version'] }}</strong>) is available.</p>
<a href="https://builtbybit.com/" target="_blank" class="btn btn-warning btn-sm">
Download from BuiltByBit
</a>
</div>
</div>
</div>
@endif
</div> </div>
<!-- Footer Info --> <!-- Footer Info -->

View File

@@ -0,0 +1,47 @@
<?php
/**
* MVC License Validation — daily phone-home cron command.
*
* USAGE:
* php artisan mvc:validate
*
* CRON SCHEDULE:
* 0 4 * * * cd /var/www/pterodactyl && php artisan mvc:validate >> /dev/null 2>&1
*
* Calls Arbiter /api/mvc/validate to confirm license is still active.
* On failure, enters 7-day grace period. After grace expires, marks expired.
*/
namespace Pterodactyl\Console\Commands;
use Illuminate\Console\Command;
use Pterodactyl\Services\LicenseService;
class ValidateLicense extends Command
{
protected $signature = 'mvc:validate';
protected $description = 'Validate ModpackChecker license (daily phone-home)';
public function __construct(private LicenseService $licenseService)
{
parent::__construct();
}
public function handle(): int
{
$this->info('Validating ModpackChecker license...');
$result = $this->licenseService->validate();
match ($result['status']) {
'active' => $this->info("✅ License active — tier: {$result['tier']}"),
'grace' => $this->warn("⚠️ Grace period — expires: {$result['expires']}"),
'expired' => $this->error("❌ License expired"),
'inactive' => $this->line("No license configured"),
default => $this->line("Status: {$result['status']}"),
};
return 0;
}
}

View File

@@ -0,0 +1,195 @@
<?php
/**
* MVC License Service — handles activation, validation, and grace period.
*
* Talks to Arbiter API at /api/mvc/* endpoints.
* Stores license state in Blueprint dbGet/dbSet.
*
* Keys stored:
* modpackchecker.order_id — BuiltByBit order ID
* modpackchecker.license_status — active|grace|expired|inactive
* modpackchecker.tier — standard|professional
* modpackchecker.grace_expires — ISO timestamp (set on first validation failure)
* modpackchecker.last_validated — ISO timestamp of last successful validation
* modpackchecker.latest_version — latest version from Arbiter
*/
namespace Pterodactyl\Services;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class LicenseService
{
private string $arbiterBase = 'https://discord-bot.firefrostgaming.com/api/mvc';
private int $graceDays = 7;
public function __construct(
private BlueprintExtensionLibrary $blueprint
) {}
/**
* Activate a license by order ID.
* Called from admin UI "Save & Activate" button.
*/
public function activate(string $orderId): array
{
$domain = config('app.url');
try {
$response = Http::timeout(15)->post("{$this->arbiterBase}/activate", [
'order_id' => $orderId,
'domain' => $domain,
'ip' => request()->server('SERVER_ADDR', ''),
]);
$data = $response->json();
if ($response->successful() && ($data['status'] ?? '') === 'active') {
$this->blueprint->dbSet('modpackchecker', 'order_id', $orderId);
$this->blueprint->dbSet('modpackchecker', 'license_status', 'active');
$this->blueprint->dbSet('modpackchecker', 'tier', $data['tier'] ?? 'standard');
$this->blueprint->dbSet('modpackchecker', 'last_validated', now()->toISOString());
$this->blueprint->dbSet('modpackchecker', 'grace_expires', '');
return ['success' => true, 'tier' => $data['tier'] ?? 'standard'];
}
return ['success' => false, 'error' => $data['error'] ?? 'Activation failed'];
} catch (\Exception $e) {
Log::error('[MVC License] Activation error: ' . $e->getMessage());
return ['success' => false, 'error' => 'Could not reach license server. Try again later.'];
}
}
/**
* Validate (phone-home). Called daily by mvc:validate Artisan command.
* On failure, enters grace period. After 7 days, marks expired.
*/
public function validate(): array
{
$orderId = $this->blueprint->dbGet('modpackchecker', 'order_id');
if (empty($orderId)) {
return ['status' => 'inactive', 'message' => 'No license configured'];
}
$domain = config('app.url');
try {
$response = Http::timeout(15)->post("{$this->arbiterBase}/validate", [
'order_id' => $orderId,
'domain' => $domain,
'version' => config('app.version', '1.0.0'),
'php_version' => PHP_VERSION,
]);
$data = $response->json();
if ($response->successful() && ($data['status'] ?? '') === 'active') {
$this->blueprint->dbSet('modpackchecker', 'license_status', 'active');
$this->blueprint->dbSet('modpackchecker', 'tier', $data['tier'] ?? 'standard');
$this->blueprint->dbSet('modpackchecker', 'last_validated', now()->toISOString());
$this->blueprint->dbSet('modpackchecker', 'grace_expires', '');
if (!empty($data['latest_version'])) {
$this->blueprint->dbSet('modpackchecker', 'latest_version', $data['latest_version']);
}
return ['status' => 'active', 'tier' => $data['tier'] ?? 'standard'];
}
return $this->enterGracePeriod($data['error'] ?? 'Validation failed');
} catch (\Exception $e) {
Log::warning('[MVC License] Validation failed: ' . $e->getMessage());
return $this->enterGracePeriod('Could not reach license server');
}
}
/**
* Enter or continue grace period. After 7 days, expire.
*/
private function enterGracePeriod(string $reason): array
{
$graceExpires = $this->blueprint->dbGet('modpackchecker', 'grace_expires');
if (empty($graceExpires)) {
// First failure — start grace period
$expires = now()->addDays($this->graceDays)->toISOString();
$this->blueprint->dbSet('modpackchecker', 'license_status', 'grace');
$this->blueprint->dbSet('modpackchecker', 'grace_expires', $expires);
Log::warning("[MVC License] Grace period started. Expires: {$expires}. Reason: {$reason}");
return ['status' => 'grace', 'expires' => $expires, 'reason' => $reason];
}
// Check if grace period expired
if (now()->greaterThan($graceExpires)) {
$this->blueprint->dbSet('modpackchecker', 'license_status', 'expired');
Log::error('[MVC License] Grace period expired. License marked as expired.');
return ['status' => 'expired', 'reason' => $reason];
}
return ['status' => 'grace', 'expires' => $graceExpires, 'reason' => $reason];
}
/**
* Deactivate license on this panel.
*/
public function deactivate(): array
{
$orderId = $this->blueprint->dbGet('modpackchecker', 'order_id');
if (empty($orderId)) {
return ['success' => true];
}
try {
Http::timeout(15)->post("{$this->arbiterBase}/deactivate", [
'order_id' => $orderId,
'domain' => config('app.url'),
]);
} catch (\Exception $e) {
// Best-effort — clear local state regardless
}
$this->blueprint->dbSet('modpackchecker', 'order_id', '');
$this->blueprint->dbSet('modpackchecker', 'license_status', 'inactive');
$this->blueprint->dbSet('modpackchecker', 'tier', 'standard');
$this->blueprint->dbSet('modpackchecker', 'grace_expires', '');
return ['success' => true];
}
/**
* Check if a pro feature is allowed based on current tier + license status.
*/
public function isProFeatureAllowed(): bool
{
$status = $this->blueprint->dbGet('modpackchecker', 'license_status');
$tier = $this->blueprint->dbGet('modpackchecker', 'tier');
if ($status === 'expired') {
return false;
}
return $tier === 'professional';
}
/**
* Get full license state for the admin UI.
*/
public function getState(): array
{
return [
'order_id' => $this->blueprint->dbGet('modpackchecker', 'order_id') ?: '',
'status' => $this->blueprint->dbGet('modpackchecker', 'license_status') ?: 'inactive',
'tier' => $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard',
'grace_expires' => $this->blueprint->dbGet('modpackchecker', 'grace_expires') ?: '',
'last_validated' => $this->blueprint->dbGet('modpackchecker', 'last_validated') ?: '',
'latest_version' => $this->blueprint->dbGet('modpackchecker', 'latest_version') ?: '',
];
}
}