- Remove legacy email template files - Use WooCommerce's email-header.php and email-footer.php templates - Render email content inline for consistent WooCommerce styling - Remove template location filter from LicenseEmailController Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
19 KiB
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
No known bugs at this time.
Version 0.0.9
Note for AI Assistants: Cleanup the versions in this section after implementation, but keep this notice!
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
ABSPATHcheck
Translation Ready
All user-facing strings use:
__('Text to translate', 'wc-licensed-product')
_e('Text to translate', 'wc-licensed-product')
Text domain: wc-licensed-product
Translation Template
- Base
.potfile 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:
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.0before building release packages - Run
composer update --no-dev --optimize-autoloaderto rebuild dependencies
- Use
- 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 atwc-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
- Run zip from the
- 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
- Always use
- Exclusion patterns must match the relative path structure used in zip command
- Always verify the package structure with
unzip -lbefore 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)
- Check all files are prefixed with
- 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.mdfile - 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:
- Plugin header comment
Version: x.x.x(line ~6 in wc-licensed-product.php) - WordPress uses THIS for admin display - 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
- Plugin header comment
Important Git Notes:
- Default branch while development is
dev - Create releases from branch
mainafter merging branchdev - 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.jsonchanges 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(butvendor/is included)
For AI Assistants:
When starting a new session on this project:
- Read this CLAUDE.md file first
- Semantic versioning follows the
MAJOR.MINOR.BUGFIXpattern - Check git log for recent changes
- Verify you're on the
devbranch before making changes - Run
composer installif vendor/ is missing - Test changes before committing
- Follow commit message format with Claude Code attribution
- Update this session history section with learnings
- Never commit backup files (
*.po~,*.bak, etc.) - checkgit statusbefore committing - 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:
-
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)
-
-
MD056 - Table column count: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths.
-
MD009 - No trailing spaces: Remove trailing whitespace from lines
-
MD012 - No multiple consecutive blank lines: Use only single blank lines between sections
-
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, usetxt. -
MD032 - Lists should be surrounded by blank lines: Add a blank line before AND after list blocks, including after bold labels like
**Attributes:**. -
MD034 - Bare URLs: Wrap URLs in angle brackets (e.g.,
<https://example.com>) or use markdown link syntax[text](url). -
Author section formatting: Use a heading (
### Name) instead of bold (**Name**) for the author name to maintain consistent document structure.
Project Architecture
Directory Structure
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 - license validation endpoints
│ ├── Checkout/ # CheckoutController - domain field at checkout
│ ├── Frontend/ # AccountController - customer licenses page
│ ├── License/ # License model and LicenseManager
│ └── Product/ # LicensedProduct type and LicensedProductType
├── 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 |
Full API documentation available in openapi.json (OpenAPI 3.1 specification).
Key Classes
Plugin(singleton) - Main controller, initializes all componentsInstaller- Database setup, activation/deactivation hooksLicenseManager- License CRUD, validation, key generationLicense- Entity model with status constantsLicensedProduct- Extends WC_Product for licensed productsLicensedProductType- 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 modelVersionManager- Version CRUD operationsVersionAdminController- Admin UI for versionsLicenseEmailController- 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_idcolumn added to versions table - ProductVersion model extended with
getEffectiveDownloadUrl()andgetDownloadFilename() - 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_versionscontainer)
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 dayssetLicenseLifetime()- Remove expiration (set to lifetime)bulkUpdateStatus()- Bulk update license statusbulkDelete()- Bulk delete licensesbulkExtend()- 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
/deactivateAPI endpoint (admin-only operation now)
New methods in LicenseManager:
transferLicense()- Transfer license to new domaingetStatistics()- Get comprehensive license statisticsexportLicensesForCsv()- Export all licenses for CSV download
New files:
templates/admin/dashboard.html.twig- Dashboard templateopenapi.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 daysmarkExpirationNotified()- Track expiration notification statewasExpirationNotified()- Check if notification already sentimportLicense()- Create license from imported data
Updated controllers:
AdminController- Added Reports integration, search/filter handling, CSV importAccountController- Added customer transfer AJAX handler with domain validationLicenseEmailController- Added cron scheduling and expiration warning emailsInstaller- 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_reportsfilter - 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()andget_major_version()now query VersionManagerLicensedProductType- Removed Current Version field from product data panelVersionAdminController- Removed automatic update of_licensed_current_versionmetaLicenseEmailController- Registers WooCommerce email class, uses WC email system for sendingSettingsController- Updated email section to link to WooCommerce email settings
Technical notes:
- Email uses WooCommerce's
emails/email-header.phpandemails/email-footer.phptemplates - 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