# easy-invoice/2.4.0/includes/Addons/ProAddonMigration.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.0. 247 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Addons/ProAddonMigration.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.0/raw/includes/Addons/ProAddonMigration.php
- Modified: 2026-05-21T08:29:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Addons/ProAddonMigration.php#L10-L20`.

```php
<?php
/**
 * Migration: convert monolithic Pro features into opt-in addons.
 *
 * For each wrapped addon we run a feature-specific "is the merchant
 * actually using this?" check. If yes → flip the addon flag ON so the
 * feature keeps working post-migration. If no → leave OFF; the admin
 * enables it explicitly from the Addons grid before the feature loads.
 *
 * Detection rules (all signals VERIFIED by reading the actual Pro source
 * — no guessing):
 *
 *   recurring_invoices   — meta `_easy_invoice_recurring_enabled` = '1' on any
 *                          post. (RecurringInvoices.php declares the constant.)
 *   partial_payments     — option `easy_invoice_pro_partial_payments_enable`
 *                          stores '1' when the admin toggled the feature on.
 *                          (PartialPayments.php line 66.)
 *   client_portal        — option `easy_invoice_pro_client_portal_settings`
 *                          is written the first time the portal admin page
 *                          is saved. (ClientPortal.php line 105.)
 *   item_library         — option `easy_invoice_pro_saved_items` exists and
 *                          contains at least one item. (ItemLibraryService.php
 *                          line 22 — ITEMS_OPTION_KEY constant.)
 *   custom_templates     — option `easy_invoice_pro_templates` exists with
 *                          at least one saved template. (TemplateBuilderService
 *                          line 22 — TEMPLATES_OPTION_KEY constant.)
 *   email_enhancements   — option `easy_invoice_pro_email_settings` exists
 *                          and is non-empty. (EmailEnhancements.php line 34.)
 *
 *   pdf_toolkit          — no verified detectable signal. Stays disabled;
 *                          admin enables manually if they want it.
 *   additional_tax       — no verified detectable signal. Stays disabled.
 *   bulk_operations      — pure UI buttons, no persistent state. Disabled.
 *   privacy_tools        — pure WP-hook wiring, no persistent state. Disabled.
 *
 * Migration is single-shot via MIGRATION_VERSION-stamped option. Bump the
 * version to re-run after adding new detection rules.
 *
 * @package EasyInvoice
 */

namespace EasyInvoice\Addons;

if (!defined('ABSPATH')) {
    exit;
}

class ProAddonMigration {

    public const MIGRATION_FLAG_OPTION = 'easy_invoice_pro_addon_migration_v1';
    // Bump this whenever a new addon is added that needs auto-enable on
    // existing Pro installs. The migration re-runs once per VERSION bump
    // and only touches addons whose flag hasn't been explicitly set by
    // the admin.
    //
    //   v1 — initial migration (10 wrapped Pro features)
    //   v2 — adds 'reports' addon (was implicit Pro-only feature)
    //   v3 — adds 'secure_links' addon; auto-enables for installs whose
    //        legacy `easy_invoice_pro_enable_secure_links` option was
    //        'yes' so signed URLs already mailed out stay resolvable.
    public const MIGRATION_VERSION     = '3';

    /**
     * Register the migration to fire on admin_init. Frontend visitors
     * never trigger it.
     */
    public static function register(): void {
        add_action('admin_init', [__CLASS__, 'maybeMigrate'], 5);
    }

    /**
     * Idempotent entry point — runs once per MIGRATION_VERSION bump.
     */
    public static function maybeMigrate(): void {
        if (!function_exists('easy_invoice_has_pro') || !easy_invoice_has_pro()) {
            return;
        }
        if (get_option(self::MIGRATION_FLAG_OPTION) === self::MIGRATION_VERSION) {
            return;
        }
        self::run();
        // Store version with autoload=no so this guard row doesn't
        // bloat the wp_options autoload set.
        update_option(self::MIGRATION_FLAG_OPTION, self::MIGRATION_VERSION, false);
    }

    /**
     * The actual migration. For every wrapped addon, set the enable flag
     * based on whether we detect the feature in use. Never overwrites a
     * flag the admin has already set explicitly.
     */
    public static function run(): void {
        $enabled = [];
        foreach (AddonRegistry::migratedProAddonIds() as $addon_id) {
            $flag_key = AddonManager::optionKey($addon_id);
            if (get_option($flag_key, null) !== null) {
                continue; // admin already chose
            }
            $in_use = self::isFeatureInUse($addon_id);
            update_option($flag_key, $in_use ? 1 : 0, false);
            if ($in_use) {
                $enabled[] = $addon_id;
            }
        }

        /**
         * Fires after the Pro→addon migration completes.
         *
         * @param array<int,string> $enabled  IDs the migration enabled
         */
        do_action('easy_invoice_pro_addon_migration_completed', $enabled);
    }

    /**
     * Dispatch to per-addon detector.
     *
     * Two categories of addon:
     *
     *   FEATURE-SPECIFIC DETECTION — the feature had its own user-facing
     *   enable/disable setting OR produced its own data signal. We can
     *   tell precisely whether this merchant was using it.
     *     • recurring_invoices  — meta on actual recurring invoices
     *     • partial_payments    — own enable option
     *     • client_portal       — settings option present
     *     • custom_templates    — saved templates option non-empty
     *
     *   PRO-INSTALL DETECTION — the feature had NO user toggle in old Pro
     *   (it always loaded, every site got it). To preserve "old users
     *   keep working", we enable for ANY install that we can prove
     *   previously ran Pro. Fresh installs leave them off.
     *     • pdf_toolkit
     *     • bulk_operations
     *     • item_library
     *     • additional_tax
     *     • email_enhancements
     *     • privacy_tools
     */
    public static function isFeatureInUse(string $addon_id): bool {
        switch ($addon_id) {
            // Feature-specific signals.
            case 'recurring_invoices': return self::detectRecurring();
            case 'partial_payments':   return self::detectPartialPayments();
            case 'client_portal':      return self::detectClientPortal();
            case 'custom_templates':   return self::detectCustomTemplates();
            case 'secure_links':       return self::detectSecureLinks();
            // Always-on Pro features — enable for any existing Pro install.
            case 'pdf_toolkit':
            case 'bulk_operations':
            case 'item_library':
            case 'additional_tax':
            case 'email_enhancements':
            case 'privacy_tools':
            // Reports was an always-on Pro feature gated only by
            // easy_invoice_has_pro(); existing Pro sites were using it.
            case 'reports':
                return self::isExistingProInstall();
            default:
                return false;
        }
    }

    /**
     * "Did this site ever run Pro before the addon-ization migration?"
     *
     * Verified signals (every entry is an option I have read the Pro
     * source for and confirmed gets written during normal Pro lifecycle):
     *
     *   easy_invoice_pro_license_details — written by License::activate()
     *     in includes/Updater/License.php line 97 the first time the
     *     admin saves a license key.
     *   easy_invoice_pro_license_key     — written by the same call,
     *     line 101.
     *
     * Both are absent on a fresh install that has never activated Pro.
     * We deliberately stick to license-flow signals because they're the
     * least ambiguous "Pro was here" indicator — other options can be
     * written by tools that don't imply real Pro use.
     */
    public static function isExistingProInstall(): bool {
        if (get_option('easy_invoice_pro_license_details', null) !== null) return true;
        if (get_option('easy_invoice_pro_license_key',     null) !== null) return true;

        /**
         * Override hook for staging clones / unusual deploys where the
         * heuristic mis-classifies. Return true to force "existing",
         * false to force "fresh", null (default) to use the heuristic.
         *
         * @param bool|null $resolved
         */
        $override = apply_filters('easy_invoice_pro_addon_migration_is_existing', null);
        return is_bool($override) ? $override : false;
    }

    // ── Verified per-feature detectors ──────────────────────────────────

    private static function detectRecurring(): bool {
        global $wpdb;
        $found = $wpdb->get_var($wpdb->prepare(
            "SELECT 1 FROM {$wpdb->postmeta}
             WHERE meta_key = %s AND meta_value = %s LIMIT 1",
            '_easy_invoice_recurring_enabled',
            '1'
        ));
        return !empty($found);
    }

    private static function detectPartialPayments(): bool {
        // The PartialPayments extension stores '1' / '0' as string. We
        // count any truthy value (including the literal '1' or a future
        // 'true') as "in use".
        $value = get_option('easy_invoice_pro_partial_payments_enable', null);
        if ($value === null) return false;
        return !empty($value) && $value !== '0';
    }

    private static function detectClientPortal(): bool {
        return get_option('easy_invoice_pro_client_portal_settings', null) !== null;
    }

    private static function detectCustomTemplates(): bool {
        $templates = get_option('easy_invoice_pro_templates', null);
        return is_array($templates) && !empty($templates);
    }

    /**
     * Secure Links: the legacy Pro setting was a Yes/No checkbox in the
     * Pro Settings page (see templates/admin/settings.php line 237 —
     * checkbox writing easy_invoice_pro_enable_secure_links). Treat any
     * truthy value as "in use" so we don't 404 secure URLs already
     * mailed out. Sites that never enabled it stay disabled.
     */
    private static function detectSecureLinks(): bool {
        $value = get_option('easy_invoice_pro_enable_secure_links', null);
        if ($value === null) return false;
        return $value === 'yes' || $value === '1' || $value === 1 || $value === true;
    }

    // detectItemLibrary() and detectEmailEnhancements() used to live here
    // but were never called from isFeatureInUse(). Those addons are
    // treated as "always-on Pro" features and fall through to
    // isExistingProInstall() — the safer migration default (any site
    // that ran Pro gets the feature, even if they hadn't actively used
    // it yet). The per-feature detectors were removed to eliminate the
    // dead-code path; reintroduce them only if a future product
    // decision wants the stricter "must have used it" rule.
}

```
