| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 87.10% covered (warning) | 87.10% | 27 / 31 | 25.00% covered (danger) | 25.00% | 1 / 4 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
LicenseHeartbeat | 87.10% covered (warning) | 87.10% | 27 / 31 | 25.00% covered (danger) | 25.00% | 1 / 4 | 13.36 | 0.00% covered (danger) | 0.00% | 0 / 1
register | 80.00% covered (warning) | 80.00% | 4 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.07 |
deregister | 66.67% covered (warning) | 66.67% | 2 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.15 |
run | 90.91% covered (success) | 90.91% | 20 / 22 | 0.00% covered (danger) | 0.00% | 0 / 1 | 7.04 |
lastHeartbeatAt | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |

1 | <?php
2 | /**
3 | * License Heartbeat (WP Cron).
4 | *
5 | * Calls the server's /v1/auth/heartbeat on a schedule to keep the
6 | * license marked as active and to update last-seen timestamps.
7 | *
8 | * Runs every 12 hours via WP Cron. If the server returns inactive,
9 | * the client marks the license as inactive locally too.
10 | *
11 | * @package SwapAds\Client\License
12 | * @since 1.0.0
13 | */
14 |
15 | declare(strict_types=1);
16 |
17 | namespace SwapAds\Client\License;
18 |
19 | use SwapAds\Client\Api\RestClient;
20 | use SwapAds\Client\License\LicenseManager;
21 |
22 | /**
23 | * Class LicenseHeartbeat.
24 | *
25 | * @since 1.0.0
26 | */
27 | final class LicenseHeartbeat
28 | {
29 | public const HOOK_NAME = 'swapads_client_license_heartbeat';
30 | public const SCHEDULE_NAME = 'twicedaily';
31 | public const OPTION_KEY = 'swapads_client_last_heartbeat_at';
32 |
33 | /**
34 | * Register WP Cron hook + schedule.
35 | *
36 | * @since 1.0.0
37 | */
38 | public static function register(): void
39 | {
40 | if (!function_exists('add_action')) {
41 | return;
42 | }
43 | add_action(self::HOOK_NAME, [self::class, 'run']);
44 | if (!wp_next_scheduled(self::HOOK_NAME)) {
45 | wp_schedule_event(time(), self::SCHEDULE_NAME, self::HOOK_NAME);
46 | }
47 | }
48 |
49 | /**
50 | * Deregister WP Cron hook + clear schedule.
51 | *
52 | * @since 1.0.0
53 | */
54 | public static function deregister(): void
55 | {
56 | $timestamp = wp_next_scheduled(self::HOOK_NAME);
57 | if ($timestamp) {
58 | wp_unschedule_event($timestamp, self::HOOK_NAME);
59 | }
60 | }
61 |
62 | /**
63 | * Run the heartbeat: call /v1/auth/heartbeat and update local state.
64 | *
65 | * @return array<string, mixed> Result {success, status, error?}
66 | *
67 | * @since 1.0.0
68 | */
69 | public static function run(): array
70 | {
71 | if (!LicenseManager::isLicensed()) {
72 | return ['success' => false, 'error' => 'NOT_LICENSED', 'status' => 'inactive'];
73 | }
74 | $serverUrl = (string) get_option('swapads_client_server_url', '');
75 | $licenseKey = (string) LicenseManager::key();
76 | $secret = (string) LicenseManager::secret();
77 | $siteId = (int) LicenseManager::siteId();
78 |
79 | if ($serverUrl === '' || $licenseKey === '' || $secret === '') {
80 | return ['success' => false, 'error' => 'INCOMPLETE_CONFIG', 'status' => 'inactive'];
81 | }
82 |
83 | try {
84 | $client = new RestClient($serverUrl);
85 | $response = $client->post('/v1/auth/heartbeat', [
86 | 'license_key' => $licenseKey,
87 | 'site_id' => $siteId,
88 | ]);
89 | $data = $response['data'] ?? [];
90 | $status = (string) ($data['status'] ?? 'unknown');
91 | update_option(self::OPTION_KEY, time());
92 | if ($status === 'inactive') {
93 | LicenseManager::store($licenseKey, $secret, $siteId, (string) LicenseManager::domain(), 'inactive');
94 | } else {
95 | LicenseManager::store($licenseKey, $secret, $siteId, (string) LicenseManager::domain(), 'active');
96 | }
97 | return ['success' => true, 'status' => $status];
98 | } catch (\Throwable $e) {
99 | return ['success' => false, 'error' => 'REQUEST_FAILED', 'message' => $e->getMessage()];
100 | }
101 | }
102 |
103 | /**
104 | * Get timestamp of last successful heartbeat (or 0).
105 | *
106 | * @return int Unix timestamp.
107 | *
108 | * @since 1.0.0
109 | */
110 | public static function lastHeartbeatAt(): int
111 | {
112 | return (int) get_option(self::OPTION_KEY, 0);
113 | }
114 | }