Implement version 0.0.3 features

- Add file attachment support for product versions (Media Library)
- Add version auto-detection from uploaded filenames
- Implement secure customer downloads with hash verification
- Add license key copy-to-clipboard functionality
- Redesign customer licenses page with card-based UI
- Fix product versions meta box visibility for non-licensed types
- Add DownloadController for secure file delivery
- Update CLAUDE.md roadmap and session history

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 19:46:50 +01:00
parent 41e5f8d467
commit 78e43b9aea
15 changed files with 1036 additions and 133 deletions

View File

@@ -61,12 +61,6 @@ final class VersionAdminController
*/
public function renderVersionsMetaBox(\WP_Post $post): void
{
$product = wc_get_product($post->ID);
if (!$product || !$product->is_type('licensed')) {
echo '<p>' . esc_html__('This meta box is only available for Licensed Products.', 'wc-licensed-product') . '</p>';
return;
}
$versions = $this->versionManager->getVersionsByProduct($post->ID);
wp_nonce_field('wc_licensed_product_versions', 'wc_licensed_product_versions_nonce');
?>
@@ -82,10 +76,24 @@ final class VersionAdminController
</td>
</tr>
<tr>
<th><label for="new_download_url"><?php esc_html_e('Download URL', 'wc-licensed-product'); ?></label></th>
<th><label for="new_attachment_id"><?php esc_html_e('Download File', 'wc-licensed-product'); ?></label></th>
<td>
<input type="hidden" id="new_attachment_id" name="new_attachment_id" value="" />
<span id="selected_file_name" class="selected-file-name"></span>
<button type="button" class="button" id="upload-version-file-btn">
<?php esc_html_e('Select File', 'wc-licensed-product'); ?>
</button>
<button type="button" class="button" id="remove-version-file-btn" style="display: none;">
<?php esc_html_e('Remove', 'wc-licensed-product'); ?>
</button>
<p class="description"><?php esc_html_e('Upload or select a file from the media library. Version will be auto-detected from filename (e.g., plugin-v1.2.3.zip).', 'wc-licensed-product'); ?></p>
</td>
</tr>
<tr>
<th><label for="new_download_url"><?php esc_html_e('Or External URL', 'wc-licensed-product'); ?></label></th>
<td>
<input type="url" id="new_download_url" name="new_download_url" class="large-text" placeholder="https://" />
<p class="description"><?php esc_html_e('URL to the downloadable file for this version.', 'wc-licensed-product'); ?></p>
<p class="description"><?php esc_html_e('Alternative: Enter an external download URL instead of uploading a file.', 'wc-licensed-product'); ?></p>
</td>
</tr>
<tr>
@@ -110,7 +118,7 @@ final class VersionAdminController
<thead>
<tr>
<th><?php esc_html_e('Version', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Download URL', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Download File', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Released', 'wc-licensed-product'); ?></th>
@@ -127,12 +135,19 @@ final class VersionAdminController
<tr data-version-id="<?php echo esc_attr($version->getId()); ?>">
<td><strong><?php echo esc_html($version->getVersion()); ?></strong></td>
<td>
<?php if ($version->getDownloadUrl()): ?>
<a href="<?php echo esc_url($version->getDownloadUrl()); ?>" target="_blank">
<?php echo esc_html(wp_basename($version->getDownloadUrl())); ?>
<?php
$effectiveUrl = $version->getEffectiveDownloadUrl();
$filename = $version->getDownloadFilename();
if ($effectiveUrl):
?>
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
</a>
<?php if ($version->getAttachmentId()): ?>
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
<?php endif; ?>
<?php else: ?>
<em><?php esc_html_e('No download URL', 'wc-licensed-product'); ?></em>
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
<?php endif; ?>
</td>
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
@@ -173,6 +188,9 @@ final class VersionAdminController
return;
}
// Enqueue WordPress media uploader
wp_enqueue_media();
wp_enqueue_script(
'wc-licensed-product-versions',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/versions.js',
@@ -189,6 +207,8 @@ final class VersionAdminController
'versionRequired' => __('Please enter a version number.', 'wc-licensed-product'),
'versionInvalid' => __('Please enter a valid version number (e.g., 1.0.0).', 'wc-licensed-product'),
'error' => __('An error occurred. Please try again.', 'wc-licensed-product'),
'selectFile' => __('Select Download File', 'wc-licensed-product'),
'useThisFile' => __('Use this file', 'wc-licensed-product'),
],
]);
@@ -215,6 +235,7 @@ final class VersionAdminController
$version = sanitize_text_field($_POST['version'] ?? '');
$downloadUrl = esc_url_raw($_POST['download_url'] ?? '');
$releaseNotes = sanitize_textarea_field($_POST['release_notes'] ?? '');
$attachmentId = absint($_POST['attachment_id'] ?? 0);
if (!$productId || !$version) {
wp_send_json_error(['message' => __('Product ID and version are required.', 'wc-licensed-product')]);
@@ -234,7 +255,8 @@ final class VersionAdminController
$productId,
$version,
$releaseNotes ?: null,
$downloadUrl ?: null
$downloadUrl ?: null,
$attachmentId ?: null
);
if (!$newVersion) {
@@ -317,12 +339,19 @@ final class VersionAdminController
<tr data-version-id="<?php echo esc_attr($version->getId()); ?>">
<td><strong><?php echo esc_html($version->getVersion()); ?></strong></td>
<td>
<?php if ($version->getDownloadUrl()): ?>
<a href="<?php echo esc_url($version->getDownloadUrl()); ?>" target="_blank">
<?php echo esc_html(wp_basename($version->getDownloadUrl())); ?>
<?php
$effectiveUrl = $version->getEffectiveDownloadUrl();
$filename = $version->getDownloadFilename();
if ($effectiveUrl):
?>
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
</a>
<?php if ($version->getAttachmentId()): ?>
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
<?php endif; ?>
<?php else: ?>
<em><?php esc_html_e('No download URL', 'wc-licensed-product'); ?></em>
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
<?php endif; ?>
</td>
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>