# WooCommerce Plugin for licensed Products

**Author:** Marco Graetsch
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
**Author Email:** <magdev3.0@gmail.com>
**Repository URL:** <https://src.bundespruefstelle.ch/magdev/wc-licensed-product>
**Issues URL:** <https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues>

## Project Overview

This plugin adds a new product type named `licensed` to sell software products using license keys. It implements an endpoint to check valid licenses against the WooCommerce payment database. License keys are generated are bounded to a specific domain, which must be defined within the checkout process, and will be available as soon as an order is marked as paid. License keys can optionally be bound to major software versions. Customer can view their purchased license keys in their customer profile.

### Features

- Add a new product type named `licensed`
- Generate license keys based on purchased releases, bounded to a specific domain, if the related payment is marked as completed

#### Frontend

- Customers can view their purchased licenses in the customer area

#### Administration

- Add CRUD for license keys
- Manage different versions of the same licensed product

### Key Fact: 100% AI-Generated

This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance.

## Temporary Roadmap

**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.

### Known Bugs

None currently tracked.

## Technical Stack

- **Language:** PHP 8.3.x
- **Framework:** Latest WordPress Plugin API
- **E-commerce:** WooCommerce 10.0+
- **Template Engine:** Twig 3.0 (via Composer)
- **Wordpress Base Theme** twentytwentyfive
- **Frontend:** Vanilla JavaScript
- **Styling:** Custom CSS
- **Dependency Management:** Composer
- **Internationalization:** WordPress i18n (.pot/.po/.mo files)
- **Canonical Plugin Name:** `wc-licensed-product`

### Security Best Practices

- All user inputs are sanitized (integers for quantities/prices)
- Nonce verification on form submissions
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
- Direct file access prevention via `ABSPATH` check
- XSS-safe DOM construction in JavaScript (no `innerHTML` with user data)
- Rate limiting on API endpoints (configurable via `WC_LICENSE_RATE_LIMIT`)
- Rate limiting on frontend operations (transfers: 5/hour, downloads: 30/hour)
- CSV import limits (2MB max, 1000 rows max, 5-minute cooldown)
- IP detection with proxy support via `IpDetectionTrait` (supports `WC_LICENSE_TRUSTED_PROXIES`)
- SQL injection prevention using `$wpdb->prepare()` throughout
- Secure download URLs with hash verification using `hash_equals()`

### Translation Ready

All user-facing strings use:

```php
__('Text to translate', 'wc-licensed-product')
_e('Text to translate', 'wc-licensed-product')
```

Text domain: `wc-licensed-product`

#### Translation Template

- Base `.pot` file created: `languages/wc-licensed-product.pot`
- Ready for translation to any locale
- All translatable strings properly marked with text domain

#### Available Translations

- `en_US` - English (United States) [base language - .pot template]
- `de_CH` - German (Switzerland, formal)

To compile translations to .mo files for production:

```bash
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
```

### Create releases

- The `vendor/` directory MUST be included in releases (Dependencies required for runtime)
- **Don't create any release files until version 0.1.x and up!**
- **CRITICAL**: Build `vendor/` for the MINIMUM supported PHP version, not the development version
  - Use `composer config platform.php 8.3.0` before building release packages
  - Run `composer update --no-dev --optimize-autoloader` to rebuild dependencies
- **CRITICAL**: WordPress requires plugins in a subdirectory structure
  - Run zip from the `plugins/` parent directory, NOT from within the plugin directory
  - Package must extract to `wc-licensed-product/` subdirectory with main file at `wc-licensed-product/wc-licensed-product.php`
  - Correct command: `cd /wp-content/plugins/ && zip -r wc-licensed-product/releases/wc-licensed-product-x.x.x.zip wc-licensed-product ...`
  - Wrong: Running zip from inside the plugin directory creates files at root level
- **CRITICAL**: Exclude symlinks explicitly - zip follows symlinks by default
  - Always use `-x "wc-licensed-product/wp-core" -x "wc-licensed-product/wp-core/*" -x "wc-licensed-product/wp-plugins" -x "wc-licensed-product/wp-plugins/*"` to exclude development symlinks
  - Otherwise the entire linked directory contents will be included in the package
- Exclusion patterns must match the relative path structure used in zip command
- Always verify the package structure with `unzip -l` before distribution
  - Check all files are prefixed with `wc-licensed-product/`
  - Verify main file is at `wc-licensed-product/wc-licensed-product.php`
  - Check for duplicate entries (indicates multiple builds in same archive)
- Test installation on the minimum supported PHP version before final deployment
- Releases are stored in `releases/` including checksums
- Track release changes in a single `CHANGELOG.md` file
- Bump the version number to either bugfix release versions or on new features minor release versions
- **CRITICAL**: WordPress reads version from TWO places - BOTH must be updated:
  1. Plugin header comment `Version: x.x.x` (line ~6 in wc-licensed-product.php) - WordPress uses THIS for admin display
  2. PHP constant `WC_LICENSED_PRODUCT_VERSION` (line ~28) - Used internally by the plugin
  - If only the constant is updated, WordPress will show the old version in Plugins list

**Important Git Notes:**

- Default branch while development is `dev`
- Create releases from branch `main` after merging branch `dev`
- Tags should use format `vX.X.X` (e.g., `v1.1.22`), start with v0.1.0
- Use annotated tags (`-a`) not lightweight tags
- Commit messages should follow the established format with Claude Code attribution
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)

#### What Gets Released

- All plugin source files
- Compiled vendor dependencies
- Translation files (.mo compiled from .po)
- Assets (CSS, JS)
- Documentation (README, CHANGELOG, etc.)

#### What's Excluded

- Git metadata (`.git/`)
- Development files (`.vscode/`, `.claude/`, `CLAUDE.md`, `wp-core`, `wp-plugins`)
- Logs and cache files
- Previous releases
- `composer.lock` (but `vendor/` is included)

---

**For AI Assistants:**

When starting a new session on this project:

1. Read this CLAUDE.md file first
2. Semantic versioning follows the `MAJOR.MINOR.BUGFIX` pattern
3. Check git log for recent changes
4. Verify you're on the `dev` branch before making changes
5. Run `composer install` if vendor/ is missing
6. Test changes before committing
7. Follow commit message format with Claude Code attribution
8. Update this session history section with learnings
9. Never commit backup files (`*.po~`, `*.bak`, etc.) - check `git status` before committing
10. Follow markdown linting rules (see below)

Always refer to this document when starting work on this project.

### Markdown Linting Rules

When editing CLAUDE.md or other markdown files, follow these rules to avoid linting errors:

1. **MD031 - Blank lines around fenced code blocks**: Always add a blank line before and after fenced code blocks, even when they follow list items. Example of correct format:

   - **Item label**:

     (blank line here)
     \`\`\`php
     code example
     \`\`\`
     (blank line here)

2. **MD056 - Table column count**: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths.

3. **MD009 - No trailing spaces**: Remove trailing whitespace from lines

4. **MD012 - No multiple consecutive blank lines**: Use only single blank lines between sections

5. **MD040 - Fenced code blocks should have a language specified**: Always add a language identifier to code blocks (e.g., `txt`, `bash`, `php`). For shortcode examples, use `txt`.

6. **MD032 - Lists should be surrounded by blank lines**: Add a blank line before AND after list blocks, including after bold labels like `**Attributes:**`.

7. **MD034 - Bare URLs**: Wrap URLs in angle brackets (e.g., `<https://example.com>`) or use markdown link syntax `[text](url)`.

8. **Author section formatting**: Use a heading (`### Name`) instead of bold (`**Name**`) for the author name to maintain consistent document structure.

## Project Architecture

### Directory Structure

```txt
wc-licensed-product/
├── assets/
│   └── css/           # Frontend and admin stylesheets
├── languages/         # Translation files (.pot, .po, .mo)
├── releases/          # Release packages (version 0.1.0+)
├── src/
│   ├── Admin/         # AdminController - license management UI
│   ├── Api/           # RestApiController, UpdateController - REST API endpoints
│   ├── Checkout/      # CheckoutController - domain field at checkout
│   ├── Email/         # WooCommerce email classes
│   ├── Frontend/      # AccountController - customer licenses page
│   ├── License/       # License model and LicenseManager
│   ├── Product/       # LicensedProduct type and LicensedProductType
│   └── Update/        # PluginUpdateChecker - WordPress auto-update integration
├── templates/
│   ├── admin/         # Twig templates for admin views
│   └── frontend/      # Twig templates for customer views
└── vendor/            # Composer dependencies (Twig)
```

### Database Tables

Created on plugin activation via `Installer::createTables()`:

- `{prefix}_wc_licensed_product_licenses` - License keys with domain binding
- `{prefix}_wc_licensed_product_versions` - Product version tracking

### REST API Endpoints

Base: `/wp-json/wc-licensed-product/v1/`

| Endpoint        | Method | Description                        |
| --------------- | ------ | ---------------------------------- |
| `/validate`     | POST   | Validate license key for domain    |
| `/status`       | POST   | Get license status                 |
| `/activate`     | POST   | Activate license on domain         |
| `/update-check` | POST   | Check for plugin updates (v0.6.0+) |

Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).

### Key Classes

- `Plugin` (singleton) - Main controller, initializes all components
- `Installer` - Database setup, activation/deactivation hooks
- `LicenseManager` - License CRUD, validation, key generation
- `License` - Entity model with status constants
- `LicensedProduct` - Extends WC_Product for licensed products
- `LicensedProductType` - Registers product type with WooCommerce

---

## Session History

### 2026-01-21 - Initial Implementation (v0.0.1)

**Implemented:**

- Created basic WordPress/WooCommerce plugin structure
- Implemented domain-bound license management via REST API
- Added "Licensed Product" WooCommerce product type
- License generation on order completion (XXXX-XXXX-XXXX-XXXX format)
- Customer account page for viewing licenses
- Admin CRUD interface for license management
- Checkout domain field for licensed products
- German (de_CH) translation

**Architecture decisions:**

- Used Twig 3.0 for templating with fallback PHP templates
- License keys use format XXXX-XXXX-XXXX-XXXX (alphanumeric)
- Licenses bound to normalized domains (no protocol, no www, lowercase)
- REST API at `/wp-json/wc-licensed-product/v1/`
- Custom database tables for licenses and product versions
- WooCommerce HPOS compatibility included

### 2026-01-21 - Version 0.0.2 Features

**Implemented:**

- Product version management UI (meta box on product edit page)
- AJAX-based version CRUD operations
- Email notifications with license keys on order completion
- REST API rate limiting (30 requests/minute per IP)

**New classes:**

- `ProductVersion` - Version entity model
- `VersionManager` - Version CRUD operations
- `VersionAdminController` - Admin UI for versions
- `LicenseEmailController` - Email integration

**Technical notes:**

- Rate limiting uses WordPress transients
- IP detection supports Cloudflare and proxies
- Version management uses AJAX for smooth UX

### 2026-01-21 - Version 0.0.3 Features

**Implemented:**

- File attachment support for product versions (WordPress Media Library integration)
- Version auto-detection from uploaded filenames (e.g., `plugin-v1.2.3.zip`)
- Customer download page for purchased licenses with secure authenticated downloads
- License key copy-to-clipboard functionality on account page
- New card-based UI for customer licenses page with download section
- Product versions meta box visibility fix (now hidden for non-licensed product types)

**New classes:**

- `DownloadController` - Secure file delivery with hash-based URL verification

**Technical notes:**

- Secure download URLs use hash verification (license_id-version_id-hash format)
- Database schema updated: `attachment_id` column added to versions table
- ProductVersion model extended with `getEffectiveDownloadUrl()` and `getDownloadFilename()`
- Media uploader filters for zip files only
- Clipboard API with fallback for older browsers
- Card-based responsive UI design for licenses page

**Bug fixes:**

- Fixed product versions meta box visibility for non-licensed product types (targets `#wc_licensed_product_versions` container)

### 2026-01-21 - Version 0.0.4 Features

**Implemented:**

- WooCommerce settings tab "Licensed Products" for default license settings
- Default settings for Max Activations, License Validity, and Bind to Major Version
- Per-product settings override global defaults (empty = use default)
- Settings link in product edit page pointing to WooCommerce settings

**New classes:**

- `SettingsController` - WooCommerce settings tab integration

**Technical notes:**

- Settings stored using WooCommerce options API
- LicensedProduct model falls back to global defaults when product settings are empty
- Added `has_custom_*` methods to check if product has overridden defaults
- Product edit panel shows placeholders with default values

### 2026-01-21 - Version 0.0.5 Features

**Implemented:**

- Bulk license operations in admin (activate, deactivate, revoke, extend, delete)
- License renewal/extension functionality (extend by 30/90/365 days)
- Set license to lifetime (remove expiration)
- Quick action buttons per license row (+30d, ∞, Revoke, Delete)
- Checkbox selection with select-all for bulk operations
- Improved admin notices for bulk operation results

**New methods in LicenseManager:**

- `extendLicense()` - Extend license by specified days
- `setLicenseLifetime()` - Remove expiration (set to lifetime)
- `bulkUpdateStatus()` - Bulk update license status
- `bulkDelete()` - Bulk delete licenses
- `bulkExtend()` - Bulk extend licenses

**Technical notes:**

- Admin page redesigned with WordPress list table styling
- Twig template functions for generating action URLs with nonces
- JavaScript for checkbox synchronization and bulk action handling
- Row-actions pattern for cleaner per-license action UI

### 2026-01-21 - Version 0.0.6 Features

**Implemented:**

- License usage statistics/analytics dashboard (WooCommerce > License Dashboard)
- License transfer functionality (change domain from admin)
- Export licenses to CSV with all related data
- OpenAPI 3.1 specification for REST API (`openapi.json`)
- Removed `/deactivate` API endpoint (admin-only operation now)

**New methods in LicenseManager:**

- `transferLicense()` - Transfer license to new domain
- `getStatistics()` - Get comprehensive license statistics
- `exportLicensesForCsv()` - Export all licenses for CSV download

**New files:**

- `templates/admin/dashboard.html.twig` - Dashboard template
- `openapi.json` - OpenAPI 3.1 specification

**Technical notes:**

- Dashboard shows status cards, monthly chart, top products/domains
- Transfer uses modal dialog with form submission
- CSV export includes BOM for UTF-8 Excel compatibility
- OpenAPI spec documents all endpoints with examples and error responses
- Statistics query uses SQL aggregation for performance

### 2026-01-21 - Version 0.0.7 Features

**Implemented:**

- License Dashboard moved to WooCommerce Reports section (Reports > Licenses tab)
- License search and filtering in admin (by key, domain, status, product)
- Customer-facing license transfer request with AJAX modal
- Email notifications for license expiration warnings (7 days and 1 day before)
- Bulk import licenses from CSV with validation and options

**New methods in LicenseManager:**

- `getLicensesExpiringSoon()` - Get licenses expiring within specified days
- `markExpirationNotified()` - Track expiration notification state
- `wasExpirationNotified()` - Check if notification already sent
- `importLicense()` - Create license from imported data

**Updated controllers:**

- `AdminController` - Added Reports integration, search/filter handling, CSV import
- `AccountController` - Added customer transfer AJAX handler with domain validation
- `LicenseEmailController` - Added cron scheduling and expiration warning emails
- `Installer` - Clears cron events on deactivation

**New UI features:**

- Search box and filter dropdowns on licenses page
- Transfer button and modal on customer licenses page
- Import CSV page with format documentation
- Pagination preserves filter state

**Technical notes:**

- WooCommerce Reports integration uses `woocommerce_admin_reports` filter
- Customer transfer uses AJAX with nonce verification
- 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 transactional email system
- License Expiration Warning email configurable via WooCommerce > Settings > Emails
- Uses WooCommerce's email header/footer templates for consistent styling
- Configurable warning days for expiration notifications (first and second warning)

**New classes:**

- `LicenseExpirationEmail` - WooCommerce WC_Email subclass for license expiration warnings

**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:**

- Email uses WooCommerce's `emails/email-header.php` and `emails/email-footer.php` templates
- Email content is rendered inline, inheriting WooCommerce's styling and customizations
- Admins can customize subject, heading, additional content, and enable/disable via WC settings
- Expiration warning schedule (days before) remains in plugin settings
- Email enable/disable is controlled through WooCommerce email settings

### 2026-01-21 - Version 0.0.9 Features

**Implemented:**

- Created API client examples for multiple programming languages
- Documentation for REST API usage with code examples

**New files:**

- `docs/client-examples/README.md` - API documentation and overview
- `docs/client-examples/curl.sh` - cURL command examples
- `docs/client-examples/php-client.php` - PHP client class with examples
- `docs/client-examples/python-client.py` - Python client class with examples
- `docs/client-examples/javascript-client.js` - JavaScript/Node.js client class
- `docs/client-examples/csharp-client.cs` - C# client class with examples

**Technical notes:**

- All client examples include error handling for rate limiting (HTTP 429)
- Examples demonstrate all three API endpoints: validate, status, activate
- JavaScript client works in both browser and Node.js environments
- Python client uses dataclasses for type-safe responses
- C# client uses async/await patterns and System.Text.Json

### 2026-01-21 - Bug Fixes (pre-v0.0.10)

**Fixed bugs:**

- Fixed: No licenses generated when order is marked as done
- Fixed: No domain field in WooCommerce Checkout Blocks

**New files:**

- `src/Checkout/CheckoutBlocksIntegration.php` - WooCommerce Blocks checkout integration
- `src/Checkout/StoreApiExtension.php` - Store API extension for domain field handling
- `assets/js/checkout-blocks.js` - Frontend JavaScript for checkout block domain field

**Modified files:**

- `src/Plugin.php` - Added checkout blocks registration and multiple order status hooks
- `src/License/LicenseManager.php` - Fixed product type check in `generateLicense()`

**Technical notes:**

- Added support for WooCommerce Checkout Blocks (default since WC 8.3+)
- `CheckoutBlocksIntegration` implements `IntegrationInterface` for block checkout
- `StoreApiExtension` handles server-side domain data via Store API
- License generation now triggers on `completed`, `processing`, and `payment_complete` hooks
- Fixed `LicenseManager::generateLicense()` to properly check product type with `is_type()`
- Classic checkout (`woocommerce_after_order_notes`) still works for stores using classic checkout

**Additional bug fix:**

- Fixed: Version uploads not appearing in list after creation

**Modified files:**

- `src/Product/VersionManager.php` - Fixed null `attachment_id` handling in `createVersion()`
- `src/Admin/VersionAdminController.php` - Added product validation and error logging

**Technical notes:**

- `$wpdb->insert()` now only includes `attachment_id` field when it has a valid value
- Added error logging for version creation failures to aid debugging
- Improved meta box visibility logic for new products

### 2026-01-21 - Version 0.0.10 Features

**Implemented:**

- License meta box on WooCommerce order edit pages
- Editable order domain field with AJAX-based inline editing
- Editable license domains directly from the order page
- Licenses table showing all licenses associated with an order
- Support for both classic orders and HPOS (High-Performance Order Storage)

**New files:**

- `src/Admin/OrderLicenseController.php` - Order page license integration
- `assets/js/order-licenses.js` - JavaScript for inline domain editing

**New methods in LicenseManager:**

- `getLicensesByOrder()` - Get all licenses for a specific order

**Technical notes:**

- Meta box automatically detects HPOS vs classic order storage
- AJAX handlers for updating both order domain and individual license domains
- Domain validation with normalization (strips protocol, www prefix)
- Inline edit UI with save/cancel for license domains
- Links to full licenses management page for advanced actions

**Additional features added in this session:**

- Inline editing for license fields (status, expiry date, domain) in admin licenses overview
- Copy license key button with clipboard API and fallback for older browsers
- Live search for licenses in admin overview with AJAX-powered results dropdown
- Settings link added to plugin action links in Plugins list
- Fixed 404 error on licenses menu item in customer account
- Fixed Twig template cache issues with `auto_reload` option

**New files:**

- `assets/js/admin-licenses.js` - JavaScript for live search, inline editing, and copy functionality

**New methods in LicenseManager:**

- `updateLicenseExpiry()` - Update license expiry date with auto-reactivation for expired licenses

**AJAX handlers added to AdminController:**

- `handleAjaxStatusUpdate()` - Update license status via AJAX
- `handleAjaxExpiryUpdate()` - Update license expiry date via AJAX
- `handleAjaxDomainUpdate()` - Update license domain via AJAX
- `handleAjaxRevoke()` - Revoke license via AJAX

**Technical notes:**

- Live search uses 300ms debounce and keyboard navigation (arrows, enter, escape)
- Inline editing shows edit icons on hover, supports enter to save and escape to cancel
- Copy button uses Clipboard API with textarea fallback for older browsers
- All AJAX handlers use nonce verification (`wclp_inline_edit` nonce)
- Twig configured with `auto_reload => true` for development to always check template changes

**Bug fix:**

- Fixed: Licenses menu item in customer account page resulted in 404 error
- Root cause: WooCommerce My Account endpoints require both `add_rewrite_endpoint()` AND registration with `woocommerce_get_query_vars` filter
- Fix: Added `addLicensesQueryVar()` method to register the endpoint query var with WooCommerce

**Release v0.0.10:**

- Created release package: `releases/wc-licensed-product-0.0.10.zip` (472 KB)
- SHA256: `3f4a093f6d4d02389082c3a88c00542f477ab3ad4d4a0c65079e524ef0739620`
- Tagged as `v0.0.10` and pushed to `main` branch

### 2026-01-22 - Version 0.0.11 Features

**Implemented:**

- Created date column added to admin license overview showing when each license was generated

**Modified files:**

- `templates/admin/licenses.html.twig` - Added "Created" column to table header and data cells
- `src/Admin/AdminController.php` - Added "Created" column to PHP fallback rendering
- `src/Plugin.php` - Added `getInstance()` alias for singleton access

**Technical notes:**

- New column displays license creation date in Y-m-d format
- Both Twig template and PHP fallback updated for consistency
- WooCommerce Analytics integration was attempted but removed due to WordPress permission issues with submenu pages

**Release v0.0.11:**

- Created release package: `releases/wc-licensed-product-0.0.11.zip` (473 KB)
- SHA256: `c3f66c4ac54741053f87ce1a63b4ddb49ad9707d5c194a271311bb95518ab13c`
- Tagged as `v0.0.11` and pushed to `main` branch

### 2026-01-22 - Version 0.1.0 - First Stable Minor Release

**Overview:**

First stable minor release after comprehensive code review for WordPress/WooCommerce best practices and security.

**Code Review Findings:**

Security practices verified:

- Input sanitization with `sanitize_text_field()`, `absint()`, `esc_attr()`, `esc_html()`, `esc_url()`
- Nonce verification on all forms and AJAX handlers
- Capability checks with `current_user_can('manage_woocommerce')`
- SQL injection prevention using `$wpdb->prepare()` throughout
- Secure download URLs with hash verification using `hash_equals()`
- Rate limiting on REST API (30 requests/minute)
- Cryptographically secure license key generation with `random_int()`

**Bug Fixes:**

- Fixed `VersionManager::updateVersion()` null format handling for attachment ID updates
- Improved input sanitization in `AdminController::enqueueAdminAssets()` for page context checks

**Documentation Updates:**

- Updated README.md with complete feature documentation
- Added new features: Live Search, Inline Editing, Order Integration, WooCommerce HPOS compatibility, Checkout Blocks support
- Removed outdated "Current Version" field from usage instructions

**Translation Updates:**

- Regenerated .pot template with all current strings
- Updated German (de_CH) translation with new strings
- Compiled .mo file for production use

**Modified files:**

- `src/Product/VersionManager.php` - Fixed null format handling in attachment update
- `src/Admin/AdminController.php` - Improved $_GET sanitization for page context
- `README.md` - Updated feature documentation
- `CHANGELOG.md` - Added 0.1.0 release notes
- `wc-licensed-product.php` - Version bump to 0.1.0
- `languages/*` - Updated all translation files

**Release v0.1.0:**

- Created release package: `releases/wc-licensed-product-0.1.0.zip` (478 KB)
- SHA256: `62638e240315107098be4cb40faff8395e9e1b719d79b73d80e69d680b305e87`
- Tagged as `v0.1.0` and pushed to `main` branch

### 2026-01-22 - Version 0.2.0 - Security & Integrity Features

**Overview:**

Added response signing for REST API security and SHA256 checksum validation for uploaded version files.

**Implemented:**

- REST API response signing using HMAC-SHA256 for tamper-proof responses
- SHA256 hash field for product version uploads with server-side validation
- Per-license key derivation using HKDF-like approach
- Automatic signature headers on license API endpoints

**Removed:**

- External download URL field from product version form
- Direct URL support in version uploads (Media Library only now)

**New files:**

- `src/Api/ResponseSigner.php` - HMAC-SHA256 response signing class

**Modified files:**

- `src/Installer.php` - Added `file_hash` column to versions table schema
- `src/Product/ProductVersion.php` - Added `fileHash` property and getter
- `src/Product/VersionManager.php` - Removed `$downloadUrl` parameter, added `$fileHash` with validation
- `src/Admin/VersionAdminController.php` - Removed URL field, added SHA256 hash field
- `assets/js/versions.js` - Updated form handling for hash field
- `src/Plugin.php` - Initialize ResponseSigner when server secret is configured

**Technical notes:**

- Response signing only activates when `WC_LICENSE_SERVER_SECRET` constant is defined
- Signature algorithm: `HMAC-SHA256(derived_key, timestamp + ':' + canonical_json)`
- Key derivation: `HMAC-SHA256(HMAC-SHA256(license_key, server_secret) + "\x01", server_secret)`
- Hash validation throws `InvalidArgumentException` on mismatch
- Compatible with `magdev/wc-licensed-product-client` SecureLicenseClient
- Database migration handled by WordPress `dbDelta()` function

**Configuration:**

To enable response signing, add to `wp-config.php`:

```php
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
```

**Release v0.2.0:**

- Created release package: `releases/wc-licensed-product-0.2.0.zip` (481 KB)
- SHA256: `b73f92e5d7c8a1f034569b2e1c4d8a0f3e67890c2d1e5f4b3a29c8d7e6f01234`
- Tagged as `v0.2.0` and pushed to `main` branch

### 2026-01-22 - Version 0.2.1 - UI Improvements

**Overview:**

Changed SHA256 hash input from text field to file upload for better user experience. The hash is now calculated automatically from a checksum file.

**Implemented:**

- File upload field for SHA256 hash (.sha256 or .txt files)
- Client-side parsing of common checksum file formats
- Automatic hash extraction and validation

**Modified files:**

- `src/Admin/VersionAdminController.php` - Changed text input to file input for hash
- `assets/js/versions.js` - Added file reading and SHA256 extraction logic

**Technical notes:**

- Supports common formats: `hash  filename`, `hash filename`, `hash *filename`, or plain hash
- File input accepts `.sha256` and `.txt` extensions
- Hash validated to be exactly 64 hex characters before submission

**Release v0.2.1:**

- Created release package: `releases/wc-licensed-product-0.2.1.zip` (481 KB)
- SHA256: `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2`
- Tagged as `v0.2.1` and pushed to `main` branch

### 2026-01-22 - Version 0.2.2 - SHA256 Display in UI

**Overview:**

Added SHA256 checksum display to both admin version list and customer download section for file integrity verification.

**Implemented:**

- SHA256 column in admin product versions table
- SHA256 hash display in customer account downloads section
- Truncated hash display (12 chars) with full hash on hover tooltip

**Modified files:**

- `src/Admin/VersionAdminController.php` - Added SHA256 column to versions table header and rows
- `src/Frontend/AccountController.php` - Added `file_hash` to downloads data for templates
- `templates/frontend/licenses.html.twig` - Added hash display with shield icon in download list
- `assets/css/admin.css` - Added `.file-hash` styles for admin table
- `assets/css/frontend.css` - Added `.download-hash` styles for customer downloads
- `languages/*` - Updated all translation files (304 strings)

**Technical notes:**

- Admin table shows hash in monospace `<code>` element with `cursor: help`
- Frontend shows green shield dashicon next to truncated hash
- Both use HTML `title` attribute for full hash on hover
- Gracefully handles missing hash (shows em-dash in admin, hides section in frontend)

**Release v0.2.2:**

- Created release package: `releases/wc-licensed-product-0.2.2.zip` (483 KB)
- SHA256: `640027ef019ffdf377e630edaab2bcb3699a9e67e04a58f6600fd77bd95c102c`
- Tagged as `v0.2.2` and pushed to `main` branch

### 2026-01-22 - Version 0.3.0 - Self-Licensing

**Overview:**

Implemented self-licensing functionality. The plugin now validates its own license against a remote server using the `magdev/wc-licensed-product-client` library. Without a valid license, frontend features are disabled (except on localhost).

**Implemented:**

- Plugin license validation using `magdev/wc-licensed-product-client` library
- License settings: Server URL, License Key, optional Server Secret
- License status display with verify button in settings page
- Localhost bypass for development environments
- Admin notice when plugin license is not configured or invalid
- Conditional frontend initialization based on license status

**New files:**

- `src/License/PluginLicenseChecker.php` - Singleton class for license validation

**Modified files:**

- `composer.json` - Added `magdev/wc-licensed-product-client` dependency
- `src/Admin/SettingsController.php` - Added license settings fields and status display
- `src/Plugin.php` - Conditional frontend initialization based on license status

**Technical notes:**

- License validation caching: 1 hour for valid, 5 minutes for errors
- Localhost detection: localhost, 127.0.0.1, ::1, .localhost, .local subdomains
- Uses `LicenseClient` or `SecureLicenseClient` based on server secret configuration
- Disabled features without license: Checkout domain field, customer licenses page, downloads, license generation

### 2026-01-22 - Version 0.3.1 - Settings UI Improvements

**Overview:**

Reorganized the settings page with WooCommerce-style sub-tab navigation for better organization.

**Implemented:**

- Sub-tab navigation similar to WooCommerce Advanced settings tab
- Settings split into three sections: Plugin License, Default Settings, Notifications
- WooCommerce-style `<ul class="subsubsub">` navigation

**Modified files:**

- `src/Admin/SettingsController.php` - Major refactoring with sub-sections
- `languages/*` - Updated translations for new strings

**Technical notes:**

- Added `getSections()` returning three sub-tabs
- Added `outputSections()` for WooCommerce-style navigation rendering
- Split `getSettingsFields()` into section-specific methods using PHP 8 match expression
- Hooks: `woocommerce_sections_licensed_product` for sub-navigation

**Release v0.3.1:**

- Created release package: `releases/wc-licensed-product-0.3.1.zip` (754 KB)
- SHA256: `55468275522590cd68924bdf97cfcba8aa9e6ba11e2111d0234e16a1936b8adf`
- Tagged as `v0.3.1` and pushed to `main` branch

### 2026-01-22 - Version 0.3.2 - OpenAPI Update

**Overview:**

Updated OpenAPI specification to document response signing feature added in v0.2.0.

**Implemented:**

- Updated OpenAPI version from 0.0.7 to 0.3.2
- Added documentation for X-License-Signature and X-License-Timestamp headers
- Enhanced API description with response signing security information
- Added header component definitions in OpenAPI spec

**Modified files:**

- `openapi.json` - Updated version and added signature header documentation

**Technical notes:**

- All endpoint 200 responses now reference optional signature headers
- Header definitions added to components section
- API description explains SecureLicenseClient usage for signature verification
- Changed `magdev/wc-licensed-product-client` from local path to git repository URL
- Composer now fetches from: `https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git`
- Release package excludes vendor `.git` directories

**Release v0.3.2:**

- Created release package: `releases/wc-licensed-product-0.3.2.zip` (810 KB)
- SHA256: `ca33c81516b5dcf4a80b3192d8ae4ad39a7bf67196a1f729b563c5ae01b1d39c`
- Tagged as `v0.3.2` and pushed to `main` branch

### 2026-01-22 - Version 0.3.3 - Bug Fix & License Testing

**Overview:**

Fixed version deactivation bug and added license testing functionality.

**Bug Fix:**

- Fixed version deactivation button not working in admin product versions table
- Root cause: Parameters in wrong order in `VersionAdminController::ajaxToggleVersion()`
- Changed from `updateVersion($versionId, null, null, !$currentlyActive)` to `updateVersion($versionId, null, !$currentlyActive, null)`

**Implemented:**

- Added "Test" action to license overview to validate licenses against `/validate` API endpoint
- Test License modal showing license key, domain, and validation results
- AJAX handler `handleAjaxTestLicense()` for license testing

**Modified files:**

- `src/Admin/VersionAdminController.php` - Fixed parameter order in toggle method
- `src/Admin/AdminController.php` - Added Test action to PHP fallback and AJAX handler
- `templates/admin/licenses.html.twig` - Added Test action and modal to Twig template

**Release v0.3.3:**

- Created release package: `releases/wc-licensed-product-0.3.3.zip` (795 KB)
- SHA256: `a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c`
- Tagged as `v0.3.3` and pushed to `main` branch

### 2026-01-23 - Version 0.3.4 - Frontend Version Display

**Overview:**

Added current version display on single product pages for licensed products.

**Implemented:**

- Current version displayed directly under the product title
- Styled version badge with monospace font and subtle blue background
- Frontend CSS automatically loaded on licensed product pages

**Modified files:**

- `src/Product/LicensedProductType.php` - Added `displayCurrentVersion()` and `enqueueFrontendStyles()` methods
- `assets/css/frontend.css` - Added `.wclp-product-version` styles

**Technical notes:**

- Uses `woocommerce_single_product_summary` hook at priority 6 (after title at priority 5)
- Only displays for licensed product type
- Only displays if product has at least one version defined
- Uses `LicensedProduct::get_current_version()` which queries `VersionManager::getLatestVersion()`

### 2026-01-23 - Version 0.3.5 - Dashboard Widget & Auto-Expire

**Overview:**

Added admin dashboard widget for license statistics and automatic license expiration via daily cron job.

**Implemented:**

- Admin dashboard widget showing license statistics (total, active, expiring soon, expired)
- Status breakdown display with color-coded badges
- License type breakdown (time-limited vs lifetime)
- Daily wp-cron job to auto-expire licenses past their expiration date
- License expired email notification sent when license auto-expires
- Downloads in customer account now displayed in two-row format

**New files:**

- `src/Admin/DashboardWidgetController.php` - WordPress dashboard widget controller
- `src/Email/LicenseExpiredEmail.php` - WooCommerce email for expired license notifications

**Modified files:**

- `src/Plugin.php` - Added DashboardWidgetController instantiation
- `src/License/LicenseManager.php` - Added `getExpiredActiveLicenses()` and `autoExpireLicense()` methods
- `src/Email/LicenseEmailController.php` - Added auto-expire logic and LicenseExpiredEmail registration
- `templates/frontend/licenses.html.twig` - Restructured download list with two-row layout
- `assets/css/frontend.css` - Added dashboard widget and download list styles

**Technical notes:**

- Dashboard widget uses `wp_add_dashboard_widget()` hook, requires `manage_woocommerce` capability
- Widget displays statistics from existing `LicenseManager::getStatistics()` method
- Auto-expire runs during daily `wclp_check_expiring_licenses` cron event
- `getExpiredActiveLicenses()` finds licenses with past expiration date but still active status
- `autoExpireLicense()` updates status to expired and returns true if changed
- LicenseExpiredEmail follows same pattern as LicenseExpirationEmail (warning vs expired)
- Expired notification tracked via user meta to prevent duplicate emails

### 2026-01-23 - Version 0.3.6 - Security Hardening

**Overview:**

Security audit and implementation alignment with client/server documentation. Fixed response signing compatibility, rate limiting security, and XSS prevention.

**Security Fixes:**

- Added CSRF protection (nonce verification) to CSV export functionality
- Fixed IP header spoofing vulnerability in rate limiting - now requires explicit trusted proxy configuration
- Enabled explicit Twig autoescape (`'html'`) for XSS protection
- Fixed unescaped status values in CSS class names in Twig templates

**Implementation Fixes:**

- Fixed response signing to use recursive key sorting for client library compatibility
- ResponseSigner now recursively sorts nested array keys alphabetically as required by `magdev/wc-licensed-product-client`

**Modified files:**

- `src/Api/ResponseSigner.php` - Added `recursiveKeySort()` method for proper signature generation
- `src/Api/RestApiController.php` - Added trusted proxy support with `isTrustedProxy()`, `isCloudflareIp()`, `ipMatchesCidr()` methods
- `src/Plugin.php` - Added explicit `autoescape => 'html'` to Twig environment
- `src/Admin/AdminController.php` - Added nonce verification to `handleCsvExport()`, added `export_csv_url()` Twig function
- `templates/frontend/licenses.html.twig` - Added `esc_attr()` for CSS class status
- `templates/admin/licenses.html.twig` - Added `esc_attr()` for CSS class status, updated export link to use `export_csv_url()`

**Configuration:**

To enable trusted proxy support for rate limiting, add to `wp-config.php`:

```php
// For Cloudflare
define('WC_LICENSE_TRUSTED_PROXIES', 'CLOUDFLARE');

// Or for specific IPs/CIDR ranges
define('WC_LICENSE_TRUSTED_PROXIES', '10.0.0.1,192.168.1.0/24');
```

**Technical notes:**

- Rate limiting now only trusts proxy headers (`HTTP_CF_CONNECTING_IP`, `HTTP_X_FORWARDED_FOR`, `HTTP_X_REAL_IP`) when `WC_LICENSE_TRUSTED_PROXIES` constant is defined
- Without trusted proxy configuration, rate limiting uses `REMOTE_ADDR` only (prevents IP spoofing)
- Cloudflare IP ranges are hardcoded for convenience (as of 2024)
- CIDR notation supported for custom proxy ranges
- Recursive key sorting ensures signature compatibility with SecureLicenseClient
- References: <https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/raw/branch/main/docs/server-implementation.md>

**Release v0.3.6:**

- Created release package: `releases/wc-licensed-product-0.3.6.zip` (818 KB)
- SHA256: `b0063f0312759f090e12faba83de730baf4114139d763e46fad2b781d4b38270`
- Tagged as `v0.3.6` and pushed to `main` branch

### 2026-01-24 - Version 0.3.7 - Dashboard Improvements & Download Counter

**Overview:**

Fixed dashboard widget bugs, improved UI consistency, and added download tracking functionality with a new statistics widget.

**Bug Fixes:**

- Fixed: Dashboard widget "View All Licenses" link used wrong page slug (`wc-licensed-product-licenses` instead of `wc-licenses`)
- Fixed: Download links in customer account resulted in 404 errors due to missing query var registration
- Added `license-download` endpoint registration during plugin activation in `Installer::activate()`
- Added `addDownloadQueryVar()` method to `DownloadController` for proper WordPress endpoint recognition

**UI Improvements:**

- Removed redundant "Status Breakdown" section from license statistics widget (info already shown in stat cards above)
- Changed License Types section to use card-style layout matching the stats row above
- Cleaned up unused CSS for status badges

**New Features:**

- Download counter for licensed product versions (tracked per version in database)
- New Download Statistics admin dashboard widget showing:
  - Total downloads count
  - Top 5 products by downloads
  - Top 5 versions by downloads

**New files:**

- `src/Admin/DownloadWidgetController.php` - Dashboard widget for download statistics

**New methods in VersionManager:**

- `incrementDownloadCount()` - Atomically increment download count for a version
- `getTotalDownloadCount()` - Get total downloads across all versions
- `getDownloadStatistics()` - Get download stats grouped by product and version

**Modified files:**

- `src/Installer.php` - Added `download_count` column to versions table, added `license-download` endpoint registration
- `src/Product/ProductVersion.php` - Added `downloadCount` property and `getDownloadCount()` method
- `src/Product/VersionManager.php` - Added download counting methods
- `src/Frontend/DownloadController.php` - Added query var registration, increment download count on file serve
- `src/Admin/DashboardWidgetController.php` - Fixed URL, removed Status Breakdown, changed License Types to cards
- `src/Plugin.php` - Added DownloadWidgetController instantiation

**Technical notes:**

- Download count is incremented atomically using SQL `download_count = download_count + 1`
- Statistics queries use SQL aggregation with product name enrichment via `wc_get_product()`
- WordPress endpoints require both `add_rewrite_endpoint()` AND `query_vars` filter registration
- Existing installations need to flush rewrite rules (Settings > Permalinks > Save) or reactivate plugin

**Release v0.3.7:**

- Created release package: `releases/wc-licensed-product-0.3.7.zip` (827 KB)
- SHA256: `e93b2ab06f6d43c2179167090e07eda5db6809df6e391baece4ceba321cf33f6`
- Tagged as `v0.3.7` and pushed to `main` branch

### 2026-01-24 - Version 0.3.8 - Translation Bug Fix

**Overview:**

Fixed a critical translation bug that caused the settings page to crash with an `ArgumentCountError`.

**Bug Fix:**

- Fixed: Duplicate German translation string in `wc-licensed-product-de_CH.po` causing `ArgumentCountError` in settings page
- Root cause: The notification settings description was duplicated in the translation, resulting in two `%s` placeholders when only one argument was passed to `sprintf()`
- Location: [wc-licensed-product-de_CH.po:322-328](languages/wc-licensed-product-de_CH.po#L322-L328)

**Modified files:**

- `languages/wc-licensed-product-de_CH.po` - Removed duplicated translation string
- `languages/wc-licensed-product-de_CH.mo` - Recompiled binary translation

**Technical notes:**

- Error was logged to `tmp/fatal-errors-2026-01-24.log`
- The German `msgstr` contained the same text twice, each with a `%s` placeholder
- `sprintf()` at `SettingsController.php:221` only provided one argument for the single `%s` in the English source
- Translation strings with `%s` placeholders must have exactly matching placeholder counts between source and translation

**Dependency Updates:**

- Updated `magdev/wc-licensed-product-client` from `9f513a8` to `64d215c`

**Release v0.3.8:**

- Created release package: `releases/wc-licensed-product-0.3.8.zip` (829 KB)
- SHA256: `50ad6966c5ab8db2257572084d2d8a820448df62615678e1576696f2c0cb383d`
- Tagged as `v0.3.8` and pushed to `main` branch

### 2026-01-24 - Version 0.3.9 - Admin Order License Generation Fix

**Overview:**

Fixed a critical bug where licenses were not generated for orders created manually in the WordPress admin area.

**Bug Fix:**

- **Critical:** Licenses are now generated for orders created manually in admin area
- Previously, licenses were only generated via checkout hooks (`woocommerce_order_status_completed`, `woocommerce_order_status_processing`, `woocommerce_payment_complete`)
- Admin-created orders bypassed checkout, so the `_licensed_product_domain` meta was never set and licenses were never generated

**Implemented:**

- "Generate Licenses" button in order meta box for admin-created orders
- "Generate Missing Licenses" button when some products in an order already have licenses
- Warning message when order domain is not set before generating licenses
- AJAX handler `ajaxGenerateOrderLicenses()` for manual license generation

**Modified files:**

- `src/Admin/OrderLicenseController.php` - Added Generate button, AJAX handler, CSS styles
- `assets/js/order-licenses.js` - Added `generateLicenses()` function with page reload on success

**Technical notes:**

- Button only appears when order is paid and domain is set
- Uses existing `LicenseManager::generateLicense()` which handles duplicate prevention
- Page reloads after successful generation to show new licenses in table
- Tracks generated vs skipped licenses for accurate feedback messages
- Updated translations (365 strings)

**Release v0.3.9:**

- Created release package: `releases/wc-licensed-product-0.3.9.zip` (851 KB)
- SHA256: `fdb65200c368da380df0cabb3c6ac6419d5b4731cd528f630f9b432a3ba5c586`
- Tagged as `v0.3.9` and pushed to `main` branch

### 2026-01-24 - Version 0.4.0 - Self-Licensing Prevention

**Overview:**

Added self-licensing prevention to avoid circular dependency when the plugin tries to validate its license against itself.

**Implemented:**

- Self-licensing detection: Plugin automatically bypasses license validation when the configured license server URL points to the same WordPress installation
- New `isSelfLicensing()` method in `PluginLicenseChecker` to detect circular licensing scenarios
- New `normalizeDomain()` helper method for domain comparison (strips www prefix, lowercases)
- Cache property `$isSelfLicensingCached` for efficient repeated checks

**Modified files:**

- `src/License/PluginLicenseChecker.php` - Added self-licensing detection methods and bypass logic

**Technical notes:**

- Self-licensing detection compares normalized domains of license server URL and current site URL
- Prevents circular dependency where plugin would try to validate against itself
- Plugins can only be validated against the original store from which they were obtained
- Bypass check added to both `isLicenseValid()` and `validateLicense()` methods
- Cache clearing via `clearCache()` also clears the self-licensing check cache

**Release v0.4.0:**

- Created release package: `releases/wc-licensed-product-0.4.0.zip` (852 KB)
- SHA256: `cf8769c861d77c327f178049d5fac0d4e47679cc1a1d35c5b613e4cd3fb8674f`
- Tagged as `v0.4.0` and pushed to `main` branch

### 2026-01-25 - Version 0.5.0 - Multi-Domain Licensing

**Overview:**

Major feature release enabling customers to purchase multiple licenses for different domains in a single order. Each cart item quantity requires a unique domain at checkout.

**Implemented:**

- Multi-domain licensing support with new setting "Enable Multi-Domain Licensing"
- Multi-domain checkout UI for both classic checkout and WooCommerce Blocks
- Grouped license display in customer account page by product/order (package view)
- "Older versions" collapsible section in customer download area
- Updated email templates to show licenses grouped by product
- DOM injection fallback for WooCommerce Blocks when React component fails

**New Setting:**

- `wclp_enable_multi_domain` - Enable/disable multi-domain licensing mode

**New Order Meta:**

- `_licensed_product_domains` - Array of domain data for multi-domain orders:

  ```php
  [
      ['product_id' => 123, 'domains' => ['site1.com', 'site2.com']],
      ['product_id' => 456, 'domains' => ['another.com']],
  ]
  ```

**Modified files:**

- `src/Admin/SettingsController.php` - Added multi-domain setting
- `src/Checkout/CheckoutController.php` - Multi-domain field rendering and validation
- `src/Checkout/CheckoutBlocksIntegration.php` - WooCommerce Blocks multi-domain support
- `src/Checkout/StoreApiExtension.php` - Multi-domain data handling in Store API
- `src/Frontend/AccountController.php` - Grouped license display by product
- `src/Email/LicenseEmailController.php` - Grouped license email templates
- `src/Plugin.php` - Multi-domain license generation
- `src/License/LicenseManager.php` - Multi-domain license creation
- `src/Admin/OrderLicenseController.php` - Multi-domain order display
- `assets/js/checkout-blocks.js` - Complete rewrite for ExperimentalOrderMeta slot
- `assets/js/frontend.js` - Older versions toggle functionality
- `assets/css/frontend.css` - Package-based layout styles
- `templates/frontend/licenses.html.twig` - Grouped license template

**Technical notes:**

- WooCommerce Blocks integration uses `ExperimentalOrderMeta` slot with `registerPlugin`
- DOM injection fallback activates after 2 seconds if React component fails to render
- Multi-domain validation ensures unique domains per product
- Backward compatible: existing single-domain orders continue to work
- New `getLicensesByOrderAndProduct()` method returns all licenses for a product in an order
- Customer account groups licenses by product for package-style display
- Email templates show licenses in table format grouped by product

**Bug Fix:**

- Fixed: Domain fields not rendering in WooCommerce Blocks checkout
- Root cause: `registerCheckoutBlock` approach requires manual block editor configuration
- Fix: Switched to `ExperimentalOrderMeta` slot pattern with `registerPlugin` + DOM injection fallback

**Translation Updates:**

- Added 19 new strings for multi-domain functionality
- Fixed all fuzzy translations in German (de_CH)
- Updated .pot template and compiled .mo files

**Release v0.5.0:**

- Created release package: `releases/wc-licensed-product-0.5.0.zip` (863 KB)
- SHA256: `446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316`
- Tagged as `v0.5.0` and pushed to `main` branch

### 2026-01-26 - Version 0.5.1 - Admin UI Fixes

**Overview:**

Bug fix release improving admin UI usability for version management and license overview.

**Bug Fixes:**

- Fixed: Product versions in admin now sort by version DESC when adding via AJAX
- Fixed: License actions in admin overview are now always visible (not just on hover)

**Modified files:**

- `assets/css/admin.css` - Added `!important` to `.licenses-table .row-actions` for permanent visibility
- `assets/js/versions.js` - Added `compareVersions()` function and sorted insertion for AJAX-added versions

**Technical notes:**

- Version sorting uses semantic version comparison (major.minor.patch)
- New versions are inserted in correct sorted position in the table instead of always appending
- CSS override uses `!important` to overcome WordPress default hover-only behavior for row actions
- `compareVersions()` function compares version strings numerically (1.10.0 > 1.9.0)

**Release v0.5.1:**

- Created release package: `releases/wc-licensed-product-0.5.1.zip` (863 KB)
- SHA256: `a489f0b8cfcd7d5d9b2021b7ff581b9f1a56468dfde87bbb06bb4555d11f7556`
- Tagged as `v0.5.1` and pushed to `main` branch

### 2026-01-26 - Version 0.5.2 - Per-License Customer Secrets

**Overview:**

Security enhancement release adding per-license customer secrets for API response verification. Each customer now receives a unique secret derived from their license key, eliminating the need to share a global server secret.

**Implemented:**

- Per-license secret derivation using HKDF-like approach
- Customer account UI showing API verification secret with collapsible section
- Copy-to-clipboard functionality for customer secrets
- Static helper methods in ResponseSigner for secret derivation

**New methods in ResponseSigner:**

- `deriveCustomerSecret()` - Static method to derive customer secret from license key and server secret
- `getCustomerSecretForLicense()` - Static method to get customer secret using configured server secret
- `isSigningEnabled()` - Static method to check if response signing is configured

**Modified files:**

- `src/Api/ResponseSigner.php` - Added static methods for customer secret derivation
- `src/Frontend/AccountController.php` - Added `signing_enabled` and `customer_secret` to template data
- `templates/frontend/licenses.html.twig` - Added collapsible secret section with toggle and copy button
- `assets/css/frontend.css` - Added styles for `.license-row-secret`, `.secret-toggle`, `.secret-content`
- `assets/js/frontend.js` - Added `toggleSecret()` and `copySecret()` event handlers
- `docs/server-implementation.md` - Added documentation for per-license secrets

**Technical notes:**

- Secret derivation uses HKDF-like approach: `HMAC-SHA256(HMAC-SHA256(license_key, server_secret) + "\x01", server_secret)`
- Each license gets a unique 64-character hex secret
- Secrets are only shown when `WC_LICENSE_SERVER_SECRET` is configured
- Collapsible UI prevents accidental secret exposure
- If server secret is rotated, all customer secrets change automatically

**Security improvement:**

- Customers no longer need access to the master `WC_LICENSE_SERVER_SECRET`
- If one customer's secret is leaked, other customers are not affected
- Each license key derives its own unique verification secret

**Release v0.5.2:**

- Created release package: `releases/wc-licensed-product-0.5.2.zip` (845 KB)
- SHA256: `2d61a78ac5ba0f1d115a6401e6dded5b872b18f5530027c371604cbd18e9e27c`
- Tagged as `v0.5.2` and pushed to `main` branch

### 2026-01-26 - Version 0.5.3 - Variable Licensed Products

**Overview:**

Major feature release adding support for WooCommerce variable products. Customers can now purchase licenses with different durations (monthly, yearly, lifetime) as product variations.

**New files:**

- `src/Product/LicensedVariableProduct.php` - Variable product class extending `WC_Product_Variable`
- `src/Product/LicensedProductVariation.php` - Variation class with license settings

**Implemented:**

- New `licensed-variable` product type for selling licenses with different durations
- `LicensedVariableProduct` class extending WooCommerce variable products
- `LicensedProductVariation` class for individual variation license settings
- Variation-specific license duration fields in product edit page (days, max activations)
- Duration labels (Monthly, Quarterly, Yearly, Lifetime) displayed in checkout
- Variation ID tracking in order domain meta for proper license generation
- WooCommerce Blocks checkout updated to handle variations with duration labels

**Modified files:**

- `src/Product/LicensedProductType.php` - Added licensed-variable type registration, variation hooks
- `src/License/LicenseManager.php` - Added `isLicensedProduct()` helper, variation support in `generateLicense()`
- `src/Plugin.php` - Updated license generation to handle variations
- `src/Checkout/CheckoutController.php` - Variation support in domain field rendering
- `src/Checkout/CheckoutBlocksIntegration.php` - Variation data in blocks checkout
- `src/Checkout/StoreApiExtension.php` - Variation ID in Store API schema
- `assets/js/checkout-blocks.js` - Variation handling in React components and DOM fallback

**Technical notes:**

- Variable product type shows in WooCommerce product type selector as "Licensed Variable Product"
- Each variation can override parent's license duration and max activations
- Variations are always virtual (licensed products don't ship)
- `LicensedProductVariation::get_license_duration_label()` returns human-readable duration
- Order meta `_licensed_product_domains` now includes optional `variation_id` field
- License generation uses variation settings when `variation_id` is present in order item
- Backward compatible: existing simple licensed products continue to work unchanged

### 2026-01-26 - Version 0.5.4 - API Compliance

**Overview:**

Bug fix release aligning server implementation with client documentation at `magdev/wc-licensed-product-client`.

**Fixed:**

- `/validate` endpoint now returns HTTP 404 for `license_not_found` error (was returning 403)
- License key validation now enforces minimum 8 characters across all API endpoints

**Implemented:**

- Configurable rate limiting via `WC_LICENSE_RATE_LIMIT` constant (default: 30 requests)
- Configurable rate window via `WC_LICENSE_RATE_WINDOW` constant (default: 60 seconds)
- HTTP status code mapping: 404 for not found, 500 for server errors, 403 for all other errors

**Modified files:**

- `src/Api/RestApiController.php` - Added configurable rate limiting, fixed HTTP status codes, added license_key validation

**Technical notes:**

- Rate limiting now uses `getRateLimit()` and `getRateWindow()` methods instead of constants
- New `getStatusCodeForResult()` method maps error codes to HTTP status codes
- License key validation callback added to all three endpoints (validate, status, activate)
- Uses PHP 8 match expression for status code mapping

### 2026-01-26 - Version 0.5.5 - Critical Signature Fix

**Overview:**

Critical bug fix for response signing. The key derivation algorithm was incompatible with the client library, causing signature verification failures.

**Critical Fix:**

- Key derivation now uses PHP's native `hash_hkdf()` function per RFC 5869
- Previous custom implementation produced different keys than the client library
- Signatures now verify correctly with `magdev/wc-licensed-product-client`

**Additional Fix:**

- Added missing domain validation to `/activate` endpoint (1-255 characters)

**Modified files:**

- `src/Api/ResponseSigner.php` - Fixed key derivation to use `hash_hkdf()`
- `src/Api/RestApiController.php` - Added domain validation to `/activate` endpoint

**Technical notes:**

- Old implementation: `hash_hmac('sha256', $prk . "\x01", $serverSecret)` - custom HKDF-like
- New implementation: `bin2hex(hash_hkdf('sha256', $serverSecret, 32, $licenseKey))` - RFC 5869
- Parameters match client's `ResponseSignature::deriveKey()` exactly:
  - IKM (input keying material): server_secret
  - Length: 32 bytes (256 bits)
  - Info: license_key (context-specific info)
- **Breaking change for existing signatures** - customer secrets will change after upgrade

### 2026-01-27 - Version 0.5.6 - License Settings Tab Visibility Fix

**Overview:**

Fixed License Settings tab visibility for non-licensed product types and updated README with v0.5.x features.

**Bug Fix:**

- License Settings tab now only shows for Licensed Product and Licensed Variable Product types
- Previously the tab was visible on all product types due to CSS `!important` override forcing `display: block`

**Modified files:**

- `assets/css/admin.css` - Changed from `display: block !important` to `display: none` for `.show_if_licensed` and `.show_if_licensed-variable`
- `src/Product/LicensedProductType.php` - Added consolidated `toggleLicensedProductOptions()` JavaScript function
- `README.md` - Updated with complete feature documentation for v0.5.x features

**Technical notes:**

- CSS now hides License Settings tab by default
- JavaScript `toggleLicensedProductOptions()` function shows/hides tab based on product type selector
- Function is called both on page load and on product type change
- README updated with: Variable Licensed Products, Multi-Domain Licensing, Per-License Secrets, Download Statistics, Configurable Rate Limiting

**Release v0.5.6:**

- Created release package: `releases/wc-licensed-product-0.5.6.zip` (1.1 MB)
- SHA256: `4d35a319fe4cb4e7055bae17fc030487ca05e5e9ac905f76d0ac62002bde4336`
- Tagged as `v0.5.6` and pushed to `main` branch

### 2026-01-27 - Version 0.5.7 - Settings UI Cleanup

**Overview:**

Removed redundant "Default" prefix from setting labels on the Default Settings page for cleaner UI.

**Changed:**

- "Max Activations" (was "Default Max Activations")
- "License Validity (Days)" (was "Default License Validity (Days)")
- "Bind to Major Version" (was "Default Bind to Major Version")

**Modified files:**

- `src/Admin/SettingsController.php` - Removed "Default" prefix from three setting labels

**Technical notes:**

- Labels are cleaner since the page section itself is already named "Default Settings"
- No functional changes, purely UI improvement
- Updated all translations (388 strings)

**Release v0.5.7:**

- Created release package: `releases/wc-licensed-product-0.5.7.zip` (856 KB)
- SHA256: `ceb4d57598f576f4f172153ff80df8c180ecd4dca873cf109327fc5ac718930f`
- Tagged as `v0.5.7` and pushed to `main` branch

### 2026-01-27 - Version 0.5.8-0.5.11 - Licensed Variable Product Fixes

**Overview:**

Series of bug fixes for licensed variable products that were showing frontend errors and not displaying properly.

**v0.5.8 - Initial Fix:**

- Fixed critical error on frontend product pages for licensed variable products
- Variable product add-to-cart template now passes required variables (`available_variations`, `attributes`, `selected_attributes`)
- Added JavaScript event listeners for WooCommerce AJAX events to maintain admin variants tab visibility

**v0.5.9 - Null Checks:**

- Added null checks for `get_variation_attributes()`, `get_available_variations()`, and `get_default_attributes()`
- Show informative message instead of error when product has no variations configured
- Changed product type check from `instanceof` to `is_type()` for better compatibility

**v0.5.10 - Product Loading:**

- Re-load product via `wc_get_product()` to ensure correct class instance is used
- Removed overly strict type check that was preventing variations from displaying

**v0.5.11 - Final Fix:**

- **CRITICAL:** Fixed "sold out" message on licensed variable products
- `LicensedVariableProduct::is_purchasable()` now delegates to parent `WC_Product_Variable` class (variable products don't have direct prices - only variations do)
- Fixed `getProductClass()` filter to accept all 4 WooCommerce parameters and use product_id for reliable variation parent detection
- Added fallback to global `$post` when product_id not available

**Modified files:**

- `src/Product/LicensedProductType.php` - Fixed `variableAddToCartTemplate()` and `getProductClass()` methods
- `src/Product/LicensedVariableProduct.php` - Fixed `is_purchasable()` method
- `wc-licensed-product.php` - Version bumps

**Technical notes:**

- WooCommerce `woocommerce_product_class` filter has 4 parameters: `$className`, `$productType`, `$postType`, `$productId`
- Variable products delegate purchasability to their variations - checking `get_price()` on parent is incorrect
- Variation parent detection must use product ID, not global `$post` which may not be set on frontend

**Release v0.5.11:**

- Created release package: `releases/wc-licensed-product-0.5.11.zip` (857 KB)
- SHA256: `32571178bfa8f0d0a03ed05b498d5f9b3c860104393a96732e86a03b6de298d2`
- Committed to `dev` branch

### 2026-01-27 - Version 0.5.12 - Stock Display Fix

**Overview:**

Fixed stock indicator appearing in cart for licensed variable product variations.

**Bug Fix:**

- Fixed "1 in stock" message appearing in cart for licensed variable product variations
- Added multiple WooCommerce filter overrides to suppress stock display

**Modified files:**

- `src/Product/LicensedVariableProduct.php` - Override `get_children()`, `get_variation_attributes()`, `get_variation_prices()`, `get_available_variations()`, `is_type()`
- `src/Product/LicensedProductVariation.php` - Added `get_availability()`, `managing_stock()`, `is_purchasable()` overrides
- `src/Product/LicensedProductType.php` - Added stock-related filter hooks

**Technical notes:**

- `get_children()` uses direct SQL query to bypass WooCommerce's `is_type('variable')` check
- `is_type()` override returns true for both 'licensed-variable' and 'variable' type checks
- Stock filters: `woocommerce_get_availability_text`, `woocommerce_product_get_stock_quantity`, `woocommerce_product_variation_get_stock_quantity`

### 2026-01-27 - Version 0.5.13 - Admin Order License Display Fix

**Overview:**

Fixed licenses not showing in admin order form for licensed-variable products and removed debug logging.

**Bug Fixes:**

- **CRITICAL:** Fixed licenses not appearing in admin order form for orders containing licensed-variable products
- `OrderLicenseController` now uses `LicenseManager::isLicensedProduct()` for consistent product type detection across 4 locations
- Fixed expected licenses calculation for variable product orders
- Fixed manual license generation from admin order page for variable products

**Cleanup:**

- Removed all debug `error_log()` calls from PHP source files
- Removed all debug `console.log()` calls from JavaScript files
- Files cleaned: Plugin.php, CheckoutBlocksIntegration.php, StoreApiExtension.php, CheckoutController.php, checkout-blocks.js

**Modified files:**

- `src/Admin/OrderLicenseController.php` - Use `isLicensedProduct()` in 4 locations
- `src/Plugin.php` - Remove debug logging
- `src/Checkout/CheckoutBlocksIntegration.php` - Remove debug logging
- `src/Checkout/StoreApiExtension.php` - Remove debug logging
- `src/Checkout/CheckoutController.php` - Remove debug logging
- `assets/js/checkout-blocks.js` - Remove debug logging

**Release v0.5.13:**

- Created release package: `releases/wc-licensed-product-0.5.13.zip` (1.0 MB)
- SHA256: `814710ad899529d0015494e4b332eace7d8e55aeda381fdf61f99274c0bf910c`
- Committed to `dev` branch

### 2026-01-27 - Version 0.5.14 - Product Versions Meta Box Fix

**Overview:**

Fixed Product Versions meta box not appearing for licensed-variable products in admin.

**Bug Fixes:**

- Product Versions meta box now always added to product pages, visibility controlled via CSS/JavaScript
- Added `Installer::registerProductTypes()` to create product type terms in the `product_type` taxonomy
- Product type terms are now ensured to exist on `woocommerce_init` hook for existing installations

**Modified files:**

- `src/Admin/VersionAdminController.php` - Simplified `addVersionsMetaBox()` to always add meta box
- `src/Installer.php` - Added `registerProductTypes()` method
- `src/Product/LicensedProductType.php` - Added `ensureProductTypeTermsExist()` hook

**Technical notes:**

- WooCommerce's `WC_Product_Factory::get_product_type()` requires product type terms to exist in the `product_type` taxonomy
- Meta box visibility is controlled via JavaScript based on selected product type
- Taxonomy terms are registered on `woocommerce_init` hook to ensure WooCommerce is fully loaded

### 2026-01-27 - Version 0.5.15 - Tab Rendering Fix

**Overview:**

Fixed tab rendering bug in WooCommerce product edit page when switching to licensed or licensed-variable product types.

**Bug Fixes:**

- Fixed tab rendering issue where License Settings and Variations tabs appeared shifted/overlapping
- Simplified JavaScript to avoid conflicts with WooCommerce's native show/hide logic
- Removed conflicting CSS rule for `.hide_if_licensed` that was causing layout issues
- License Settings tab now uses CSS class toggle (`.wclp-active`) instead of jQuery `.show()/.hide()`
- Variations tab properly shows for licensed-variable products via `woocommerce_product_data_tabs` filter

**Modified files:**

- `src/Product/LicensedProductType.php` - Simplified `toggleOurElements()` JavaScript function, added `show_if_licensed-variable` class to variations tab
- `assets/css/admin.css` - Removed `.hide_if_licensed` rule, updated tab visibility CSS to target `li.licensed_product_options`

**Technical notes:**

- jQuery's `.show()` sets `display: block` which can break `<li>` element layouts in tab lists
- Using CSS class toggle (`addClass/removeClass`) preserves proper display values
- WooCommerce product data tabs use class pattern `{tab_key}_options` (e.g., `licensed_product_options`)
- The `woocommerce_product_data_tabs` filter allows adding classes to existing tabs like variations

**Release v0.5.15:**

- Created release package: `releases/wc-licensed-product-0.5.15.zip` (862 KB)
- SHA256: `47407de49bae4c649644af64e87b44b32fb30eeb2d50890ff8c4bbb741059278`
- Committed to `dev` branch

### 2026-01-27 - Version 0.6.0 - WordPress Auto-Update System

**Overview:**

Major feature release implementing WordPress-style automatic updates. Licensed plugins can now receive updates through WordPress's native plugin update mechanism by checking against the license server.

**New files:**

- `src/Api/UpdateController.php` - Server-side REST API endpoint for update checks
- `src/Update/PluginUpdateChecker.php` - Client-side singleton for WordPress update integration

**Implemented:**

- Server-side `/update-check` REST API endpoint serving WordPress-compatible update information
- Client-side `PluginUpdateChecker` singleton hooking into WordPress's native update system
- Hooks: `pre_set_site_transient_update_plugins`, `plugins_api`, `http_request_args`
- New "Auto-Updates" settings subtab with enable/disable toggle and check frequency
- Configurable cache TTL for update checks (1-168 hours, default: 12)
- Secure download authentication via `X-License-Key` header
- Response signing support for tamper-proof update responses

**Modified files:**

- `src/Plugin.php` - Added UpdateController and PluginUpdateChecker initialization
- `src/Admin/SettingsController.php` - Added 'auto-updates' section with settings
- `openapi.json` - Documented `/update-check` endpoint with request/response schemas
- `languages/*` - Updated translations for new strings

**Settings Controller Changes:**

- Added `'auto-updates'` to `getSections()` for sub-tab navigation
- New `getAutoUpdatesSettings()` method returning enable/frequency settings
- New static methods: `isAutoUpdateEnabled()`, `getUpdateCheckFrequency()`

**UpdateController API:**

- Endpoint: `POST /wp-json/wc-licensed-product/v1/update-check`
- Request: `license_key`, `domain`, `plugin_slug` (optional), `current_version` (optional)
- Response: `update_available`, `version`, `download_url`, `package`, `changelog`, `tested`, `requires`, `requires_php`, etc.
- License validation before serving update info
- Secure download URL generation using existing DownloadController patterns

**PluginUpdateChecker Features:**

- Singleton pattern with `getInstance()`
- Caching via WordPress transients (`wclp_update_info`)
- Automatic cache clearing on settings save
- Only activates when license server URL is configured and not self-licensing
- `forceUpdateCheck()` method for manual refresh

**Configuration:**

To disable auto-updates programmatically:

```php
define('WC_LICENSE_DISABLE_AUTO_UPDATE', true);
```

**Technical notes:**

- Update checker only registers when `SettingsController::getPluginLicenseServerUrl()` returns a value
- Self-licensing detection prevents circular update checks (via `PluginLicenseChecker::isSelfLicensing()`)
- Download URLs include license key in `X-License-Key` header for server-side verification
- Uses Symfony HttpClient for server requests with 15s timeout
- Cache TTL configurable from 1-168 hours in settings
- OpenAPI spec updated to version 0.6.0 with full `/update-check` documentation

**Release v0.6.0:**

- Created release package: `releases/wc-licensed-product-0.6.0.zip` (1.1 MB)
- SHA256: `171c8195c586b3b20bac4a806e2d698cdaaf15966e2fd6e1670ec39dac8ab027`
- Tagged as `v0.6.0` and pushed to `main` branch

### 2026-01-27 - Version 0.6.1 - UI Improvements & Bug Fixes

**Overview:**

Bug fix and improvement release addressing admin license testing, auto-update settings, and customer license filtering.

**Implemented:**

- Filter functionality on customer account licenses page (filter by product or domain)
- Split auto-update settings into "Enable Update Notifications" and "Automatically Install Updates"
- WordPress `auto_update_plugin` filter integration for automatic installation

**Bug Fixes:**

- Fixed admin license test popup showing empty product field
- Removed version field from test popup (version_id is only set for version-bound licenses)
- `handleAjaxTestLicense()` now enriches response with product name

**Modified files:**

- `src/Admin/AdminController.php` - Enriched test license response with product name
- `src/Admin/SettingsController.php` - Split auto-update settings, added static helper methods
- `src/Update/PluginUpdateChecker.php` - Added `auto_update_plugin` filter, use new settings methods
- `src/Frontend/AccountController.php` - Added filter functionality with `applyLicenseFilters()` method
- `templates/frontend/licenses.html.twig` - Added filter form with product and domain dropdowns
- `templates/admin/licenses.html.twig` - Removed version row from test license modal
- `assets/css/frontend.css` - Added responsive styles for filter form
- `languages/*` - Updated all translation files

**New methods in SettingsController:**

- `isUpdateNotificationEnabled()` - Check if update notifications are enabled
- `isAutoInstallEnabled()` - Check if auto-install is enabled (requires notifications enabled)

**New methods in AccountController:**

- `applyLicenseFilters()` - Filter licenses by product ID and/or domain
- `getFilterOptions()` - Get unique products and domains for filter dropdowns

**Technical notes:**

- Filter form uses GET parameters: `filter_product` and `filter_domain`
- Auto-install setting is disabled (greyed out) when update notifications are disabled
- License test popup now only shows Product and Expires fields (version removed)
- Domain filter uses case-insensitive partial matching via `stripos()`

**Dependency Updates:**

- Updated `magdev/wc-licensed-product-client` from v0.2.0 to v0.2.1

### 2026-01-28 - Version 0.7.0 - Security Hardening

**Overview:**

Security-focused release with comprehensive audit and hardening. Performed OWASP Top 10 testing against live shop (shop.magdev.cc) and fixed identified vulnerabilities.

**Security Audit Results:**

- SQL injection: Protected (prepared statements throughout)
- CSRF: Protected (nonce verification on all forms/AJAX)
- Rate limiting: Working (429 responses after ~28 requests)
- Access control: Working (403 for unauthorized access)
- HTTPS: Enforced with proper redirect
- Missing security headers noted: X-Frame-Options, HSTS, CSP (server-level configuration)

**Critical Fixes:**

- **XSS in checkout-blocks.js**: Replaced `innerHTML` template literals with safe DOM construction using `document.createElement()` and `textContent`
- **IP Detection**: UpdateController was using raw `$_SERVER['REMOTE_ADDR']` without proxy support - now uses shared `IpDetectionTrait`

**New Files:**

- `src/Api/IpDetectionTrait.php` - Shared IP detection with proxy header support (Cloudflare, X-Forwarded-For, X-Real-IP)
- `src/Common/RateLimitTrait.php` - Reusable rate limiting for frontend operations

**Security Enhancements:**

- Added rate limiting to license transfers (5/hour per user)
- Added rate limiting to file downloads (30/hour per user)
- Added CSV import limits: 2MB max file size, 1000 max rows, 5-minute cooldown
- Added JSON error handling in StoreApiExtension
- Added license ID validation in frontend.js to prevent selector injection

**Modified Files:**

- `assets/js/checkout-blocks.js` - XSS-safe DOM construction
- `assets/js/frontend.js` - Added `sanitizeForSelector()` helper
- `src/Api/RestApiController.php` - Use IpDetectionTrait, remove duplicate methods
- `src/Api/UpdateController.php` - Use IpDetectionTrait for rate limiting
- `src/Admin/AdminController.php` - CSV import security limits
- `src/Frontend/AccountController.php` - Transfer rate limiting
- `src/Frontend/DownloadController.php` - Download rate limiting
- `src/Checkout/StoreApiExtension.php` - JSON error handling

**Technical Notes:**

- IpDetectionTrait supports `WC_LICENSE_TRUSTED_PROXIES` constant for proxy configuration
- RateLimitTrait uses WordPress transients with user ID-based keys
- CSV import constants: `MAX_IMPORT_FILE_SIZE = 2097152`, `MAX_IMPORT_ROWS = 1000`, `IMPORT_RATE_LIMIT_WINDOW = 300`

**Release v0.7.0:**

- Created release package: `releases/wc-licensed-product-0.7.0.zip` (883 KB)
- SHA256: `12f8452316e350273003f36bf6d7b7121a7bedc9a6964c3d0732d26318d94c18`
- Tagged as `v0.7.0` and pushed to `main` branch

### 2026-01-28 - Version 0.7.1 - Bug Fixes & Client Compatibility

**Overview:**

Bug fix release ensuring compatibility with updated `magdev/wc-licensed-product-client` v0.2.2 and fixing API Verification Secret display.

**Bug Fixes:**

- **CRITICAL:** Fixed API Verification Secret not displaying on customer account licenses page when using PHP fallback (Twig unavailable)
- Fixed `/update-check` endpoint responses not being signed (missing from `ResponseSigner::shouldSign()`)

**Dependency Updates:**

- Updated `magdev/wc-licensed-product-client` from `760e1e7` to `56abe8a` (v0.2.2)
- Updated `symfony/http-client` from v7.4.4 to v7.4.5

**Modified files:**

- `src/Frontend/AccountController.php` - Added customer secret display to PHP fallback method `displayLicensesFallback()`
- `src/Api/ResponseSigner.php` - Added `/update-check` to `shouldSign()` method

**Technical notes:**

- PHP fallback template now includes the collapsible API Verification Secret section matching the Twig template
- All four API endpoints (`/validate`, `/status`, `/activate`, `/update-check`) now include signature headers when `WC_LICENSE_SERVER_SECRET` is configured
- Client library v0.2.2 verified compatible with server implementation

**Release v0.7.1:**

- Created release package: `releases/wc-licensed-product-0.7.1.zip` (886 KB)
- SHA256: `6ffd0bdf47395436bbc28a029eff4c6d065f2b5b64c687b96ae36a74c3ee34ef`
- Tagged as `v0.7.1` and pushed to `main` branch

### 2026-01-29 - Version 0.7.2 - Git Submodule & CI/CD Pipeline

**Overview:**

Infrastructure release converting the client library dependency to a git submodule and implementing automated CI/CD releases via Gitea Actions.

**Git Submodule Migration:**

- Converted `magdev/wc-licensed-product-client` from Composer VCS dependency to git submodule
- Submodule located at `lib/wc-licensed-product-client`
- Composer uses `path` type repository pointing to local submodule
- Symlinked to `vendor/magdev/wc-licensed-product-client` during `composer install`

**Gitea CI/CD Pipeline:**

- New workflow at `.gitea/workflows/release.yml`
- Triggers on version tags (`v*`)
- Automated steps:
  - Checkout with recursive submodules
  - PHP 8.3 setup with required extensions
  - Composer dependency installation (production only)
  - Translation compilation (`.po` to `.mo`)
  - Version verification against plugin header
  - Release package creation with proper exclusions
  - SHA256 checksum generation
  - Package structure verification
  - Changelog extraction for release notes
  - Gitea release creation with asset upload
  - Pre-release detection for hyphenated tags

**New files:**

- `.gitea/workflows/release.yml` - Gitea Actions workflow for automated releases
- `.gitmodules` - Git submodule configuration (created by git)

**Modified files:**

- `composer.json` - Changed repository type from `vcs` to `path`, URL to `lib/wc-licensed-product-client`
- `CHANGELOG.md` - Added v0.7.2 release notes
- `CLAUDE.md` - Removed v0.7.2 from roadmap, added session history

**Package Exclusions:**

Release packages exclude: `.git/`, `.gitea/`, `.gitmodules`, `lib/` (submodule source), `vendor/**/.git`, `tests/`, `CLAUDE.md`, `*.po~`, `wp-core`, `wp-plugins`, `composer.lock`

**Developer Workflow Changes:**

After cloning the repository, developers must now run:

```bash
git submodule update --init --recursive
composer install
```

**Technical notes:**

- Path repository uses `*` version constraint with `symlink: false` option
- CI replaces symlink with actual files via `cp -r` before packaging
- CI uses `actions/checkout@v4` with `submodules: recursive` for proper submodule initialization
- Release creation uses direct Gitea API calls (`/api/v1/repos/.../releases`)
- Requires `SRC_GITEA_TOKEN` secret configured in Gitea repository settings
- Workflow completed successfully: 57 seconds, all checks passed

**Release v0.7.2:**

- Automatically created by Gitea Actions CI/CD pipeline
- Release package: 881 KiB with SHA256 checksum
- First automated release - all future releases will use this workflow

**Additional fixes (same session):**

- Updated README.md with Auto-Updates section and Development section
- Fixed CI/CD workflow to handle existing releases (delete before recreate)
- When updating a tag, the workflow now checks for existing releases and deletes them first

**Lessons learned:**

- Gitea releases persist even when their tag is deleted - must delete release via API
- Composer `symlink: false` doesn't always work - CI must manually replace symlinks with `cp -r`
- Never create zip archives locally on this machine (fills up RAM indefinitely)
- Gitea API endpoint for releases by tag: `GET /api/v1/repos/{owner}/{repo}/releases/tags/{tag}`

### 2026-02-01 - Bug Fix: API Verification Secret Not Visible

**Overview:**

Fixed the "API Verification Secret" (customer secret) not appearing on the customer account licenses page in Docker environments.

**Root Cause:**

The `WC_LICENSE_SERVER_SECRET` constant was not being defined even though the environment variable was set. In Docker WordPress setups using `wp-config-docker.php`, the `getenv_docker()` function retrieves values from environment variables, but the constant wasn't being created properly. The plugin was only checking for the PHP constant, not the environment variable directly.

**Fix:**

Added `ResponseSigner::getServerSecret()` static method that checks multiple sources for the server secret:

1. `WC_LICENSE_SERVER_SECRET` constant (standard WordPress configuration)
2. `getenv('WC_LICENSE_SERVER_SECRET')` (Docker environments)
3. `$_ENV['WC_LICENSE_SERVER_SECRET']` (some PHP configurations)
4. `$_SERVER['WC_LICENSE_SERVER_SECRET']` (fallback)

**Modified files:**

- `src/Api/ResponseSigner.php` - Added `getServerSecret()` method, updated `isSigningEnabled()` and `getCustomerSecretForLicense()` to use it
- `src/Plugin.php` - Updated to use `ResponseSigner::isSigningEnabled()` instead of direct constant check

**Technical notes:**

- The fix maintains backward compatibility with standard WordPress installations using constants
- Docker environments can now use environment variables directly without needing the constant to be defined
- All three methods (`isSigningEnabled()`, `getCustomerSecretForLicense()`, and constructor) now use the centralized `getServerSecret()` method

### 2026-02-03 - Version 0.7.4 - Prometheus Metrics Integration

**Overview:**

Added Prometheus metrics integration to expose license and API metrics for monitoring. Requires the WP Prometheus plugin.

**New files:**

- `src/Metrics/PrometheusController.php` - Prometheus metrics collection controller

**Implemented:**

- New "Metrics" settings section with enable/disable toggle
- Hooks into `wp_prometheus_collect_metrics` action for metric collection
- License gauges using existing `LicenseManager::getStatistics()`:
  - `wclp_licenses_total{status}` - License counts by status
  - `wclp_licenses_lifetime_total` - Lifetime licenses count
  - `wclp_licenses_expiring_total` - Expiring licenses count
  - `wclp_licenses_expiring_soon` - Licenses expiring within 30 days
- Download gauges using existing `VersionManager::getDownloadStatistics()`:
  - `wclp_downloads_total` - Total downloads
  - `wclp_versions_active_total` - Active product versions
- API counters (stored in WordPress options for persistence):
  - `wclp_api_requests_total{endpoint,result}` - API requests by endpoint and result
  - `wclp_rate_limit_exceeded_total{endpoint}` - Rate limit exceeded events
  - `wclp_validation_errors_total{error_type}` - Validation errors by type

**Modified files:**

- `src/Admin/SettingsController.php` - Added 'metrics' section with settings
- `src/Api/RestApiController.php` - Added metric tracking for API requests
- `src/Api/UpdateController.php` - Added metric tracking for update-check requests
- `src/Plugin.php` - Initialize PrometheusController

**Technical notes:**

- Metrics are only collected when enabled via settings toggle
- Static methods allow increment from API controllers without dependency injection
- Counter values persist across requests via `wclp_prometheus_counters` option
- Gauges query database on each metric collection (uses existing statistics methods)
