updated plugin Menu Icons version 0.13.23

This commit is contained in:
2026-06-03 21:29:09 +00:00
committed by Gitium
parent af21e84842
commit 44cba94bcb
38 changed files with 1556 additions and 546 deletions

View File

@ -0,0 +1,184 @@
# ThemeIsle SDK — Agent Reference
> Quick-reference guide for AI agents working on this codebase.
## What This Is
A shared WordPress library bundled into Themeisle plugins and themes. It provides common features (licensing, analytics, notifications, promotions, etc.) so each product doesn't reimplement them. Multiple products may bundle different versions; only the highest version ever loads.
## Directory Map
```
themeisle-sdk-main/
├── load.php Entry point bundled by each product. Handles version arbitration.
├── start.php Bootstrap: requires all class files, calls Loader::init().
├── src/
│ ├── Loader.php Singleton. Owns $products, $available_modules, $labels.
│ ├── Product.php Model for a registered plugin/theme. Reads file headers.
│ ├── Common/
│ │ ├── Abstract_module.php Base class every module extends.
│ │ └── Module_factory.php Instantiates + attaches modules to products.
│ └── Modules/ One file per feature module (18 total).
├── tests/ PHPUnit tests. One file per module.
├── docs/ Integration guides. One file per feature.
└── assets/ Compiled JS/CSS for SDK UI components.
```
## Key Concepts
### How Products Register
Products add their base file to the `themeisle_sdk_products` filter — that is the *only* required step:
```php
add_filter( 'themeisle_sdk_products', function( $products ) {
$products[] = __FILE__;
return $products;
} );
```
### Product File Headers
The SDK reads WordPress file headers to configure itself per product:
```
WordPress Available: yes # yes = on WP.org (free). no = premium only.
Requires License: yes # yes = activates the Licenser module.
Pro Slug: neve-pro # Slug of the companion pro plugin.
```
### Module Loading Contract
Each module in `src/Modules/` extends `Abstract_Module` and implements:
- `can_load( $product ) : bool` — Should this module run for this product?
- `load( $product ) : self` — Register WordPress hooks.
`Module_Factory::attach()` calls both methods for every registered module/product pair.
### Labels (UI Strings)
All UI strings are in `Loader::$labels` (see [src/Loader.php](src/Loader.php) lines 73328). Products and plugins override them via:
```php
add_filter( 'themeisle_sdk_labels', function( $labels ) {
$labels['review']['notice'] = __( 'Custom message', 'text-domain' );
return $labels;
} );
```
The merge logic ensures the first real translation wins; later callbacks cannot overwrite already-translated values.
## All 18 Modules
| Module | File | Loads when | Doc |
|--------|------|-----------|-----|
| `licenser` | `Licenser.php` | `Requires License: yes` in header | [docs/LICENSER.md](docs/LICENSER.md) |
| `logger` | `Logger.php` | Always (filterable) | [docs/LOGGER.md](docs/LOGGER.md) |
| `notification` | `Notification.php` | Installed >100h, admin user | [docs/NOTIFICATIONS.md](docs/NOTIFICATIONS.md) |
| `review` | `Review.php` | `WordPress Available: yes`, not partner | [docs/REVIEW.md](docs/REVIEW.md) |
| `promotions` | `Promotions.php` | Not partner, not recently dismissed | [docs/PROMOTIONS.md](docs/PROMOTIONS.md) |
| `rollback` | `Rollback.php` | Always | [docs/ROLLBACK.md](docs/ROLLBACK.md) |
| `uninstall_feedback` | `Uninstall_feedback.php` | Always | [docs/UNINSTALL-FEEDBACK.md](docs/UNINSTALL-FEEDBACK.md) |
| `about_us` | `About_us.php` | `{key}_about_us_metadata` filter returns data | [docs/ABOUT-US.md](docs/ABOUT-US.md) |
| `float_widget` | `Float_widget.php` | `{key}_float_widget_metadata` filter returns data | [docs/FLOAT-WIDGET.md](docs/FLOAT-WIDGET.md) |
| `announcements` | `Announcements.php` | Not partner | [docs/ANNOUNCEMENTS.md](docs/ANNOUNCEMENTS.md) |
| `welcome` | `Welcome.php` | `{key}_welcome_metadata` filter returns enabled data | [docs/WELCOME.md](docs/WELCOME.md) |
| `compatibilities` | `Compatibilities.php` | Not partner, admin user | [docs/COMPATIBILITIES.md](docs/COMPATIBILITIES.md) |
| `dashboard_widget` | `Dashboard_widget.php` | Not partner | — |
| `featured_plugins` | `Featured_plugins.php` | Always | — |
| `recommendation` | `Recommendation.php` | Always | — |
| `script_loader` | `Script_loader.php` | Always | [docs/TELEMETRY.md](docs/TELEMETRY.md) |
| `translate` | `Translate.php` | Always | — |
| `translations` | `Translations.php` | Always | — |
## Common Filter Reference
| Filter | Purpose |
|--------|---------|
| `themeisle_sdk_products` | Register a product base file |
| `themeisle_sdk_labels` | Override any UI string |
| `themeisle_sdk_modules` | Add custom module names |
| `themeisle_sdk_required_files` | Add custom module PHP files |
| `themeisle_sdk_enable_telemetry` | Enable JS telemetry (return `true`) |
| `themeisle_sdk_disable_telemetry` | Disable all telemetry (return `true`) |
| `themeisle_sdk_hide_notifications` | Suppress all admin notices |
| `themeisle_sdk_is_black_friday_sale` | Force Black Friday banner on/off |
| `themeisle_sdk_promo_debug` | Force promotions to show (dev only) |
| `themeisle_sdk_welcome_debug` | Force welcome notice to show (dev only) |
| `themeisle_sdk_current_date` | Override current date (useful in tests) |
| `{product_key}_about_us_metadata` | Configure About Us page |
| `{product_key}_float_widget_metadata` | Configure floating help widget |
| `{product_key}_welcome_metadata` | Configure welcome/upgrade notice |
| `{product_key}_load_promotions` | Add promotion slugs for this product |
| `{product_key}_dissallowed_promotions` | Block specific promotion slugs |
| `{product_slug}_sdk_enable_logger` | Enable/disable logger for product |
| `{product_slug}_sdk_should_review` | Enable/disable review prompt |
| `{product_key}_enable_licenser` | Enable/disable licenser module |
| `{product_key}_hide_license_field` | Hide license field on settings page |
| `{product_key}_hide_license_notices` | Suppress license admin notices |
| `themeisle_sdk_compatibilities/{slug}` | Declare version compatibility requirements |
| `themesle_sdk_namespace_{md5(basefile)}` | Set product namespace for license filters |
| `themeisle_sdk_license_process_{ns}` | Trigger license activate/deactivate |
| `product_{ns}_license_status` | Read license status |
| `product_{ns}_license_key` | Read license key |
| `product_{ns}_license_plan` | Read license plan/price ID |
| `tsdk_utmify_{content}` | Override UTM params for a URL |
| `tsdk_utmify_url_{content}` | Override final UTM-ified URL |
## Global Helper Functions
Defined in `load.php`, available everywhere after `init`:
```php
tsdk_utmify( $url, $area, $location ) // Append UTM params
tsdk_lstatus( $file ) // License status string
tsdk_lis_valid( $file ) // bool — is license valid?
tsdk_lplan( $file ) // int — license price_id
tsdk_lkey( $file ) // string — license key
tsdk_translate_link( $url, $type, $langs ) // Localize a URL
tsdk_support_link( $file ) // Pre-filled support URL or false
```
## Options Written by the SDK
All options use `{product_key}` where key = slug with hyphens replaced by underscores.
| Option | Content |
|--------|---------|
| `{key}_install` | Unix timestamp of first activation |
| `{key}_version` | Last known product version |
| `{key}_license` | Raw license key (free products) |
| `{key}_license_data` | JSON object from license API |
| `{key}_license_status` | `valid` \| `not_active` \| `active_expired` |
| `{key}_logger_flag` | `yes` \| `no` |
| `themeisle_sdk_notifications` | Notification queue metadata |
| `themeisle_sdk_promotions` | Promotion dismiss timestamps |
| `themeisle_sdk_promotions_{promo}_installed` | Whether a promoted plugin was installed |
## API Endpoints
```
https://api.themeisle.com/license/check/{product}/{key}/{url}/{token}
https://api.themeisle.com/license/activate/{product}/{key}
https://api.themeisle.com/license/deactivate/{product}/{key}
https://api.themeisle.com/license/version/{product}/{key}/{version}/{url}
https://api.themeisle.com/license/versions/{product}/{key}/{url}/{version}
https://api.themeisle.com/tracking/log
https://api.themeisle.com/tracking/events
https://api.themeisle.com/tracking/uninstall
```
## Tests
```bash
composer install
./vendor/bin/phpunit
```
Test files mirror module names: `tests/licenser-test.php`, `tests/loader-test.php`, etc.
Sample products used in tests live in `tests/sample_products/`.
## Adding a New Module
1. Create `src/Modules/My_Feature.php` extending `Abstract_Module`
2. Implement `can_load()` and `load()`
3. Add `'my_feature'` to `$available_modules` in `Loader.php`
4. Add the file path to the `$files_to_load` array in `start.php`
5. Add a test in `tests/my-feature-test.php`
6. Add a doc in `docs/MY-FEATURE.md`

View File

@ -1,3 +1,38 @@
##### [Version 3.3.51](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.50...v3.3.51) (2026-03-30)
- Add SDK docs
- Add Migration Module
- Update Black Friday module
- Update the labels for the sharing data notice
- Update the design of the expired notice
##### [Version 3.3.50](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.49...v3.3.50) (2025-11-25)
> Things are getting better every day. 🚀
##### [Version 3.3.49](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.48...v3.3.49) (2025-09-18)
> Things are getting better every day. 🚀
##### [Version 3.3.48](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.47...v3.3.48) (2025-08-11)
Development
##### [Version 3.3.47](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.46...v3.3.47) (2025-07-21)
- Fixed review link
- Fixed plugins ranking
##### [Version 3.3.46](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.45...v3.3.46) (2025-05-16)
- Add masteriyo recommandation
- Add bf helpers
- update formbricks
##### [Version 3.3.45](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.44...v3.3.45) (2025-04-28)
- feat: add review_link param to about_us filter
##### [Version 3.3.44](https://github.com/Codeinwp/themeisle-sdk-main/compare/v3.3.43...v3.3.44) (2025-02-18)
- Fix variable mismatch in the install category function.

View File

@ -1,54 +0,0 @@
.tsdk-banner-cta {
position: relative;
display: inline-block;
}
.tsdk-banner-urgency-text {
position: absolute;
top: 6%;
left: 1%;
color: white;
padding: 5px;
font-size: 16px;
z-index: 10;
text-transform: uppercase;
font-weight: 700;
}
.tsdk-banner-img {
width: 100%;
height: auto;
}
@media (max-width: 1100px) {
.tsdk-banner-urgency-text {
font-size: 10px;
top: 0;
left: 1%;
}
}
@media (max-width: 950px) {
.tsdk-banner-urgency-text {
font-size: 10px;
left: 1%;
}
}
@media (max-width: 500px) {
.tsdk-banner-urgency-text {
font-size: 6px;
top: -10%;
left: 1%;
}
}
@media (max-width: 420px) {
.tsdk-banner-urgency-text {
left: 0%;
}
}
.notice:not(#tsdk_banner) {
display: none;
}

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element'), 'version' => '7d782affa8469fa8f48d');
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element'), 'version' => '8d9c74cada5a40e4082b');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<?php return array('dependencies' => array(), 'version' => '9c795bb600f6ae533935');

View File

@ -1 +0,0 @@
document.addEventListener("DOMContentLoaded",(()=>{document.dispatchEvent(new Event("themeisle:banner:init"))})),document.addEventListener("themeisle:banner:init",(()=>{!function(){if(void 0===window.tsdk_banner_data)return;const n=document.getElementById("tsdk_banner");n&&(n.innerHTML=window.tsdk_banner_data.content)}()}));

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-data', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-plugins'), 'version' => '3997ba6be36742082cb2');
<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-data', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-plugins'), 'version' => '1e086c2d21f2850672d5');

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '92a432317d1433f31603');
<?php return array('dependencies' => array(), 'version' => '44eb6f2a376d36991f76');

View File

@ -1 +1 @@
(()=>{"use strict";let i=!1,r=!1;const o=[],t=new Proxy({},{get:(t,e,n)=>(...t)=>(async(t,...e)=>{if(r){if(window.formbricks){const i=t;await window.formbricks[i](...e)}}else if("init"===t){if(i)return void console.warn("🧱 Formbricks - Warning: Formbricks is already initializing.");i=!0;const t=e[0].apiHost;if((await(async i=>{if(!window.formbricks){const r=document.createElement("script");r.type="text/javascript",r.src=`${i}/js/formbricks.umd.cjs`,r.async=!0;const o=async()=>new Promise(((i,o)=>{const t=setTimeout((()=>{o(new Error("Formbricks SDK loading timed out"))}),1e4);r.onload=()=>{clearTimeout(t),i()},r.onerror=()=>{clearTimeout(t),o(new Error("Failed to load Formbricks SDK"))}}));document.head.appendChild(r);try{return await o(),{ok:!0,data:void 0}}catch(i){return{ok:!1,error:new Error(i.message??"Failed to load Formbricks SDK")}}}return{ok:!0,data:void 0}})(t)).ok&&window.formbricks){window.formbricks.init(...e),i=!1,r=!0;for(const{prop:i,args:r}of o)"function"==typeof window.formbricks[i]?window.formbricks[i](...r):console.error(`🧱 Formbricks - Error: Method ${i} does not exist on formbricks`)}}else console.warn("🧱 Formbricks - Warning: Formbricks not initialized. This method will be queued and executed after initialization."),o.push({prop:t,args:e})})(e,...t)});document.addEventListener("DOMContentLoaded",(()=>{window.tsdk_formbricks={init:i=>{var r,o;"object"==typeof i&&null!==i||(i={});const e={...window.tsdk_survey_data,...i,attributes:{...null!==(r=window.tsdk_survey_data.attributes)&&void 0!==r?r:{},...null!==(o=i.attributes)&&void 0!==o?o:{}}};t?.init(e)}};let i=null;var r;r=window.tsdk_survey_data?.attributes?.install_days_number,isNaN(r)||"boolean"==typeof r||(i=setTimeout((()=>{window.tsdk_formbricks?.init()}),350)),window.addEventListener("themeisle:survey:trigger:cancel",(()=>{clearTimeout(i)})),window.dispatchEvent(new Event("themeisle:survey:loaded"))}))})();
(()=>{"use strict";let r=!1,e=!1;const t=[],o=new Proxy({},{get:(o,i,n)=>(...o)=>(async(o,...i)=>{if(e){if(window.formbricks){const r=window.formbricks,e=o;await r[e](...i)}}else if("setup"===o){if(r)return void console.warn("🧱 Formbricks - Warning: Formbricks is already initializing.");r=!0;const o=i[0],{appUrl:n,environmentId:s}=o;if(!n)return void console.error("🧱 Formbricks - Error: appUrl is required");if(!s)return void console.error("🧱 Formbricks - Error: environmentId is required");if((await(async r=>{if(!window.formbricks){const e=document.createElement("script");e.type="text/javascript",e.src=`${r}/js/formbricks.umd.cjs`,e.async=!0;const t=async()=>new Promise(((r,t)=>{const o=setTimeout((()=>{t(new Error("Formbricks SDK loading timed out"))}),1e4);e.onload=()=>{clearTimeout(o),r()},e.onerror=()=>{clearTimeout(o),t(new Error("Failed to load Formbricks SDK"))}}));document.head.appendChild(e);try{return await t(),{ok:!0,data:void 0}}catch(r){return{ok:!1,error:new Error(r.message??"Failed to load Formbricks SDK")}}}return{ok:!0,data:void 0}})(n)).ok&&window.formbricks){const o=window.formbricks;o.setup(...i),r=!1,e=!0;for(const{prop:r,args:e}of t)"function"==typeof o[r]?o[r](...e):console.error(`🧱 Formbricks - Error: Method ${r} does not exist on formbricks`)}}else console.warn("🧱 Formbricks - Warning: Formbricks not initialized. This method will be queued and executed after initialization."),t.push({prop:o,args:i})})(i,...o)});document.addEventListener("DOMContentLoaded",(()=>{window.tsdk_formbricks={init:async r=>{var e,t;"object"==typeof r&&null!==r||(r={});const i={...window.tsdk_survey_data,...r,attributes:{...null!==(e=window.tsdk_survey_data.attributes)&&void 0!==e?e:{},...null!==(t=r.attributes)&&void 0!==t?t:{}}},{environmentId:n,appUrl:s,attributes:a,userId:d}=i;await(o?.setup({environmentId:n,appUrl:s})),o?.setAttributes(a),function(){const r=localStorage.getItem("formbricks-js");if(!r)return!0;try{const e=JSON.parse(r);if(e?.user?.data?.userId)return!1}catch(r){console.warn(r)}return!0}()&&o?.setUserId(d)}};let r=null;var e;e=window.tsdk_survey_data?.attributes?.install_days_number,isNaN(e)||"boolean"==typeof e||(r=setTimeout((()=>{window.tsdk_formbricks?.init()}),350)),window.addEventListener("themeisle:survey:trigger:cancel",(()=>{clearTimeout(r)})),window.dispatchEvent(new Event("themeisle:survey:loaded"))}))})();

View File

@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
return;
}
// Current SDK version and path.
$themeisle_sdk_version = '3.3.44';
$themeisle_sdk_version = '3.3.51';
$themeisle_sdk_path = dirname( __FILE__ );
global $themeisle_sdk_max_version;

View File

@ -41,6 +41,7 @@ abstract class Abstract_Module {
'templates-patterns-collection' => 'templates-patterns-collection/templates-patterns-collection.php',
'wpcf7-redirect' => 'wpcf7-redirect/wpcf7-redirect.php',
'wp-full-stripe-free' => 'wp-full-stripe-free/wp-full-stripe.php',
'learning-management-system' => 'learning-management-system/lms.php',
];
/**
@ -220,4 +221,13 @@ abstract class Abstract_Module {
return is_plugin_active( $plugin );
}
/**
* Get the current date.
*
* @return \DateTime The date time.
*/
public function get_current_date() {
return apply_filters( 'themeisle_sdk_current_date', new \DateTime( 'now' ) );
}
}

View File

@ -64,6 +64,7 @@ final class Loader {
'announcements',
'featured_plugins',
'float_widget',
'migrator',
];
/**
* Holds the labels for the modules.
@ -72,10 +73,11 @@ final class Loader {
*/
public static $labels = [
'announcements' => [
'hurry_up' => 'Hurry up! Only %s left.',
'sale_live' => 'Themeisle Black Friday Sale is Live!',
'learn_more' => 'Learn more',
'max_savings' => 'Enjoy Maximum Savings on %s',
'notice_link_label' => 'See the deals',
'max_savings' => 'Best WordPress Black Friday deals of %s — themes, plugins, hosting. Curated by the Themeisle team.',
'black_friday' => 'Black Friday Sale',
'time_left' => '%s left',
'plugin_meta_message' => 'Black Friday Sale - 60% OFF',
],
'compatibilities' => [
'notice' => '%s requires a newer version of %s. Please %supdate%s %s %s to the latest version.',
@ -108,10 +110,14 @@ final class Loader {
'valid' => 'Valid',
'invalid' => 'Invalid',
'notice' => 'Enter your license from %s purchase history in order to get %s updates',
'expired' => 'Your %s\'s License Key has expired. In order to continue receiving support and software updates you must %srenew%s your license key.',
'expired' => '%s license expired',
'expired_date' => 'Expired on %s',
'expired_notice' => 'Your current setup continues working, but premium features are disabled and you\'re no longer receive updates - including critical patches - or support.',
'inactive' => 'In order to benefit from updates and support for %s, please add your license code from your %spurchase history%s and validate it %shere%s.',
'no_activations' => 'No more activations left for %s. You need to upgrade your plan in order to use %s on more websites. If you need assistance, please get in touch with %s staff.',
'renew_license' => 'Renew License',
'learn_more' => 'Learn More',
],
'promotions' => [
'recommended' => 'Recommended by %s',
@ -172,6 +178,12 @@ final class Loader {
'dismisscta' => 'Dismiss this notice.',
'message' => 'Enhance your donation page with WP Full Pay—create custom Stripe forms for one-time and recurring donations, manage transactions easily, and boost support with a seamless setup.',
],
'masteriyo' => [
'gotodash' => 'Go to Masteriyo Dashboard',
'install' => 'Install Masteriyo',
'dismisscta' => 'Dismiss this notice.',
'message' => 'Transform your site into a learning hub with Masteriyo LMS. Build engaging courses with intuitive tools, track student progress effortlessly, and grow your education business with powerful marketing features and seamless payment integration.',
],
],
'welcome' => [
'ctan' => 'No, thanks.',
@ -243,9 +255,9 @@ final class Loader {
'cta' => 'Rollback to v%s',
],
'logger' => [
'notice' => 'Do you enjoy <b>{product}</b>? Become a contributor by opting in to our anonymous data tracking. We guarantee no sensitive data is collected.',
'cta_y' => 'Sure, I would love to help.',
'cta_n' => 'No, thanks.',
'notice' => 'Help improve <b>{product}</b> by sharing anonymous usage data about your setup. No personal data collected.',
'cta_y' => 'Count me in',
'cta_n' => 'No thanks',
],
'about_us' => [
'title' => 'About Us',
@ -325,10 +337,7 @@ final class Loader {
* Initialize the sdk logic.
*/
public static function init() {
/**
* This filter can be used to localize the labels inside each product.
*/
self::$labels = apply_filters( 'themeisle_sdk_labels', self::$labels );
self::localize_labels();
if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Loader ) ) {
self::$instance = new Loader();
$modules = array_merge( self::$available_modules, apply_filters( 'themeisle_sdk_modules', [] ) );
@ -338,8 +347,92 @@ final class Loader {
}
}
self::$available_modules = $modules;
add_action( 'themeisle_sdk_first_activation', array( __CLASS__, 'activate' ) );
}
}
/**
* Localize the labels.
*/
public static function localize_labels() {
$originals = self::$labels;
$all_translations = [];
global $wp_filter;
if ( isset( $wp_filter['themeisle_sdk_labels'] ) ) {
foreach ( $wp_filter['themeisle_sdk_labels']->callbacks as $priority => $hooks ) {
foreach ( $hooks as $hook ) {
// Each callback gets fresh originals, not previous callback's output
$result = call_user_func( $hook['function'], $originals );
$all_translations[] = $result;
}
}
// Remove the filter so it doesn't run again via apply_filters
remove_all_filters( 'themeisle_sdk_labels' );
}
// Merge all results, first real translation wins
self::$labels = self::merge_all_translations( $originals, $all_translations );
}
/**
* Merge all translations.
*
* @param array $originals The original labels.
* @param array $all_translations The all translations.
*
* @return array The merged labels.
*/
private static function merge_all_translations( $originals, $all_translations ) {
$result = $originals;
foreach ( $all_translations as $translations ) {
$result = self::merge_if_translated( $result, $translations, $originals );
}
return $result;
}
/**
* Merge if translated.
*
* @param array $current The current labels.
* @param array $new The new labels.
* @param array $originals The original labels.
* @return array The merged labels.
*/
private static function merge_if_translated( $current, $new, $originals ) {
foreach ( $new as $key => $value ) {
if ( ! isset( $originals[ $key ] ) ) {
// New key, accept it
if ( ! isset( $current[ $key ] ) ) {
$current[ $key ] = $value;
}
continue;
}
if ( is_array( $value ) && is_array( $originals[ $key ] ) ) {
$current[ $key ] = self::merge_if_translated(
$current[ $key ],
$value,
$originals[ $key ]
);
} else {
// Only accept if:
// 1. New value is actually translated (differs from original)
// 2. Current value is NOT already translated
$is_new_translated = ( $value !== $originals[ $key ] );
$is_current_untranslated = ( $current[ $key ] === $originals[ $key ] );
if ( $is_new_translated && $is_current_untranslated ) {
$current[ $key ] = $value;
}
}
}
return $current;
}
/**
* Get cache token used in API requests.
@ -384,6 +477,28 @@ final class Loader {
return self::$instance;
}
/**
* Activate the product routine.
*
* @param string $file The base file of the product.
*
* @return void
*/
public static function activate( $file ) {
$dirname = trailingslashit( dirname( ( $file ) ) );
if ( ! file_exists( $dirname . '_reference.php' ) ) {
return;
}
$reference_data = require_once $dirname . '_reference.php';
if ( ! is_array( $reference_data ) ||
! isset( $reference_data['key'] ) ||
! isset( $reference_data['value'] ) ||
! preg_match( '/^[a-zA-Z0-9_]+_reference_key$/', $reference_data['key'] ) ) {
return;
}
add_option( $reference_data['key'], sanitize_key( $reference_data['value'] ) );
}
/**
* Get all registered modules by the SDK.
*

View File

@ -14,6 +14,7 @@
* 'has_upgrade_menu' => <condition>,
* 'upgrade_link' => <url>,
* 'upgrade_text' => 'Get Pro Version',
* 'review_link' => false, // Leave it empty for default WPorg link or false to hide it.
* ]
* }
*
@ -95,6 +96,9 @@ class About_Us extends Abstract_Module {
return;
}
// Refresh the about data to get the latest changes.
$this->about_data = apply_filters( $this->product->get_key() . '_about_us_metadata', array() );
add_submenu_page(
$this->about_data['location'],
Loader::$labels['about_us']['title'],
@ -182,6 +186,8 @@ class About_Us extends Abstract_Module {
$asset_file = require $themeisle_sdk_max_path . '/assets/js/build/about/about.asset.php';
$deps = array_merge( $asset_file['dependencies'], [ 'updates' ] );
do_action( 'themeisle_internal_page', $this->product->get_slug(), 'about_us' );
wp_register_script( $handle, $this->get_sdk_uri() . 'assets/js/build/about/about.js', $deps, $asset_file['version'], true );
wp_localize_script( $handle, 'tiSDKAboutData', $this->get_about_localization_data() );
@ -228,6 +234,7 @@ class About_Us extends Abstract_Module {
],
'canInstallPlugins' => current_user_can( 'install_plugins' ),
'canActivatePlugins' => current_user_can( 'activate_plugins' ),
'showReviewLink' => ! ( isset( $this->about_data['review_link'] ) && false === $this->about_data['review_link'] ),
];
}
@ -334,6 +341,9 @@ class About_Us extends Abstract_Module {
'description' => Loader::$labels['about_us']['others']['neve_desc'],
'icon' => $this->get_sdk_uri() . 'assets/images/neve.png',
],
'learning-management-system' => [
'name' => 'Masteriyo LMS',
],
'otter-blocks' => [
'name' => 'Otter',
],

View File

@ -0,0 +1,88 @@
<?php
/**
* The abstract migration class for ThemeIsle SDK.
*
* @package ThemeIsleSDK
* @subpackage Modules
* @copyright Copyright (c) 2024, Themeisle
* @license http://opensource.org/licenses/gpl-3.0.php GNU Public License
* @since 3.3.50
*/
namespace ThemeisleSDK\Modules;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Abstract base class for SDK migrations.
*
* Migration files should return an anonymous class instance extending this class:
*
* return new class extends \ThemeisleSDK\Modules\Abstract_Migration {
* public function up() { ... }
* };
*/
abstract class Abstract_Migration {
/**
* WordPress database object.
*
* @var \wpdb
*/
protected $wpdb;
/**
* WordPress table prefix.
*
* @var string
*/
protected $prefix;
/**
* WordPress charset and collation string.
*
* @var string
*/
protected $charset_collate;
/**
* Constructor. Populates database helpers.
*/
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->prefix = $wpdb->prefix;
$this->charset_collate = $wpdb->get_charset_collate();
}
/**
* Run the migration.
*/
abstract public function up();
/**
* Reverse the migration.
*
* Override in concrete migrations to undo what up() did. Called by
* Migrator::rollback() — never invoked automatically.
*
* @return void
*/
public function down() {
// No-op by default. Override to implement rollback logic.
}
/**
* Determine whether this migration should run.
*
* Override to add a custom idempotency check beyond name-based tracking.
* Return false to skip the migration without recording it.
*
* @return bool
*/
public function should_run() {
return true;
}
}

View File

@ -13,6 +13,7 @@
namespace ThemeisleSDK\Modules;
use DateTime;
use ThemeisleSDK\Common\Abstract_Module;
use ThemeisleSDK\Loader;
use ThemeisleSDK\Product;
@ -22,56 +23,29 @@ use ThemeisleSDK\Product;
*/
class Announcements extends Abstract_Module {
const SALE_DURATION_BLACK_FRIDAY = '+7 days'; // DateTime modifier. (Include Cyber Monday)
const MINIMUM_INSTALL_AGE = 3 * DAY_IN_SECONDS;
/**
* Holds the timeline for the announcements.
* Mark if the notice was already loaded.
*
* @var array
* @var boolean
*/
private static $timeline = array(
'black_friday' => array(
'start' => '2024-11-25 00:00:00',
'end' => '2024-12-03 23:59:59',
'rendered' => false,
),
);
private static $notice_loaded = false;
/**
* Mark is a banner for a product was already loaded.
* Mark if the plugin meta link was already loaded.
*
* @var array
* @var boolean
*/
private static $banner_loaded = array();
const PLUGIN_PAGE = 'https://themeisle.com/plugins';
const THEME_PAGE = 'https://themeisle.com/themes';
const REVIVE_SOCIAL = 'https://revive.social/plugins';
private static $meta_link_loaded = false;
/**
* Holds the option prefix for the announcements.
*
* This is used to store the dismiss date for each announcement.
* The product to be used.
*
* @var string
*/
public $option_prefix = 'themeisle_sdk_announcement_';
/**
* Holds the time for the current request.
*
* @var string
*/
public $time = '';
/**
* Constructor for the Announcements module.
*
* @param array $timeline Optional. An array representing the timeline of announcements. Default is an empty array.
*/
public function __construct( $timeline = array() ) {
if ( is_array( $timeline ) && ! empty( $timeline ) ) {
self::$timeline = $timeline;
}
}
private static $current_product = '';
/**
* Check if the module can be loaded.
@ -96,16 +70,17 @@ class Announcements extends Abstract_Module {
* @return void
*/
public function load( $product ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
$this->product = $product;
add_action( 'admin_init', array( $this, 'load_announcements' ) );
add_filter( 'themeisle_sdk_active_announcements', array( $this, 'get_active_announcements' ) );
add_filter( 'themeisle_sdk_announcements', array( $this, 'get_announcements_for_plugins' ) );
add_action( 'themeisle_sdk_load_banner', array( $this, 'load_dashboard_banner_renderer' ) );
add_filter(
'themeisle_sdk_is_black_friday_sale',
function( $is_black_friday ) {
return $this->is_black_friday_sale( $this->get_current_date() );
}
);
add_action( 'admin_menu', array( $this, 'load_announcements' ), 9 );
add_action( 'wp_ajax_themeisle_sdk_dismiss_black_friday_notice', array( $this, 'disable_notification_ajax' ) );
}
/**
@ -114,197 +89,132 @@ class Announcements extends Abstract_Module {
* @return void
*/
public function load_announcements() {
$active = $this->get_active_announcements();
if ( empty( $active ) ) {
$current_date = $this->get_current_date();
if ( ! $this->is_black_friday_sale( $current_date ) ) {
return;
}
foreach ( $active as $announcement ) {
$method = $announcement . '_notice_render';
if ( method_exists( $this, $method ) ) {
add_action( 'admin_notices', array( $this, $method ) );
}
if ( self::MINIMUM_INSTALL_AGE > ( $current_date->getTimestamp() - $this->product->get_install_time() ) ) {
return;
}
// Load the ajax handler.
add_action( 'wp_ajax_themeisle_sdk_dismiss_announcement', array( $this, 'disable_notification_ajax' ) );
add_action( 'admin_notices', array( $this, 'black_friday_notice_render' ) );
add_action(
'themeisle_internal_page',
function( $plugin, $page_slug ) {
self::$current_product = $plugin;
},
10,
2
);
add_filter( 'plugin_row_meta', array( $this, 'add_plugin_meta_links' ), 10, 2 );
add_filter( $this->product->get_key() . '_about_us_metadata', array( $this, 'override_about_us_metadata' ), 100 );
}
/**
* Get all active announcements.
* Get the remaining time for the event in a human-readable format.
*
* @return array List of active announcements.
*/
public function get_active_announcements() {
$active = array();
foreach ( self::$timeline as $announcement_slug => $dates ) {
if ( $this->is_active( $dates ) && $this->can_show( $announcement_slug, $dates ) ) {
$active[] = $announcement_slug;
}
}
return $active;
}
/**
* Get all announcements along with plugin specific data.
*
* @return array List of announcements.
*/
public function get_announcements_for_plugins() {
$announcements = array();
foreach ( self::$timeline as $announcement => $dates ) {
$announcements[ $announcement ] = $dates;
if ( false !== strpos( $announcement, 'black_friday' ) ) {
$announcements[ $announcement ]['active'] = $this->is_active( $dates );
// Dashboard banners URLs.
$announcements[ $announcement ]['neve_dashboard_url'] = tsdk_utmify( self::THEME_PAGE . '/neve/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['hestia_dashboard_url'] = tsdk_utmify( self::THEME_PAGE . '/hestia/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['feedzy_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/feedzy-rss-feeds/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['otter_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/otter-blocks/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['mpg_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/multi-pages-generator/blackfriday', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['ppom_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/ppom-pro/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['rfc7r_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/wpcf7-redirect/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['hyve_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/hyve/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['spc_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/super-page-cache-pro/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['visualizer_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/visualizer-charts-and-graphs/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['feedzy_dashboard_url'] = tsdk_utmify( self::PLUGIN_PAGE . '/feedzy-rss-feeds/blackfriday/', 'bfcm24', 'dashboard' );
$announcements[ $announcement ]['rop_dashboard_url'] = tsdk_utmify( self::REVIVE_SOCIAL . '/revive-old-post/', 'bfcm24', 'dashboard' );
// Customizer banners URLs.
$announcements[ $announcement ]['hestia_customizer_url'] = tsdk_utmify( 'https://themeisle.com/black-friday/', 'bfcm24', 'hestiacustomizer' );
$announcements[ $announcement ]['neve_customizer_url'] = tsdk_utmify( 'https://themeisle.com/black-friday/', 'bfcm24', 'nevecustomizer' );
// Banners urgency text.
$remaining_time = $this->get_remaining_time_for_event( $dates['end'] );
$announcements[ $announcement ]['remaining_time'] = $remaining_time;
$announcements[ $announcement ]['urgency_text'] = ! empty( $remaining_time ) ? sprintf( Loader::$labels['announcements']['hurry_up'], $remaining_time ) : '';
$announcements[ $announcement ]['feedzy_banner_src'] = defined( 'FEEDZY_ABSURL' ) ? FEEDZY_ABSURL . 'img/black-friday.jpg' : '';
$announcements[ $announcement ]['visualizer_banner_src'] = defined( 'VISUALIZER_ABSURL' ) ? VISUALIZER_ABSURL . 'images/black-friday.jpg' : '';
$announcements[ $announcement ]['ppom_banner_src'] = defined( 'PPOM_URL' ) ? PPOM_URL . '/images/black-friday.jpg' : '';
$announcements[ $announcement ]['mpg_banner_src'] = defined( 'MPG_BASE_IMG_PATH' ) ? MPG_BASE_IMG_PATH . '/black-friday.jpg' : '';
$announcements[ $announcement ]['spc_banner_src'] = defined( 'SWCFPC_PLUGIN_URL' ) ? SWCFPC_PLUGIN_URL . 'assets/img/black-friday.jpg' : '';
$announcements[ $announcement ]['hestia_banner_src'] = defined( 'HESTIA_ASSETS_URL' ) ? HESTIA_ASSETS_URL . 'img/black-friday.jpg' : '';
$announcements[ $announcement ]['hyve_banner_src'] = defined( 'HYVE_LITE_URL' ) ? HYVE_LITE_URL . 'assets/images/black-friday.jpg' : '';
$announcements[ $announcement ]['rfc7r_banner_src'] = defined( 'WPCF7_PRO_REDIRECT_ASSETS_PATH' ) ? WPCF7_PRO_REDIRECT_ASSETS_PATH . 'images/black-friday.jpg' : '';
$announcements[ $announcement ]['rop_banner_src'] = defined( 'ROP_LITE_URL' ) ? ROP_LITE_URL . 'assets/img/black-friday.jpg' : '';
foreach ( $announcements[ $announcement ] as $key => $value ) {
if ( strpos( $key, '_url' ) !== false ) {
$announcements[ $announcement ][ $key ] = tsdk_translate_link( $value );
}
}
}
}
return apply_filters( 'themeisle_sdk_announcements_data', $announcements );
}
/**
* Get the announcement data.
*
* @param string $announcement The announcement to get the data for.
*
* @return array
*/
public function get_announcement_data( $announcement ) {
return ! empty( $announcement ) && is_string( $announcement ) && isset( self::$timeline[ $announcement ] ) ? self::$timeline[ $announcement ] : array();
}
/**
* Check if the announcement has an active timeline.
*
* @param array $dates The announcement to check.
*
* @return bool
*/
public function is_active( $dates ) {
if ( empty( $this->time ) ) {
$this->time = current_time( 'Y-m-d' );
}
$start = isset( $dates['start'] ) ? $dates['start'] : null;
$end = isset( $dates['end'] ) ? $dates['end'] : null;
if ( $start && $end ) {
return $start <= $this->time && $this->time <= $end;
} elseif ( $start ) {
return $this->time >= $start;
} elseif ( $end ) {
return $this->time <= $end;
}
return false;
}
/**
* Get the remaining time for the event in a human readable format.
*
* @param string $end_date The end date for event.
* @param DateTime $end_date The end date for event.
*
* @return string Remaining time for the event.
*/
public function get_remaining_time_for_event( $end_date ) {
if ( empty( $end_date ) || ! is_string( $end_date ) ) {
return '';
}
return human_time_diff( time(), strtotime( $end_date ) );
return human_time_diff( $this->get_current_date()->getTimestamp(), $end_date->getTimestamp() );
}
/**
* Check if the announcement can be shown.
*
* @param string $announcement_slug The announcement to check.
* @param array $dates The announcement to check.
* @param DateTime $current_date The announcement to check.
* @param int $user_id The user id to show the notice.
*
* @return bool
*/
public function can_show( $announcement_slug, $dates ) {
$dismiss_date = get_option( $this->option_prefix . $announcement_slug, false );
public function can_show_notice( $current_date, $user_id ) {
$current_year = $current_date->format( 'Y' );
$user_notice_dismiss_timestamp = get_user_meta( $user_id, 'themeisle_sdk_dismissed_notice_black_friday', true );
if ( false === $dismiss_date ) {
if ( empty( $user_notice_dismiss_timestamp ) ) {
return true;
}
// If the start date is after the dismiss date, show the notice.
$start = isset( $dates['start'] ) ? $dates['start'] : null;
if ( $start && $dismiss_date < $start ) {
return true;
}
$dismissed_year = wp_date( 'Y', $user_notice_dismiss_timestamp );
return false;
return $current_year !== $dismissed_year;
}
/**
* Disable the notification via ajax.
* Calculate the start date for Black Friday based on the year of the given date.
*
* @return void
* Black Friday is the day after the Thanksgiving and the sale starts on the Monday of that week.
*
* @param DateTime $date The current date object, used to determine the year.
* @return DateTime The start date of Black Friday for the given year.
*/
public function disable_notification_ajax() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'dismiss_themeisle_event_notice' ) ) {
wp_die( 'Invalid nonce! Refresh the page and try again.' );
public function get_start_date( $date ) {
$year = $date->format( 'Y' );
$black_friday = new DateTime( "last friday of november {$year}" );
$sale_start = clone $black_friday;
$sale_start->modify( 'monday this week' );
$sale_start->setTime( 0, 0 );
return $sale_start;
}
/**
* Calculate the event end date.
*
* @param DateTime $start_date The start date.
* @return DateTime The end date.
*/
public function get_end_date( $start_date ) {
$black_friday_end = clone $start_date;
$black_friday_end->modify( self::SALE_DURATION_BLACK_FRIDAY );
$black_friday_end->setTime( 23, 59, 59 );
return $black_friday_end;
}
/**
* Check if the current date falls within the Black Friday sale period.
*
* @param DateTime $current_date The date to check.
* @return bool True if the date is within the Black Friday sale period, false otherwise.
*/
public function is_black_friday_sale( $current_date ) {
$black_friday_start_date = $this->get_start_date( $current_date );
$black_friday_end = $this->get_end_date( $black_friday_start_date );
return $black_friday_start_date <= $current_date && $current_date <= $black_friday_end;
}
/**
* Get the notice data.
*
* @return array The notice data.
*/
public function get_notice_data() {
$time_left_label = $this->get_remaining_time_for_event( $this->get_end_date( $this->get_start_date( $this->get_current_date() ) ) );
$time_left_label = sprintf( Loader::$labels['announcements']['time_left'], $time_left_label );
$utm_location = 'globalnotice';
if ( ! empty( $this->product ) ) {
$utm_location = $this->product->get_friendly_name();
}
if ( ! isset( $_POST['announcement'] ) || ! is_string( $_POST['announcement'] ) ) {
wp_die( 'Invalid announcement! Refresh the page and try again.' );
}
$sale_title = Loader::$labels['announcements']['black_friday'];
$sale_url = tsdk_translate_link( tsdk_utmify( 'https://themeisle.com/blackfriday/', 'bfcm26', $utm_location ) );
$announcement = sanitize_key( $_POST['announcement'] );
$current_year = $this->get_current_date()->format( 'Y' );
$sale_message = sprintf( Loader::$labels['announcements']['max_savings'], $current_year );
update_option( $this->option_prefix . $announcement, current_time( 'Y-m-d' ) );
wp_die( 'success' );
return array(
'title' => $sale_title,
'sale_url' => $sale_url,
'message' => $sale_message,
'time_left' => $time_left_label,
);
}
/**
@ -315,183 +225,322 @@ class Announcements extends Abstract_Module {
public function black_friday_notice_render() {
// Prevent the notice from being rendered twice.
if ( self::$timeline['black_friday']['rendered'] ) {
if ( self::$notice_loaded ) {
return;
}
self::$timeline['black_friday']['rendered'] = true;
self::$notice_loaded = true;
$product_names = array();
$current_user_id = get_current_user_id();
foreach ( Loader::get_products() as $product ) {
$slug = $product->get_slug();
// NOTE: No notice if the user has at least one Pro product.
if ( $product->requires_license() ) {
return;
}
$product_names[] = $product->get_name();
if ( ! $this->can_show_notice( $this->get_current_date(), $current_user_id ) ) {
return;
}
// Randomize the products and get only 4.
shuffle( $product_names );
$product_names = array_slice( $product_names, 0, 4 );
$all_configs = apply_filters( 'themeisle_sdk_blackfriday_data', array( 'default' => $this->get_notice_data() ) );
if ( empty( $all_configs ) || ! is_array( $all_configs ) ) {
return;
}
$data = isset( $all_configs['default'] ) ? $all_configs['default'] : $this->get_notice_data();
$products = Loader::get_products();
$current_time = $this->get_current_date()->getTimestamp();
$can_show = false;
// Check if we have products that are eligible to show the notice with the default data. If the product provide its own config, use it.
foreach ( $products as $product ) {
$slug = $product->get_slug();
if ( self::MINIMUM_INSTALL_AGE < ( $current_time - $product->get_install_time() ) ) {
$can_show = true;
if ( isset( $all_configs[ $slug ] ) && ! empty( $all_configs[ $slug ] ) && is_array( $all_configs[ $slug ] ) ) {
$data = $all_configs[ $slug ];
if ( self::$current_product === $slug ) {
$data = $all_configs[ $slug ];
break;
}
}
}
}
if ( ! $can_show ) {
return;
}
$displayed_on_internal_page = 0 < did_action( 'themeisle_internal_page' );
$title = ! empty( $data['title'] ) ? $data['title'] : Loader::$labels['announcements']['black_friday'];
$time_left_label = ! empty( $data['time_left'] ) ? $data['time_left'] : '';
$message = ! empty( $data['message'] ) ? $data['message'] : '';
$logo_url = ! empty( $data['logo_url'] ) ? $data['logo_url'] : $this->get_sdk_uri() . 'assets/images/themeisle-logo.png';
$cta_label = ! empty( $data['cta_label'] ) ? $data['cta_label'] : Loader::$labels['announcements']['notice_link_label'];
$sale_url = ! empty( $data['sale_url'] ) ? $data['sale_url'] : '';
$hide_other_notices = ! empty( $data['hide_other_notices'] ) ? $data['hide_other_notices'] : $displayed_on_internal_page;
$dismiss_notice_url = wp_nonce_url(
add_query_arg(
array( 'action' => 'themeisle_sdk_dismiss_black_friday_notice' ),
admin_url( 'admin-ajax.php' )
),
'dismiss_themeisle_event_notice'
);
if ( empty( $sale_url ) ) {
return;
}
if ( ! current_user_can( 'install_plugins' ) ) {
$sale_url = remove_query_arg( 'lkey', $sale_url );
}
?>
<style>
.themeisle-sale {
border-left-color: #0466CB;
}
.themeisle-sale :is(.themeisle-sale-title, p) {
margin: 0;
}
.themeisle-sale-container {
display: flex;
align-items: center;
padding: 0.5rem 0;
gap: 0.5rem;
padding-right: 10px;
}
.themeisle-sale-content {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.themeisle-sale a {
text-decoration: none;
}
.themeisle-sale p a {
margin-left: 1rem;
padding: 7px 12px;
border-radius: 4px;
background: #0466CB;
color: white;
font-weight: 700;
}
.themeisle-sale-dismiss {
padding-top: 5px;
}
.themeisle-sale-dismiss span {
color: #787c82;
font-size: 16px;
}
.notice.themeisle-sale {
padding: 0;
}
.themeisle-sale-logo {
display: flex;
justify-content: center;
align-items: center;
margin-left: 5px;
}
.themeisle-sale-time-left {
margin-left: 5px;
padding: 3px 5px;
border-radius: 4px;
background-color: #dfdfdf;
font-weight: 600;
font-size: x-small;
line-height: 1;
}
.themeisle-sale-title {
font-size: 14px;
display: flex;
align-items: center;
}
.themeisle-sale-action {
flex-grow: 1;
display: flex;
justify-content: flex-end;
}
<?php if ( $hide_other_notices ) : ?>
.notice:not(.themeisle-sale) {
display: none;
}
<?php endif; ?>
</style>
<div class="themeisle-sale notice notice-info is-dismissible" data-announcement="black_friday">
<img width="24" src="<?php echo esc_url_raw( $this->get_sdk_uri() . 'assets/images/themeisle-logo.png' ); ?>"/>
<p>
<strong><?php echo esc_html( Loader::$labels['announcements']['sale_live'] ); ?></strong>
- <?php echo sprintf( esc_html( Loader::$labels['announcements']['max_savings'] ), esc_html( implode( ', ', $product_names ) ) ); ?>.
<a href="<?php echo esc_url_raw( tsdk_utmify( 'https://themeisle.com/blackfriday/', 'bfcm24', 'globalnotice' ) ); ?>"
target="_blank"><?php echo esc_html( Loader::$labels['announcements']['learn_more'] ); ?></a>
<span class="themeisle-sale-error"></span>
</p>
<div class="themeisle-sale notice notice-info" data-event-slug="black_friday">
<div class="themeisle-sale-container">
<div class="themeisle-sale-logo">
<img
width="45"
src="<?php echo esc_url( $logo_url ); ?>"
/>
</div>
<div class="themeisle-sale-content">
<h4 class="themeisle-sale-title">
<?php echo esc_html( $title ); ?>
<span class="themeisle-sale-time-left">
<?php echo esc_html( $time_left_label ); ?>
</span>
</h4>
<p>
<?php echo wp_kses_post( $message ); ?>
</p>
</div>
<div class="themeisle-sale-action">
<a
href="<?php echo esc_url( $sale_url ); ?>"
target="_blank"
class="button button-primary themeisle-sale-button"
>
<?php echo esc_html( $cta_label ); ?>
</a>
</div>
<a href="<?php echo esc_url( $dismiss_notice_url ); ?>" class="themeisle-sale-dismiss">
<span class="dashicons dashicons-dismiss"></span>
</a>
</div>
</div>
<script type="text/javascript" data-origin="themeisle-sdk">
window.document.addEventListener('DOMContentLoaded', () => {
const observer = new MutationObserver((mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
const container = document.querySelector('.themeisle-sale.notice');
const button = container?.querySelector('button');
if (button) {
button.addEventListener('click', e => {
e.preventDefault();
fetch('<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
action: 'themeisle_sdk_dismiss_announcement',
nonce: '<?php echo esc_attr( wp_create_nonce( 'dismiss_themeisle_event_notice' ) ); ?>',
announcement: container.dataset.announcement
})
})
.then(response => response.text())
.then(response => {
if (!response?.includes('success')) {
document.querySelector('.themeisle-sale-error').innerHTML = response;
return;
}
<script>
// Note: Some plugins use React and the content is ready after the `DOMContentLoaded` event. Use this function to reposition the notice after components have rendered.
window.tsdk_reposition_notice = function() {
const bannerRoot = document.getElementById('tsdk_banner');
const saleNotice = document.querySelector('.themeisle-sale');
if ( ! bannerRoot || ! saleNotice ) {
return;
}
document.querySelectorAll('.themeisle-sale.notice').forEach(el => {
el.classList.add('hidden');
setTimeout(() => {
el.remove();
}, 800);
});
})
.catch(error => {
console.error('Error:', error);
document.querySelector('.themeisle-sale-error').innerHTML = error;
});
});
observer.disconnect();
break;
}
}
}
});
bannerRoot.appendChild(saleNotice);
};
observer.observe(document.body, {childList: true, subtree: true});
});
document.addEventListener( 'DOMContentLoaded', function() {
window.tsdk_reposition_notice();
} );
</script>
<?php
}
/**
* Load the dashboard banner renderer.
*
* @param string $product_key The product key.
* Disable the notification via ajax.
*
* @return void
*/
public function load_dashboard_banner_renderer( $product_key ) {
public function disable_notification_ajax() {
check_ajax_referer( 'dismiss_themeisle_event_notice' );
$banner_handler = apply_filters( 'themeisle_sdk_dependency_script_handler', 'banner' );
update_user_meta( get_current_user_id(), 'themeisle_sdk_dismissed_notice_black_friday', $this->get_current_date()->getTimestamp() );
if ( empty( $banner_handler ) ) {
return;
$return_page_url = wp_get_referer();
if ( empty( $return_page_url ) ) {
$return_page_url = admin_url();
}
if ( isset( self::$banner_loaded[ $product_key ] ) && true === self::$banner_loaded[ $product_key ] ) {
return;
}
self::$banner_loaded[ $product_key ] = true;
$banner_data = array();
// Get the first active banner.
foreach ( $this->get_announcements_for_plugins() as $announcement ) {
if ( false === $announcement['active'] ) {
continue;
}
$cta_key = $product_key . '_dashboard_url';
$banner_src_key = $product_key . '_banner_src';
if (
! isset( $announcement[ $cta_key ] ) ||
! isset( $announcement[ $banner_src_key ] ) ||
empty( $announcement[ $banner_src_key ] ) ||
! isset( $announcement['urgency_text'] )
) {
continue;
}
$banner_data = array(
'content' => $this->render_banner(
array(
'cta_url' => $announcement[ $cta_key ],
'img_src' => $announcement[ $banner_src_key ],
'urgency_text' => $announcement['urgency_text'],
)
),
);
break;
}
if ( empty( $banner_data ) ) {
return;
}
do_action( 'themeisle_sdk_dependency_enqueue_script', 'banner' );
wp_localize_script( $banner_handler, 'tsdk_banner_data', $banner_data );
wp_safe_redirect( $return_page_url );
exit;
}
/**
* Renders a banner with the provided settings.
* Add the plugin meta links.
*
* @param array $settings {
* Optional. An array of settings for the banner.
*
* @type string $cta_url The URL for the call-to-action link.
* @type string $img_src The source URL for the banner image.
* @type string $urgency_text The urgency text to display on the banner.
* }
* @return string The HTML output of the banner.
* @param array<string, string> $links The plugin meta links.
* @param string $plugin_file The plugin file.
* @return array<string, string> The plugin meta links.
*/
public function render_banner( $settings = array() ) {
if ( empty( $settings ) ) {
return '';
public function add_plugin_meta_links( $links, $plugin_file ) {
if ( self::$meta_link_loaded ) {
return $links;
}
return wp_kses_post(
wp_sprintf(
'<a href="%s" target="_blank" class="tsdk-banner-cta"><img src="%s" class="tsdk-banner-img"><div class="tsdk-banner-urgency-text">%s</div></a>',
esc_url_raw( $settings['cta_url'] ),
esc_url_raw( $settings['img_src'] ),
sanitize_text_field( $settings['urgency_text'] )
)
);
if ( $plugin_file !== plugin_basename( $this->product->get_basefile() ) ) {
return $links;
}
$configs = apply_filters( 'themeisle_sdk_blackfriday_data', array( 'default' => $this->get_notice_data() ) );
if ( empty( $configs ) || ! is_array( $configs ) ) {
return $links;
}
$current_slug = $this->product->get_slug();
$data = isset( $configs[ $current_slug ] ) && ! empty( $configs[ $current_slug ] ) && is_array( $configs[ $current_slug ] ) ? $configs[ $current_slug ] : array();
$plugin_meta_message = '';
$plugin_meta_url = '';
if ( isset( $data['plugin_meta_targets'] ) && ! empty( $data['plugin_meta_targets'] ) && ! in_array( $current_slug, $data['plugin_meta_targets'] ) ) {
return $links; // The current configuration is for another plugins.
}
$plugin_meta_message = ! empty( $data['plugin_meta_message'] ) ? $data['plugin_meta_message'] : '';
$plugin_meta_url = ! empty( $data['sale_url'] ) ? $data['sale_url'] : '';
if ( empty( $plugin_meta_url ) || empty( $plugin_meta_message ) ) {
// Check if a configuration is in another plugin.
$products = Loader::get_products();
foreach ( $products as $product ) {
$slug = $product->get_slug();
if ( $slug === $current_slug || ! isset( $configs[ $slug ] ) || empty( $configs[ $slug ] ) || ! is_array( $configs[ $slug ] ) ) {
continue;
}
if ( ! empty( $configs[ $slug ]['plugin_meta_targets'] ) && in_array( $current_slug, $configs[ $slug ]['plugin_meta_targets'] ) ) {
$plugin_meta_message = ! empty( $configs[ $slug ]['plugin_meta_message'] ) ? $configs[ $slug ]['plugin_meta_message'] : '';
$plugin_meta_url = ! empty( $configs[ $slug ]['sale_url'] ) ? $configs[ $slug ]['sale_url'] : '';
break;
}
}
}
if ( empty( $plugin_meta_url ) || empty( $plugin_meta_message ) ) {
return $links;
}
$links[] = sprintf( '<a class="themeisle-sale-plugin-meta-link" style="color: red;" href="%s" target="_blank">%s</a>', esc_url( $plugin_meta_url ), esc_html( $plugin_meta_message ) );
self::$meta_link_loaded = true;
return $links;
}
/**
* Override the About Us upgrade menu during Black Friday.
*
* Registered dynamically during admin_menu when sale is active.
* Only applies if About_Us module is loaded for the product.
*
* @param array<string, mixed> $about_data About Us metadata.
*
* @return array<string, mixed>
*/
public function override_about_us_metadata( $about_data ) {
if ( ! $this->is_black_friday_sale( $this->get_current_date() ) ) {
return $about_data;
}
if ( empty( $about_data ) || ! is_array( $about_data ) ) {
return $about_data;
}
if ( empty( $about_data['has_upgrade_menu'] ) || true !== $about_data['has_upgrade_menu'] ) {
return $about_data;
}
$configs = apply_filters( 'themeisle_sdk_blackfriday_data', array( 'default' => $this->get_notice_data() ) );
$current_slug = $this->product->get_slug();
if ( ! isset( $configs[ $current_slug ] ) || empty( $configs[ $current_slug ] ) || ! is_array( $configs[ $current_slug ] ) ) {
return $about_data;
}
$config = $configs[ $current_slug ];
if ( empty( $config['upgrade_menu_text'] ) || empty( $config['sale_url'] ) ) {
return $about_data;
}
$about_data['upgrade_text'] = $config['upgrade_menu_text'];
$about_data['upgrade_link'] = $config['sale_url'];
return $about_data;
}
}

View File

@ -13,6 +13,7 @@
namespace ThemeisleSDK\Modules;
use ThemeisleSDK\Common\Abstract_Module;
use ThemeisleSDK\Loader;
use ThemeisleSDK\Product;
// Exit if accessed directly.
@ -32,6 +33,13 @@ class Featured_Plugins extends Abstract_Module {
*/
private $transient_key = 'themeisle_sdk_featured_plugins_';
/**
* The current product instance.
*
* @var Product|null
*/
protected $product = null;
/**
* Check if the module can be loaded.
*
@ -59,6 +67,8 @@ class Featured_Plugins extends Abstract_Module {
* @return void
*/
public function load( $product ) {
$this->product = $product;
if ( ! current_user_can( 'install_plugins' ) ) {
return;
}
@ -69,7 +79,58 @@ class Featured_Plugins extends Abstract_Module {
}
add_filter( 'themeisle_sdk_plugin_api_filter_registered', '__return_true' );
add_filter( 'plugins_api_result', [ $this, 'filter_plugin_api_results' ], 10, 3 );
add_filter( 'plugins_api_result', [ $this, 'filter_plugin_api_results' ], 11, 3 );
// Enqueue inline JS only on plugin-install.php.
add_action( 'admin_enqueue_scripts', [ $this, 'maybe_add_inline_js' ] );
}
/**
* Enqueue inline JavaScript only on plugin-install.php.
*
* @return void
*/
public function maybe_add_inline_js() {
$screen = get_current_screen();
if ( isset( $screen->base ) && 'plugin-install' === $screen->base ) {
add_action(
'admin_footer',
function() {
$text = esc_html( sprintf( Loader::$labels['promotions']['recommended'], $this->product->get_friendly_name() ) );
echo '<script>(function(){
function onPluginCardFound(card) {
var recommendedDiv = document.createElement("div");
Object.assign(recommendedDiv.style, {
display: "block",
textAlign: "center",
padding: "0 12px 12px",
background: "#f6f7f7"
});
recommendedDiv.innerHTML = "' . esc_html( $text ) . '";
card.appendChild(recommendedDiv);
}
function checkAndRun() {
var card = document.querySelector(".plugin-card-learning-management-system");
if (card && !card.dataset.recommendedAdded) {
onPluginCardFound(card);
card.dataset.recommendedAdded = "true";
}
}
var observer = new MutationObserver(function(mutations) {
checkAndRun();
});
observer.observe(document.body, { childList: true, subtree: true });
// Initial check in case the card is already present.
checkAndRun();
})();</script>';
}
);
}
}
/**
@ -87,6 +148,11 @@ class Featured_Plugins extends Abstract_Module {
return $res;
}
if ( isset( $args->page ) && 1 === (int) $args->page && isset( $args->search ) && ! empty( $args->search ) ) {
$res->plugins = $this->maybe_prepend_lms_plugin( $res->plugins, $args );
return $res;
}
if ( ! isset( $args->browse ) || $args->browse !== 'featured' ) {
return $res;
}
@ -100,6 +166,38 @@ class Featured_Plugins extends Abstract_Module {
return $res;
}
/**
* Prepend the LMS plugin if the search query matches LMS-related terms.
*
* @param array $plugins The plugins array.
* @param object $args The plugin API arguments.
* @return array
*/
private function maybe_prepend_lms_plugin( $plugins, $args ) {
$search = isset( $args->search ) ? strtolower( $args->search ) : '';
if (
strpos( $search, 'lms' ) !== false ||
strpos( $search, 'learn' ) !== false
) {
$filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
$masteriyo = $this->get_plugins_filtered_from_author( $args, $filter_slugs, 'masteriyo' );
if ( ! empty( $masteriyo ) ) {
// Remove existing LMS plugin if present to avoid duplicates.
$plugins = array_filter(
$plugins,
function( $plugin ) {
return ( is_object( $plugin ) && isset( $plugin->slug ) && $plugin->slug !== 'learning-management-system' ) ||
( is_array( $plugin ) && isset( $plugin['slug'] ) && $plugin['slug'] !== 'learning-management-system' );
}
);
$plugins = array_merge( $masteriyo, $plugins );
}
}
return $plugins;
}
/**
* Query plugins by author.
*
@ -114,7 +212,7 @@ class Featured_Plugins extends Abstract_Module {
$filtered_from_optimole = $this->get_plugins_filtered_from_author( $args, $optimole_filter_slugs, 'Optimole' );
$featured = array_merge( $featured, $filtered_from_optimole );
$themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks' ] );
$themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks', 'wp-cloudflare-page-cache' ] );
$filtered_from_themeisle = $this->get_plugins_filtered_from_author( $args, $themeisle_filter_slugs );
$featured = array_merge( $featured, $filtered_from_themeisle );
@ -130,7 +228,7 @@ class Featured_Plugins extends Abstract_Module {
*
* @return array
*/
private function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
protected function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
$cached = get_transient( $this->transient_key . $author );
if ( $cached ) {

View File

@ -380,7 +380,7 @@ class Licenser extends Abstract_Module {
$status = $this->get_license_status( true );
$no_activations_string = apply_filters( $this->product->get_key() . '_lc_no_activations_string', Loader::$labels['licenser']['no_activations'] );
$no_valid_string = apply_filters( $this->product->get_key() . '_lc_no_valid_string', sprintf( Loader::$labels['licenser']['inactive'], '%s', '<a href="%s" target="_blank">', '</a>', '<a href="%s">', '</a>' ) );
$expired_license_string = apply_filters( $this->product->get_key() . '_lc_expired_string', sprintf( Loader::$labels['licenser']['expired'], '%s', '<a href="%s" target="_blank">', '</a>' ) );
$expired_license_string = apply_filters( $this->product->get_key() . '_lc_expired_heading_string', Loader::$labels['licenser']['expired'] );
// No activations left for this license.
if ( 'valid' != $status && $this->check_activation() ) {
?>
@ -403,12 +403,72 @@ class Licenser extends Abstract_Module {
// Invalid license key.
if ( 'active_expired' === $status ) {
// Check if the notice was dismissed.
$dismiss_option_key = $this->product->get_key() . '_expired_notice_dismissed';
if ( get_option( $dismiss_option_key, false ) ) {
return false;
}
$license_data = get_option( $this->product->get_key() . '_license_data', '' );
$expiration_date = '';
if ( is_object( $license_data ) && isset( $license_data->expires ) ) {
$timestamp = strtotime( (string) $license_data->expires );
if ( false !== $timestamp ) {
$expiration_date = gmdate( 'F j, Y', $timestamp );
}
}
$discount_config = apply_filters( $this->product->get_key() . '_lc_renew_discount', false );
if ( is_array( $discount_config ) && isset( $discount_config['url'] ) && isset( $discount_config['renew_button'] ) ) {
$renew_url = $discount_config['url'];
$renew_button = $discount_config['renew_button'];
} else {
$renew_url = apply_filters( $this->product->get_key() . '_lc_renew_url', $this->renew_url() );
$renew_button = apply_filters( $this->product->get_key() . '_lc_renew_button_string', Loader::$labels['licenser']['renew_license'] );
}
$learn_more_url = apply_filters( $this->product->get_key() . '_lc_learn_more_url', $this->get_api_url() );
$learn_more_button = apply_filters( $this->product->get_key() . '_lc_learn_more_button_string', Loader::$labels['licenser']['learn_more'] );
$notice_message = apply_filters( $this->product->get_key() . '_lc_expired_notice_message', Loader::$labels['licenser']['expired_notice'] );
$expired_date_string = apply_filters( $this->product->get_key() . '_lc_expired_date_string', sprintf( Loader::$labels['licenser']['expired_date'], esc_html( $expiration_date ) ) );
$heading = apply_filters( $this->product->get_key() . '_lc_expired_heading_string', sprintf( Loader::$labels['licenser']['expired'], $this->product->get_name() ) );
$notice_id = $this->product->get_key() . '_expired_notice';
?>
<div class="error">
<p>
<strong><?php echo sprintf( wp_kses_data( $expired_license_string ), esc_attr( $this->product->get_name() . ' ' . $this->product->get_type() ), esc_url( $this->get_api_url() . '?license=' . $this->license_key ) ); ?> </strong>
<div class="notice notice-warning notice-alt is-dismissible themeisle-sdk-license-notice" id="<?php echo esc_attr( $notice_id ); ?>" data-notice-id="<?php echo esc_attr( $notice_id ); ?>" style="position: relative; border-left: 4px solid #d63638; padding: 12px; background-color: #fff;">
<p style="margin: 0.5em 0; font-size: 13px;">
<strong><?php echo wp_kses_post( $heading ); ?></strong>
· <?php echo esc_html( $expired_date_string ); ?>
</p>
<p style="margin: 0.5em 0 1em 0; font-size: 13px;">
<?php echo esc_html( $notice_message ); ?>
</p>
<p style="margin: 0.5em 0;">
<a href="<?php echo esc_url( $renew_url ); ?>" class="button button-primary" target="_blank" rel="noopener noreferrer">
<?php echo esc_html( $renew_button ); ?>
</a>
<a href="<?php echo esc_url( $learn_more_url ); ?>" class="button" target="_blank" rel="noopener noreferrer" style="border: none; background-color: transparent;">
<?php echo esc_html( $learn_more_button ); ?>
</a>
</p>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#<?php echo esc_js( $notice_id ); ?>').on('click', '.notice-dismiss', function(e) {
const noticeId = '<?php echo esc_js( $notice_id ); ?>';
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'themeisle_sdk_dismiss_license_notice',
notice_id: noticeId,
nonce: '<?php echo esc_js( wp_create_nonce( 'themeisle_sdk_dismiss_license_notice' ) ); ?>'
}
});
});
});
</script>
<?php
return false;
@ -1045,6 +1105,32 @@ class Licenser extends Abstract_Module {
add_action( 'admin_init', array( $this, 'product_valid' ), 99999999 );
add_action( 'admin_notices', array( $this, 'show_notice' ) );
add_filter( $this->product->get_key() . '_license_status', array( $this, 'get_license_status' ) );
add_action( 'wp_ajax_themeisle_sdk_dismiss_license_notice', array( $this, 'dismiss_license_notice' ) );
}
/**
* Handle AJAX request to dismiss the license notice.
*/
public function dismiss_license_notice() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'themeisle_sdk_dismiss_license_notice' ) ) {
wp_send_json_error( 'Invalid nonce' );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
$notice_id = isset( $_POST['notice_id'] ) ? sanitize_text_field( $_POST['notice_id'] ) : '';
if ( empty( $notice_id ) ) {
wp_send_json_error( 'Missing notice ID' );
}
// Save the dismissal option.
$dismiss_option_key = $notice_id . '_dismissed';
update_option( $dismiss_option_key, true );
wp_send_json_success();
}
/**

View File

@ -55,8 +55,8 @@ class Logger extends Abstract_Module {
*/
public function load( $product ) {
$this->product = $product;
$this->setup_notification();
$this->setup_actions();
add_action( 'wp_loaded', array( $this, 'setup_actions' ) );
add_action( 'admin_init', array( $this, 'setup_notification' ) );
return $this;
}
@ -64,12 +64,10 @@ class Logger extends Abstract_Module {
* Setup notification on admin.
*/
public function setup_notification() {
if ( ! $this->product->is_wordpress_available() ) {
if ( $this->is_logger_active() ) {
return;
}
add_filter( 'themeisle_sdk_registered_notifications', [ $this, 'add_notification' ] );
}
/**
@ -79,7 +77,6 @@ class Logger extends Abstract_Module {
if ( ! $this->is_logger_active() ) {
return;
}
add_action(
'admin_enqueue_scripts',
function() {
@ -89,7 +86,7 @@ class Logger extends Abstract_Module {
$this->load_telemetry();
},
PHP_INT_MAX
PHP_INT_MAX
);
$action_key = $this->product->get_key() . '_log_activity';
@ -105,24 +102,26 @@ class Logger extends Abstract_Module {
* @return bool Is logger active?
*/
private function is_logger_active() {
if ( apply_filters( 'themeisle_sdk_disable_telemetry', false ) ) {
return false;
}
$default = 'no';
if ( ! $this->product->is_wordpress_available() ) {
$default = 'yes';
} else {
$pro_slug = $this->product->get_pro_slug();
if ( ! empty( $pro_slug ) ) {
$all_products = Loader::get_products();
if ( isset( $all_products[ $pro_slug ] ) ) {
$all_products = Loader::get_products();
foreach ( $all_products as $product ) {
if ( $product->requires_license() ) {
$default = 'yes';
break;
}
}
}
return ( get_option( $this->product->get_key() . '_logger_flag', $default ) === 'yes' );
}
/**
* Add notification to queue.
*
@ -179,14 +178,15 @@ class Logger extends Abstract_Module {
'timeout' => 3,
'redirection' => 5,
'body' => array(
'site' => get_site_url(),
'slug' => $this->product->get_slug(),
'version' => $this->product->get_version(),
'wp_version' => $wp_version,
'locale' => get_locale(),
'data' => apply_filters( $this->product->get_key() . '_logger_data', array() ),
'environment' => $environment,
'license' => apply_filters( $this->product->get_key() . '_license_status', '' ),
'site' => get_site_url(),
'slug' => $this->product->get_slug(),
'version' => $this->product->get_version(),
'wp_version' => $wp_version,
'install_time' => $this->product->get_install_time(),
'locale' => get_locale(),
'data' => apply_filters( $this->product->get_key() . '_logger_data', array() ),
'environment' => $environment,
'license' => apply_filters( $this->product->get_key() . '_license_status', '' ),
),
)
);

View File

@ -0,0 +1,177 @@
<?php
/**
* The migrator module for ThemeIsle SDK.
*
* @package ThemeIsleSDK
* @subpackage Modules
* @copyright Copyright (c) 2024, Themeisle
* @license http://opensource.org/licenses/gpl-3.0.php GNU Public License
* @since 3.3.50
*/
namespace ThemeisleSDK\Modules;
use ThemeisleSDK\Common\Abstract_Module;
use ThemeisleSDK\Product;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Migrator module for ThemeIsle SDK.
*
* Allows products to ship PHP migration files that run automatically on
* admin page loads. Each product opts in by registering its migrations
* directory via the `{product_slug}_sdk_migrations_path` filter.
*/
class Migrator extends Abstract_Module {
/**
* Option key suffix used to store the list of ran migrations.
*/
const OPTION_SUFFIX = '_ran_migrations';
/**
* Check if we should load the module for this product.
*
* Always returns true — the actual path check happens lazily at admin_init.
*
* @param Product $product Product to load the module for.
*
* @return bool
*/
public function can_load( $product ) {
return apply_filters( $product->get_slug() . '_sdk_enable_migrator', true );
}
/**
* Load module logic.
*
* @param Product $product Product to load.
*
* @return Migrator
*/
public function load( $product ) {
$this->product = $product;
add_action( 'admin_init', array( $this, 'run_pending' ) );
add_action( 'themeisle_sdk_rollback_migration_' . $product->get_slug(), array( $this, 'rollback' ) );
return $this;
}
/**
* Discover and run any pending migrations for the product.
*
* Only runs when a version upgrade was detected during this request, indicated
* by the themeisle_sdk_update_{slug} action having fired.
*
* @return void
*/
public function run_pending() {
if ( ! did_action( 'themeisle_sdk_update_' . $this->product->get_slug() ) ) {
return;
}
$path = $this->get_migrations_path();
if ( empty( $path ) || ! is_dir( $path ) ) {
return;
}
$files = glob( trailingslashit( $path ) . '*.php' );
if ( empty( $files ) ) {
return;
}
sort( $files ); // Alphabetical order = chronological order given timestamp naming.
$option_key = $this->product->get_key() . self::OPTION_SUFFIX;
$ran = get_option( $option_key, array() );
foreach ( $files as $file ) {
$name = basename( $file, '.php' );
if ( in_array( $name, $ran, true ) ) {
continue;
}
try {
$migration = require $file; // Migration files return an anonymous class instance.
if ( ! ( $migration instanceof Abstract_Migration ) ) {
continue;
}
if ( ! $migration->should_run() ) {
continue;
}
$migration->up();
$ran[] = $name;
update_option( $option_key, $ran );
} catch ( \Throwable $e ) {
// Log and stop — leave the migration unrecorded so it retries next load.
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'ThemeIsle SDK Migrator: failed to run ' . $name . ': ' . $e->getMessage() );
break;
}
}
}
/**
* Roll back a single migration by name.
*
* Calls down() on the migration and removes it from the ran list so it will
* be picked up again on the next upgrade. This method is never called
* automatically — products invoke it explicitly when needed.
*
* @param string $migration_name Migration basename without .php extension.
*
* @return bool True if rolled back successfully, false if not found or not previously run.
*/
public function rollback( $migration_name ) {
$option_key = $this->product->get_key() . self::OPTION_SUFFIX;
$ran = get_option( $option_key, array() );
if ( ! in_array( $migration_name, $ran, true ) ) {
return false;
}
$path = $this->get_migrations_path();
$file = trailingslashit( $path ) . $migration_name . '.php';
if ( ! is_file( $file ) ) {
return false;
}
try {
$migration = require $file;
if ( ! ( $migration instanceof Abstract_Migration ) ) {
return false;
}
$migration->down();
update_option( $option_key, array_values( array_diff( $ran, array( $migration_name ) ) ) );
return true;
} catch ( \Throwable $e ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'ThemeIsle SDK Migrator: failed to roll back ' . $migration_name . ': ' . $e->getMessage() );
return false;
}
}
/**
* Get the migrations directory path for the current product.
*
* Products register their path via the `{slug}_sdk_migrations_path` filter.
*
* @return string Absolute path to the migrations directory, or empty string.
*/
private function get_migrations_path() {
return (string) apply_filters( $this->product->get_slug() . '_sdk_migrations_path', '' );
}
}

View File

@ -105,6 +105,13 @@ class Promotions extends Abstract_Module {
*/
private $option_feedzy = 'themeisle_sdk_promotions_feedzy_installed';
/**
* Option key for Masteriyo promos.
*
* @var string
*/
private $option_masteriyo = 'themeisle_sdk_promotions_masteriyo_installed';
/**
* Loaded promotion.
*
@ -152,6 +159,7 @@ class Promotions extends Abstract_Module {
$promotions_to_load[] = 'hyve';
$promotions_to_load[] = 'wp_full_pay';
$promotions_to_load[] = 'feedzy_import';
$promotions_to_load[] = 'learning-management-system';
if ( defined( 'NEVE_VERSION' ) || defined( 'WPMM_PATH' ) || defined( 'OTTER_BLOCKS_VERSION' ) || defined( 'OBFX_URL' ) ) {
$promotions_to_load[] = 'feedzy_embed';
@ -257,6 +265,7 @@ class Promotions extends Abstract_Module {
if ( isset( $_GET['wp_full_pay_reference_key'] ) ) {
update_option( 'wp_full_pay_reference_key', sanitize_key( $_GET['wp_full_pay_reference_key'] ) );
}
if ( isset( $_GET['feedzy_reference_key'] ) || ( isset( $_GET['from'], $_GET['plugin'] ) && $_GET['from'] === 'import' && str_starts_with( sanitize_key( $_GET['plugin'] ), 'feedzy' ) ) ) {
update_option( 'feedzy_reference_key', sanitize_key( $_GET['feedzy_reference_key'] ?? 'i-' . $this->product->get_key() ) );
update_option( $this->option_feedzy, 1 );
@ -350,6 +359,16 @@ class Promotions extends Abstract_Module {
'default' => false,
)
);
register_setting(
'themeisle_sdk_settings',
$this->option_masteriyo,
array(
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'show_in_rest' => true,
'default' => false,
)
);
}
/**
@ -415,13 +434,16 @@ class Promotions extends Abstract_Module {
$has_neve_from_promo = get_option( $this->option_neve, false );
$has_enough_attachments = $this->has_min_media_attachments();
$has_enough_old_posts = $this->has_old_posts();
$is_min_php_8_1 = version_compare( PHP_VERSION, '8.1', '>=' );
$has_feedzy = defined( 'FEEDZY_BASEFILE' ) || $this->is_plugin_installed( 'feedzy-rss-feedss' );
$had_feedzy_from_promo = get_option( $this->option_feedzy, false );
$is_min_php_7_4 = version_compare( PHP_VERSION, '7.4', '>=' );
$has_feedzy = defined( 'FEEDZY_BASEFILE' ) || $this->is_plugin_installed( 'feedzy-rss-feedss' );
$had_feedzy_from_promo = get_option( $this->option_feedzy, false );
$has_masteriyo = defined( 'MASTERIYO_VERSION' ) || $this->is_plugin_installed( 'learning-management-system' );
$had_masteriyo_from_promo = get_option( $this->option_masteriyo, false );
$has_masteriyo_conditions = $this->has_lms_tagline();
$is_min_php_7_2 = version_compare( PHP_VERSION, '7.2', '>=' );
$all = [
'optimole' => [
'optimole' => [
'om-editor' => [
'env' => ! $has_optimole && $is_min_req_v && ! $had_optimole_from_promo,
'screen' => 'editor',
@ -446,20 +468,20 @@ class Promotions extends Abstract_Module {
'delayed' => true,
],
],
'feedzy_import' => [
'feedzy_import' => [
'feedzy-import' => [
'env' => true,
'screen' => 'import',
'always' => true,
],
],
'feedzy_embed' => [
'feedzy_embed' => [
'feedzy-editor' => [
'env' => ! $has_feedzy && is_main_site() && ! $had_feedzy_from_promo,
'screen' => 'editor',
],
],
'otter' => [
'otter' => [
'blocks-css' => [
'env' => ! $has_otter && $is_min_req_v && ! $had_otter_from_promo,
'screen' => 'editor',
@ -476,14 +498,14 @@ class Promotions extends Abstract_Module {
'delayed' => true,
],
],
'rop' => [
'rop' => [
'rop-posts' => [
'env' => ! $has_rop && ! $had_rop_from_promo && $has_enough_old_posts,
'screen' => 'edit-post',
'delayed' => true,
],
],
'woo_plugins' => [
'woo_plugins' => [
'ppom' => [
'env' => ! $has_ppom && $has_woocommerce,
'screen' => 'edit-product',
@ -501,31 +523,37 @@ class Promotions extends Abstract_Module {
'screen' => 'edit-product',
],
],
'neve' => [
'neve' => [
'neve-themes-popular' => [
'env' => ! $has_neve && ! $has_neve_from_promo,
'screen' => 'themes-install-popular',
],
],
'redirection-cf7' => [
'redirection-cf7' => [
'wpcf7' => [
'env' => ! $has_redirection_cf7 && ! $had_redirection_cf7_promo,
'screen' => 'wpcf7',
'delayed' => true,
],
],
'hyve' => [
'hyve' => [
'hyve-plugins-install' => [
'env' => $is_min_php_8_1 && ! $has_hyve && ! $had_hyve_from_promo && $has_hyve_conditions,
'env' => $is_min_php_7_4 && ! $has_hyve && ! $had_hyve_from_promo && $has_hyve_conditions,
'screen' => 'plugin-install',
],
],
'wp_full_pay' => [
'wp_full_pay' => [
'wp-full-pay-plugins-install' => [
'env' => ! $has_wfp_full_pay && ! $had_wfp_from_promo && $has_wfp_conditions,
'screen' => 'plugin-install',
],
],
'learning-management-system' => [
'masteriyo-plugins-install' => [
'env' => $is_min_php_7_2 && ! $has_masteriyo && ! $had_masteriyo_from_promo && $has_masteriyo_conditions,
'screen' => 'plugin-install',
],
],
];
foreach ( $all as $slug => $data ) {
@ -723,6 +751,10 @@ class Promotions extends Abstract_Module {
add_action( 'admin_notices', [ $this, 'render_wp_full_pay_notice' ] );
}
if ( $this->get_upsells_dismiss_time( 'masteriyo-plugins-install' ) === false ) {
add_action( 'admin_notices', [ $this, 'render_masteriyo_notice' ] );
}
add_action( 'load-import.php', [ $this, 'add_import' ] );
$this->load_woo_promos();
@ -788,6 +820,10 @@ class Promotions extends Abstract_Module {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
add_action( 'admin_notices', [ $this, 'render_wp_full_pay_notice' ] );
break;
case 'masteriyo-plugins-install':
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
add_action( 'admin_notices', [ $this, 'render_masteriyo_notice' ] );
break;
}
}
@ -881,6 +917,8 @@ class Promotions extends Abstract_Module {
'hyveDash' => esc_url( add_query_arg( [ 'page' => 'wpfs-settings-stripe' ], admin_url( 'admin.php' ) ) ),
'wpFullPayActivationUrl' => $this->get_plugin_activation_link( 'wp-full-stripe-free' ),
'wpFullPayDash' => esc_url( add_query_arg( [ 'page' => 'wpfs-settings-stripe' ], admin_url( 'admin.php' ) ) ),
'masteriyoActivationUrl' => $this->get_plugin_activation_link( 'masteriyo' ),
'masteriyoDash' => esc_url( add_query_arg( [ 'page' => 'masteriyo-onboard' ], admin_url( 'index.php' ) ) ),
'nevePreviewURL' => esc_url( add_query_arg( [ 'theme' => 'neve' ], admin_url( 'theme-install.php' ) ) ),
'neveAction' => $neve_action,
'activateNeveURL' => esc_url(
@ -940,6 +978,13 @@ class Promotions extends Abstract_Module {
echo '<div id="ti-redirection-cf7-notice" class="notice notice-info ti-sdk-om-notice"></div>';
}
/**
* Render Masteriyo notice.
*/
public function render_masteriyo_notice() {
echo '<div id="ti-masteriyo-notice" class="notice notice-info ti-sdk-om-notice"></div>';
}
/**
* Add promo to attachment modal.
*
@ -1406,4 +1451,22 @@ class Promotions extends Abstract_Module {
return 'yes' === $has_donate;
}
/**
* Check if the tagline contains LMS related keywords.
*
* @return bool True if the tagline contains LMS-related keywords, false otherwise.
*/
public function has_lms_tagline() {
$tagline = strtolower( get_bloginfo( 'description' ) );
$lms_keywords = array( 'learning', 'courses' );
foreach ( $lms_keywords as $keyword ) {
if ( strpos( $tagline, $keyword ) !== false ) {
return true;
}
}
return false;
}
}

View File

@ -98,7 +98,7 @@ class Script_Loader extends Abstract_Module {
return '';
}
if ( 'tracking' !== $slug && 'survey' !== $slug && 'banner' !== $slug ) {
if ( 'tracking' !== $slug && 'survey' !== $slug ) {
return '';
}
@ -120,8 +120,6 @@ class Script_Loader extends Abstract_Module {
$this->load_tracking( $handler );
} elseif ( 'survey' === $slug ) {
$this->load_survey( $handler );
} elseif ( 'banner' === $slug ) {
$this->load_banner( $handler );
}
}
@ -174,7 +172,7 @@ class Script_Loader extends Abstract_Module {
$common_data = [
'userId' => $user_id,
'apiHost' => 'https://app.formbricks.com',
'appUrl' => 'https://app.formbricks.com',
'attributes' => [
'language' => $lang_code,
],
@ -237,33 +235,6 @@ class Script_Loader extends Abstract_Module {
);
}
/**
* Load the banner script.
*
* @param string $handler The script handler.
*
* @return void
*/
public function load_banner( $handler ) {
global $themeisle_sdk_max_path;
$asset_file = require $themeisle_sdk_max_path . '/assets/js/build/banner/banner.asset.php';
wp_enqueue_script(
$handler,
$this->get_sdk_uri() . 'assets/js/build/banner/banner.js',
$asset_file['dependencies'],
$asset_file['version'],
true
);
wp_enqueue_style(
$handler . '_style',
$this->get_sdk_uri() . 'assets/css/banner.css',
[],
$asset_file['version']
);
}
/**
* Mask a secret with `*` for half of its length.
*

View File

@ -139,10 +139,36 @@ class Product {
$install = get_option( $this->get_key() . '_install', 0 );
if ( 0 === $install ) {
$install = time();
/**
* Action to be triggered when the product is first activated.
*
* @param string $basefile The basefile of the product.
*/
do_action( 'themeisle_sdk_first_activation', $basefile );
update_option( $this->get_key() . '_install', time() );
}
$this->install = $install;
self::$cached_products[ crc32( $basefile ) ] = $this;
$current_version = get_option( $this->slug . '_version', '' );
if ( $current_version !== $this->version && wp_cache_get( "{$this->slug}_version_upgrade" ) === false ) {
// Set the cache lock to avoid multiple calls.
wp_cache_set( "{$this->slug}_version_upgrade", true, HOUR_IN_SECONDS );
/**
* Action to be triggered when the product is updated.
*
* @param string $current_version The current version of the product.
* @param string $new_version The new version of the product.
* @param string $basefile The basefile of the product.
*/
do_action( "themeisle_sdk_update_{$this->slug}", $current_version, $this->version, $basefile );
// Update the version of the product.
update_option( "{$this->slug}_version", $this->version );
// Delete the cache lock.
wp_cache_delete( "{$this->slug}_version_upgrade" );
}
}
/**

View File

@ -41,6 +41,8 @@ $files_to_load = [
$themeisle_library_path . '/src/Modules/Announcements.php',
$themeisle_library_path . '/src/Modules/Featured_plugins.php',
$themeisle_library_path . '/src/Modules/Float_widget.php',
$themeisle_library_path . '/src/Modules/Abstract_Migration.php',
$themeisle_library_path . '/src/Modules/Migrator.php',
];
$files_to_load = array_merge( $files_to_load, apply_filters( 'themeisle_sdk_required_files', [] ) );