PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
mxchat-basic / includes / class-mxchat-license-check.php

class-mxchat-license-check.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.25, at includes/class-mxchat-license-check.php

630 lines 28.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MxChat Licence Check — asks mxchat.ai whether this site's domain is still
4 * switched on for its licence key, and honours an explicit "off" from the
5 * licence holder's My Account after a three-day grace period.
6 *
7 * Plan: plan-mxchat-20261007-c93ede (Support Ticket 467). An Agency licence
8 * holder who resells to clients could see the domains activated on a key but
9 * had no way to switch one off, and even a server-side unlink left the old
10 * install running Pro: `mxchat_license_status` is a local option written at
11 * activation and never re-checked.
12 *
13 * The rules, from Maxwell's approval note (they override the original spec):
14 *
15 * 1. Turning back on is immediate. A site that is off (or in its grace
16 * period) re-checks when an admin opens any MxChat screen, at most once a
17 * minute; on the "Check license now" button; and hourly in the background.
18 * Activating from the site's own Activate button stays instant as before.
19 * 2. A site that is on checks once a day, at a random time per site.
20 * 3. ONLY AN EXPLICIT "OFF" COUNTS. A timeout, a 5xx, a non-JSON body, a DNS
21 * failure or an mxchat.ai outage never switches a site off. Today's
22 * production endpoint does not know this route yet and answers
23 * {"success":false,"data":"Invalid action"} — that is an error here too,
24 * so a core shipped ahead of the website change keeps every site on.
25 * 4. A site move is not "off". A valid key on a domain mxchat.ai does not
26 * recognise ("unknown") stays on.
27 * 5. Grace period: after the first explicit off the admin sees a notice and
28 * Pro is switched off only when an off answer arrives three days after the
29 * first one with no on/unknown in between. Nothing else is touched:
30 * settings, knowledge, the saved key and email, and the free features all
31 * stay exactly as they are, so switching back on restores Pro in place.
32 *
33 * What is sent to mxchat.ai: the licence key, the account email, the site's
34 * host name and the plugin version (the same three identifiers the existing
35 * activate / deactivate / check-domain calls already send, plus the version).
36 * The readme's "External services" section documents all four calls.
37 */
38
39 if (!defined('ABSPATH')) {
40 exit;
41 }
42
43 class MxChat_License_Check {
44
45 const ENDPOINT = 'https://mxchat.ai/mxchat-api/license-status';
46 const OPTION = 'mxchat_license_check';
47 const CRON_HOOK = 'mxchat_license_check';
48 const NONCE_CHECK = 'mxchat_license_check_now';
49 const NONCE_DISMISS = 'mxchat_license_notice_dismiss';
50
51 const GRACE_SECONDS = 3 * DAY_IN_SECONDS;
52 const DAILY_SECONDS = DAY_IN_SECONDS;
53 const HOURLY_SECONDS = HOUR_IN_SECONDS;
54 const RETRY_SECONDS = 6 * HOUR_IN_SECONDS; // after an unanswered check on a site that is on
55 const ADMIN_SECONDS = MINUTE_IN_SECONDS; // admin-screen re-check while off / in grace
56 const BUTTON_SECONDS = 10; // "Check license now" rate limit
57 const DISMISS_SECONDS = DAY_IN_SECONDS; // a dismissed notice comes back after a day
58 const TIMEOUT_CRON = 15;
59 const TIMEOUT_ADMIN = 8;
60
61 /**
62 * The refusals mxchat.ai's activation gate answers with, in the WooCommerce
63 * Software add-on's own error shape (plan c93ede rework): each one is the
64 * licence owner's (or an administrator's) explicit decision, never a slot
65 * count or a transport problem.
66 */
67 public static function owner_reasons() {
68 return array(
69 '106' => 'switched_off',
70 '107' => 'new_sites_blocked',
71 '108' => 'revoked',
72 );
73 }
74
75 public static function is_owner_reason($reason) {
76 return in_array((string) $reason, array('switched_off', 'new_sites_blocked', 'revoked'), true);
77 }
78
79 public static function init() {
80 add_action(self::CRON_HOOK, array(__CLASS__, 'cron_check'));
81
82 if (is_admin()) {
83 add_action('admin_init', array(__CLASS__, 'admin_screen_check'), 5);
84 add_action('admin_notices', array(__CLASS__, 'render_notice'));
85 add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_assets'));
86 add_action('wp_ajax_mxchat_license_check_now', array(__CLASS__, 'ajax_check_now'));
87 add_action('wp_ajax_mxchat_license_notice_dismiss', array(__CLASS__, 'ajax_dismiss_notice'));
88 }
89
90 self::ensure_scheduled();
91 }
92
93 /* ---------------------------------------------------------------------
94 * State
95 * ------------------------------------------------------------------ */
96
97 public static function defaults() {
98 return array(
99 'last_check' => 0, // unix time of the last answer of any kind
100 'last_result' => '', // on | off | unknown | error | ''
101 'last_reason' => '', // the server's reason, or the transport / http / malformed code
102 'last_source' => '', // cron | admin | button
103 'off_since' => 0, // first explicit off of the current streak (0 = not off)
104 'off_count' => 0,
105 'revoked_at' => 0, // when this class set mxchat_license_status to inactive (0 = it did not)
106 'restored_at' => 0, // set when Pro was switched back on by a check; cleared once the notice has shown
107 'next_check' => 0, // unix time the next background check is due
108 'dismissed_at' => 0, // the grace / off notice was dismissed at this time
109 'refusal' => '', // the sentence mxchat.ai answered the last refused Activate click with
110 );
111 }
112
113 public static function state() {
114 $saved = get_option(self::OPTION, array());
115 return is_array($saved) ? array_merge(self::defaults(), $saved) : self::defaults();
116 }
117
118 private static function save_state(array $state) {
119 update_option(self::OPTION, $state, false);
120 }
121
122 /** The saved key, email and this site's host name; null when the site has no activation to check. */
123 public static function credentials() {
124 $key = trim((string) get_option('mxchat_activation_key', ''));
125 $email = trim((string) get_option('mxchat_pro_email', ''));
126 if ($key === '' || $email === '') {
127 return null;
128 }
129 return array('key' => $key, 'email' => $email, 'domain' => self::domain());
130 }
131
132 /** Exactly what activation sent as the domain, so the server-side match is the same string. */
133 public static function domain() {
134 $host = parse_url(home_url(), PHP_URL_HOST);
135 return is_string($host) ? $host : '';
136 }
137
138 public static function is_revoked() {
139 $state = self::state();
140 return !empty($state['revoked_at']);
141 }
142
143 public static function in_grace() {
144 $state = self::state();
145 return !empty($state['off_since']) && empty($state['revoked_at']);
146 }
147
148 /**
149 * Clear the state. Called when the site activates (fresh start, first daily
150 * check at a random time) or deactivates its licence (nothing left to check).
151 */
152 public static function reset($reschedule) {
153 delete_option(self::OPTION);
154 wp_clear_scheduled_hook(self::CRON_HOOK);
155 if ($reschedule) {
156 self::ensure_scheduled();
157 }
158 }
159
160 /* ---------------------------------------------------------------------
161 * Scheduling
162 * ------------------------------------------------------------------ */
163
164 /**
165 * Make sure a background check is on the calendar. The first one lands at
166 * a random moment within the next day and every later one is set from the
167 * previous answer, so 850 sites spread their requests across the day.
168 */
169 public static function ensure_scheduled() {
170 if (!self::credentials()) {
171 return;
172 }
173 $state = self::state();
174 $now = time();
175 if (empty($state['next_check'])) {
176 $state['next_check'] = $now + wp_rand(MINUTE_IN_SECONDS, DAY_IN_SECONDS);
177 self::save_state($state);
178 }
179 if (false === wp_next_scheduled(self::CRON_HOOK)) {
180 wp_schedule_single_event(max($now + MINUTE_IN_SECONDS, (int) $state['next_check']), self::CRON_HOOK);
181 }
182 }
183
184 private static function reschedule($timestamp) {
185 wp_clear_scheduled_hook(self::CRON_HOOK);
186 wp_schedule_single_event(max(time() + MINUTE_IN_SECONDS, (int) $timestamp), self::CRON_HOOK);
187 }
188
189 /**
190 * Is a check due? $context is 'cron' (background) or 'admin' (an admin
191 * opened a MxChat screen). While the domain is off or in its grace period
192 * the gap is an hour in the background and a minute on an admin screen;
193 * otherwise the daily timetable applies.
194 */
195 public static function is_due($now, $context = 'cron') {
196 if (!self::credentials()) {
197 return false;
198 }
199 $state = self::state();
200 $status = get_option('mxchat_license_status', 'inactive');
201 if ($status !== 'active' && empty($state['revoked_at'])) {
202 return false; // never activated, or deactivated by hand: nothing to check
203 }
204 if (!empty($state['off_since']) || !empty($state['revoked_at'])) {
205 $gap = ($context === 'admin') ? self::ADMIN_SECONDS : self::HOURLY_SECONDS;
206 return ($now - (int) $state['last_check']) >= $gap;
207 }
208 return !empty($state['next_check']) && $now >= (int) $state['next_check'];
209 }
210
211 public static function cron_check() {
212 if (self::is_due(time(), 'cron')) {
213 self::run_check('cron');
214 return;
215 }
216 $state = self::state();
217 if (!empty($state['next_check'])) {
218 self::reschedule((int) $state['next_check']);
219 }
220 }
221
222 public static function admin_screen_check() {
223 if (wp_doing_ajax() || wp_doing_cron() || !current_user_can('manage_options')) {
224 return;
225 }
226 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
227 if ($page === '' || strpos($page, 'mxchat') !== 0) {
228 return;
229 }
230 if (self::is_due(time(), 'admin')) {
231 self::run_check('admin');
232 }
233 }
234
235 /* ---------------------------------------------------------------------
236 * The check
237 * ------------------------------------------------------------------ */
238
239 /**
240 * One request to mxchat.ai and the state change it implies. Returns the
241 * interpreted answer plus the resulting Pro status, or null when the site
242 * has nothing to check.
243 */
244 public static function run_check($source = 'cron') {
245 $cred = self::credentials();
246 if (!$cred) {
247 return null;
248 }
249 $response = wp_remote_post(self::ENDPOINT, array(
250 'timeout' => ($source === 'cron') ? self::TIMEOUT_CRON : self::TIMEOUT_ADMIN,
251 'sslverify' => true,
252 'body' => array(
253 'license_key' => $cred['key'],
254 'email' => $cred['email'],
255 'domain' => $cred['domain'],
256 'plugin_version' => defined('MXCHAT_BASE_VERSION') ? MXCHAT_BASE_VERSION : '',
257 ),
258 ));
259 return self::apply(self::interpret($response), $source, time());
260 }
261
262 /**
263 * Turn an HTTP response into one of four words. Anything that is not a
264 * well-formed answer is 'error' and changes nothing (rule 3).
265 *
266 * Expected body: {"success":true,"data":{"state":"on|off|unknown","reason":"…"}}
267 */
268 public static function interpret($response) {
269 if (is_wp_error($response)) {
270 return array('state' => 'error', 'reason' => 'transport_' . sanitize_key($response->get_error_code()));
271 }
272 $code = (int) wp_remote_retrieve_response_code($response);
273 if ($code !== 200) {
274 return array('state' => 'error', 'reason' => 'http_' . $code);
275 }
276 $body = json_decode((string) wp_remote_retrieve_body($response), true);
277 if (!is_array($body) || empty($body['success']) || !isset($body['data']) || !is_array($body['data']) || !isset($body['data']['state'])) {
278 return array('state' => 'error', 'reason' => 'malformed');
279 }
280 $state = strtolower(trim((string) $body['data']['state']));
281 $reason = isset($body['data']['reason']) ? sanitize_key((string) $body['data']['reason']) : '';
282 if ($state === 'on' || $state === 'off') {
283 return array('state' => $state, 'reason' => $reason);
284 }
285 return array('state' => 'unknown', 'reason' => ($reason !== '') ? $reason : 'state_' . sanitize_key($state));
286 }
287
288 /**
289 * Apply an interpreted answer at time $now. Public so the verification rig
290 * can drive the state machine without a network; the plugin only calls it
291 * from run_check().
292 */
293 public static function apply(array $result, $source, $now) {
294 $state = self::state();
295 $status = get_option('mxchat_license_status', 'inactive');
296
297 $state['last_check'] = (int) $now;
298 $state['last_result'] = $result['state'];
299 $state['last_reason'] = isset($result['reason']) ? (string) $result['reason'] : '';
300 $state['last_source'] = (string) $source;
301
302 switch ($result['state']) {
303 case 'off':
304 if (empty($state['off_since'])) {
305 $state['off_since'] = (int) $now;
306 $state['off_count'] = 1;
307 $state['dismissed_at'] = 0; // a new streak brings the notice back
308 } else {
309 $state['off_count'] = (int) $state['off_count'] + 1;
310 }
311 if (empty($state['revoked_at'])
312 && ($now - (int) $state['off_since']) >= self::GRACE_SECONDS
313 && $status === 'active') {
314 // Three days of off and still off: pause Pro. Only this one option moves.
315 update_option('mxchat_license_status', 'inactive');
316 $state['revoked_at'] = (int) $now;
317 $state['dismissed_at'] = 0;
318 }
319 $state['next_check'] = $now + self::HOURLY_SECONDS;
320 break;
321
322 case 'on':
323 case 'unknown':
324 if (!empty($state['revoked_at'])) {
325 update_option('mxchat_license_status', 'active');
326 $state['restored_at'] = (int) $now;
327 }
328 $state['off_since'] = 0;
329 $state['off_count'] = 0;
330 $state['revoked_at'] = 0;
331 $state['dismissed_at'] = 0;
332 $state['next_check'] = $now + self::DAILY_SECONDS - 1800 + wp_rand(0, 3600);
333 break;
334
335 default: // error: fail open, keep whatever streak there is
336 $state['next_check'] = (!empty($state['off_since']) || !empty($state['revoked_at']))
337 ? $now + self::HOURLY_SECONDS
338 : $now + self::RETRY_SECONDS;
339 break;
340 }
341
342 self::save_state($state);
343 self::reschedule((int) $state['next_check']);
344
345 $result['pro'] = get_option('mxchat_license_status', 'inactive');
346 return $result;
347 }
348
349 /**
350 * The Activate button was refused by mxchat.ai's gate (plan c93ede rework):
351 * the licence owner switched this domain off, blocked new sites on the key,
352 * or the key was revoked. The activation handler has already set Pro
353 * inactive. When Pro was on until this click, treat the refusal as the
354 * owner's explicit off (so the hourly check keeps running and the owner's
355 * switch-on restores Pro); when the site was never on, just remember the
356 * sentence for the Pro page. The add-on's own errors (invalid key, no
357 * slots, incomplete order) are not the owner's doing and change nothing here.
358 */
359 public static function note_activation_refusal($code, $message, $was_active) {
360 $reasons = self::owner_reasons();
361 $code = (string) $code;
362 if (!isset($reasons[$code])) {
363 return null;
364 }
365 $now = time();
366 $state = self::state();
367 $state['last_check'] = $now;
368 $state['last_result'] = 'off';
369 $state['last_reason'] = $reasons[$code];
370 $state['last_source'] = 'activate';
371 $state['refusal'] = sanitize_text_field((string) $message);
372 if ($was_active) {
373 if (empty($state['off_since'])) {
374 $state['off_since'] = $now;
375 $state['off_count'] = 1;
376 } else {
377 $state['off_count'] = (int) $state['off_count'] + 1;
378 }
379 $state['revoked_at'] = $now;
380 $state['dismissed_at'] = 0;
381 $state['next_check'] = $now + self::HOURLY_SECONDS;
382 self::save_state($state);
383 self::reschedule((int) $state['next_check']);
384 return $reasons[$code];
385 }
386 self::save_state($state);
387 return $reasons[$code];
388 }
389
390 /**
391 * The owner's refusal to show under the activation form while Pro is off and
392 * no check is running (a site that was never on, or was deactivated by hand).
393 */
394 public static function last_refusal() {
395 $state = self::state();
396 if ($state['last_result'] !== 'off' || !self::is_owner_reason($state['last_reason']) || $state['refusal'] === '') {
397 return '';
398 }
399 return (string) $state['refusal'];
400 }
401
402 /* ---------------------------------------------------------------------
403 * AJAX
404 * ------------------------------------------------------------------ */
405
406 public static function ajax_check_now() {
407 if (!current_user_can('manage_options') || !check_ajax_referer(self::NONCE_CHECK, 'nonce', false)) {
408 wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat')), 403);
409 }
410 if (!self::credentials()) {
411 wp_send_json_error(array('message' => esc_html__('No licence key is saved on this site.', 'mxchat')));
412 }
413 $before = get_option('mxchat_license_status', 'inactive');
414 $state = self::state();
415 $throttled = false;
416 if ($state['last_result'] !== '' && (time() - (int) $state['last_check']) < self::BUTTON_SECONDS) {
417 $throttled = true;
418 $result = array('state' => $state['last_result'], 'reason' => $state['last_reason']);
419 } else {
420 $result = self::run_check('button');
421 }
422 $after = get_option('mxchat_license_status', 'inactive');
423 $view = self::view_model();
424 wp_send_json_success(array(
425 'state' => $result['state'],
426 'reason' => $result['reason'],
427 'pro' => $after,
428 'reload' => ($before !== $after),
429 'throttled' => $throttled,
430 'pill' => $view['pill_text'],
431 'pillClass' => $view['pill_class'],
432 'message' => $view['sentence'],
433 ));
434 }
435
436 public static function ajax_dismiss_notice() {
437 if (!current_user_can('manage_options') || !check_ajax_referer(self::NONCE_DISMISS, 'nonce', false)) {
438 wp_send_json_error();
439 }
440 $state = self::state();
441 $state['dismissed_at'] = time();
442 self::save_state($state);
443 wp_send_json_success();
444 }
445
446 /* ---------------------------------------------------------------------
447 * Presentation
448 * ------------------------------------------------------------------ */
449
450 /** The pill and the sentence the Pro page and the AJAX reply both show. */
451 public static function view_model() {
452 $state = self::state();
453 $status = get_option('mxchat_license_status', 'inactive');
454 $now = time();
455
456 if (!empty($state['revoked_at'])) {
457 $since = wp_date(get_option('date_format'), (int) $state['revoked_at']);
458 if ($state['last_reason'] === 'revoked') {
459 $sentence = sprintf(
460 /* translators: %s: a date */
461 __('Managed by the licence owner. This licence key has been revoked, and Pro features have been paused since %s; your settings and knowledge are untouched. Contact MxChat support if you believe this is a mistake.', 'mxchat'),
462 $since
463 );
464 } elseif ($state['last_reason'] === 'new_sites_blocked') {
465 $sentence = sprintf(
466 /* translators: 1: this site's domain, 2: a date */
467 __('Managed by the licence owner. New sites are blocked on this licence, so %1$s could not activate; Pro features have been paused since %2$s. Ask the licence holder to allow new sites in their mxchat.ai account, then click Check license now.', 'mxchat'),
468 self::domain(),
469 $since
470 );
471 } else {
472 $sentence = sprintf(
473 /* translators: 1: this site's domain, 2: a date */
474 __('Managed by the licence owner. %1$s was switched off in the licence holder\'s MxChat account, and only they can switch it back on; this site cannot. Pro features have been paused since %2$s; your settings and knowledge are untouched. Once the domain is switched back on at mxchat.ai they return within the hour, or now with Check license now.', 'mxchat'),
475 self::domain(),
476 $since
477 );
478 }
479 return array(
480 'pill_class' => 'mxch-status-pill-disabled',
481 'pill_text' => ($state['last_reason'] === 'revoked') ? __('Revoked', 'mxchat') : __('Switched off', 'mxchat'),
482 'sentence' => $sentence,
483 );
484 }
485
486 if (!empty($state['off_since'])) {
487 return array(
488 'pill_class' => 'mxch-status-pill-warning',
489 'pill_text' => __('Switching off', 'mxchat'),
490 'sentence' => sprintf(
491 /* translators: 1: this site's domain, 2: a date, 3: a date */
492 __('Managed by the licence owner. %1$s was switched off in the licence holder\'s MxChat account on %2$s, and only they can switch it back on. Pro features stay on until %3$s; if the domain is switched back on before then, nothing changes.', 'mxchat'),
493 self::domain(),
494 wp_date(get_option('date_format'), (int) $state['off_since']),
495 wp_date(get_option('date_format'), (int) $state['off_since'] + self::GRACE_SECONDS)
496 ),
497 );
498 }
499
500 if (empty($state['last_check'])) {
501 return array(
502 'pill_class' => ($status === 'active') ? 'mxch-status-pill-active' : 'mxch-status-pill-disabled',
503 'pill_text' => __('Not checked yet', 'mxchat'),
504 'sentence' => __('The first check with mxchat.ai runs within a day.', 'mxchat'),
505 );
506 }
507
508 $ago = human_time_diff((int) $state['last_check'], $now);
509 if ($state['last_result'] === 'error') {
510 return array(
511 'pill_class' => ($status === 'active') ? 'mxch-status-pill-active' : 'mxch-status-pill-disabled',
512 'pill_text' => ($status === 'active') ? __('Active', 'mxchat') : __('Inactive', 'mxchat'),
513 'sentence' => sprintf(
514 /* translators: 1: how long ago, 2: the error code */
515 __('mxchat.ai could not be reached %1$s ago (%2$s). Nothing changed; the check is retried later.', 'mxchat'),
516 $ago,
517 $state['last_reason']
518 ),
519 );
520 }
521 if ($state['last_result'] === 'unknown') {
522 return array(
523 'pill_class' => 'mxch-status-pill-active',
524 'pill_text' => __('Active', 'mxchat'),
525 'sentence' => sprintf(
526 /* translators: %s: how long ago */
527 __('Checked %s ago. mxchat.ai does not list this domain for the licence, which is normal after a site move; Pro stays on.', 'mxchat'),
528 $ago
529 ),
530 );
531 }
532 return array(
533 'pill_class' => 'mxch-status-pill-active',
534 'pill_text' => __('Active', 'mxchat'),
535 'sentence' => sprintf(
536 /* translators: %s: how long ago */
537 __('Confirmed on by mxchat.ai %s ago. Checked once a day.', 'mxchat'),
538 $ago
539 ),
540 );
541 }
542
543 /** True when the grace / off / restored notice has something to say and has not been dismissed today. */
544 public static function notice_visible() {
545 $state = self::state();
546 if (!empty($state['restored_at'])) {
547 return true;
548 }
549 if (empty($state['off_since']) && empty($state['revoked_at'])) {
550 return false;
551 }
552 return empty($state['dismissed_at']) || (time() - (int) $state['dismissed_at']) >= self::DISMISS_SECONDS;
553 }
554
555 /** Pro & Extensions always; any other admin screen only while there is a notice to act on. */
556 public static function enqueue_assets($hook_suffix = '') {
557 if (!current_user_can('manage_options')) {
558 return;
559 }
560 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
561 if ($page !== 'mxchat-activation' && !self::notice_visible()) {
562 return;
563 }
564 $plugin_url = plugin_dir_url(dirname(__FILE__));
565 $version = defined('MXCHAT_VERSION') ? MXCHAT_VERSION : '1.0';
566 wp_enqueue_script('mxchat-license-check-js', $plugin_url . 'js/license-check.js', array('jquery'), $version, true);
567 wp_localize_script('mxchat-license-check-js', 'MxChatLicenseCheck', array(
568 'ajaxUrl' => admin_url('admin-ajax.php'),
569 'nonce' => wp_create_nonce(self::NONCE_CHECK),
570 'dismissNonce' => wp_create_nonce(self::NONCE_DISMISS),
571 'checking' => __('Checking with mxchat.ai…', 'mxchat'),
572 'failed' => __('The check could not run. Try again in a moment.', 'mxchat'),
573 'reloading' => __('Pro status changed. Reloading…', 'mxchat'),
574 ));
575 }
576
577 /**
578 * The WordPress admin notice, on every admin screen except Pro & Extensions
579 * (which renders the same thing inside its own card). Three shapes: grace
580 * period, switched off, and a one-time "back on".
581 */
582 public static function render_notice() {
583 if (!current_user_can('manage_options')) {
584 return;
585 }
586 $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
587 if ($page === 'mxchat-activation') {
588 return;
589 }
590 $state = self::state();
591
592 if (!empty($state['restored_at'])) {
593 $state['restored_at'] = 0;
594 self::save_state($state);
595 ?>
596 <div class="notice notice-success is-dismissible mxchat-license-check-notice">
597 <p><strong><?php esc_html_e('MxChat Pro is back on.', 'mxchat'); ?></strong>
598 <?php echo esc_html(sprintf(
599 /* translators: %s: this site's domain */
600 __('%s was switched back on in the licence holder\'s MxChat account, so Pro features are available again.', 'mxchat'),
601 self::domain()
602 )); ?></p>
603 </div>
604 <?php
605 return;
606 }
607
608 if (!self::notice_visible()) {
609 return;
610 }
611 $view = self::view_model();
612 $revoked = !empty($state['revoked_at']);
613 $pro_url = admin_url('admin.php?page=mxchat-activation');
614 ?>
615 <div class="notice <?php echo $revoked ? 'notice-error' : 'notice-warning'; ?> is-dismissible mxchat-license-check-notice" data-mxchat-license-notice="1">
616 <p><strong><?php echo $revoked
617 ? esc_html__('MxChat Pro is paused on this site.', 'mxchat')
618 : esc_html__('MxChat Pro: this site has been switched off in the licence holder\'s account.', 'mxchat'); ?></strong></p>
619 <p><?php echo esc_html($view['sentence']); ?></p>
620 <p>
621 <button type="button" class="button mxchat-license-check-now"><?php esc_html_e('Check license now', 'mxchat'); ?></button>
622 <a class="button button-link" href="<?php echo esc_url($pro_url); ?>"><?php esc_html_e('Open Pro & Extensions', 'mxchat'); ?></a>
623 <a class="button button-link" href="https://mxchat.ai/my-account/licenses/" target="_blank" rel="noopener"><?php esc_html_e('Open your licenses at mxchat.ai', 'mxchat'); ?></a>
624 <span class="mxchat-license-check-msg" role="status"></span>
625 </p>
626 </div>
627 <?php
628 }
629 }
630