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
15declare(strict_types=1);
16
17namespace SwapAds\Client\License;
18
19use SwapAds\Client\Api\RestClient;
20use SwapAds\Client\License\LicenseManager;
21
22/**
23 * Class LicenseHeartbeat.
24 *
25 * @since 1.0.0
26 */
27final 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}