PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 All 138 releases
wpforo / admin / pages / license / src / Services / LicenseService.php

LicenseService.php in wpForo Forum 3.1.6, at admin/pages/license/src/Services/LicenseService.php

763 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace gVectors\License\Services;
4
5 use gVectors\License\Config;
6 use gVectors\License\LicenseModule;
7
8 // Exit if accessed directly
9 if( ! defined( 'ABSPATH' ) ) exit;
10
11 /**
12 * Manages license storage, validation, and revalidation.
13 * Licenses are stored locally, but the proxy server is the source of truth.
14 */
15 class LicenseService {
16 /**
17 * Terminal statuses/reasons: license is removed AND the addon is deactivated + deleted.
18 */
19 const TERMINAL_STATUSES = [ 'invalid' ];
20 const REFUND_REASONS = [ 'refunded' ];
21 public $apiService;
22 public $tampered_option;
23 public $expired_notice_option;
24 private $config;
25 private $batch_cache_key;
26 private $option_key;
27
28 public function __construct( Config $config, ApiService $apiService ) {
29 $this->config = $config;
30 $this->apiService = $apiService;
31 $this->batch_cache_key = $this->config->get_core_plugin_slug() . '_gvectors_batch_validation';
32 $this->option_key = $this->config->get_core_plugin_slug() . '_gvectors_licenses';
33 $this->tampered_option = $this->config->get_core_plugin_slug() . '_gvectors_tampered_addons';
34 $this->expired_notice_option = $this->config->get_core_plugin_slug() . '_gvectors_expired_license_notices';
35 $this->init_hooks();
36 }
37
38 private function init_hooks() {
39 add_action( 'gvectors_daily_cron', [ $this, 'maybe_revalidate_all' ] );
40 if( ! wp_next_scheduled( 'gvectors_revalidate' ) ) {
41 wp_schedule_event( time(), 'daily', 'gvectors_revalidate' );
42 }
43 add_action( 'gvectors_revalidate', [ $this, 'maybe_revalidate_all' ] );
44 }
45
46 /**
47 * Check if a product has an active license
48 */
49 public function is_active( string $product_id ): bool {
50 $license = $this->get( $product_id );
51 if( empty( $license ) || empty( $license['status'] ) ) return false;
52 if( ! in_array( $license['status'], [ 'active', 'trial' ], true ) ) return false;
53 $expires_ts = ! empty( $license['expires_at'] ) ? strtotime( $license['expires_at'] ) : false;
54 if( $expires_ts !== false && $expires_ts < time() ) return false;
55
56 return true;
57 }
58
59 /**
60 * Get a license for a specific product
61 */
62 public function get( string $product_id ): array {
63 $licenses = $this->get_all();
64
65 return $licenses[ $product_id ] ?? [];
66 }
67
68 /**
69 * Get all stored licenses
70 */
71 public function get_all(): array {
72 return get_option( $this->option_key, [] );
73 }
74
75 /**
76 * Check if a product is on trial
77 */
78 public function is_trial( string $product_id ): bool {
79 $license = $this->get( $product_id );
80
81 return ! empty( $license ) && $license['status'] === 'trial';
82 }
83
84 /**
85 * Validate all licenses via a single batch call (or use cache), then return
86 * the result for the requested product along with ALL updated licenses.
87 *
88 * Called by the AJAX validate button. One click validates everything.
89 *
90 * Returns the same array shape as revalidate(), plus:
91 * 'all_licenses' => array (full refreshed licenses keyed by product_id)
92 * 'cached' => bool
93 */
94 public function validate_with_cache( string $product_id = '' ): array {
95 // If a recent batch cache exists, all local data is already up to date
96 $cached = get_transient( $this->batch_cache_key );
97 $is_cached = ! empty( $cached ) && ! empty( $cached['time'] );
98
99 if( ! $is_cached ) {
100 // No cache — fetch fresh batch and process ALL licenses
101 $status = $this->batch_validate_and_cache();
102
103 if( $status === 'server_unavailable' ) {
104 return [
105 'valid' => false,
106 'reason' => 'server_unavailable',
107 'error' => __( 'Could not reach the license server. Your current license status is preserved. Will retry automatically.', 'gvectors' ),
108 'status' => '',
109 'removed' => false,
110 'addon_deleted' => false,
111 'all_licenses' => $this->get_all(),
112 ];
113 }
114 }
115
116 $all_licenses = $this->get_all();
117
118 // If a specific product was requested, return its individual result
119 if( ! empty( $product_id ) ) {
120 $license = $this->get( $product_id );
121 $valid = ! empty( $license ) && ! empty( $license['status'] ) && in_array( $license['status'], [ 'active', 'trial' ], true );
122
123 if( $valid && ! empty( $license['expires_at'] ) ) {
124 $expires_ts = strtotime( $license['expires_at'] );
125 if( $expires_ts !== false && $expires_ts < time() ) {
126 $valid = false;
127 }
128 }
129
130 $reason = '';
131 if( ! $valid && ! empty( $license['status'] ) ) {
132 $reason = $license['status'];
133 }
134 if( empty( $license ) ) {
135 $reason = 'missing';
136 }
137
138 return [
139 'valid' => $valid,
140 'reason' => $reason,
141 'error' => '',
142 'status' => $license['status'] ?? '',
143 'removed' => empty( $license ),
144 'addon_deleted' => false,
145 'all_licenses' => $all_licenses,
146 'cached' => $is_cached,
147 ];
148 }
149
150 // No specific product — return summary for all
151 // Consider valid if all licenses are active/trial
152 $all_valid = true;
153 foreach( $all_licenses as $lic ) {
154 if( empty( $lic['status'] ) || ! in_array( $lic['status'], [ 'active', 'trial' ], true ) ) {
155 $all_valid = false;
156 break;
157 }
158 if( ! empty( $lic['expires_at'] ) ) {
159 $expires_ts = strtotime( $lic['expires_at'] );
160 if( $expires_ts !== false && $expires_ts < time() ) {
161 $all_valid = false;
162 break;
163 }
164 }
165 }
166
167 return [
168 'valid' => $all_valid,
169 'reason' => $all_valid ? '' : 'some_invalid',
170 'error' => '',
171 'status' => '',
172 'removed' => false,
173 'addon_deleted' => false,
174 'all_licenses' => $all_licenses,
175 'cached' => $is_cached,
176 ];
177 }
178
179 /**
180 * Statuses that indicate a license is permanently invalid and should be removed locally.
181 */
182
183 /**
184 * Perform a batch validation for all stored licenses, process each result
185 * (update local DB/options), and cache the raw results.
186 *
187 * Returns 'server_unavailable' on server error, 'ok' on success, 'empty' if no licenses.
188 */
189 public function batch_validate_and_cache(): string {
190 $licenses = $this->get_all();
191 if( empty( $licenses ) ) return 'empty';
192
193 $key_to_product = []; // license_key => product_id
194 foreach( $licenses as $product_id => $license ) {
195 if( ! empty( $license['license_key'] ) ) {
196 $key_to_product[ $license['license_key'] ] = $product_id;
197 }
198 }
199 if( empty( $key_to_product ) ) return 'empty';
200
201 $response = $this->apiService->validate_licenses_batch( array_keys( $key_to_product ) );
202
203 $response_code = $response['code'] ?? 0;
204 $is_server_error = (
205 $response_code === 'wp_error'
206 || $response_code === 'invalid_response'
207 || ( is_int( $response_code ) && ( $response_code >= 500 || $response_code === 401 || $response_code === 403 || $response_code === 429 ) )
208 );
209 if( $is_server_error ) return 'server_unavailable';
210
211 if( empty( $response['success'] ) || empty( $response['data']['licenses'] ) ) return 'empty';
212
213 $batch_results = $response['data']['licenses'];
214
215 // Cache the raw batch results
216 set_transient( $this->batch_cache_key, [
217 'time' => time(),
218 'licenses' => $batch_results,
219 ], $this->config->get_license_batch_cache_ttl() );
220
221 // Process every license result — update local storage and notices
222 foreach( $key_to_product as $license_key => $product_id ) {
223 if( ! isset( $batch_results[ $license_key ] ) ) continue;
224
225 $result = $batch_results[ $license_key ];
226 $license = $licenses[ $product_id ];
227
228 if( ! empty( $result['success'] ) && ! empty( $result['data'] ) ) {
229 $this->save( $product_id, $result['data'] );
230
231 // Clear any expired notice for this license
232 $plugin_slug = $result['data']['plugin_slug'] ?? '';
233 if( $plugin_slug ) {
234 $expired_notices = get_option( $this->expired_notice_option, [] );
235 if( isset( $expired_notices[ $plugin_slug ] ) ) {
236 unset( $expired_notices[ $plugin_slug ] );
237 update_option( $this->expired_notice_option, $expired_notices );
238 }
239 }
240 } else {
241 $this->process_failed_revalidation( $product_id, $license, $result );
242 }
243 }
244
245 return 'ok';
246 }
247
248 /**
249 * Store/update a license locally
250 */
251 public function save( string $product_id, array $license_data ): bool {
252 $licenses = $this->get_all();
253
254 // Guard: Never overwrite a locally-stored active lifetime license with a subscription license.
255 // Only protect lifetime licenses that are still valid (active/trial status).
256 $existing = $licenses[ $product_id ] ?? [];
257 $existing_status = $existing['status'] ?? '';
258 if( ! empty( $existing ) && empty( $existing['expires_at'] ) && ! empty( $license_data['expires_at'] )
259 && in_array( $existing_status, [ 'active', 'trial', '' ] ) ) {
260 return true;
261 }
262
263 $license_data['last_validated'] = time();
264 $licenses[ $product_id ] = wp_parse_args( $license_data, [
265 'license_key' => '',
266 'product_id' => $product_id,
267 'subscription_id' => '',
268 'status' => '',
269 'expires_at' => '',
270 'activated_at' => '',
271 'site_domain' => LicenseModule::get_site_domain(),
272 'customer_id' => '',
273 'plan_name' => '',
274 'last_validated' => time(),
275 ] );
276
277 return update_option( $this->option_key, $licenses );
278 }
279
280 /**
281 * Process a failed batch revalidation result for a single license.
282 */
283 private function process_failed_revalidation( string $product_id, array $license, array $result ): void {
284 $server_status = $result['data']['status'] ?? '';
285 $server_error = $result['error'] ?? '';
286 $server_reason = '';
287
288 if( ! empty( $result['data']['invalid_reason'] ) ) {
289 $server_reason = $result['data']['invalid_reason'];
290 }
291 if( empty( $server_reason ) && preg_match( '/invalid:\\s*(.+)$/i', $server_error, $m ) ) {
292 $server_reason = trim( $m[1] );
293 }
294 if( empty( $server_reason ) && stripos( $server_error, 'expired' ) !== false ) {
295 $server_reason = 'expired';
296 }
297 if( empty( $server_reason ) && stripos( $server_error, 'not found' ) !== false ) {
298 $server_reason = 'not_found';
299 $server_status = $server_status ?: 'invalid';
300 }
301
302 $is_terminal = in_array( $server_reason, self::REFUND_REASONS, true )
303 || in_array( $server_status, self::TERMINAL_STATUSES, true )
304 || $server_reason === 'not_found';
305
306 if( $is_terminal ) {
307 $plugin_slug = $license['plugin_slug'] ?? '';
308 $this->remove( $product_id );
309
310 if( $plugin_slug ) {
311 $this->deactivate_and_delete_addon( $plugin_slug );
312
313 $expired_notices = get_option( $this->expired_notice_option, [] );
314 if( isset( $expired_notices[ $plugin_slug ] ) ) {
315 unset( $expired_notices[ $plugin_slug ] );
316 update_option( $this->expired_notice_option, $expired_notices );
317 }
318 $tampered = get_option( $this->tampered_option, [] );
319 if( isset( $tampered[ $plugin_slug ] ) ) {
320 unset( $tampered[ $plugin_slug ] );
321 update_option( $this->tampered_option, $tampered );
322 }
323 }
324 } else {
325 // Non-terminal: update local license with server data
326 $server_data = ( ! empty( $result['data'] ) && is_array( $result['data'] ) ) ? $result['data'] : [];
327 $has_license_fields = ! empty( $server_data['license_key'] ) || ! empty( $server_data['status'] ) || ! empty( $server_data['expires_at'] );
328
329 if( $has_license_fields ) {
330 $updated = wp_parse_args( $server_data, $license );
331 $updated['last_validated'] = time();
332 $this->save( $product_id, $updated );
333 } else {
334 if( $server_status ) {
335 $license['status'] = $server_status;
336 } elseif( $server_reason === 'expired' ) {
337 $license['status'] = 'expired';
338 }
339 $license['last_validated'] = time();
340 $this->save( $product_id, $license );
341 }
342
343 if( in_array( $server_status, [ 'expired', 'cancelled' ], true ) || $server_reason === 'expired' ) {
344 $this->update_expired_notice( $product_id );
345 }
346 }
347 }
348
349 /**
350 * Remove a license for a product
351 */
352 public function remove( string $product_id ): bool {
353 $licenses = $this->get_all();
354 if( isset( $licenses[ $product_id ] ) ) {
355 unset( $licenses[ $product_id ] );
356
357 return update_option( $this->option_key, $licenses );
358 }
359
360 return true;
361 }
362
363 /**
364 * Deactivate and delete an addon plugin by its slug.
365 * Used when a license is refunded to fully remove the addon.
366 */
367 private function deactivate_and_delete_addon( string $plugin_slug ): bool {
368 if( empty( $plugin_slug ) ) return false;
369
370 if( ! function_exists( 'get_plugins' ) ) {
371 require_once ABSPATH . 'wp-admin/includes/plugin.php';
372 }
373 if( ! function_exists( 'delete_plugins' ) ) {
374 require_once ABSPATH . 'wp-admin/includes/file.php';
375 }
376
377 // Find the installed plugin file
378 $plugin_file = '';
379 $all_plugins = get_plugins();
380 foreach( $all_plugins as $file => $data ) {
381 if( strpos( $file, $plugin_slug . '/' ) === 0 ) {
382 $plugin_file = $file;
383 break;
384 }
385 }
386
387 if( empty( $plugin_file ) ) return false;
388
389 // Deactivate if active
390 if( is_plugin_active( $plugin_file ) ) {
391 deactivate_plugins( $plugin_file );
392 }
393
394 // Delete the plugin files
395 $result = delete_plugins( [ $plugin_file ] );
396
397 return ! is_wp_error( $result );
398 }
399
400 /**
401 * Update the expired license notice for a specific product.
402 */
403 private function update_expired_notice( string $product_id ): void {
404 $license = $this->get( $product_id );
405 if( empty( $license ) ) return;
406
407 $plugin_slug = $license['plugin_slug'] ?? '';
408 if( empty( $plugin_slug ) ) return;
409
410 $expired_notices = get_option( $this->expired_notice_option, [] );
411 $expired_notices[ $plugin_slug ] = [
412 'product_name' => $license['product_name'] ?? $plugin_slug,
413 'status' => $license['status'] ?? 'expired',
414 'expires_at' => $license['expires_at'] ?? '',
415 'has_update' => false,
416 ];
417 update_option( $this->expired_notice_option, $expired_notices );
418 }
419
420 /**
421 * Revalidate a single license against the proxy server.
422 *
423 * Returns an associative array with:
424 * 'valid' => bool
425 * 'reason' => string (failure reason from server, e.g. 'refunded', 'expired')
426 * 'error' => string (human-readable error message from server)
427 * 'status' => string (license status from server response)
428 * 'removed' => bool (whether the license was removed locally)
429 * 'addon_deleted' => bool (whether the addon plugin was deactivated and deleted - only on refund)
430 */
431 public function revalidate( string $product_id ): array {
432 $license = $this->get( $product_id );
433 if( empty( $license ) || empty( $license['license_key'] ) ) {
434 return [
435 'valid' => false,
436 'reason' => 'missing',
437 'error' => __( 'No license found for this product', 'gvectors' ),
438 'status' => '',
439 'removed' => false,
440 'addon_deleted' => false,
441 ];
442 }
443
444 $response = $this->apiService->validate_license( $license['license_key'] );
445
446 // ── Server/network error — keep current local state, retry later ──
447 // Only trust the response if the server actually processed our request.
448 // Connection failures, HTTP errors (401, 403, 500), and invalid responses
449 // should NOT alter the local license status.
450 $response_code = $response['code'] ?? 0;
451 $is_server_error = (
452 $response_code === 'wp_error' // Connection failure, timeout, DNS error
453 || $response_code === 'invalid_response' // Server returned non-JSON
454 || ( is_int( $response_code ) && ( $response_code >= 500 || $response_code === 401 || $response_code === 403 || $response_code === 429 ) )
455 );
456
457 if( $is_server_error ) {
458 // Don't update last_validated — will retry on next cron run
459 return [
460 'valid' => ! empty( $license['status'] ) && in_array( $license['status'], [ 'active', 'trial' ], true ),
461 'reason' => 'server_unavailable',
462 'error' => $response['error'] ?? 'Server unavailable',
463 'status' => $license['status'] ?? '',
464 'removed' => false,
465 'addon_deleted' => false,
466 ];
467 }
468
469 // License is valid on the server
470 if( ! empty( $response['success'] ) && ! empty( $response['data'] ) ) {
471 $this->save( $product_id, $response['data'] );
472
473 return [
474 'valid' => true,
475 'reason' => '',
476 'error' => '',
477 'status' => $response['data']['status'] ?? 'active',
478 'removed' => false,
479 'addon_deleted' => false,
480 ];
481 }
482
483 // License validation failed - extract details from server response
484 $server_status = $response['data']['status'] ?? '';
485 $server_error = $response['error'] ?? '';
486 $server_reason = '';
487
488 // Extract invalid_reason from response data if available
489 if( ! empty( $response['data']['invalid_reason'] ) ) {
490 $server_reason = $response['data']['invalid_reason'];
491 }
492 // Parse reason from the error message (e.g. "License is invalid: refunded")
493 if( empty( $server_reason ) && preg_match( '/invalid:\s*(.+)$/i', $server_error, $m ) ) {
494 $server_reason = trim( $m[1] );
495 }
496 // If the error indicates expiration, set reason accordingly
497 if( empty( $server_reason ) && stripos( $server_error, 'expired' ) !== false ) {
498 $server_reason = 'expired';
499 }
500 // If the license key was not found on server
501 if( empty( $server_reason ) && stripos( $server_error, 'not found' ) !== false ) {
502 $server_reason = 'not_found';
503 $server_status = $server_status ?: 'invalid';
504 }
505
506 // Determine if this is a terminal state: license removed + addon deactivated + deleted
507 // Terminal = refunded, not found on server, or invalid status
508 $is_terminal = in_array( $server_reason, self::REFUND_REASONS, true )
509 || in_array( $server_status, self::TERMINAL_STATUSES, true )
510 || $server_reason === 'not_found';
511
512 $removed = false;
513 $addon_deleted = false;
514
515 if( $is_terminal ) {
516 // Terminal: remove license, deactivate and delete the addon plugin
517 $plugin_slug = $license['plugin_slug'] ?? '';
518 $this->remove( $product_id );
519 $removed = true;
520
521 // Deactivate and delete the addon if installed
522 if( $plugin_slug ) {
523 $addon_deleted = $this->deactivate_and_delete_addon( $plugin_slug );
524
525 // Clean-up-related notices
526 $expired_notices = get_option( $this->expired_notice_option, [] );
527 if( isset( $expired_notices[ $plugin_slug ] ) ) {
528 unset( $expired_notices[ $plugin_slug ] );
529 update_option( $this->expired_notice_option, $expired_notices );
530 }
531 $tampered = get_option( $this->tampered_option, [] );
532 if( isset( $tampered[ $plugin_slug ] ) ) {
533 unset( $tampered[ $plugin_slug ] );
534 update_option( $this->tampered_option, $tampered );
535 }
536 }
537 } else {
538 // Non-terminal failure (expired, cancelled, past_due, paused, etc.)
539 // Overwrite local license with authoritative server data to purge stale values.
540 $server_data = ( ! empty( $response['data'] ) && is_array( $response['data'] ) ) ? $response['data'] : [];
541
542 // Only treat server data as license fields if it has actual license keys
543 // (not just a raw error envelope like {success:false, error:'...'})
544 $has_license_fields = ! empty( $server_data['license_key'] ) || ! empty( $server_data['status'] ) || ! empty( $server_data['expires_at'] );
545
546 if( $has_license_fields ) {
547 // Server returned full license data — use it as the source of truth
548 $updated = wp_parse_args( $server_data, $license );
549 $updated['last_validated'] = time();
550 $this->save( $product_id, $updated );
551 } else {
552 // Server didn't return license fields — force-update status from error context
553 if( $server_status ) {
554 $license['status'] = $server_status;
555 } elseif( $server_reason === 'expired' ) {
556 $license['status'] = 'expired';
557 }
558 $license['last_validated'] = time();
559 $this->save( $product_id, $license );
560 }
561
562 // Trigger expired notice update for this license
563 if( in_array( $server_status, [ 'expired', 'cancelled' ], true ) || $server_reason === 'expired' ) {
564 $this->update_expired_notice( $product_id );
565 }
566 }
567
568 return [
569 'valid' => false,
570 'reason' => $server_reason ?: $server_status,
571 'error' => $server_error,
572 'status' => $server_status,
573 'removed' => $removed,
574 'addon_deleted' => $addon_deleted,
575 ];
576 }
577
578 /**
579 * Revalidate all stored licenses (called by cron).
580 * Uses batch validation to check all licenses in a single API call.
581 */
582 public function maybe_revalidate_all(): void {
583 $licenses = $this->get_all();
584 if( empty( $licenses ) ) return;
585
586 // Collect license keys that need revalidation
587 $keys_to_validate = []; // license_key => product_id
588 foreach( $licenses as $product_id => $license ) {
589 if( $this->needs_revalidation( $product_id ) && ! empty( $license['license_key'] ) ) {
590 $keys_to_validate[ $license['license_key'] ] = $product_id;
591 }
592 }
593
594 if( empty( $keys_to_validate ) ) return;
595
596 $response = $this->apiService->validate_licenses_batch( array_keys( $keys_to_validate ) );
597
598 // Server/network error — don't touch local state, retry on next cron
599 $response_code = $response['code'] ?? 0;
600 $is_server_error = (
601 $response_code === 'wp_error'
602 || $response_code === 'invalid_response'
603 || ( is_int( $response_code ) && ( $response_code >= 500 || $response_code === 401 || $response_code === 403 || $response_code === 429 ) )
604 );
605 if( $is_server_error ) return;
606
607 if( empty( $response['success'] ) || empty( $response['data']['licenses'] ) ) return;
608
609 $batch_results = $response['data']['licenses'];
610
611 // Cache the raw batch results so individual Validate clicks can reuse them
612 set_transient( $this->batch_cache_key, [
613 'time' => time(),
614 'licenses' => $batch_results,
615 ], $this->config->get_license_batch_cache_ttl() );
616
617 foreach( $keys_to_validate as $license_key => $product_id ) {
618 if( ! isset( $batch_results[ $license_key ] ) ) continue;
619
620 $result = $batch_results[ $license_key ];
621 $license = $licenses[ $product_id ];
622
623 if( ! empty( $result['success'] ) && ! empty( $result['data'] ) ) {
624 // License is valid — update local data
625 $this->save( $product_id, $result['data'] );
626 } else {
627 // License validation failed — process like single "revalidate"
628 $this->process_failed_revalidation( $product_id, $license, $result );
629 }
630 }
631 }
632
633 /**
634 * Check if the license needs revalidation
635 */
636 public function needs_revalidation( string $product_id ): bool {
637 $license = $this->get( $product_id );
638 if( empty( $license ) ) return false;
639 $last = isset( $license['last_validated'] ) ? (int) $license['last_validated'] : 0;
640
641 return ( time() - $last ) > $this->config->get_license_revalidation_period();
642 }
643
644 /**
645 * Unified activation: proxy detects key type (gvl_*, txn_*, legacy, or empty=by-domain).
646 * Saves whatever license(s) the proxy returns into the local gvectors_licenses option.
647 */
648 public function activate_unified( string $key, string $product_id = '' ): array {
649 $response = $this->apiService->activate_unified( $key, $product_id );
650
651 if( ! empty( $response['success'] ) ) {
652 // Transaction / by-domain response: data.activated[] array
653 if( ! empty( $response['data']['activated'] ) ) {
654 foreach( $response['data']['activated'] as $license_data ) {
655 $pid = ! empty( $license_data['product_id'] ) ? $license_data['product_id'] : '';
656 if( $pid ) {
657 $this->save( $pid, $license_data );
658 }
659 }
660 } // Single license response (gvl_* or legacy key): data.license_key present
661 elseif( ! empty( $response['data']['license_key'] ) ) {
662 $pid = ! empty( $response['data']['product_id'] ) ? $response['data']['product_id'] : $product_id;
663 if( $pid ) {
664 $this->save( $pid, $response['data'] );
665 }
666 }
667 }
668
669 return $response;
670 }
671
672 /**
673 * Activate a license key
674 */
675 public function activate( string $license_key, string $product_id = '' ): array {
676 $response = $this->apiService->activate_license( $license_key, $product_id );
677
678 if( ! empty( $response['success'] ) && ! empty( $response['data'] ) ) {
679 $pid = ! empty( $response['data']['product_id'] ) ? $response['data']['product_id'] : $product_id;
680 if( $pid ) {
681 $this->save( $pid, $response['data'] );
682 }
683 }
684
685 return $response;
686 }
687
688 /**
689 * Activate licenses by transaction ID
690 */
691 public function activate_by_transaction( string $transaction_id, string $product_id = '' ): array {
692 $response = $this->apiService->activate_license_by_transaction( $transaction_id, $product_id );
693
694 if( ! empty( $response['success'] ) && ! empty( $response['data']['activated'] ) ) {
695 foreach( $response['data']['activated'] as $license_data ) {
696 $pid = ! empty( $license_data['product_id'] ) ? $license_data['product_id'] : '';
697 if( $pid ) {
698 $this->save( $pid, $license_data );
699 }
700 }
701 }
702
703 return $response;
704 }
705
706 /**
707 * Activate all licenses registered for this site's domain
708 */
709 public function activate_by_domain(): array {
710 $response = $this->apiService->activate_licenses_by_domain();
711
712 if( ! empty( $response['success'] ) && ! empty( $response['data']['activated'] ) ) {
713 foreach( $response['data']['activated'] as $license_data ) {
714 $pid = ! empty( $license_data['product_id'] ) ? $license_data['product_id'] : '';
715 if( $pid ) {
716 $this->save( $pid, $license_data );
717 }
718 }
719 }
720
721 return $response;
722 }
723
724 /**
725 * Deactivate a license key
726 */
727 public function deactivate( string $product_id ): array {
728 $license = $this->get( $product_id );
729 if( empty( $license ) || empty( $license['license_key'] ) ) {
730 return [ 'success' => false, 'error' => 'No license found for this product' ];
731 }
732
733 $response = $this->apiService->deactivate_license( $license['license_key'] );
734
735 if( ! empty( $response['success'] ) ) {
736 $this->remove( $product_id );
737 }
738
739 return $response;
740 }
741
742 /**
743 * Get a license status label
744 */
745 public function get_status_label( string $product_id ): string {
746 $license = $this->get( $product_id );
747 if( empty( $license ) ) return __( 'Not Licensed', 'gvectors' );
748
749 $labels = [
750 'active' => __( 'Active', 'gvectors' ),
751 'trial' => __( 'Trial', 'gvectors' ),
752 'expired' => __( 'Expired', 'gvectors' ),
753 'cancelled' => __( 'Cancelled', 'gvectors' ),
754 'past_due' => __( 'Past Due', 'gvectors' ),
755 'paused' => __( 'Paused', 'gvectors' ),
756 ];
757
758 $status = $license['status'] ?? '';
759
760 return $labels[ $status ] ?? __( 'Unknown', 'gvectors' );
761 }
762 }
763