Implement version 0.0.2 features

Add product version management:
- ProductVersion model and VersionManager class
- VersionAdminController with meta box on product edit page
- AJAX-based version CRUD (add, delete, toggle status)
- JavaScript for version management UI

Add email notifications:
- LicenseEmailController for order emails
- License keys included in order completed emails
- Support for both HTML and plain text emails

Add REST API rate limiting:
- 30 requests per minute per IP
- Cloudflare and proxy-aware IP detection
- HTTP 429 response with Retry-After header

Other changes:
- Bump version to 0.0.2
- Update CHANGELOG.md
- Add version status styles to admin.css

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 19:15:19 +01:00
parent 82c18633a1
commit dec4bd609b
10 changed files with 1269 additions and 4 deletions

181
assets/js/versions.js Normal file
View File

@@ -0,0 +1,181 @@
/**
* WC Licensed Product - Version Management
*
* @package Jeremias\WcLicensedProduct
*/
(function($) {
'use strict';
var WCLicensedProductVersions = {
init: function() {
this.bindEvents();
},
bindEvents: function() {
$('#add-version-btn').on('click', this.addVersion);
$(document).on('click', '.delete-version-btn', this.deleteVersion);
$(document).on('click', '.toggle-version-btn', this.toggleVersion);
},
addVersion: function(e) {
e.preventDefault();
var $btn = $(this);
var $spinner = $btn.siblings('.spinner');
var productId = $btn.data('product-id');
var version = $('#new_version').val().trim();
var downloadUrl = $('#new_download_url').val().trim();
var releaseNotes = $('#new_release_notes').val().trim();
// Validate version
if (!version) {
alert(wcLicensedProductVersions.strings.versionRequired);
return;
}
if (!/^\d+\.\d+\.\d+$/.test(version)) {
alert(wcLicensedProductVersions.strings.versionInvalid);
return;
}
$btn.prop('disabled', true);
$spinner.addClass('is-active');
$.ajax({
url: wcLicensedProductVersions.ajaxUrl,
type: 'POST',
data: {
action: 'wc_licensed_product_add_version',
nonce: wcLicensedProductVersions.nonce,
product_id: productId,
version: version,
download_url: downloadUrl,
release_notes: releaseNotes
},
success: function(response) {
if (response.success) {
// Remove "no versions" row if present
$('#versions-table tbody .no-versions').remove();
// Add new row to table
$('#versions-table tbody').prepend(response.data.html);
// Clear form
$('#new_version').val('');
$('#new_download_url').val('');
$('#new_release_notes').val('');
} else {
alert(response.data.message || wcLicensedProductVersions.strings.error);
}
},
error: function() {
alert(wcLicensedProductVersions.strings.error);
},
complete: function() {
$btn.prop('disabled', false);
$spinner.removeClass('is-active');
}
});
},
deleteVersion: function(e) {
e.preventDefault();
if (!confirm(wcLicensedProductVersions.strings.confirmDelete)) {
return;
}
var $btn = $(this);
var $row = $btn.closest('tr');
var versionId = $btn.data('version-id');
$btn.prop('disabled', true);
$.ajax({
url: wcLicensedProductVersions.ajaxUrl,
type: 'POST',
data: {
action: 'wc_licensed_product_delete_version',
nonce: wcLicensedProductVersions.nonce,
version_id: versionId
},
success: function(response) {
if (response.success) {
$row.fadeOut(300, function() {
$(this).remove();
// Show "no versions" message if table is empty
if ($('#versions-table tbody tr').length === 0) {
$('#versions-table tbody').append(
'<tr class="no-versions"><td colspan="6">' +
'No versions found. Add your first version above.' +
'</td></tr>'
);
}
});
} else {
alert(response.data.message || wcLicensedProductVersions.strings.error);
$btn.prop('disabled', false);
}
},
error: function() {
alert(wcLicensedProductVersions.strings.error);
$btn.prop('disabled', false);
}
});
},
toggleVersion: function(e) {
e.preventDefault();
var $btn = $(this);
var $row = $btn.closest('tr');
var versionId = $btn.data('version-id');
var currentlyActive = $btn.data('active') === 1 || $btn.data('active') === '1';
$btn.prop('disabled', true);
$.ajax({
url: wcLicensedProductVersions.ajaxUrl,
type: 'POST',
data: {
action: 'wc_licensed_product_toggle_version',
nonce: wcLicensedProductVersions.nonce,
version_id: versionId,
currently_active: currentlyActive ? 1 : 0
},
success: function(response) {
if (response.success) {
var isActive = response.data.isActive;
var $status = $row.find('.version-status');
// Update status badge
$status
.removeClass('version-status-active version-status-inactive')
.addClass('version-status-' + (isActive ? 'active' : 'inactive'))
.text(isActive ? 'Active' : 'Inactive');
// Update button
$btn
.data('active', isActive ? 1 : 0)
.text(isActive ? 'Deactivate' : 'Activate');
} else {
alert(response.data.message || wcLicensedProductVersions.strings.error);
}
},
error: function() {
alert(wcLicensedProductVersions.strings.error);
},
complete: function() {
$btn.prop('disabled', false);
}
});
}
};
$(document).ready(function() {
WCLicensedProductVersions.init();
});
})(jQuery);