PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.10
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.10
4.4.10 4.4.9.1 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 All 142 releases
learnpress / inc / TemplateHooks / Admin / AdminAddonsPage.php

AdminAddonsPage.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.10, at inc/TemplateHooks/Admin/AdminAddonsPage.php

1,034 lines 34.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace LearnPress\TemplateHooks\Admin;
4
5 use DateTime;
6 use Exception;
7 use LearnPress\Helpers\LPDateTime;
8 use LearnPress\Helpers\Response;
9 use LearnPress\Helpers\Singleton;
10 use LearnPress\Helpers\Template;
11 use LearnPress\Models\UserModel;
12 use LearnPress\Services\AddonService;
13 use LearnPress\TemplateHooks\TemplateAJAX;
14 use LP_Debug;
15 use LP_Helper;
16 use LP_Settings;
17 use stdClass;
18 use Throwable;
19
20 defined( 'ABSPATH' ) || exit();
21
22 /**
23 * Admin Add-ons page renderer.
24 *
25 * @since 4.2.8
26 * @version 1.0.2
27 */
28 class AdminAddonsPage {
29 use Singleton;
30
31 /**
32 * @var array Addons installed, not on wp.org, having newer version. Keyed by plugin basename.
33 */
34 private $addons_need_update = [];
35
36 /**
37 * Init hooks.
38 *
39 * @return void
40 */
41 public function init(): void {
42 if ( is_admin() ) {
43 // Show update notice for addons not on wp.org on plugins.php page.
44 add_filter( 'site_transient_update_plugins', array( $this, 'inject_update_wp_plugins' ) );
45 add_action( 'load-plugins.php', array( $this, 'wp_update_plugin_message' ) );
46 }
47 }
48
49 /**
50 * Inject update info of addons not on wp.org into update_plugins transient
51 * so WP core renders update rows on wp-admin/plugins.php.
52 *
53 * @param object $transient Value of site_transient_update_plugins.
54 *
55 * @return object
56 */
57 public function inject_update_wp_plugins( $transient ): object {
58 try {
59 if ( ! is_object( $transient ) ) {
60 $transient = new stdClass();
61 }
62
63 if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
64 $transient->response = [];
65 }
66
67 if ( ! isset( $transient->no_update ) || ! is_array( $transient->no_update ) ) {
68 $transient->no_update = [];
69 }
70
71 $addons = AddonService::instance()->get_addons();
72 $plugins = get_plugins();
73
74 foreach ( $addons as $slug => $addon ) {
75 // Skip addons hosted on wp.org - core already handles them.
76 if ( ! empty( $addon->is_org ) ) {
77 continue;
78 }
79
80 $basename = $addon->basename ?? '';
81 if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) {
82 continue;
83 }
84
85 $item = (object) [
86 'id' => 'learnpress/' . $slug,
87 'slug' => $slug,
88 'plugin' => $basename,
89 'new_version' => $addon->version ?? '',
90 'url' => $addon->link ?? '',
91 'package' => '',
92 'icons' => isset( $addon->image ) ? [ 'default' => $addon->image ] : [],
93 ];
94
95 $current_version = $plugins[ $basename ]['Version'];
96 if ( ! empty( $item->new_version ) && version_compare( $current_version, $item->new_version, '<' ) ) {
97 $transient->response[ $basename ] = $item;
98 } else {
99 $transient->no_update[ $basename ] = $item;
100 }
101 }
102 } catch ( Throwable $e ) {
103 LP_Debug::error_log( $e );
104 }
105
106 return $transient;
107 }
108
109 /**
110 * Register hooks to append update link into plugin update rows on plugins.php.
111 * Only for installed addons not on wp.org and having a newer version.
112 *
113 * @return void
114 */
115 public function wp_update_plugin_message(): void {
116 try {
117 $addons = AddonService::instance()->get_addons();
118 $plugins = get_plugins();
119
120 foreach ( $addons as $addon ) {
121 if ( ! empty( $addon->is_org ) ) {
122 continue;
123 }
124
125 $basename = $addon->basename ?? '';
126 if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) {
127 continue;
128 }
129
130 $new_version = $addon->version ?? '';
131 if ( empty( $new_version )
132 || version_compare( $plugins[ $basename ]['Version'], $new_version, '>=' ) ) {
133 continue;
134 }
135
136 $this->addons_need_update[ $basename ] = $addon;
137
138 // Priority 5: run before core wp_plugin_update_row (priority 10) to override it.
139 add_action( "after_plugin_row_{$basename}", array( $this, 'render_custom_update_row' ), 5, 2 );
140 }
141 } catch ( Throwable $e ) {
142 LP_Debug::error_log( $e );
143 }
144 }
145
146 /**
147 * Render custom update row to override core default message.
148 * Hook: after_plugin_row_{$basename} - priority 5, before core wp_plugin_update_row.
149 *
150 * @param string $file Plugin basename.
151 * @param array $plugin_data Plugin data.
152 *
153 * @return void
154 */
155 public function render_custom_update_row( string $file, array $plugin_data ): void {
156 // Remove core default update row to override the message.
157 remove_action( "after_plugin_row_{$file}", 'wp_plugin_update_row' );
158
159 $addon = $this->addons_need_update[ $file ] ?? null;
160 if ( ! $addon ) {
161 return;
162 }
163
164 $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
165 $slug = sanitize_key( $addon->basename ?? $file );
166 $new_version = $addon->version ?? '';
167 $link_update = admin_url( 'admin.php?page=learn-press-addons&tab=update' );
168
169 $section = array(
170 'wrap' => sprintf(
171 '<tr class="plugin-update-tr active" id="%s-update" data-slug="%s" data-plugin="%s">',
172 esc_attr( $slug ),
173 esc_attr( $slug ),
174 esc_attr( $file )
175 ),
176 'cell_open' => sprintf(
177 '<td colspan="%s" class="plugin-update colspanchange">',
178 esc_attr( $wp_list_table->get_column_count() )
179 ),
180 'message_open' => '<div class="update-message notice inline notice-warning notice-alt"><p>',
181 'text' => sprintf(
182 /* translators: 1: addon name, 2: new version */
183 esc_html__( 'There is a new version of %1$s (v%2$s) available.', 'learnpress' ),
184 esc_html( $addon->name ?? $plugin_data['Name'] ),
185 esc_html( $new_version )
186 ),
187 'link' => sprintf(
188 ' <a href="%s">%s</a>',
189 esc_url( $link_update ),
190 esc_html__( 'Go to LearnPress Add-ons page to update.', 'learnpress' )
191 ),
192 'message_close' => '</p></div>',
193 'cell_close' => '</td>',
194 'warp-end' => '</tr>',
195 );
196
197 echo Template::combine_components( $section );
198 }
199
200 /**
201 * HTML the LearnPress Add-ons page.
202 *
203 * @return void
204 */
205 public function html_page() {
206 $section = apply_filters(
207 'learn-press/admin/addons/page-section',
208 array(
209 'wrapper' => '<div class="lp-addons-page">',
210 'notice' => sprintf(
211 '<div class="lp-addon-notice">%s</div>',
212 Template::print_message(
213 sprintf(
214 '<div>%s</div>',
215 sprintf(
216 __( 'To download or update add-ons: enter your purchase code (%s), or check your theme\'s plugins tab if using a bundled premium theme.', 'learnpress' ),
217 sprintf(
218 '<a href="%s">%s</a>',
219 'https://thimpress.com/my-account/',
220 __( 'get key', 'learnpress' )
221 ),
222 )
223 ),
224 'info',
225 false
226 )
227 ),
228 'title' => sprintf(
229 '<h1 class="lp-be-page-title">%s</h1>',
230 esc_html__( 'LearnPress Add-ons New', 'learnpress' )
231 ),
232 'subtitle' => sprintf(
233 '<p class="lp-be-page-subtitle">%s</p>',
234 esc_html__(
235 'Discover high-performance Premium & Education themes optimized 100% for LearnPress LMS.',
236 'learnpress'
237 )
238 ),
239 /** @use self::render_addons */
240 'addons' => TemplateAJAX::load_content_via_ajax(
241 array(
242 'id_url' => 'data-addons',
243 'tab' => $_REQUEST['tab'] ?? '',
244 'ex-date-test' => $_REQUEST['ex-date-test'] ?? '',
245 ),
246 array(
247 'class' => self::class,
248 'method' => 'render_addons',
249 )
250 ),
251 'wrapper_end' => '</div>',
252 )
253 );
254
255 echo AdminTemplate::html_on_wp_admin_screen(
256 array(
257 'content' => Template::combine_components( $section ),
258 'title' => '',
259 'id' => 'learn-press-addons',
260 'hide_notices_wp' => true,
261 )
262 );
263 }
264
265 /**
266 * Render add-ons list for the AJAX response.
267 *
268 * @param array $data
269 * @return stdClass
270 */
271 public static function render_addons( array $data = [] ): stdClass {
272 $response = new stdClass();
273
274 try {
275 // Check permission
276 if ( is_multisite() ? ! is_super_admin() : ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) {
277 throw new Exception( __( 'Access denied.', 'learnpress' ) );
278 }
279
280 require_once ABSPATH . 'wp-admin/includes/plugin.php';
281
282 $addons = AddonService::instance()->get_addons();
283 $plugins_installed = get_plugins();
284 $plugins_activated = get_option( 'active_plugins', array() );
285 $active_tab = ! empty( $data['tab'] ) ? LP_Helper::sanitize_params_submitted( $data['tab'] ) : 'all';
286 $keys_purchase = LP_Settings::get_option( AddonService::instance()->key_purchase_addons, array() );
287
288 $section = apply_filters(
289 'learn-press/admin/addons/section',
290 array(
291 'wrapper' => '<div class="lp-addons-wrapper">',
292 'controls' => self::html_controls(
293 array(
294 'active_tab' => $active_tab,
295 )
296 ),
297 'list' => self::html_addons_list(
298 array(
299 'addons' => $addons,
300 'plugins_installed' => $plugins_installed,
301 'plugins_activated' => $plugins_activated,
302 'active_tab' => $active_tab,
303 'keys_purchase' => $keys_purchase,
304 'ex-date-test' => $data['ex-date-test'] ?? '',
305 )
306 ),
307 'wrapper_end' => '</div>',
308 ),
309 $addons,
310 $data
311 );
312
313 $response->content = Template::combine_components( $section );
314 } catch ( Throwable $e ) {
315 $response->content = Template::print_message(
316 $e->getMessage(),
317 Response::STATUS_ERROR,
318 false
319 );
320 }
321
322 return $response;
323 }
324
325 /**
326 * Render the add-ons filter controls: tabs + search.
327 *
328 * @param array $data Renderer data.
329 *
330 * @return string
331 */
332 public static function html_controls( array $data = [] ): string {
333 $active_tab = ! empty( $data['active_tab'] ) ? $data['active_tab'] : 'all';
334 $tabs = array(
335 'all' => sprintf( '%s (<span></span>)', __( 'All', 'learnpress' ) ),
336 'installed' => sprintf( '%s (<span></span>)', __( 'Installed', 'learnpress' ) ),
337 'purchase' => sprintf( '%s (<span></span>)', __( 'Paid', 'learnpress' ) ),
338 'free' => sprintf( '%s (<span></span>)', __( 'Free', 'learnpress' ) ),
339 'update' => sprintf( '%s (<span></span>)', __( 'Updated Available', 'learnpress' ) ),
340 'license' => sprintf( '%s (<span></span>)', __( 'Licensed', 'learnpress' ) ),
341 'not_installed' => sprintf( '%s (<span></span>)', __( 'Not Installed', 'learnpress' ) ),
342 );
343 $addon_categories = array(
344 'create-course' => __( 'Create Course', 'learnpress' ),
345 'manage-course' => __( 'Manage Course', 'learnpress' ),
346 'monetize-course' => __( 'Monetize Course', 'learnpress' ),
347 'payment' => __( 'Payment', 'learnpress' ),
348 'engagement' => __( 'Engagement', 'learnpress' ),
349 'marketing-optimization' => __( 'Marketing Optimization', 'learnpress' ),
350 );
351
352 $section_tabs = array();
353 foreach ( $tabs as $tab => $tab_title ) {
354 $is_active = ( $tab === $active_tab );
355 $active_class = $is_active ? ' nav-tab-active' : '';
356 $aria_pressed = $is_active ? 'true' : 'false';
357
358 $section_tabs[ $tab ] = sprintf(
359 '<button type="button" class="lp-addons-filter__item nav-tab lp-be-nav-tab%s" data-tab="%s" aria-pressed="%s">%s</button>',
360 esc_attr( $active_class ),
361 esc_attr( $tab ),
362 esc_attr( $aria_pressed ),
363 wp_kses_post( $tab_title )
364 );
365 }
366
367 $section = array(
368 'wrapper' => '<div class="lp-addons-controls">',
369 'toolbar' => '<div class="lp-nav-tab-wrapper lp-addons-toolbar lp-be-toolbar">',
370 'toolbar_primary' => '<div class="lp-addons-toolbar__primary lp-be-toolbar__main">',
371 'filter' => sprintf(
372 '<div class="lp-addons-filter" role="group" aria-label="%s">
373 <button type="button" class="lp-addons-filter__toggle" aria-label="%s" aria-expanded="false" aria-haspopup="true">
374 <span class="lp-addons-filter__toggle-content">
375 <span class="lp-addons-filter__toggle-state lp-addons-filter__toggle-state--filter">
376 <svg class="lp-addons-filter__toggle-icon lp-addons-filter__toggle-icon--filter" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
377 <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
378 </svg>
379 <span class="lp-addons-filter__toggle-text">%s</span>
380 </span>
381 <span class="lp-addons-filter__toggle-state lp-addons-filter__toggle-state--close">
382 <svg class="lp-addons-filter__toggle-icon lp-addons-filter__toggle-icon--close" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
383 <line x1="18" y1="6" x2="6" y2="18"></line>
384 <line x1="6" y1="6" x2="18" y2="18"></line>
385 </svg>
386 <span class="lp-addons-filter__toggle-text">%s</span>
387 </span>
388 </span>
389 </button>
390 <div class="lp-addons-filter__menu">',
391 esc_attr__( 'Filter add-ons', 'learnpress' ),
392 esc_attr__( 'Filter add-ons', 'learnpress' ),
393 esc_html__( 'Filter', 'learnpress' ),
394 esc_html__( 'Close', 'learnpress' )
395 ),
396 'tabs' => Template::combine_components( $section_tabs ),
397 'filter_end' => '</div></div>',
398 'search' => '<div class="lp-search-addons lp-addons-search lp-be-search">',
399 'search_btn' => sprintf(
400 '<button type="button" class="lp-addons-search__btn lp-be-search__btn" aria-label="%s" aria-expanded="false">
401 <span class="lp-addons-search__icon lp-icon-search" aria-hidden="true"></span>
402 </button>',
403 esc_attr__( 'Search add-ons', 'learnpress' )
404 ),
405 'search_field' => sprintf(
406 '<div class="lp-addons-search__field lp-be-search__field">
407 <span class="screen-reader-text">%s</span>
408 <div class="lp-addons-search__input-wrap lp-be-search__input-wrap">
409 <span class="lp-addons-search__field-icon lp-be-search__field-icon lp-icon-search" aria-hidden="true"></span>
410 <input id="lp-search-addons__input"
411 class="lp-addons-search__input lp-be-search__input" type="search" placeholder="%s" autocomplete="off"/>
412 </div>
413 <button type="button" class="lp-addons-search__btn-clear lp-be-search__btn-clear" disabled>%s</button>
414 <button type="button" class="lp-addons-search__close lp-be-search__close" aria-label="%s">
415 <span aria-hidden="true">&times;</span>
416 </button>
417 </div>',
418 esc_html__( 'Search add-ons', 'learnpress' ),
419 esc_attr__( 'Search add-ons by name…', 'learnpress' ),
420 esc_html__( 'Clear', 'learnpress' ),
421 esc_attr__( 'Close search', 'learnpress' )
422 ),
423 'search_end' => '</div>',
424 'toolbar_primary_end' => '</div>',
425 'categories' => self::html_categories(
426 array(
427 'addon_categories' => $addon_categories,
428 )
429 ),
430 'toolbar_end' => '</div>',
431 'wrapper_end' => '</div>',
432 );
433
434 return Template::combine_components( $section );
435 }
436
437 /**
438 * Render the add-ons category filter buttons.
439 *
440 * @param array $data Renderer data.
441 *
442 * @return string
443 */
444 public static function html_categories( array $data = [] ): string {
445 $addon_categories = $data['addon_categories'] ?? array();
446 $section = array(
447 'wrapper' => sprintf(
448 '<div class="lp-addons-categories lp-be-categories" role="group" aria-label="%s">',
449 esc_attr__( 'Filter add-ons by category', 'learnpress' )
450 ),
451 'all' => sprintf(
452 '<button type="button" class="lp-addons-category lp-be-category active"
453 data-category="all" aria-pressed="true">%s</button>',
454 esc_html__( 'All', 'learnpress' )
455 ),
456 );
457
458 foreach ( $addon_categories as $category_slug => $category_label ) {
459 $section[ $category_slug ] = sprintf(
460 '<button type="button" class="lp-addons-category lp-be-category"
461 data-category="%s" aria-pressed="false">%s</button>',
462 esc_attr( $category_slug ),
463 esc_html( $category_label )
464 );
465 }
466
467 $section['wrapper_end'] = '</div>';
468
469 return Template::combine_components( $section );
470 }
471
472 /**
473 * Render the list of add-on items.
474 *
475 * @param array $data Renderer data.
476 *
477 * @return string
478 * @throws Exception
479 */
480 public static function html_addons_list( array $data = [] ): string {
481 $addons = $data['addons'] ?? array();
482 $html = '';
483 foreach ( $addons as $slug => $addon ) {
484 $addon->slug = $slug;
485 $html .= self::html_addon_item( $addon, $data );
486 }
487
488 return sprintf( '<div id="lp-addons">%s</div>', $html );
489 }
490
491 /**
492 * Render a single add-on card.
493 *
494 * @param object $addon Current addon object.
495 * @param array $data Renderer data.
496 *
497 * @return string
498 * @throws Exception
499 */
500 public static function html_addon_item( object $addon, array $data = [] ): string {
501 $state = self::get_addon_classes_and_state( $addon, $data );
502
503 $section = apply_filters(
504 'learn-press/admin/addons/item',
505 array(
506 'wrapper' => sprintf(
507 '<div class="lp-addon-item %s" data-slug="%s" data-category="%s" data-addon="%s">',
508 esc_attr( implode( ' ', $state['classes_status'] ) ),
509 esc_attr( $addon->slug ),
510 esc_attr( $state['addon_category'] ),
511 esc_attr( htmlentities2( wp_json_encode( $addon ) ) )
512 ),
513 'content' => '<div class="lp-addon-item__content">',
514 'header' => self::html_addon_header( $addon ),
515 'license' => ! $state['is_free']
516 ? self::html_addon_license( $addon, $state )
517 : '',
518 'description' => sprintf(
519 '<p class="lp-addon-item__description" title="%s">%s</p>',
520 esc_attr( $addon->description ?? '' ),
521 esc_html( $addon->description ?? '' )
522 ),
523 'meta' => self::html_addon_meta(
524 $addon,
525 array(
526 'version_current' => $state['version_current'],
527 )
528 ),
529 'new_update' => $state['is_updated']
530 ? sprintf(
531 '<span class="new-update">%s: <strong>%s</strong></span>',
532 esc_html__( 'Latest version', 'learnpress' ),
533 esc_html( $state['version_latest'] )
534 )
535 : '',
536 'content_end' => '</div>',
537 'actions' => self::html_addon_actions( $addon ),
538 'purchase' => self::html_purchase_panel(
539 $addon,
540 array(
541 'purchase_code_masked' => $state['purchase_code_masked'],
542 )
543 ),
544 'wrapper_end' => '</div>',
545 ),
546 $addon,
547 $data
548 );
549
550 return Template::combine_components( $section );
551 }
552
553 /**
554 * Compute status classes and state flags for an addon.
555 *
556 * @param object $addon Current addon object.
557 * @param array $data Addon state data.
558 *
559 * @return array
560 * @throws Exception
561 */
562 public static function get_addon_classes_and_state( object $addon, array $data = [] ): array {
563 $plugins_installed = $data['plugins_installed'] ?? array();
564 $plugins_activated = $data['plugins_activated'] ?? array();
565 $active_tab = $data['active_tab'] ?? 'all';
566 $keys_purchase = $data['keys_purchase'] ?? array();
567 $is_free = $addon->is_free ?? 0;
568 $addon_base = $addon->basename ?? '';
569 $version_latest = $addon->version ?? 0;
570 $version_current = $addon->version ?? 0;
571 $classes_status = array();
572 $addon_purchased = $addon->purchase_info ?? false;
573 $addon_category = implode( ' ', array_map( 'sanitize_title', $addon->category ?? array() ) );
574 $date_expired_str = '';
575 $number_days_remaining = null;
576 $date_expired = null;
577
578 if ( ! $is_free ) {
579 $classes_status[] = 'purchase';
580 } else {
581 $classes_status[] = 'free';
582 }
583
584 if ( isset( $plugins_installed[ $addon_base ] ) ) {
585 $is_installed = true;
586 $classes_status[] = 'installed';
587 $version_current = $plugins_installed[ $addon_base ]['Version'];
588 } else {
589 $is_installed = false;
590 $classes_status[] = 'not_installed';
591 }
592
593 if ( in_array( $addon_base, $plugins_activated, true ) ) {
594 $classes_status[] = 'activated';
595 }
596
597 $is_updated = false;
598 if ( $is_installed && version_compare( $version_current, $version_latest, '<' ) ) {
599 $classes_status[] = 'update';
600 $is_updated = true;
601 }
602
603 if ( $addon_purchased ) {
604 $classes_status[] = 'license';
605 $date_expired_str = ! empty( $data['ex-date-test'] ) ? $data['ex-date-test'] : ( $addon_purchased->date_expire ?? '' );
606 if ( ! empty( $date_expired_str ) ) {
607 $date_expired = new DateTime( $date_expired_str );
608 $date_now = new DateTime( gmdate( 'Y-m-d' ) );
609 $date_diff = date_diff( $date_now, $date_expired );
610 $number_days_remaining = $date_diff->invert ? 0 : $date_diff->days;
611 }
612 }
613
614 $show_license_panel = ! $is_free && $is_installed;
615 $license_status = 'not-activated';
616 if ( $addon_purchased ) {
617 $license_status = 'active';
618 if ( ! empty( $date_expired_str )
619 && isset( $number_days_remaining )
620 && 0 === $number_days_remaining ) {
621 $license_status = 'expired';
622 $classes_status[] = 'expired';
623 }
624 }
625
626 $purchase_code = $keys_purchase[ $addon->slug ] ?? '';
627 $purchase_code_masked = AddonService::mask_purchase_code( $purchase_code );
628
629 if ( ! in_array( $active_tab, $classes_status, true ) && 'all' !== $active_tab ) {
630 $classes_status[] = 'hide';
631 }
632
633 return array(
634 'classes_status' => $classes_status,
635 'is_free' => $is_free,
636 'is_installed' => $is_installed,
637 'is_updated' => $is_updated,
638 'version_current' => $version_current,
639 'version_latest' => $version_latest,
640 'license_status' => $license_status,
641 'number_days_remaining' => $number_days_remaining,
642 'date_expired' => $date_expired,
643 'purchase_code' => $purchase_code,
644 'purchase_code_masked' => $purchase_code_masked,
645 'show_license_panel' => $show_license_panel,
646 'addon_category' => $addon_category,
647 );
648 }
649
650 /**
651 * Render the header section of an addon item.
652 *
653 * @param object $addon Current addon object.
654 * @param array $data Renderer data.
655 *
656 * @return string
657 */
658 public static function html_addon_header( object $addon, array $data = [] ): string {
659 $section = array(
660 'wrapper' => '<div class="lp-addon-item__header">',
661 'image' => sprintf(
662 '<img class="lp-addon-item__image" src="%s" alt=""/>',
663 esc_url( $addon->image ?? '' )
664 ),
665 'heading' => '<div class="lp-addon-item__heading">',
666 'title' => '<h3 class="lp-addon-item__title">',
667 'link' => sprintf(
668 '<a href="%s" target="_blank" rel="noopener">%s</a>',
669 esc_url( $addon->link ?? '' ),
670 esc_html( $addon->name ?? '' )
671 ),
672 'title_end' => '</h3>',
673 'badge' => ! empty( $addon->badge ) ? sprintf(
674 '<span class="lp-addon-item__badge">%s</span>',
675 esc_html( $addon->badge )
676 ) : '',
677 'heading_end' => '</div>',
678 'wrapper_end' => '</div>',
679 );
680
681 return Template::combine_components( $section );
682 }
683
684 /**
685 * Render the license status block for a paid addon.
686 *
687 * @param object $addon Current addon object.
688 * @param array $data Renderer data.
689 *
690 * @return string
691 */
692 public static function html_addon_license( object $addon, array $data = [] ): string {
693 $license_status = $data['license_status'] ?? '';
694 $number_days_remaining = $data['number_days_remaining'] ?? null;
695 /** @var DateTime $date_expired */
696 $date_expired = $data['date_expired'] ?? null;
697 $purchase_code = $data['purchase_code'] ?? '';
698 $purchase_code_masked = $data['purchase_code_masked'] ?? '';
699 $show_license_panel = $data['show_license_panel'] ?? false;
700 $license_status_label = 'active' === $license_status
701 ? __( 'Active', 'learnpress' )
702 : ( 'expired' === $license_status
703 ? __( 'Expired', 'learnpress' )
704 : ( 'deactivated' === $license_status
705 ? __( 'Deactivated', 'learnpress' )
706 : __( 'Not Activated', 'learnpress' ) ) );
707
708 $expiry_hidden = empty( $date_expired )
709 || ! in_array( $license_status, array( 'active', 'expired' ), true ) ? ' hidden' : '';
710
711 $expiry_text = '';
712 if ( ! empty( $date_expired ) ) {
713 $lpDate = new LPDateTime( $date_expired->format( LPDateTime::FORMAT_MYSQL ) );
714
715 $expiry_format = 'expired' === $license_status
716 ? __( '(on %s)', 'learnpress' )
717 : __( '(Updates until %s)', 'learnpress' );
718 $expiry_text = esc_html( sprintf( $expiry_format, $lpDate->format( LPDateTime::FORMAT_I18N_DATE ) ) );
719 }
720
721 $link_extend = add_query_arg(
722 'purchase_code',
723 $purchase_code,
724 AddonService::instance()->link_extend_site
725 );
726 $html_extend_link = sprintf(
727 '<a class="need-extend__link" href="%s" target="_blank" rel="noopener">%s</a>',
728 esc_url( $link_extend ),
729 esc_html__( 'Extend now', 'learnpress' )
730 );
731
732 $html_near_expire = '';
733 if ( ! empty( $addon->purchase_info ) ) {
734 if ( isset( $number_days_remaining ) && $number_days_remaining > 0 && $number_days_remaining < 61 ) {
735 $message_near_expire = sprintf(
736 __( 'You have a license for this item with %s day(s) of update & support remaining. Please extend your update & support license to continue receiving the latest versions and customer support from Thimpress before it expires.', 'learnpress' ),
737 sprintf( '<strong class="need-extend__days">%d</strong>', $number_days_remaining )
738 );
739
740 $html_near_expire = sprintf(
741 '<span class="need-extend">%s %s</span>',
742 $message_near_expire,
743 $html_extend_link
744 );
745 }
746 }
747
748 $text_btn_to_purchase_code = esc_html__( 'Activate Now', 'learnpress' );
749 if ( ! empty( $purchase_code ) ) {
750 $text_btn_to_purchase_code = esc_html__( 'Change License', 'learnpress' );
751 }
752
753 $section = array(
754 'wrapper' => sprintf(
755 '<div class="lp-addon-license lp-addon-license--%s" data-purchase-code-masked="%s"%s>',
756 esc_attr( $license_status ),
757 esc_attr( $purchase_code_masked ),
758 $show_license_panel ? '' : ' hidden'
759 ),
760 'summary' => '<div class="lp-addon-license__summary">',
761 'label' => sprintf(
762 '<span>%s</span>',
763 esc_html__( 'License:', 'learnpress' )
764 ),
765 'status' => sprintf(
766 '<strong class="lp-addon-license__status">%s</strong>',
767 esc_html( $license_status_label )
768 ),
769 'expiry' => sprintf(
770 '<span class="lp-addon-license__expiry"%s>%s</span>',
771 $expiry_hidden,
772 $expiry_text
773 ),
774 'extend_link' => 'expired' === $license_status ? $html_extend_link : '',
775 'summary_end' => '</div>',
776 'near_expire' => $html_near_expire,
777 'cancel_btn' => '<button
778 class="lp-button btn-addon-action lp-be-btn lp-be-btn--outline lp-addon-purchase__cancel-clear lp-hidden"
779 data-action="cancel-clear-license" type="button">',
780 'cancel_text' => sprintf(
781 '<span class="text">%s</span>',
782 esc_html__( 'Cancel', 'learnpress' )
783 ),
784 'cancel_end' => '</button>',
785 'manage' => sprintf(
786 '<button class="btn-addon-action lp-addon-license__manage"
787 data-action="update-purchase-code" type="button">%s</button>',
788 $text_btn_to_purchase_code
789 ),
790 'wrapper_end' => '</div>',
791 );
792
793 return Template::combine_components( $section );
794 }
795
796 /**
797 * Render addon meta: version, docs, view details.
798 *
799 * @param object $addon Current addon object.
800 * @param array $data Renderer data.
801 *
802 * @return string
803 */
804 public static function html_addon_meta( object $addon, array $data = [] ): string {
805 $version_current = $data['version_current'] ?? '';
806 $section = array(
807 'wrapper' => '<div class="lp-addon-item__meta">',
808 'version' => $version_current
809 ? sprintf(
810 '<span>%s <span class="addon-version-current">%s</span></span>',
811 esc_html__( 'Version', 'learnpress' ),
812 esc_html( $version_current )
813 )
814 : '',
815 'docs' => ! empty( $addon->link_doc )
816 ? sprintf(
817 '<a href="%s" target="_blank" rel="noopener">%s</a>',
818 esc_url( $addon->link_doc ),
819 esc_html__( 'Docs', 'learnpress' )
820 )
821 : '',
822 'details' => sprintf(
823 '<a href="%s" target="_blank" rel="noopener">%s</a>',
824 esc_url( $addon->link ?? '' ),
825 esc_html__( 'View Details', 'learnpress' )
826 ),
827 'settings' => ! empty( $addon->setting )
828 ? sprintf(
829 '<a data-action="setting" href="%s" target="_blank" rel="noopener">%s</a>',
830 esc_url( site_url( $addon->setting ) ),
831 esc_html__( 'Settings', 'learnpress' )
832 )
833 : '',
834 'wrapper_end' => '</div>',
835 );
836
837 return Template::combine_components( $section );
838 }
839
840 /**
841 * Render addon price block.
842 *
843 * @param object $addon Current addon object.
844 * @param array $data Renderer data.
845 *
846 * @return string
847 */
848 public static function html_addon_price( object $addon, array $data = [] ): string {
849 $is_free = $addon->is_free ?? 0;
850
851 if ( $is_free ) {
852 return sprintf(
853 '<strong class="lp-addon-item__price-regular lp-addon-item__price-regular--free">%s</strong>',
854 esc_html__( 'Free', 'learnpress' )
855 );
856 }
857
858 if ( ! empty( $addon->sale_price ) ) {
859 $section = array(
860 'sale' => sprintf(
861 '<strong class="lp-addon-item__price-sale">$%s</strong>',
862 esc_html( number_format_i18n( $addon->sale_price ) )
863 ),
864 'regular' => sprintf(
865 '<del class="lp-addon-item__price-regular">$%s</del>',
866 esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
867 ),
868 );
869
870 return Template::combine_components( $section );
871 }
872
873 return sprintf(
874 '<strong class="lp-addon-item__price-regular lp-addon-item__price-regular--current">$%s</strong>',
875 esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
876 );
877 }
878
879 /**
880 * Render addon action buttons.
881 *
882 * @param object $addon Current addon object.
883 * @param array $data Renderer data.
884 *
885 * @return string
886 */
887 public static function html_addon_actions( object $addon, array $data = [] ): string {
888 $section_right = apply_filters(
889 'learn-press/admin/addons/actions/section_right',
890 array(
891 'install' => $addon->is_free && ! $addon->is_org
892 ? sprintf(
893 '<a class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="install" href="%s"
894 target="_blank" rel="noopener">%s</a>',
895 esc_url( $addon->link ?? '' ),
896 esc_html__( 'Install', 'learnpress' )
897 )
898 : sprintf(
899 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="install">
900 <span class="text">%s</span>
901 </button>',
902 esc_html__( 'Install', 'learnpress' )
903 ),
904 'purchase' => sprintf(
905 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="purchase">%s</button>',
906 esc_html__( 'Install', 'learnpress' )
907 ),
908 'update' => sprintf(
909 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm lp-be-btn--primary" data-action="update">
910 <span class="text">%s</span>
911 </button>',
912 esc_html__( 'Update', 'learnpress' )
913 ),
914 'deactivate' => sprintf(
915 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm" data-action="deactivate">
916 <span class="text">%s</span>
917 </button>',
918 esc_html__( 'Deactivate', 'learnpress' )
919 ),
920 'activate' => sprintf(
921 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--sm" data-action="activate">
922 <span class="text">%s</span>
923 </button>',
924 esc_html__( 'Activate', 'learnpress' )
925 ),
926 ),
927 $addon,
928 $data
929 );
930
931 $section = array(
932 'wrapper' => '<div class="lp-addon-item__actions">',
933 'actions_left' => '<div class="lp-addon-item__actions__left">',
934 'price' => sprintf(
935 '<div class="lp-addon-item__price">%s</div>',
936 self::html_addon_price( $addon )
937 ),
938 'actions_left_end' => '</div>',
939 'actions_right' => '<div class="lp-addon-item__actions__right">',
940 'actions' => Template::combine_components( $section_right ),
941 'actions_right_end' => '</div>',
942 'wrapper_end' => '</div>',
943 );
944
945 return Template::combine_components( $section );
946 }
947
948 /**
949 * Render the purchase-code overlay panel.
950 *
951 * @param object $addon Current addon object.
952 * @param array $data Renderer data.
953 *
954 * @return string
955 */
956 public static function html_purchase_panel( object $addon, array $data = [] ): string {
957 $purchase_code_masked = $data['purchase_code_masked'] ?? '';
958 $has_purchase_code = ! empty( $purchase_code_masked );
959
960 $panel = array(
961 'wrapper' => '<div class="purchase-install">',
962 'header' => '<div class="lp-addon-purchase__header">',
963 'title' => sprintf(
964 '<strong class="lp-addon-purchase__title">%s</strong>',
965 esc_html__( 'Purchase Code', 'learnpress' )
966 ),
967 'close' => sprintf(
968 '<button class="btn-addon-action
969 lp-addon-purchase__close" data-action="cancel" type="button" aria-label="%s">',
970 esc_attr__( 'Close', 'learnpress' )
971 ),
972 'close_icon' => '<span class="lp-addon-purchase__close-symbol" aria-hidden="true">&times;</span>',
973 'close_end' => '</button>',
974 'header_end' => '</div>',
975 'field' => '<label class="lp-addon-purchase__field">',
976 'field_text' => sprintf(
977 '<span class="screen-reader-text">%s</span>',
978 esc_html__( 'Purchase Code', 'learnpress' )
979 ),
980 'field_input' => sprintf(
981 '<input type="text" class="enter-purchase-code" placeholder="%s" value="%s"%s>',
982 esc_attr__( 'Enter Purchase Code', 'learnpress' ),
983 esc_attr( $purchase_code_masked ),
984 $has_purchase_code ? ' disabled' : ''
985 ),
986 'field_end' => '</label>',
987 'submit' => sprintf(
988 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--primary lp-addon-purchase__submit%s"
989 data-action="install" type="button">',
990 $has_purchase_code ? ' lp-hidden' : ''
991 ),
992 'submit_text' => sprintf( '<span class="text">%s</span>', esc_html__( 'Verify & Activate', 'learnpress' ) ),
993 'submit_end' => '</button>',
994 'cancel_clear' => '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--outline lp-addon-purchase__cancel-clear lp-hidden"
995 data-action="cancel-clear-license" type="button">',
996 'cancel_clear_text' => sprintf(
997 '<span class="text">%s</span>',
998 esc_html__( 'Cancel', 'learnpress' )
999 ),
1000 'cancel_clear_end' => '</button>',
1001 'clear' => sprintf(
1002 '<button class="lp-button btn-addon-action lp-be-btn lp-be-btn--primary lp-addon-purchase__clear%s"
1003 data-action="clear-license" type="button">',
1004 $has_purchase_code ? '' : ' lp-hidden'
1005 ),
1006 'clear_text' => sprintf( '<span class="text">%s</span>', esc_html__( 'Clear', 'learnpress' ) ),
1007 'clear_end' => '</button>',
1008 'divider' => sprintf(
1009 '<div class="lp-addon-purchase__divider"><span>%s</span></div>',
1010 $has_purchase_code
1011 ? esc_html__( 'Or', 'learnpress' )
1012 : esc_html__( 'Don\'t have a code?', 'learnpress' )
1013 ),
1014 'buy' => sprintf(
1015 '<a class="btn-addon-action lp-addon-purchase__buy lp-be-btn lp-be-btn--outline" href="%s" target="_blank" rel="noopener">%s</a>',
1016 esc_url( $addon->link ?? '' ),
1017 esc_html__( 'Buy Now', 'learnpress' )
1018 ),
1019 'wrapper_end' => '</div>',
1020 );
1021
1022 $section = array(
1023 'wrapper' => '<div class="lp-addon-item__purchase">',
1024 'purchase' => '<div class="lp-addon-item__purchase__wrapper">',
1025 'panel' => Template::combine_components( $panel ),
1026 'purchase_code' => '<input type="hidden" name="purchase-code" value="">',
1027 'purchase_end' => '</div>',
1028 'wrapper_end' => '</div>',
1029 );
1030
1031 return Template::combine_components( $section );
1032 }
1033 }
1034