response ) || ! is_array( $transient->response ) ) {
$transient->response = [];
}
if ( ! isset( $transient->no_update ) || ! is_array( $transient->no_update ) ) {
$transient->no_update = [];
}
$addons = AddonService::instance()->get_remote_data();
$plugins = get_plugins();
foreach ( $addons as $slug => $addon ) {
// Skip addons hosted on wp.org - core already handles them.
if ( ! empty( $addon->is_org ) ) {
continue;
}
$basename = $addon->basename ?? '';
if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) {
continue;
}
$item = (object) [
'id' => 'learnpress/' . $slug,
'slug' => $slug,
'plugin' => $basename,
'new_version' => $addon->version ?? '',
'url' => $addon->link ?? '',
'package' => '',
'icons' => isset( $addon->image ) ? [ 'default' => $addon->image ] : [],
];
$current_version = $plugins[ $basename ]['Version'];
if ( ! empty( $item->new_version ) && version_compare( $current_version, $item->new_version, '<' ) ) {
$transient->response[ $basename ] = $item;
} else {
$transient->no_update[ $basename ] = $item;
}
}
} catch ( Throwable $e ) {
LP_Debug::error_log( $e );
}
return $transient;
}
/**
* Register hooks to append update link into plugin update rows on plugins.php.
* Only for installed addons not on wp.org and having a newer version.
*
* @return void
*/
public function wp_update_plugin_message(): void {
try {
$addons = AddonService::instance()->get_remote_data();
$plugins = get_plugins();
foreach ( $addons as $addon ) {
if ( ! empty( $addon->is_org ) ) {
continue;
}
$basename = $addon->basename ?? '';
if ( empty( $basename ) || ! isset( $plugins[ $basename ] ) ) {
continue;
}
$new_version = $addon->version ?? '';
if ( empty( $new_version )
|| version_compare( $plugins[ $basename ]['Version'], $new_version, '>=' ) ) {
continue;
}
$this->addons_need_update[ $basename ] = $addon;
// Priority 5: run before core wp_plugin_update_row (priority 10) to override it.
add_action( "after_plugin_row_{$basename}", array( $this, 'render_custom_update_row' ), 5, 2 );
}
} catch ( Throwable $e ) {
LP_Debug::error_log( $e );
}
}
/**
* Render custom update row to override core default message.
* Hook: after_plugin_row_{$basename} - priority 5, before core wp_plugin_update_row.
*
* @param string $file Plugin basename.
* @param array $plugin_data Plugin data.
*
* @return void
*/
public function render_custom_update_row( string $file, array $plugin_data ): void {
// Remove core default update row to override the message.
remove_action( "after_plugin_row_{$file}", 'wp_plugin_update_row' );
$addon = $this->addons_need_update[ $file ] ?? null;
if ( ! $addon ) {
return;
}
$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
$slug = sanitize_key( $addon->basename ?? $file );
$new_version = $addon->version ?? '';
$link_update = admin_url( 'admin.php?page=learn-press-addons&tab=update' );
$section = array(
'wrap' => sprintf(
'
',
esc_attr( $slug ),
esc_attr( $slug ),
esc_attr( $file )
),
'cell_open' => sprintf(
'',
esc_attr( $wp_list_table->get_column_count() )
),
'message_open' => '',
'text' => sprintf(
/* translators: 1: addon name, 2: new version */
esc_html__( 'There is a new version of %1$s (v%2$s) available.', 'learnpress' ),
esc_html( $addon->name ?? $plugin_data['Name'] ),
esc_html( $new_version )
),
'link' => sprintf(
' %s',
esc_url( $link_update ),
esc_html__( 'Go to LearnPress Add-ons page to update.', 'learnpress' )
),
'message_close' => ' ',
'cell_close' => ' | ',
'warp-end' => '
',
);
echo Template::combine_components( $section );
}
/**
* HTML the LearnPress Add-ons page.
*
* @return void
*/
public function html_page() {
/** @use self::render_addons */
$section = apply_filters(
'learn-press/admin/addons/page-section',
array(
'wrapper' => '',
'notice' => sprintf(
'
%s
',
Template::print_message(
sprintf(
'
%s
',
sprintf(
__( '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' ),
sprintf(
'
%s',
'https://thimpress.com/my-account/',
__( 'get key', 'learnpress' )
),
)
),
'info',
false
)
),
'subtitle' => sprintf(
'
%s
',
esc_html__(
'Discover high-performance Premium & Education themes optimized 100% for LearnPress LMS.',
'learnpress'
)
),
'addons' => TemplateAJAX::load_content_via_ajax(
array(
'id_url' => 'data-addons',
'tab' => $_REQUEST['tab'] ?? '',
),
array(
'class' => self::class,
'method' => 'render_addons',
)
),
'wrapper_end' => '
',
)
);
echo AdminTemplate::html_on_wp_admin_screen(
array(
'content' => Template::combine_components( $section ),
'title' => __( 'LearnPress Add-ons', 'learnpress' ),
'id' => 'learn-press-addons',
)
);
}
/**
* Render add-ons list for the AJAX response.
*
* @param array $data
* @return stdClass
*/
public static function render_addons( array $data = [] ): stdClass {
$response = new stdClass();
try {
// Check permission
if ( is_multisite() ? ! is_super_admin() : ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) {
throw new Exception( __( 'Access denied.', 'learnpress' ) );
}
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$addons = AddonService::instance()->get_addons();
$plugins_installed = get_plugins();
$plugins_activated = get_option( 'active_plugins', array() );
$active_tab = LP_Helper::sanitize_params_submitted( $data['tab'] ) ?? 'all';
$keys_purchase = LP_Settings::get_option( AddonService::instance()->key_purchase_addons, array() );
$section = apply_filters(
'learn-press/admin/addons/section',
array(
'wrapper' => '',
'controls' => self::html_controls(
array(
'active_tab' => $active_tab,
)
),
'list' => self::html_addons_list(
array(
'addons' => $addons,
'plugins_installed' => $plugins_installed,
'plugins_activated' => $plugins_activated,
'active_tab' => $active_tab,
'keys_purchase' => $keys_purchase,
)
),
'wrapper_end' => '
',
),
$addons,
$data
);
$response->content = Template::combine_components( $section );
} catch ( Throwable $e ) {
$response->content = Template::print_message(
$e->getMessage(),
Response::STATUS_ERROR,
false
);
}
return $response;
}
/**
* Render the add-ons filter controls: tabs + search.
*
* @param array $data Renderer data.
*
* @return string
*/
public static function html_controls( array $data = [] ): string {
$active_tab = $data['active_tab'] ?? 'all';
$tabs = array(
'all' => sprintf( '%s ()', __( 'All', 'learnpress' ) ),
'installed' => sprintf( '%s ()', __( 'Installed', 'learnpress' ) ),
'purchase' => sprintf( '%s ()', __( 'Paid', 'learnpress' ) ),
'free' => sprintf( '%s ()', __( 'Free', 'learnpress' ) ),
'update' => sprintf( '%s ()', __( 'Updated', 'learnpress' ) ),
'license' => sprintf( '%s ()', __( 'License', 'learnpress' ) ),
'not_installed' => sprintf( '%s ()', __( 'Not Installed', 'learnpress' ) ),
);
$addon_categories = array(
'create-course' => __( 'Create Course', 'learnpress' ),
'engagement' => __( 'Engagement', 'learnpress' ),
'learnpress' => __( 'LearnPress', 'learnpress' ),
'manage-course' => __( 'Manage Course', 'learnpress' ),
'marketing-optimization' => __( 'Marketing Optimization', 'learnpress' ),
'monetize-course' => __( 'Monetize Course', 'learnpress' ),
'payment' => __( 'Payment', 'learnpress' ),
);
$section_tabs = array();
foreach ( $tabs as $tab => $tab_title ) {
$active_class = ( $tab === $active_tab ) ? ' nav-tab-active' : '';
if ( $active_class ) {
$section_tabs[ $tab ] = sprintf(
'%s',
esc_attr( $active_class ),
esc_attr( $tab ),
wp_kses_post( $tab_title )
);
} else {
$section_tabs[ $tab ] = sprintf(
'%s',
esc_attr( $tab ),
esc_attr( $tab ),
wp_kses_post( $tab_title )
);
}
}
$section = array(
'wrapper' => '',
'toolbar' => '
',
'categories' => self::html_categories(
array(
'addon_categories' => $addon_categories,
)
),
'wrapper_end' => '
',
);
return Template::combine_components( $section );
}
/**
* Render the add-ons category filter buttons.
*
* @param array $data Renderer data.
*
* @return string
*/
public static function html_categories( array $data = [] ): string {
$addon_categories = $data['addon_categories'] ?? array();
$section = array(
'wrapper' => sprintf(
'',
esc_attr__( 'Filter add-ons by category', 'learnpress' )
),
'all' => sprintf(
'',
esc_html__( 'All', 'learnpress' )
),
);
foreach ( $addon_categories as $category_slug => $category_label ) {
$section[ $category_slug ] = sprintf(
'',
esc_attr( $category_slug ),
esc_html( $category_label )
);
}
$section['wrapper_end'] = '
';
return Template::combine_components( $section );
}
/**
* Render the list of add-on items.
*
* @param array $data Renderer data.
*
* @return string
* @throws Exception
*/
public static function html_addons_list( array $data = [] ): string {
$addons = $data['addons'] ?? array();
$html = '';
foreach ( $addons as $slug => $addon ) {
$addon->slug = $slug;
$html .= self::html_addon_item( $addon, $data );
}
return sprintf( '%s
', $html );
}
/**
* Render a single add-on card.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
* @throws Exception
*/
public static function html_addon_item( object $addon, array $data = [] ): string {
$state = self::get_addon_classes_and_state( $addon, $data );
$section = apply_filters(
'learn-press/admin/addons/item',
array(
'wrapper' => sprintf(
'',
esc_attr( implode( ' ', $state['classes_status'] ) ),
esc_attr( $addon->slug ),
esc_attr( $state['addon_category'] ),
esc_attr( htmlentities2( wp_json_encode( $addon ) ) )
),
'content' => '
',
'header' => self::html_addon_header( $addon ),
'license' => ! $state['is_free']
? self::html_addon_license(
$addon,
array(
'license_status' => $state['license_status'],
'number_days_remaining' => $state['number_days_remaining'],
'date_expired' => $state['date_expired'],
'purchase_code_masked' => $state['purchase_code_masked'],
'show_license_panel' => $state['show_license_panel'],
)
)
: '',
'description' => sprintf(
'
%s
',
esc_attr( $addon->description ?? '' ),
esc_html( $addon->description ?? '' )
),
'meta' => self::html_addon_meta(
$addon,
array(
'version_current' => $state['version_current'],
)
),
'new_update' => $state['is_updated']
? sprintf(
'
%s: %s',
esc_html__( 'Latest version', 'learnpress' ),
esc_html( $state['version_latest'] )
)
: '',
'content_end' => '
',
'actions' => self::html_addon_actions(
$addon,
array(
'is_free' => $state['is_free'],
'is_installed' => $state['is_installed'],
'is_updated' => $state['is_updated'],
)
),
'purchase' => self::html_purchase_panel(
$addon,
array(
'purchase_code_masked' => $state['purchase_code_masked'],
)
),
'wrapper_end' => '
',
),
$addon,
$data
);
return Template::combine_components( $section );
}
/**
* Compute status classes and state flags for an addon.
*
* @param object $addon Current addon object.
* @param array $data Addon state data.
*
* @return array
* @throws Exception
*/
public static function get_addon_classes_and_state( object $addon, array $data = [] ): array {
$plugins_installed = $data['plugins_installed'] ?? array();
$plugins_activated = $data['plugins_activated'] ?? array();
$active_tab = $data['active_tab'] ?? 'all';
$keys_purchase = $data['keys_purchase'] ?? array();
$is_free = $addon->is_free ?? 0;
$addon_base = $addon->basename ?? '';
$version_latest = $addon->version ?? 0;
$version_current = $addon->version ?? 0;
$classes_status = array();
$addon_purchased = $addon->purchase_info ?? false;
$addon_category = implode( ' ', array_map( 'sanitize_title', $addon->category ?? array() ) );
$date_expired_str = '';
$number_days_remaining = null;
$date_expired = null;
if ( ! $is_free ) {
$classes_status[] = 'purchase';
} else {
$classes_status[] = 'free';
}
if ( isset( $plugins_installed[ $addon_base ] ) ) {
$is_installed = true;
$classes_status[] = 'installed';
$version_current = $plugins_installed[ $addon_base ]['Version'];
} else {
$is_installed = false;
$classes_status[] = 'not_installed';
}
if ( in_array( $addon_base, $plugins_activated, true ) ) {
$classes_status[] = 'activated';
}
$is_updated = false;
if ( $is_installed && version_compare( $version_current, $version_latest, '<' ) ) {
$classes_status[] = 'update';
$is_updated = true;
}
if ( $addon_purchased ) {
$classes_status[] = 'license';
$date_expired_str = $addon_purchased->date_expire ?? '';
if ( ! empty( $date_expired_str ) ) {
$date_expired = new DateTime( $date_expired_str );
$date_now = new DateTime( gmdate( 'Y-m-d' ) );
$date_diff = date_diff( $date_now, $date_expired );
$number_days_remaining = $date_diff->invert ? 0 : $date_diff->days;
}
}
$show_license_panel = ! $is_free && $is_installed;
$license_status = 'not-activated';
if ( $addon_purchased ) {
$license_status = 'active';
if ( ! empty( $date_expired_str )
&& isset( $number_days_remaining )
&& 0 === $number_days_remaining ) {
$license_status = 'expired';
$classes_status[] = 'expired';
}
}
$purchase_code_masked = AddonService::mask_purchase_code( $keys_purchase[ $addon->slug ] ?? '' );
if ( ! in_array( $active_tab, $classes_status, true ) && 'all' !== $active_tab ) {
$classes_status[] = 'hide';
}
return array(
'classes_status' => $classes_status,
'is_free' => $is_free,
'is_installed' => $is_installed,
'is_updated' => $is_updated,
'version_current' => $version_current,
'version_latest' => $version_latest,
'license_status' => $license_status,
'number_days_remaining' => $number_days_remaining,
'date_expired' => $date_expired,
'purchase_code_masked' => $purchase_code_masked,
'show_license_panel' => $show_license_panel,
'addon_category' => $addon_category,
);
}
/**
* Render the header section of an addon item.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
*/
public static function html_addon_header( object $addon, array $data = [] ): string {
$section = array(
'wrapper' => '',
);
return Template::combine_components( $section );
}
/**
* Render the license status block for a paid addon.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
*/
public static function html_addon_license( object $addon, array $data = [] ): string {
$license_status = $data['license_status'] ?? '';
$number_days_remaining = $data['number_days_remaining'] ?? null;
/** @var DateTime $date_expired */
$date_expired = $data['date_expired'] ?? null;
$purchase_code_masked = $data['purchase_code_masked'] ?? '';
$show_license_panel = $data['show_license_panel'] ?? false;
$license_status_label = 'active' === $license_status
? __( 'Active', 'learnpress' )
: ( 'expired' === $license_status
? __( 'Expired', 'learnpress' )
: ( 'deactivated' === $license_status
? __( 'Deactivated', 'learnpress' )
: __( 'Not Activated', 'learnpress' ) ) );
$expiry_hidden = empty( $date_expired )
|| ! in_array( $license_status, array( 'active', 'expired' ), true ) ? ' hidden' : '';
$expiry_text = '';
if ( ! empty( $date_expired ) ) {
$lpDate = new LPDateTime( $date_expired->format( LPDateTime::FORMAT_MYSQL ) );
$expiry_format = 'expired' === $license_status
? __( '(on %s)', 'learnpress' )
: __( '(Updates until %s)', 'learnpress' );
$expiry_text = esc_html( sprintf( $expiry_format, $lpDate->format( LPDateTime::FORMAT_I18N_DATE ) ) );
}
$extend_link = '';
if ( 'expired' === $license_status ) {
$extend_link = sprintf(
'%s',
esc_url( $addon->link ?? '' ),
esc_html__( 'Extend now', 'learnpress' )
);
}
$message = '';
$button_extends = '';
if ( ! empty( $addon->purchase_info ) ) {
$button_extends = sprintf(
'%s',
esc_url( $addon->link ?? '' ),
esc_html__( 'Extend now', 'learnpress' )
);
if ( isset( $number_days_remaining ) && $number_days_remaining > 0 && $number_days_remaining < 61 ) {
$message = sprintf(
__( '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' ),
sprintf( '%d', $number_days_remaining )
);
} else {
$button_extends = '';
}
}
$need_extend = '';
if ( ! empty( $message ) || ! empty( $button_extends ) ) {
$need_extend = sprintf(
'%s %s',
$message,
$button_extends
);
}
$section = array(
'wrapper' => sprintf(
'',
esc_attr( $license_status ),
esc_attr( $purchase_code_masked ),
$show_license_panel ? '' : ' hidden'
),
'summary' => '
',
'label' => sprintf(
'%s',
esc_html__( 'License:', 'learnpress' )
),
'status' => sprintf(
'%s',
esc_html( $license_status_label )
),
'expiry' => sprintf(
'%s',
$expiry_hidden,
$expiry_text
),
'extend_link' => $extend_link,
'summary_end' => '
',
'need_extend' => $need_extend,
'manage' => sprintf(
'
',
esc_html__( 'Manage', 'learnpress' )
),
'wrapper_end' => '
',
);
return Template::combine_components( $section );
}
/**
* Render addon meta: version, docs, view details.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
*/
public static function html_addon_meta( object $addon, array $data = [] ): string {
$version_current = $data['version_current'] ?? '';
$section = array(
'wrapper' => '',
);
return Template::combine_components( $section );
}
/**
* Render addon price block.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
*/
public static function html_addon_price( object $addon, array $data = [] ): string {
$is_free = $addon->is_free ?? 0;
if ( $is_free ) {
return sprintf(
'%s',
esc_html__( 'Free', 'learnpress' )
);
}
if ( ! empty( $addon->sale_price ) ) {
$section = array(
'sale' => sprintf(
'$%s',
esc_html( number_format_i18n( $addon->sale_price ) )
),
'regular' => sprintf(
'$%s',
esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
),
);
return Template::combine_components( $section );
}
return sprintf(
'$%s',
esc_html( number_format_i18n( $addon->regular_price ?? 0 ) )
);
}
/**
* Render addon action buttons.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
*/
public static function html_addon_actions( object $addon, array $data = [] ): string {
$is_free = $data['is_free'] ?? false;
$section_right = apply_filters(
'learn-press/admin/addons/actions/section_right',
array(
'install' => $is_free
? sprintf(
'%s',
esc_url( $addon->link ?? '' ),
esc_html__( 'Install', 'learnpress' )
)
: sprintf(
'',
esc_html__( 'Install', 'learnpress' )
),
'purchase' => sprintf(
'',
esc_html__( 'Install', 'learnpress' )
),
'update' => sprintf(
'',
esc_html__( 'Update', 'learnpress' )
),
'deactivate' => sprintf(
'',
esc_html__( 'Deactivate', 'learnpress' )
),
'activate' => sprintf(
'',
esc_html__( 'Activate', 'learnpress' )
),
),
$addon,
$data
);
$section = array(
'wrapper' => '',
'actions_left' => '
',
'price' => sprintf(
'
%s
',
self::html_addon_price( $addon )
),
'actions_left_end' => '
',
'actions_right' => '
',
'actions' => Template::combine_components( $section_right ),
'actions_right_end' => '
',
'wrapper_end' => '
',
);
return Template::combine_components( $section );
}
/**
* Render the purchase-code overlay panel.
*
* @param object $addon Current addon object.
* @param array $data Renderer data.
*
* @return string
*/
public static function html_purchase_panel( object $addon, array $data = [] ): string {
$panel = array(
'wrapper' => '',
'header' => '',
'field' => '
',
'submit' => '
',
'divider' => sprintf(
'
%s
',
esc_html__( 'Don\'t have a code?', 'learnpress' )
),
'buy' => sprintf(
'
%s',
esc_url( $addon->link ?? '' ),
esc_html__( 'Buy Now', 'learnpress' )
),
'wrapper_end' => '
',
);
$section = array(
'wrapper' => '',
'purchase' => '
',
'panel' => Template::combine_components( $panel ),
'purchase_code' => '',
'purchase_end' => '
',
'wrapper_end' => '
',
);
return Template::combine_components( $section );
}
}