From c6726703e4e198c175a235bc4ffc709c5bfc46b9 Mon Sep 17 00:00:00 2001 From: magdev Date: Wed, 21 Jan 2026 20:55:28 +0100 Subject: [PATCH] Implement version 0.0.8 features - Remove Current Version field from product license settings - Derive current version from latest product version in database - Refactor email system to use WooCommerce email notification classes - Add LicenseExpirationEmail WC_Email class for expiration warnings - Add customizable email templates (HTML and plain text) - Update settings to link to WooCommerce email configuration - Update translations for new email-related strings Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 39 ++- languages/wc-licensed-product-de_CH.mo | Bin 21788 -> 23690 bytes languages/wc-licensed-product-de_CH.po | 61 ++++- languages/wc-licensed-product.pot | 64 ++++- src/Admin/SettingsController.php | 71 +++++ src/Admin/VersionAdminController.php | 3 - src/Email/LicenseEmailController.php | 243 ++++++------------ src/Email/LicenseExpirationEmail.php | 240 +++++++++++++++++ src/Product/LicensedProduct.php | 19 +- src/Product/LicensedProductType.php | 14 - templates/emails/license-expiration.php | 97 +++++++ templates/emails/plain/license-expiration.php | 64 +++++ wc-licensed-product.php | 4 +- 13 files changed, 713 insertions(+), 206 deletions(-) create mode 100644 src/Email/LicenseExpirationEmail.php create mode 100644 templates/emails/license-expiration.php create mode 100644 templates/emails/plain/license-expiration.php diff --git a/CLAUDE.md b/CLAUDE.md index 247f876..eb13b68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,10 +36,6 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w _No known bugs at this time._ -### Version 0.0.8 (Next) - -_No planned features yet. Add items as needed._ - ## Technical Stack - **Language:** PHP 8.3.x @@ -429,3 +425,38 @@ Full API documentation available in `openapi.json` (OpenAPI 3.1 specification). - Expiration warnings use WordPress cron with daily schedule - CSV import supports both exported format and simplified format - User meta tracks expiration notifications to prevent duplicates + +### 2026-01-21 - Version 0.0.8 Features + +**Implemented:** + +- Removed "Current Version" field from product license settings +- Current version now automatically derived from latest product version in database +- Refactored email system to use WooCommerce email notification classes +- License Expiration Warning email is now configurable via WooCommerce > Settings > Emails +- Email templates can be overridden in themes (woocommerce/emails/license-expiration.php) +- Configurable warning days for expiration notifications (first and second warning) + +**New classes:** + +- `LicenseExpirationEmail` - WooCommerce WC_Email subclass for license expiration warnings + +**New files:** + +- `templates/emails/license-expiration.php` - HTML email template +- `templates/emails/plain/license-expiration.php` - Plain text email template + +**Modified classes:** + +- `LicensedProduct` - `get_current_version()` and `get_major_version()` now query VersionManager +- `LicensedProductType` - Removed Current Version field from product data panel +- `VersionAdminController` - Removed automatic update of `_licensed_current_version` meta +- `LicenseEmailController` - Registers WooCommerce email class, uses WC email system for sending +- `SettingsController` - Updated email section to link to WooCommerce email settings + +**Technical notes:** + +- WooCommerce email system allows admins to customize subject, heading, content, and enable/disable +- Email templates follow WooCommerce conventions and can be overridden in themes +- Expiration warning schedule (days before) remains in plugin settings +- Email enable/disable is controlled through WooCommerce email settings diff --git a/languages/wc-licensed-product-de_CH.mo b/languages/wc-licensed-product-de_CH.mo index 803a5e3d2d71cce73e65f7a9ddc5b27d7731e820..1a2b486f2b13085f1dcf0d11fd6bec413fb51258 100644 GIT binary patch delta 7602 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 delta 5846 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 diff --git a/languages/wc-licensed-product-de_CH.po b/languages/wc-licensed-product-de_CH.po index d73d6be..de41c00 100644 --- a/languages/wc-licensed-product-de_CH.po +++ b/languages/wc-licensed-product-de_CH.po @@ -3,7 +3,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: WC Licensed Product 0.0.7\n" +"Project-Id-Version: WC Licensed Product 0.0.8\n" "Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n" "POT-Creation-Date: 2026-01-21T00:00:00+00:00\n" "PO-Revision-Date: 2026-01-21T00:00:00+00:00\n" @@ -63,12 +63,6 @@ msgstr "Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt g msgid "If enabled, licenses are bound to the major version at purchase time by default." msgstr "Falls aktiviert, werden Lizenzen standardmässig an die Hauptversion zum Kaufzeitpunkt gebunden." -msgid "Current Version" -msgstr "Aktuelle Version" - -msgid "Current software version (e.g., 1.0.0)" -msgstr "Aktuelle Software-Version (z.B. 1.0.0)" - #. Global settings msgid "Default License Settings" msgstr "Standard Lizenz-Einstellungen" @@ -818,3 +812,56 @@ msgstr "Ja" msgid "No" msgstr "Nein" + +#. Email settings +msgid "Expiration Warning Schedule" +msgstr "Ablaufwarnung Zeitplan" + +msgid "Configure when expiration warning emails are sent. To customize the email template, enable/disable, or change the subject, go to %s." +msgstr "Konfigurieren Sie, wann Ablaufwarnungs-E-Mails gesendet werden. Um die E-Mail-Vorlage anzupassen, zu aktivieren/deaktivieren oder den Betreff zu ändern, gehen Sie zu %s." + +msgid "WooCommerce > Settings > Emails > License Expiration Warning" +msgstr "WooCommerce > Einstellungen > E-Mails > Lizenzablauf-Warnung" + +msgid "First Warning (Days Before)" +msgstr "Erste Warnung (Tage vorher)" + +msgid "Days before expiration to send the first warning email." +msgstr "Tage vor Ablauf, um die erste Warn-E-Mail zu senden." + +msgid "Second Warning (Days Before)" +msgstr "Zweite Warnung (Tage vorher)" + +msgid "Days before expiration to send the second warning email. Set to 0 to disable." +msgstr "Tage vor Ablauf, um die zweite Warn-E-Mail zu senden. Setzen Sie auf 0, um sie zu deaktivieren." + +#. WooCommerce Email Class +msgid "License Expiration Warning" +msgstr "Lizenzablauf-Warnung" + +msgid "License expiration warning emails are sent to customers when their licenses are about to expire." +msgstr "Lizenzablauf-Warnungs-E-Mails werden an Kunden gesendet, wenn ihre Lizenzen bald ablaufen." + +msgid "[{site_title}] Your license for {product_name} expires in {days_remaining} days" +msgstr "[{site_title}] Ihre Lizenz für {product_name} läuft in {days_remaining} Tagen ab" + +msgid "Available placeholders: %s" +msgstr "Verfügbare Platzhalter: %s" + +msgid "Enable this email notification" +msgstr "Diese E-Mail-Benachrichtigung aktivieren" + +msgid "Email heading" +msgstr "E-Mail-Überschrift" + +msgid "Additional content" +msgstr "Zusätzlicher Inhalt" + +msgid "Text to appear below the main email content." +msgstr "Text, der unter dem Haupt-E-Mail-Inhalt erscheinen soll." + +msgid "Email type" +msgstr "E-Mail-Typ" + +msgid "Choose which format of email to send." +msgstr "Wählen Sie, welches E-Mail-Format gesendet werden soll." diff --git a/languages/wc-licensed-product.pot b/languages/wc-licensed-product.pot index 81b58d0..30fe460 100644 --- a/languages/wc-licensed-product.pot +++ b/languages/wc-licensed-product.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: WC Licensed Product 0.0.7\n" +"Project-Id-Version: WC Licensed Product 0.0.8\n" "Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n" "POT-Creation-Date: 2026-01-21T00:00:00+00:00\n" "MIME-Version: 1.0\n" @@ -60,12 +60,6 @@ msgstr "" msgid "If enabled, licenses are bound to the major version at purchase time by default." msgstr "" -msgid "Current Version" -msgstr "" - -msgid "Current software version (e.g., 1.0.0)" -msgstr "" - #. Global settings msgid "Default License Settings" msgstr "" @@ -815,3 +809,59 @@ msgstr "" msgid "No" msgstr "" + +#. Email settings +msgid "Expiration Warning Schedule" +msgstr "" + +msgid "Configure when expiration warning emails are sent. To customize the email template, enable/disable, or change the subject, go to %s." +msgstr "" + +msgid "WooCommerce > Settings > Emails > License Expiration Warning" +msgstr "" + +msgid "First Warning (Days Before)" +msgstr "" + +msgid "Days before expiration to send the first warning email." +msgstr "" + +msgid "Second Warning (Days Before)" +msgstr "" + +msgid "Days before expiration to send the second warning email. Set to 0 to disable." +msgstr "" + +#. WooCommerce Email Class +msgid "License Expiration Warning" +msgstr "" + +msgid "License expiration warning emails are sent to customers when their licenses are about to expire." +msgstr "" + +msgid "[{site_title}] Your license for {product_name} expires in {days_remaining} days" +msgstr "" + +msgid "Available placeholders: %s" +msgstr "" + +msgid "Enable this email notification" +msgstr "" + +msgid "Email heading" +msgstr "" + +msgid "Additional content" +msgstr "" + +msgid "Text to appear below the main email content." +msgstr "" + +msgid "Email type" +msgstr "" + +msgid "Choose which format of email to send." +msgstr "" + +msgid "Customer" +msgstr "" diff --git a/src/Admin/SettingsController.php b/src/Admin/SettingsController.php index d9e4f29..258017c 100644 --- a/src/Admin/SettingsController.php +++ b/src/Admin/SettingsController.php @@ -92,6 +92,44 @@ final class SettingsController 'type' => 'sectionend', 'id' => 'wc_licensed_product_section_defaults_end', ], + // Email settings section + 'email_section_title' => [ + 'name' => __('Expiration Warning Schedule', 'wc-licensed-product'), + 'type' => 'title', + 'desc' => sprintf( + /* translators: %s: URL to WooCommerce email settings */ + __('Configure when expiration warning emails are sent. To customize the email template, enable/disable, or change the subject, go to %s.', 'wc-licensed-product'), + '' . + __('WooCommerce > Settings > Emails > License Expiration Warning', 'wc-licensed-product') . '' + ), + 'id' => 'wc_licensed_product_section_email', + ], + 'expiration_warning_days_first' => [ + 'name' => __('First Warning (Days Before)', 'wc-licensed-product'), + 'type' => 'number', + 'desc' => __('Days before expiration to send the first warning email.', 'wc-licensed-product'), + 'id' => 'wc_licensed_product_expiration_warning_days_first', + 'default' => '7', + 'custom_attributes' => [ + 'min' => '1', + 'step' => '1', + ], + ], + 'expiration_warning_days_second' => [ + 'name' => __('Second Warning (Days Before)', 'wc-licensed-product'), + 'type' => 'number', + 'desc' => __('Days before expiration to send the second warning email. Set to 0 to disable.', 'wc-licensed-product'), + 'id' => 'wc_licensed_product_expiration_warning_days_second', + 'default' => '1', + 'custom_attributes' => [ + 'min' => '0', + 'step' => '1', + ], + ], + 'email_section_end' => [ + 'type' => 'sectionend', + 'id' => 'wc_licensed_product_section_email_end', + ], ]; } @@ -139,4 +177,37 @@ final class SettingsController { return get_option('wc_licensed_product_default_bind_to_version', 'no') === 'yes'; } + + /** + * Check if expiration warning emails are enabled + * This checks both the WooCommerce email setting and the old setting for backwards compatibility + */ + public static function isExpirationEmailsEnabled(): bool + { + // Check WooCommerce email enabled status + $emailEnabled = get_option('woocommerce_wclp_license_expiration_settings'); + if (is_array($emailEnabled) && isset($emailEnabled['enabled'])) { + return $emailEnabled['enabled'] === 'yes'; + } + // Default to enabled if not yet configured + return true; + } + + /** + * Get first warning days before expiration + */ + public static function getFirstWarningDays(): int + { + $value = get_option('wc_licensed_product_expiration_warning_days_first', 7); + return max(1, (int) $value); + } + + /** + * Get second warning days before expiration + */ + public static function getSecondWarningDays(): int + { + $value = get_option('wc_licensed_product_expiration_warning_days_second', 1); + return max(0, (int) $value); + } } diff --git a/src/Admin/VersionAdminController.php b/src/Admin/VersionAdminController.php index 2c3de78..c38ca5a 100644 --- a/src/Admin/VersionAdminController.php +++ b/src/Admin/VersionAdminController.php @@ -263,9 +263,6 @@ final class VersionAdminController wp_send_json_error(['message' => __('Failed to create version.', 'wc-licensed-product')]); } - // Also update the product's current version meta - update_post_meta($productId, '_licensed_current_version', $version); - wp_send_json_success([ 'message' => __('Version added successfully.', 'wc-licensed-product'), 'version' => $newVersion->toArray(), diff --git a/src/Email/LicenseEmailController.php b/src/Email/LicenseEmailController.php index 915a2f7..930d39c 100644 --- a/src/Email/LicenseEmailController.php +++ b/src/Email/LicenseEmailController.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace Jeremias\WcLicensedProduct\Email; use Jeremias\WcLicensedProduct\License\LicenseManager; +use Jeremias\WcLicensedProduct\Admin\SettingsController; /** * Handles email notifications for licenses @@ -29,6 +30,9 @@ final class LicenseEmailController */ private function registerHooks(): void { + // Register custom WooCommerce email classes + add_filter('woocommerce_email_classes', [$this, 'registerEmailClasses']); + // Add license info to order completed email add_action('woocommerce_email_after_order_table', [$this, 'addLicenseInfoToEmail'], 20, 4); @@ -40,6 +44,45 @@ final class LicenseEmailController // Cron action for checking expiring licenses add_action('wclp_check_expiring_licenses', [$this, 'sendExpirationWarnings']); + + // Add email templates location for theme overrides + add_filter('woocommerce_locate_template', [$this, 'locateTemplate'], 10, 3); + } + + /** + * Register custom email classes with WooCommerce + * + * @param array $email_classes Existing email classes + * @return array Modified email classes + */ + public function registerEmailClasses(array $email_classes): array + { + $email_classes['WCLP_License_Expiration'] = new LicenseExpirationEmail(); + return $email_classes; + } + + /** + * Locate templates in plugin directory + * + * @param string $template Template path + * @param string $template_name Template name + * @param string $template_path Template path prefix + * @return string Modified template path + */ + public function locateTemplate(string $template, string $template_name, string $template_path): string + { + // Only handle our email templates + if (strpos($template_name, 'emails/license-') !== 0) { + return $template; + } + + $plugin_template = WC_LICENSED_PRODUCT_PLUGIN_DIR . 'templates/' . $template_name; + + if (file_exists($plugin_template)) { + return $plugin_template; + } + + return $template; } /** @@ -57,20 +100,48 @@ final class LicenseEmailController */ public function sendExpirationWarnings(): void { - // Check for licenses expiring in 7 days - $this->processExpirationWarnings(7, 'expiring_7_days'); + // Check if expiration emails are enabled in settings + if (!SettingsController::isExpirationEmailsEnabled()) { + return; + } - // Check for licenses expiring in 1 day - $this->processExpirationWarnings(1, 'expiring_1_day'); + // Get the WooCommerce email instance + $mailer = WC()->mailer(); + $emails = $mailer->get_emails(); + + if (!isset($emails['WCLP_License_Expiration'])) { + return; + } + + /** @var LicenseExpirationEmail $expirationEmail */ + $expirationEmail = $emails['WCLP_License_Expiration']; + + // Check if the email is enabled + if (!$expirationEmail->is_enabled()) { + return; + } + + // Get configurable warning days + $firstWarningDays = SettingsController::getFirstWarningDays(); + $secondWarningDays = SettingsController::getSecondWarningDays(); + + // Check for licenses expiring at first warning threshold + $this->processExpirationWarnings($expirationEmail, $firstWarningDays, 'expiring_first_warning'); + + // Check for licenses expiring at second warning threshold (if enabled) + if ($secondWarningDays > 0 && $secondWarningDays < $firstWarningDays) { + $this->processExpirationWarnings($expirationEmail, $secondWarningDays, 'expiring_second_warning'); + } } /** * Process and send expiration warnings for a specific time frame * + * @param LicenseExpirationEmail $email Email instance * @param int $days Days until expiration * @param string $notificationType Notification type identifier */ - private function processExpirationWarnings(int $days, string $notificationType): void + private function processExpirationWarnings(LicenseExpirationEmail $email, int $days, string $notificationType): void { $licenses = $this->licenseManager->getLicensesExpiringSoon($days); @@ -80,166 +151,14 @@ final class LicenseEmailController continue; } - // Send the warning email - if ($this->sendExpirationWarningEmail($license, $days)) { + // Send the warning email using WooCommerce email system + if ($email->trigger($license, $days)) { // Mark as notified $this->licenseManager->markExpirationNotified($license->getId(), $notificationType); } } } - /** - * Send expiration warning email to customer - * - * @param \Jeremias\WcLicensedProduct\License\License $license License object - * @param int $daysRemaining Days until expiration - * @return bool Whether email was sent successfully - */ - private function sendExpirationWarningEmail($license, int $daysRemaining): bool - { - $customer = get_userdata($license->getCustomerId()); - if (!$customer || !$customer->user_email) { - return false; - } - - $product = wc_get_product($license->getProductId()); - $productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'); - - $siteName = get_bloginfo('name'); - $expiresAt = $license->getExpiresAt(); - $expirationDate = $expiresAt ? $expiresAt->format(get_option('date_format')) : ''; - - // Email subject - if ($daysRemaining === 1) { - $subject = sprintf( - /* translators: 1: Product name, 2: Site name */ - __('[%2$s] Your license for %1$s expires tomorrow', 'wc-licensed-product'), - $productName, - $siteName - ); - } else { - $subject = sprintf( - /* translators: 1: Product name, 2: Number of days, 3: Site name */ - __('[%3$s] Your license for %1$s expires in %2$d days', 'wc-licensed-product'), - $productName, - $daysRemaining, - $siteName - ); - } - - // Email content - $message = $this->buildExpirationWarningHtml($license, $customer, $productName, $daysRemaining, $expirationDate); - - // Send email - $headers = [ - 'Content-Type: text/html; charset=UTF-8', - 'From: ' . $siteName . ' <' . get_option('admin_email') . '>', - ]; - - return wp_mail($customer->user_email, $subject, $message, $headers); - } - - /** - * Build HTML content for expiration warning email - * - * @param \Jeremias\WcLicensedProduct\License\License $license License object - * @param \WP_User $customer Customer user object - * @param string $productName Product name - * @param int $daysRemaining Days until expiration - * @param string $expirationDate Formatted expiration date - * @return string HTML email content - */ - private function buildExpirationWarningHtml($license, $customer, string $productName, int $daysRemaining, string $expirationDate): string - { - $siteName = get_bloginfo('name'); - $siteUrl = home_url(); - $accountUrl = wc_get_account_endpoint_url('licenses'); - - ob_start(); - ?> - - - - - - - -
-

- -

- -

display_name)); ?>

- - -

- -

- -

- -

- - -
-

- - - - - - - - - - - - - - - - - -
- - getLicenseKey()); ?> - -
getDomain()); ?>
-
- -

- -

- - - -

- -
- -

- ' . esc_html($siteName) . '' - ); ?> -

-
- - - getLicenseKey()) . "\n"; } else { ?> -
+
getLicenseKey()); ?> @@ -324,7 +243,7 @@ final class LicenseEmailController

- +

@@ -340,7 +259,7 @@ final class LicenseEmailController - + diff --git a/src/Email/LicenseExpirationEmail.php b/src/Email/LicenseExpirationEmail.php new file mode 100644 index 0000000..d695c4b --- /dev/null +++ b/src/Email/LicenseExpirationEmail.php @@ -0,0 +1,240 @@ +id = 'wclp_license_expiration'; + $this->customer_email = true; + $this->title = __('License Expiration Warning', 'wc-licensed-product'); + $this->description = __('License expiration warning emails are sent to customers when their licenses are about to expire.', 'wc-licensed-product'); + + $this->template_html = 'emails/license-expiration.php'; + $this->template_plain = 'emails/plain/license-expiration.php'; + $this->template_base = WC_LICENSED_PRODUCT_PLUGIN_DIR . 'templates/'; + + $this->placeholders = [ + '{site_title}' => $this->get_blogname(), + '{product_name}' => '', + '{days_remaining}' => '', + '{expiration_date}' => '', + ]; + + // Call parent constructor + parent::__construct(); + } + + /** + * Get email subject + */ + public function get_default_subject(): string + { + return __('[{site_title}] Your license for {product_name} expires in {days_remaining} days', 'wc-licensed-product'); + } + + /** + * Get email heading + */ + public function get_default_heading(): string + { + return __('License Expiration Notice', 'wc-licensed-product'); + } + + /** + * Trigger the email + * + * @param License $license License object + * @param int $days_remaining Days until expiration + */ + public function trigger(License $license, int $days_remaining): bool + { + $this->setup_locale(); + + $customer = get_userdata($license->getCustomerId()); + if (!$customer || !$customer->user_email) { + $this->restore_locale(); + return false; + } + + $this->license = $license; + $this->days_remaining = $days_remaining; + $this->recipient = $customer->user_email; + + $product = wc_get_product($license->getProductId()); + $this->product_name = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'); + + $expiresAt = $license->getExpiresAt(); + $this->expiration_date = $expiresAt ? $expiresAt->format(get_option('date_format')) : ''; + + // Update placeholders + $this->placeholders['{product_name}'] = $this->product_name; + $this->placeholders['{days_remaining}'] = (string) $days_remaining; + $this->placeholders['{expiration_date}'] = $this->expiration_date; + + if (!$this->is_enabled() || !$this->get_recipient()) { + $this->restore_locale(); + return false; + } + + $result = $this->send( + $this->get_recipient(), + $this->get_subject(), + $this->get_content(), + $this->get_headers(), + $this->get_attachments() + ); + + $this->restore_locale(); + + return $result; + } + + /** + * Get content HTML + */ + public function get_content_html(): string + { + return wc_get_template_html( + $this->template_html, + [ + 'license' => $this->license, + 'days_remaining' => $this->days_remaining, + 'product_name' => $this->product_name, + 'expiration_date' => $this->expiration_date, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ], + '', + $this->template_base + ); + } + + /** + * Get content plain text + */ + public function get_content_plain(): string + { + return wc_get_template_html( + $this->template_plain, + [ + 'license' => $this->license, + 'days_remaining' => $this->days_remaining, + 'product_name' => $this->product_name, + 'expiration_date' => $this->expiration_date, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ], + '', + $this->template_base + ); + } + + /** + * Default content to show below main email content + */ + public function get_default_additional_content(): string + { + return __('To continue using this product, please renew your license before the expiration date.', 'wc-licensed-product'); + } + + /** + * Initialize settings form fields + */ + public function init_form_fields(): void + { + $placeholder_text = sprintf( + /* translators: %s: list of placeholders */ + __('Available placeholders: %s', 'wc-licensed-product'), + '{site_title}, {product_name}, {days_remaining}, {expiration_date}' + ); + + $this->form_fields = [ + 'enabled' => [ + 'title' => __('Enable/Disable', 'wc-licensed-product'), + 'type' => 'checkbox', + 'label' => __('Enable this email notification', 'wc-licensed-product'), + 'default' => 'yes', + ], + 'subject' => [ + 'title' => __('Subject', 'wc-licensed-product'), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ], + 'heading' => [ + 'title' => __('Email heading', 'wc-licensed-product'), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ], + 'additional_content' => [ + 'title' => __('Additional content', 'wc-licensed-product'), + 'description' => __('Text to appear below the main email content.', 'wc-licensed-product') . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => $this->get_default_additional_content(), + 'type' => 'textarea', + 'default' => '', + 'desc_tip' => true, + ], + 'email_type' => [ + 'title' => __('Email type', 'wc-licensed-product'), + 'type' => 'select', + 'description' => __('Choose which format of email to send.', 'wc-licensed-product'), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ], + ]; + } +} diff --git a/src/Product/LicensedProduct.php b/src/Product/LicensedProduct.php index 17ae0f4..d628f80 100644 --- a/src/Product/LicensedProduct.php +++ b/src/Product/LicensedProduct.php @@ -11,6 +11,7 @@ namespace Jeremias\WcLicensedProduct\Product; use Jeremias\WcLicensedProduct\Admin\SettingsController; use WC_Product; +use Jeremias\WcLicensedProduct\Product\VersionManager; /** * Licensed Product type extending WooCommerce Product @@ -121,11 +122,14 @@ class LicensedProduct extends WC_Product } /** - * Get current software version + * Get current software version (derived from latest product version) */ public function get_current_version(): string { - return $this->get_meta('_licensed_current_version', true) ?: ''; + $versionManager = new VersionManager(); + $latestVersion = $versionManager->getLatestVersion($this->get_id()); + + return $latestVersion ? $latestVersion->getVersion() : ''; } /** @@ -133,12 +137,13 @@ class LicensedProduct extends WC_Product */ public function get_major_version(): int { - $version = $this->get_current_version(); - if (empty($version)) { - return 1; + $versionManager = new VersionManager(); + $latestVersion = $versionManager->getLatestVersion($this->get_id()); + + if ($latestVersion) { + return $latestVersion->getMajorVersion(); } - $parts = explode('.', $version); - return (int) ($parts[0] ?? 1); + return 1; } } diff --git a/src/Product/LicensedProductType.php b/src/Product/LicensedProductType.php index 78b3bef..3b30e28 100644 --- a/src/Product/LicensedProductType.php +++ b/src/Product/LicensedProductType.php @@ -164,14 +164,6 @@ final class LicensedProductType 'value' => $currentBindToVersion ?: ($defaultBindToVersion ? 'yes' : 'no'), 'cbvalue' => 'yes', ]); - - woocommerce_wp_text_input([ - 'id' => '_licensed_current_version', - 'label' => __('Current Version', 'wc-licensed-product'), - 'description' => __('Current software version (e.g., 1.0.0)', 'wc-licensed-product'), - 'desc_tip' => true, - 'placeholder' => '1.0.0', - ]); ?>

@@ -223,12 +215,6 @@ final class LicensedProductType // phpcs:ignore WordPress.Security.NonceVerification.Missing $bindToVersion = isset($_POST['_licensed_bind_to_version']) ? 'yes' : 'no'; update_post_meta($postId, '_licensed_bind_to_version', $bindToVersion); - - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $currentVersion = isset($_POST['_licensed_current_version']) - ? sanitize_text_field($_POST['_licensed_current_version']) - : ''; - update_post_meta($postId, '_licensed_current_version', $currentVersion); } /** diff --git a/templates/emails/license-expiration.php b/templates/emails/license-expiration.php new file mode 100644 index 0000000..461ca7b --- /dev/null +++ b/templates/emails/license-expiration.php @@ -0,0 +1,97 @@ +getCustomerId()); +$customer_name = $customer ? $customer->display_name : __('Customer', 'wc-licensed-product'); +$account_url = wc_get_account_endpoint_url('licenses'); +?> + +

+ + +

+ +

+ +

+ +

+ + +

+ +
+ + + + + + + + + + + + + + + + + + + +
+ + getLicenseKey()); ?> + +
getDomain()); ?>
+
+ + +

+ + +

+ + + +

+ +getCustomerId()); +$customer_name = $customer ? $customer->display_name : __('Customer', 'wc-licensed-product'); + +echo sprintf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($customer_name)) . "\n\n"; + +if ($days_remaining === 1) { + echo sprintf( + esc_html__('Your license for %s will expire tomorrow (%s).', 'wc-licensed-product'), + esc_html($product_name), + esc_html($expiration_date) + ) . "\n\n"; +} else { + echo sprintf( + esc_html__('Your license for %1$s will expire in %2$d days (%3$s).', 'wc-licensed-product'), + esc_html($product_name), + $days_remaining, + esc_html($expiration_date) + ) . "\n\n"; +} + +echo "----------\n"; +echo esc_html__('License Details', 'wc-licensed-product') . "\n"; +echo "----------\n\n"; + +echo esc_html__('Product:', 'wc-licensed-product') . ' ' . esc_html($product_name) . "\n"; +echo esc_html__('License Key:', 'wc-licensed-product') . ' ' . esc_html($license->getLicenseKey()) . "\n"; +echo esc_html__('Domain:', 'wc-licensed-product') . ' ' . esc_html($license->getDomain()) . "\n"; +echo esc_html__('Expires:', 'wc-licensed-product') . ' ' . esc_html($expiration_date) . "\n\n"; + +if ($additional_content) { + echo "----------\n\n"; + echo esc_html(wp_strip_all_tags(wptexturize($additional_content))); + echo "\n\n"; +} + +echo esc_html__('View My Licenses', 'wc-licensed-product') . ': ' . esc_url(wc_get_account_endpoint_url('licenses')) . "\n\n"; + +echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"; diff --git a/wc-licensed-product.php b/wc-licensed-product.php index d63c429..e8cb07d 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Licensed Product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Description: WooCommerce plugin to sell software products using license keys with domain-based validation. - * Version: 0.0.7 + * Version: 0.0.8 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * License: GPL-2.0-or-later @@ -28,7 +28,7 @@ if (!defined('ABSPATH')) { } // Plugin constants -define('WC_LICENSED_PRODUCT_VERSION', '0.0.7'); +define('WC_LICENSED_PRODUCT_VERSION', '0.0.8'); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));