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 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 20:55:28 +01:00
parent 50388c2019
commit c6726703e4
13 changed files with 713 additions and 206 deletions

View File

@@ -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

View File

@@ -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."

View File

@@ -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 ""

View File

@@ -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'),
'<a href="' . esc_url(admin_url('admin.php?page=wc-settings&tab=email&section=wclp_license_expiration')) . '">' .
__('WooCommerce > Settings > Emails > License Expiration Warning', 'wc-licensed-product') . '</a>'
),
'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);
}
}

View File

@@ -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(),

View File

@@ -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();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: #f8f9fa; padding: 30px; border-radius: 8px;">
<h1 style="color: #333; margin-top: 0; font-size: 24px;">
<?php esc_html_e('License Expiration Notice', 'wc-licensed-product'); ?>
</h1>
<p><?php printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($customer->display_name)); ?></p>
<?php if ($daysRemaining === 1): ?>
<p style="color: #dc3545; font-weight: 600;">
<?php printf(
esc_html__('Your license for %s will expire tomorrow (%s).', 'wc-licensed-product'),
esc_html($productName),
esc_html($expirationDate)
); ?>
</p>
<?php else: ?>
<p style="color: #ffc107; font-weight: 600;">
<?php printf(
esc_html__('Your license for %1$s will expire in %2$d days (%3$s).', 'wc-licensed-product'),
esc_html($productName),
$daysRemaining,
esc_html($expirationDate)
); ?>
</p>
<?php endif; ?>
<div style="background: #fff; padding: 20px; border-radius: 4px; margin: 20px 0; border: 1px solid #e5e5e5;">
<h3 style="margin-top: 0; font-size: 16px;"><?php esc_html_e('License Details', 'wc-licensed-product'); ?></h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('Product:', 'wc-licensed-product'); ?></td>
<td style="padding: 8px 0; font-weight: 600;"><?php echo esc_html($productName); ?></td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></td>
<td style="padding: 8px 0;">
<code style="background: #f5f5f5; padding: 3px 8px; border-radius: 3px; font-family: monospace;">
<?php echo esc_html($license->getLicenseKey()); ?>
</code>
</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></td>
<td style="padding: 8px 0;"><?php echo esc_html($license->getDomain()); ?></td>
</tr>
<tr>
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></td>
<td style="padding: 8px 0; color: #dc3545; font-weight: 600;"><?php echo esc_html($expirationDate); ?></td>
</tr>
</table>
</div>
<p><?php esc_html_e('To continue using this product, please renew your license before the expiration date.', 'wc-licensed-product'); ?></p>
<p style="margin-top: 25px;">
<a href="<?php echo esc_url($accountUrl); ?>"
style="display: inline-block; background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: 600;">
<?php esc_html_e('View My Licenses', 'wc-licensed-product'); ?>
</a>
</p>
<hr style="border: none; border-top: 1px solid #e5e5e5; margin: 30px 0;">
<p style="font-size: 14px; color: #666; margin-bottom: 0;">
<?php printf(
esc_html__('This email was sent from %s.', 'wc-licensed-product'),
'<a href="' . esc_url($siteUrl) . '" style="color: #2271b1;">' . esc_html($siteName) . '</a>'
); ?>
</p>
</div>
</body>
</html>
<?php
return ob_get_clean();
}
/**
* Add license information to order completed email
*/
@@ -281,7 +200,7 @@ final class LicenseEmailController
echo "\n" . esc_html__('License Key:', 'wc-licensed-product') . ' ' . esc_html($license->getLicenseKey()) . "\n";
} else {
?>
<div style="margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-left: 3px solid #0073aa;">
<div style="margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-left: 3px solid #7f54b3;">
<strong><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></strong>
<code style="display: block; margin-top: 5px; padding: 5px; background: #fff; font-family: monospace;">
<?php echo esc_html($license->getLicenseKey()); ?>
@@ -324,7 +243,7 @@ final class LicenseEmailController
<div style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border: 1px solid #e5e5e5; border-radius: 4px;">
<h2 style="margin-top: 0; color: #333;"><?php esc_html_e('Your License Keys', 'wc-licensed-product'); ?></h2>
<?php if ($domain): ?>
<?php if ($domain) : ?>
<p style="margin-bottom: 15px;">
<strong><?php esc_html_e('Licensed Domain:', 'wc-licensed-product'); ?></strong>
<?php echo esc_html($domain); ?>
@@ -340,7 +259,7 @@ final class LicenseEmailController
</tr>
</thead>
<tbody>
<?php foreach ($licenses as $item): ?>
<?php foreach ($licenses as $item) : ?>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><?php echo esc_html($item['product_name']); ?></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">

View File

@@ -0,0 +1,240 @@
<?php
/**
* License Expiration Warning Email
*
* @package Jeremias\WcLicensedProduct\Email
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Email;
use Jeremias\WcLicensedProduct\License\License;
use WC_Email;
/**
* License Expiration Warning Email class
*
* Sends email notifications to customers when their licenses are about to expire.
*/
class LicenseExpirationEmail extends WC_Email
{
/**
* License object
*/
public ?License $license = null;
/**
* Days remaining until expiration
*/
public int $days_remaining = 0;
/**
* Product name
*/
public string $product_name = '';
/**
* Expiration date formatted
*/
public string $expiration_date = '';
/**
* Constructor
*/
public function __construct()
{
$this->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'),
'<code>{site_title}, {product_name}, {days_remaining}, {expiration_date}</code>'
);
$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,
],
];
}
}

View File

@@ -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;
}
}

View File

@@ -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',
]);
?>
</div>
</div>
@@ -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);
}
/**

View File

@@ -0,0 +1,97 @@
<?php
/**
* License Expiration Warning Email (HTML)
*
* This template can be overridden by copying it to yourtheme/woocommerce/emails/license-expiration.php.
*
* @package Jeremias\WcLicensedProduct\Templates\Emails
* @version 0.0.8
*
* @var \Jeremias\WcLicensedProduct\License\License $license
* @var int $days_remaining
* @var string $product_name
* @var string $expiration_date
* @var string $email_heading
* @var string $additional_content
* @var bool $sent_to_admin
* @var bool $plain_text
* @var \Jeremias\WcLicensedProduct\Email\LicenseExpirationEmail $email
*/
defined('ABSPATH') || exit;
/*
* @hooked WC_Emails::email_header() Output the email header
*/
do_action('woocommerce_email_header', $email_heading, $email);
$customer = get_userdata($license->getCustomerId());
$customer_name = $customer ? $customer->display_name : __('Customer', 'wc-licensed-product');
$account_url = wc_get_account_endpoint_url('licenses');
?>
<p><?php printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($customer_name)); ?></p>
<?php if ($days_remaining === 1) : ?>
<p style="color: #dc3545; font-weight: 600;">
<?php printf(
esc_html__('Your license for %s will expire tomorrow (%s).', 'wc-licensed-product'),
esc_html($product_name),
esc_html($expiration_date)
); ?>
</p>
<?php else : ?>
<p style="color: #856404; font-weight: 600;">
<?php printf(
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)
); ?>
</p>
<?php endif; ?>
<h2><?php esc_html_e('License Details', 'wc-licensed-product'); ?></h2>
<div style="margin-bottom: 40px;">
<table class="td" cellspacing="0" cellpadding="6" style="width: 100%; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;" border="1">
<tbody>
<tr>
<th class="td" scope="row" style="text-align:left;"><?php esc_html_e('Product:', 'wc-licensed-product'); ?></th>
<td class="td" style="text-align:left;"><?php echo esc_html($product_name); ?></td>
</tr>
<tr>
<th class="td" scope="row" style="text-align:left;"><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></th>
<td class="td" style="text-align:left;">
<code style="background: #f5f5f5; padding: 3px 8px; border-radius: 3px; font-family: monospace;">
<?php echo esc_html($license->getLicenseKey()); ?>
</code>
</td>
</tr>
<tr>
<th class="td" scope="row" style="text-align:left;"><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></th>
<td class="td" style="text-align:left;"><?php echo esc_html($license->getDomain()); ?></td>
</tr>
<tr>
<th class="td" scope="row" style="text-align:left;"><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></th>
<td class="td" style="text-align:left; color: #dc3545; font-weight: 600;"><?php echo esc_html($expiration_date); ?></td>
</tr>
</tbody>
</table>
</div>
<?php if ($additional_content) : ?>
<p><?php echo wp_kses_post($additional_content); ?></p>
<?php endif; ?>
<p style="margin-top: 25px;">
<a href="<?php echo esc_url($account_url); ?>" style="display: inline-block; background: #7f54b3; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: 600;">
<?php esc_html_e('View My Licenses', 'wc-licensed-product'); ?>
</a>
</p>
<?php
/*
* @hooked WC_Emails::email_footer() Output the email footer
*/
do_action('woocommerce_email_footer', $email);

View File

@@ -0,0 +1,64 @@
<?php
/**
* License Expiration Warning Email (Plain Text)
*
* This template can be overridden by copying it to yourtheme/woocommerce/emails/plain/license-expiration.php.
*
* @package Jeremias\WcLicensedProduct\Templates\Emails
* @version 0.0.8
*
* @var \Jeremias\WcLicensedProduct\License\License $license
* @var int $days_remaining
* @var string $product_name
* @var string $expiration_date
* @var string $email_heading
* @var string $additional_content
* @var bool $sent_to_admin
* @var bool $plain_text
* @var \Jeremias\WcLicensedProduct\Email\LicenseExpirationEmail $email
*/
defined('ABSPATH') || exit;
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
echo esc_html(wp_strip_all_tags($email_heading));
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
$customer = get_userdata($license->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";

View File

@@ -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__));