PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.9
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 4.1.7.3.1 4.1.7.3.2 All 140 releases
← All changes | inc/TemplateHooks/Admin/AdminAddonsPage.php +948 -25 4.4.7 → 4.4.9 View file →
@@ -1,10 +1,22 @@
1 1 <?php
2 2
3 3 namespace LearnPress\TemplateHooks\Admin;
4 4
5 +use DateTime;
6 +use Exception;
7 +use LearnPress\Helpers\LPDateTime;
8 +use LearnPress\Helpers\Response;
5 9 use LearnPress\Helpers\Singleton;
6 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;
7 19
8 20 defined( 'ABSPATH' ) || exit();
9 21
10 22 /**
@@ -10,54 +22,965 @@
10 22 /**
11 23 * Admin Add-ons page renderer.
12 24 *
13 25 * @since 4.2.8
26 + * @version 1.0.2
14 27 */
15 28 class AdminAddonsPage {
16 29 use Singleton;
17 30
18 31 /**
19 - * Singleton initialization hook.
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.
20 38 *
21 39 * @return void
22 40 */
23 - public function init(): void {}
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 + }
24 48
25 49 /**
26 - * Render the LearnPress Add-ons page.
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.
27 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_remote_data();
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 + *
28 113 * @return void
29 114 */
115 + public function wp_update_plugin_message(): void {
116 + try {
117 + $addons = AddonService::instance()->get_remote_data();
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 + */
30 205 public function html_page() {
31 - ob_start();
32 - lp_skeleton_animation_html( 20 );
33 - $html_loading = ob_get_clean();
206 + /** @use self::render_addons */
207 + $section = apply_filters(
208 + 'learn-press/admin/addons/page-section',
209 + array(
210 + 'wrapper' => '<div class="lp-addons-page">',
211 + 'notice' => sprintf(
212 + '<div class="lp-addon-notice">%s</div>',
213 + Template::print_message(
214 + sprintf(
215 + '<div>%s</div>',
216 + sprintf(
217 + __( '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' ),
218 + sprintf(
219 + '<a href="%s">%s</a>',
220 + 'https://thimpress.com/my-account/',
221 + __( 'get key', 'learnpress' )
222 + ),
223 + )
224 + ),
225 + 'info',
226 + false
227 + )
228 + ),
229 + 'subtitle' => sprintf(
230 + '<p class="lp-addons-page-subtitle">%s</p>',
231 + esc_html__(
232 + 'Discover high-performance Premium & Education themes optimized 100% for LearnPress LMS.',
233 + 'learnpress'
234 + )
235 + ),
236 + 'addons' => TemplateAJAX::load_content_via_ajax(
237 + array(
238 + 'id_url' => 'data-addons',
239 + 'tab' => $_REQUEST['tab'] ?? '',
240 + ),
241 + array(
242 + 'class' => self::class,
243 + 'method' => 'render_addons',
244 + )
245 + ),
246 + 'wrapper_end' => '</div>',
247 + )
248 + );
34 249
250 + echo AdminTemplate::html_on_wp_admin_screen(
251 + array(
252 + 'content' => Template::combine_components( $section ),
253 + 'title' => __( 'LearnPress Add-ons', 'learnpress' ),
254 + 'id' => 'learn-press-addons',
255 + )
256 + );
257 + }
258 +
259 + /**
260 + * Render add-ons list for the AJAX response.
261 + *
262 + * @param array $data
263 + * @return stdClass
264 + */
265 + public static function render_addons( array $data = [] ): stdClass {
266 + $response = new stdClass();
267 +
268 + try {
269 + // Check permission
270 + if ( is_multisite() ? ! is_super_admin() : ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) {
271 + throw new Exception( __( 'Access denied.', 'learnpress' ) );
272 + }
273 +
274 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
275 +
276 + $addons = AddonService::instance()->get_addons();
277 + $plugins_installed = get_plugins();
278 + $plugins_activated = get_option( 'active_plugins', array() );
279 + $active_tab = LP_Helper::sanitize_params_submitted( $data['tab'] ) ?? 'all';
280 + $keys_purchase = LP_Settings::get_option( AddonService::instance()->key_purchase_addons, array() );
281 +
282 + $section = apply_filters(
283 + 'learn-press/admin/addons/section',
284 + array(
285 + 'wrapper' => '<div class="lp-addons-wrapper">',
286 + 'controls' => self::html_controls(
287 + array(
288 + 'active_tab' => $active_tab,
289 + )
290 + ),
291 + 'list' => self::html_addons_list(
292 + array(
293 + 'addons' => $addons,
294 + 'plugins_installed' => $plugins_installed,
295 + 'plugins_activated' => $plugins_activated,
296 + 'active_tab' => $active_tab,
297 + 'keys_purchase' => $keys_purchase,
298 + )
299 + ),
300 + 'wrapper_end' => '</div>',
301 + ),
302 + $addons,
303 + $data
304 + );
305 +
306 + $response->content = Template::combine_components( $section );
307 + } catch ( Throwable $e ) {
308 + $response->content = Template::print_message(
309 + $e->getMessage(),
310 + Response::STATUS_ERROR,
311 + false
312 + );
313 + }
314 +
315 + return $response;
316 + }
317 +
318 + /**
319 + * Render the add-ons filter controls: tabs + search.
320 + *
321 + * @param array $data Renderer data.
322 + *
323 + * @return string
324 + */
325 + public static function html_controls( array $data = [] ): string {
326 + $active_tab = $data['active_tab'] ?? 'all';
327 + $tabs = array(
328 + 'all' => sprintf( '%s (<span></span>)', __( 'All', 'learnpress' ) ),
329 + 'installed' => sprintf( '%s (<span></span>)', __( 'Installed', 'learnpress' ) ),
330 + 'purchase' => sprintf( '%s (<span></span>)', __( 'Paid', 'learnpress' ) ),
331 + 'free' => sprintf( '%s (<span></span>)', __( 'Free', 'learnpress' ) ),
332 + 'update' => sprintf( '%s (<span></span>)', __( 'Updated', 'learnpress' ) ),
333 + 'license' => sprintf( '%s (<span></span>)', __( 'License', 'learnpress' ) ),
334 + 'not_installed' => sprintf( '%s (<span></span>)', __( 'Not Installed', 'learnpress' ) ),
335 + );
336 + $addon_categories = array(
337 + 'create-course' => __( 'Create Course', 'learnpress' ),
338 + 'engagement' => __( 'Engagement', 'learnpress' ),
339 + 'learnpress' => __( 'LearnPress', 'learnpress' ),
340 + 'manage-course' => __( 'Manage Course', 'learnpress' ),
341 + 'marketing-optimization' => __( 'Marketing Optimization', 'learnpress' ),
342 + 'monetize-course' => __( 'Monetize Course', 'learnpress' ),
343 + 'payment' => __( 'Payment', 'learnpress' ),
344 + );
345 +
346 + $section_tabs = array();
347 + foreach ( $tabs as $tab => $tab_title ) {
348 + $active_class = ( $tab === $active_tab ) ? ' nav-tab-active' : '';
349 +
350 + if ( $active_class ) {
351 + $section_tabs[ $tab ] = sprintf(
352 + '<a class="lp-addons-filter__item nav-tab%s" data-tab="%s" href="#" aria-pressed="true">%s</a>',
353 + esc_attr( $active_class ),
354 + esc_attr( $tab ),
355 + wp_kses_post( $tab_title )
356 + );
357 + } else {
358 + $section_tabs[ $tab ] = sprintf(
359 + '<a class="lp-addons-filter__item nav-tab"
360 + data-tab="%s" aria-pressed="false" href="?page=learn-press-addons&tab=%s">%s</a>',
361 + esc_attr( $tab ),
362 + esc_attr( $tab ),
363 + wp_kses_post( $tab_title )
364 + );
365 + }
366 + }
367 +
368 + $section = array(
369 + 'wrapper' => '<div class="lp-addons-controls">',
370 + 'toolbar' => '<div class="lp-nav-tab-wrapper lp-addons-toolbar">',
371 + 'filter' => sprintf(
372 + '<div class="lp-addons-filter" role="group" aria-label="%s">',
373 + esc_attr__( 'Filter add-ons', 'learnpress' )
374 + ),
375 + 'tabs' => Template::combine_components( $section_tabs ),
376 + 'filter_end' => '</div>',
377 + 'search' => '<label class="lp-search-addons lp-addons-search">',
378 + 'search_text' => sprintf(
379 + '<span class="screen-reader-text">%s</span>',
380 + esc_html__( 'Search add-ons', 'learnpress' )
381 + ),
382 + 'search_icon' => '<span class="lp-addons-search__icon lp-icon-search" aria-hidden="true"></span>',
383 + 'search_input' => sprintf(
384 + '<input id="lp-search-addons__input"
385 + class="lp-addons-search__input" type="search" placeholder="%s"/>',
386 + esc_attr__( 'Search add-ons by name…', 'learnpress' )
387 + ),
388 + 'search_end' => '</label>',
389 + 'toolbar_end' => '</div>',
390 + 'categories' => self::html_categories(
391 + array(
392 + 'addon_categories' => $addon_categories,
393 + )
394 + ),
395 + 'wrapper_end' => '</div>',
396 + );
397 +
398 + return Template::combine_components( $section );
399 + }
400 +
401 + /**
402 + * Render the add-ons category filter buttons.
403 + *
404 + * @param array $data Renderer data.
405 + *
406 + * @return string
407 + */
408 + public static function html_categories( array $data = [] ): string {
409 + $addon_categories = $data['addon_categories'] ?? array();
410 + $section = array(
411 + 'wrapper' => sprintf(
412 + '<div class="lp-addons-categories" role="group" aria-label="%s">',
413 + esc_attr__( 'Filter add-ons by category', 'learnpress' )
414 + ),
415 + 'all' => sprintf(
416 + '<button type="button" class="lp-addons-category active"
417 + data-category="all" aria-pressed="true">%s <span class="lp-addons-category__count"></span>
418 + </button>',
419 + esc_html__( 'All', 'learnpress' )
420 + ),
421 + );
422 +
423 + foreach ( $addon_categories as $category_slug => $category_label ) {
424 + $section[ $category_slug ] = sprintf(
425 + '<button type="button" class="lp-addons-category"
426 + data-category="%s" aria-pressed="false">%s <span class="lp-addons-category__count"></span>
427 + </button>',
428 + esc_attr( $category_slug ),
429 + esc_html( $category_label )
430 + );
431 + }
432 +
433 + $section['wrapper_end'] = '</div>';
434 +
435 + return Template::combine_components( $section );
436 + }
437 +
438 + /**
439 + * Render the list of add-on items.
440 + *
441 + * @param array $data Renderer data.
442 + *
443 + * @return string
444 + * @throws Exception
445 + */
446 + public static function html_addons_list( array $data = [] ): string {
447 + $addons = $data['addons'] ?? array();
448 + $html = '';
449 + foreach ( $addons as $slug => $addon ) {
450 + $addon->slug = $slug;
451 + $html .= self::html_addon_item( $addon, $data );
452 + }
453 +
454 + return sprintf( '<div id="lp-addons">%s</div>', $html );
455 + }
456 +
457 + /**
458 + * Render a single add-on card.
459 + *
460 + * @param object $addon Current addon object.
461 + * @param array $data Renderer data.
462 + *
463 + * @return string
464 + * @throws Exception
465 + */
466 + public static function html_addon_item( object $addon, array $data = [] ): string {
467 + $state = self::get_addon_classes_and_state( $addon, $data );
468 +
35 469 $section = apply_filters(
36 - 'learn-press/admin/manager-addons/section',
470 + 'learn-press/admin/addons/item',
37 471 array(
38 - 'label' => sprintf(
39 - '<h1>%s</h1>',
40 - __( 'LearnPress Add-ons', 'learnpress' )
472 + 'wrapper' => sprintf(
473 + '<div class="lp-addon-item %s" data-slug="%s" data-category="%s" data-addon="%s">',
474 + esc_attr( implode( ' ', $state['classes_status'] ) ),
475 + esc_attr( $addon->slug ),
476 + esc_attr( $state['addon_category'] ),
477 + esc_attr( htmlentities2( wp_json_encode( $addon ) ) )
41 478 ),
42 - 'note-theme' => sprintf(
43 - '<p style="color: rgba(255,0,0,0.76)"><strong><i>%s</i></strong></p>',
44 - __( '* If you use a Premium Theme that includes LearnPress add-ons, you can go to the <strong>Plugins</strong> tab on Dashboard of theme to download or update them.', 'learnpress' )
479 + 'content' => '<div class="lp-addon-item__content">',
480 + 'header' => self::html_addon_header( $addon ),
481 + 'license' => ! $state['is_free']
482 + ? self::html_addon_license(
483 + $addon,
484 + array(
485 + 'license_status' => $state['license_status'],
486 + 'number_days_remaining' => $state['number_days_remaining'],
487 + 'date_expired' => $state['date_expired'],
488 + 'purchase_code_masked' => $state['purchase_code_masked'],
489 + 'show_license_panel' => $state['show_license_panel'],
490 + )
491 + )
492 + : '',
493 + 'description' => sprintf(
494 + '<p class="lp-addon-item__description" title="%s">%s</p>',
495 + esc_attr( $addon->description ?? '' ),
496 + esc_html( $addon->description ?? '' )
45 497 ),
46 - 'note-addon' => sprintf(
47 - '<p>%s</p>',
48 - sprintf(
49 - __( 'If you have purchased a premium add-on separately, you can enter your purchase code (%s) to download or update the add-ons here.', 'learnpress' ),
50 - sprintf(
51 - '<a href="%s" target="_blank">%s</a>',
52 - 'https://thimpress.com/my-account/',
53 - __( 'get from your account', 'learnpress' )
54 - )
498 + 'meta' => self::html_addon_meta(
499 + $addon,
500 + array(
501 + 'version_current' => $state['version_current'],
55 502 )
56 503 ),
57 - 'list' => sprintf( '<div class="lp-addons-page">%s</div>', $html_loading ),
58 - )
504 + 'new_update' => $state['is_updated']
505 + ? sprintf(
506 + '<span class="new-update">%s: <strong>%s</strong></span>',
507 + esc_html__( 'Latest version', 'learnpress' ),
508 + esc_html( $state['version_latest'] )
509 + )
510 + : '',
511 + 'content_end' => '</div>',
512 + 'actions' => self::html_addon_actions(
513 + $addon,
514 + array(
515 + 'is_free' => $state['is_free'],
516 + 'is_installed' => $state['is_installed'],
517 + 'is_updated' => $state['is_updated'],
518 + )
519 + ),
520 + 'purchase' => self::html_purchase_panel(
521 + $addon,
522 + array(
523 + 'purchase_code_masked' => $state['purchase_code_masked'],
524 + )
525 + ),
526 + 'wrapper_end' => '</div>',
527 + ),
528 + $addon,
529 + $data
59 530 );
60 531
61 - echo Template::combine_components( $section );
532 + return Template::combine_components( $section );
533 + }
534 +
535 + /**
536 + * Compute status classes and state flags for an addon.
537 + *
538 + * @param object $addon Current addon object.
539 + * @param array $data Addon state data.
540 + *
541 + * @return array
542 + * @throws Exception
543 + */
544 + public static function get_addon_classes_and_state( object $addon, array $data = [] ): array {
545 + $plugins_installed = $data['plugins_installed'] ?? array();
546 + $plugins_activated = $data['plugins_activated'] ?? array();
547 + $active_tab = $data['active_tab'] ?? 'all';
548 + $keys_purchase = $data['keys_purchase'] ?? array();
549 + $is_free = $addon->is_free ?? 0;
550 + $addon_base = $addon->basename ?? '';
551 + $version_latest = $addon->version ?? 0;
552 + $version_current = $addon->version ?? 0;
553 + $classes_status = array();
554 + $addon_purchased = $addon->purchase_info ?? false;
555 + $addon_category = implode( ' ', array_map( 'sanitize_title', $addon->category ?? array() ) );
556 + $date_expired_str = '';
557 + $number_days_remaining = null;
558 + $date_expired = null;
559 +
560 + if ( ! $is_free ) {
561 + $classes_status[] = 'purchase';
562 + } else {
563 + $classes_status[] = 'free';
564 + }
565 +
566 + if ( isset( $plugins_installed[ $addon_base ] ) ) {
567 + $is_installed = true;
568 + $classes_status[] = 'installed';
569 + $version_current = $plugins_installed[ $addon_base ]['Version'];
570 + } else {
571 + $is_installed = false;
572 + $classes_status[] = 'not_installed';
573 + }
574 +
575 + if ( in_array( $addon_base, $plugins_activated, true ) ) {
576 + $classes_status[] = 'activated';
577 + }
578 +
579 + $is_updated = false;
580 + if ( $is_installed && version_compare( $version_current, $version_latest, '<' ) ) {
581 + $classes_status[] = 'update';
582 + $is_updated = true;
583 + }
584 +
585 + if ( $addon_purchased ) {
586 + $classes_status[] = 'license';
587 + $date_expired_str = $addon_purchased->date_expire ?? '';
588 + if ( ! empty( $date_expired_str ) ) {
589 + $date_expired = new DateTime( $date_expired_str );
590 + $date_now = new DateTime( gmdate( 'Y-m-d' ) );
591 + $date_diff = date_diff( $date_now, $date_expired );
592 + $number_days_remaining = $date_diff->invert ? 0 : $date_diff->days;
593 + }
594 + }
595 +
596 + $show_license_panel = ! $is_free && $is_installed;
597 + $license_status = 'not-activated';
598 + if ( $addon_purchased ) {
599 + $license_status = 'active';
600 + if ( ! empty( $date_expired_str )
601 + && isset( $number_days_remaining )
602 + && 0 === $number_days_remaining ) {
603 + $license_status = 'expired';
604 + $classes_status[] = 'expired';
605 + }
606 + }
607 +
608 + $purchase_code_masked = AddonService::mask_purchase_code( $keys_purchase[ $addon->slug ] ?? '' );
609 +
610 + if ( ! in_array( $active_tab, $classes_status, true ) && 'all' !== $active_tab ) {
611 + $classes_status[] = 'hide';
612 + }
613 +
614 + return array(
615 + 'classes_status' => $classes_status,
616 + 'is_free' => $is_free,
617 + 'is_installed' => $is_installed,
618 + 'is_updated' => $is_updated,
619 + 'version_current' => $version_current,
620 + 'version_latest' => $version_latest,
621 + 'license_status' => $license_status,
622 + 'number_days_remaining' => $number_days_remaining,
623 + 'date_expired' => $date_expired,
624 + 'purchase_code_masked' => $purchase_code_masked,
625 + 'show_license_panel' => $show_license_panel,
626 + 'addon_category' => $addon_category,
627 + );
628 + }
629 +
630 + /**
631 + * Render the header section of an addon item.
632 + *
633 + * @param object $addon Current addon object.
634 + * @param array $data Renderer data.
635 + *
636 + * @return string
637 + */
638 + public static function html_addon_header( object $addon, array $data = [] ): string {
639 + $section = array(
640 + 'wrapper' => '<div class="lp-addon-item__header">',
641 + 'image' => sprintf(
642 + '<img class="lp-addon-item__image" src="%s" alt=""/>',
643 + esc_url( $addon->image ?? '' )
644 + ),
645 + 'heading' => '<div class="lp-addon-item__heading">',
646 + 'title' => '<h3 class="lp-addon-item__title">',
647 + 'link' => sprintf(
648 + '<a href="%s" target="_blank" rel="noopener">%s</a>',
649 + esc_url( $addon->link ?? '' ),
650 + esc_html( $addon->name ?? '' )
651 + ),
652 + 'title_end' => '</h3>',
653 + 'badge' => ! empty( $addon->badge ) ? sprintf(
654 + '<span class="lp-addon-item__badge">%s</span>',
655 + esc_html( $addon->badge )
656 + ) : '',
657 + 'heading_end' => '</div>',
658 + 'wrapper_end' => '</div>',
659 + );
660 +
661 + return Template::combine_components( $section );
662 + }
663 +
664 + /**
665 + * Render the license status block for a paid addon.
666 + *
667 + * @param object $addon Current addon object.
668 + * @param array $data Renderer data.
669 + *
670 + * @return string
671 + */
672 + public static function html_addon_license( object $addon, array $data = [] ): string {
673 + $license_status = $data['license_status'] ?? '';
674 + $number_days_remaining = $data['number_days_remaining'] ?? null;
675 + /** @var DateTime $date_expired */
676 + $date_expired = $data['date_expired'] ?? null;
677 + $purchase_code_masked = $data['purchase_code_masked'] ?? '';
678 + $show_license_panel = $data['show_license_panel'] ?? false;
679 + $license_status_label = 'active' === $license_status
680 + ? __( 'Active', 'learnpress' )
681 + : ( 'expired' === $license_status
682 + ? __( 'Expired', 'learnpress' )
683 + : ( 'deactivated' === $license_status
684 + ? __( 'Deactivated', 'learnpress' )
685 + : __( 'Not Activated', 'learnpress' ) ) );
686 +
687 + $expiry_hidden = empty( $date_expired )
688 + || ! in_array( $license_status, array( 'active', 'expired' ), true ) ? ' hidden' : '';
689 +
690 + $expiry_text = '';
691 + if ( ! empty( $date_expired ) ) {
692 + $lpDate = new LPDateTime( $date_expired->format( LPDateTime::FORMAT_MYSQL ) );
693 +
694 + $expiry_format = 'expired' === $license_status
695 + ? __( '(on %s)', 'learnpress' )
696 + : __( '(Updates until %s)', 'learnpress' );
697 + $expiry_text = esc_html( sprintf( $expiry_format, $lpDate->format( LPDateTime::FORMAT_I18N_DATE ) ) );
698 + }
699 +
700 + $extend_link = '';
701 + if ( 'expired' === $license_status ) {
702 + $extend_link = sprintf(
703 + '<a class="need-extend__link" href="%s" target="_blank" rel="noopener">%s</a>',
704 + esc_url( $addon->link ?? '' ),
705 + esc_html__( 'Extend now', 'learnpress' )
706 + );
707 + }
708 +
709 + $message = '';
710 + $button_extends = '';
711 + if ( ! empty( $addon->purchase_info ) ) {
712 + $button_extends = sprintf(
713 + '<a class="need-extend__link" href="%s" target="_blank" rel="noopener">%s</a>',
714 + esc_url( $addon->link ?? '' ),
715 + esc_html__( 'Extend now', 'learnpress' )
716 + );
717 +
718 + if ( isset( $number_days_remaining ) && $number_days_remaining > 0 && $number_days_remaining < 61 ) {
719 + $message = sprintf(
720 + __( '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' ),
721 + sprintf( '<strong class="need-extend__days">%d</strong>', $number_days_remaining )
722 + );
723 + } else {
724 + $button_extends = '';
725 + }
726 + }
727 +
728 + $need_extend = '';
729 + if ( ! empty( $message ) || ! empty( $button_extends ) ) {
730 + $need_extend = sprintf(
731 + '<span class="need-extend">%s %s</span>',
732 + $message,
733 + $button_extends
734 + );
735 + }
736 +
737 + $section = array(
738 + 'wrapper' => sprintf(
739 + '<div class="lp-addon-license lp-addon-license--%s" data-purchase-code-masked="%s"%s>',
740 + esc_attr( $license_status ),
741 + esc_attr( $purchase_code_masked ),
742 + $show_license_panel ? '' : ' hidden'
743 + ),
744 + 'summary' => '<div class="lp-addon-license__summary">',
745 + 'label' => sprintf(
746 + '<span>%s</span>',
747 + esc_html__( 'License:', 'learnpress' )
748 + ),
749 + 'status' => sprintf(
750 + '<strong class="lp-addon-license__status">%s</strong>',
751 + esc_html( $license_status_label )
752 + ),
753 + 'expiry' => sprintf(
754 + '<span class="lp-addon-license__expiry"%s>%s</span>',
755 + $expiry_hidden,
756 + $expiry_text
757 + ),
758 + 'extend_link' => $extend_link,
759 + 'summary_end' => '</div>',
760 + 'need_extend' => $need_extend,
761 + 'manage' => sprintf(
762 + '<button class="btn-addon-action lp-addon-license__manage"
763 + data-action="update-purchase-code" type="button">%s</button>',
764 + esc_html__( 'Manage', 'learnpress' )
765 + ),
766 + 'wrapper_end' => '</div>',
767 + );
768 +
769 + return Template::combine_components( $section );
770 + }
771 +
772 + /**
773 + * Render addon meta: version, docs, view details.
774 + *
775 + * @param object $addon Current addon object.
776 + * @param array $data Renderer data.
777 + *
778 + * @return string
779 + */
780 + public static function html_addon_meta( object $addon, array $data = [] ): string {
781 + $version_current = $data['version_current'] ?? '';
782 + $section = array(
783 + 'wrapper' => '<div class="lp-addon-item__meta">',
784 + 'version' => $version_current
785 + ? sprintf(
786 + '<span>%s <span class="addon-version-current">%s</span></span>',
787 + esc_html__( 'Version', 'learnpress' ),
788 + esc_html( $version_current )
789 + )
790 + : '',
791 + 'docs' => ! empty( $addon->link_doc )
792 + ? sprintf(
793 + '<a href="%s" target="_blank" rel="noopener">%s</a>',
794 + esc_url( $addon->link_doc ),
795 + esc_html__( 'Docs', 'learnpress' )
796 + )
797 + : '',
798 + 'details' => sprintf(
799 + '<a href="%s" target="_blank" rel="noopener">%s</a>',
800 + esc_url( $addon->link ?? '' ),
801 + esc_html__( 'View Details', 'learnpress' )
802 + ),
803 + 'settings' => ! empty( $addon->setting )
804 + ? sprintf(
805 + '<a data-action="setting" href="%s" target="_blank" rel="noopener">%s</a>',
806 + esc_url( site_url( $addon->setting ) ),
807 + esc_html__( 'Settings', 'learnpress' )
808 + )
809 + : '',
810 + 'wrapper_end' => '</div>',
811 + );
812 +
813 + return Template::combine_components( $section );
814 + }
815 +
816 + /**
817 + * Render addon price block.
818 + *
819 + * @param object $addon Current addon object.
820 + * @param array $data Renderer data.
821 + *
822 + * @return string
823 + */
824 + public static function html_addon_price( object $addon, array $data = [] ): string {
825 + $is_free = $addon->is_free ?? 0;
826 +
827 + if ( $is_free ) {
828 + return sprintf(
829 + '<strong class="lp-addon-item__price-regular lp-addon-item__price-regular--free">%s</strong>',
830 + esc_html__( 'Free', 'learnpress' )
831 + );
832 + }
833 +
834 + if ( ! empty( $addon->sale_price ) ) {
835 + $section = array(
836 + 'sale' => sprintf(
837 + '<strong class="lp-addon-item__price-sale">$%s</strong>',
838 + esc_html( number_format_i18n( $addon->sale_price ) )
839 + ),
840 + 'regular' => sprintf(
841 + '<del class="lp-addon-item__price-regular">$%s</del>',
842 + esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
843 + ),
844 + );
845 +
846 + return Template::combine_components( $section );
847 + }
848 +
849 + return sprintf(
850 + '<strong class="lp-addon-item__price-regular lp-addon-item__price-regular--current">$%s</strong>',
851 + esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
852 + );
853 + }
854 +
855 + /**
856 + * Render addon action buttons.
857 + *
858 + * @param object $addon Current addon object.
859 + * @param array $data Renderer data.
860 + *
861 + * @return string
862 + */
863 + public static function html_addon_actions( object $addon, array $data = [] ): string {
864 + $is_free = $data['is_free'] ?? false;
865 + $section_right = apply_filters(
866 + 'learn-press/admin/addons/actions/section_right',
867 + array(
868 + 'install' => $is_free
869 + ? sprintf(
870 + '<a class="lp-button btn-addon-action" data-action="install" href="%s"
871 + target="_blank" rel="noopener">%s</a>',
872 + esc_url( $addon->link ?? '' ),
873 + esc_html__( 'Install', 'learnpress' )
874 + )
875 + : sprintf(
876 + '<button class="lp-button btn-addon-action" data-action="install">
877 + <span class="text">%s</span>
878 + </button>',
879 + esc_html__( 'Install', 'learnpress' )
880 + ),
881 + 'purchase' => sprintf(
882 + '<button class="lp-button btn-addon-action" data-action="purchase">%s</button>',
883 + esc_html__( 'Install', 'learnpress' )
884 + ),
885 + 'update' => sprintf(
886 + '<button class="lp-button btn-addon-action" data-action="update">
887 + <span class="text">%s</span>
888 + </button>',
889 + esc_html__( 'Update', 'learnpress' )
890 + ),
891 + 'deactivate' => sprintf(
892 + '<button class="lp-button btn-addon-action" data-action="deactivate">
893 + <span class="text">%s</span>
894 + </button>',
895 + esc_html__( 'Deactivate', 'learnpress' )
896 + ),
897 + 'activate' => sprintf(
898 + '<button class="lp-button btn-addon-action" data-action="activate">
899 + <span class="text">%s</span>
900 + </button>',
901 + esc_html__( 'Activate', 'learnpress' )
902 + ),
903 + ),
904 + $addon,
905 + $data
906 + );
907 +
908 + $section = array(
909 + 'wrapper' => '<div class="lp-addon-item__actions">',
910 + 'actions_left' => '<div class="lp-addon-item__actions__left">',
911 + 'price' => sprintf(
912 + '<div class="lp-addon-item__price">%s</div>',
913 + self::html_addon_price( $addon )
914 + ),
915 + 'actions_left_end' => '</div>',
916 + 'actions_right' => '<div class="lp-addon-item__actions__right">',
917 + 'actions' => Template::combine_components( $section_right ),
918 + 'actions_right_end' => '</div>',
919 + 'wrapper_end' => '</div>',
920 + );
921 +
922 + return Template::combine_components( $section );
923 + }
924 +
925 + /**
926 + * Render the purchase-code overlay panel.
927 + *
928 + * @param object $addon Current addon object.
929 + * @param array $data Renderer data.
930 + *
931 + * @return string
932 + */
933 + public static function html_purchase_panel( object $addon, array $data = [] ): string {
934 + $panel = array(
935 + 'wrapper' => '<div class="purchase-install">',
936 + 'header' => '<div class="lp-addon-purchase__header">',
937 + 'title' => sprintf(
938 + '<strong class="lp-addon-purchase__title">%s</strong>',
939 + esc_html__( 'Purchase Code', 'learnpress' )
940 + ),
941 + 'close' => sprintf(
942 + '<button class="btn-addon-action
943 + lp-addon-purchase__close" data-action="cancel" type="button" aria-label="%s">',
944 + esc_attr__( 'Close', 'learnpress' )
945 + ),
946 + 'close_icon' => '<span class="lp-addon-purchase__close-symbol" aria-hidden="true">&times;</span>',
947 + 'close_end' => '</button>',
948 + 'header_end' => '</div>',
949 + 'field' => '<label class="lp-addon-purchase__field">',
950 + 'field_text' => sprintf(
951 + '<span class="screen-reader-text">%s</span>',
952 + esc_html__( 'Purchase Code', 'learnpress' )
953 + ),
954 + 'field_input' => sprintf(
955 + '<input type="text" class="enter-purchase-code" placeholder="%s" value="">',
956 + esc_attr__( 'Enter Purchase Code', 'learnpress' )
957 + ),
958 + 'field_end' => '</label>',
959 + 'submit' => '<button class="lp-button btn-addon-action lp-addon-purchase__submit"
960 + data-action="install" type="button">',
961 + 'submit_text' => sprintf( '<span class="text">%s</span>', esc_html__( 'Submit', 'learnpress' ) ),
962 + 'submit_end' => '</button>',
963 + 'divider' => sprintf(
964 + '<div class="lp-addon-purchase__divider"><span>%s</span></div>',
965 + esc_html__( 'Don\'t have a code?', 'learnpress' )
966 + ),
967 + 'buy' => sprintf(
968 + '<a class="btn-addon-action lp-addon-purchase__buy" href="%s" target="_blank" rel="noopener">%s</a>',
969 + esc_url( $addon->link ?? '' ),
970 + esc_html__( 'Buy Now', 'learnpress' )
971 + ),
972 + 'wrapper_end' => '</div>',
973 + );
974 +
975 + $section = array(
976 + 'wrapper' => '<div class="lp-addon-item__purchase">',
977 + 'purchase' => '<div class="lp-addon-item__purchase__wrapper">',
978 + 'panel' => Template::combine_components( $panel ),
979 + 'purchase_code' => '<input type="hidden" name="purchase-code" value="">',
980 + 'purchase_end' => '</div>',
981 + 'wrapper_end' => '</div>',
982 + );
983 +
984 + return Template::combine_components( $section );
62 985 }
63 986 }