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

@@ -22,6 +22,7 @@ class ProductVersion
private int $patchVersion;
private ?string $releaseNotes;
private ?string $downloadUrl;
private ?int $attachmentId;
private bool $isActive;
private \DateTimeInterface $releasedAt;
private \DateTimeInterface $createdAt;
@@ -40,6 +41,7 @@ class ProductVersion
$version->patchVersion = (int) $data['patch_version'];
$version->releaseNotes = $data['release_notes'] ?: null;
$version->downloadUrl = $data['download_url'] ?: null;
$version->attachmentId = !empty($data['attachment_id']) ? (int) $data['attachment_id'] : null;
$version->isActive = (bool) $data['is_active'];
$version->releasedAt = new \DateTimeImmutable($data['released_at']);
$version->createdAt = new \DateTimeImmutable($data['created_at']);
@@ -60,6 +62,36 @@ class ProductVersion
];
}
/**
* Extract version from filename
*
* Supports patterns like:
* - product-name-v1.2.3.zip
* - product-name-1.2.3.zip
* - product_v1.2.3.zip
* - v1.2.3.zip
*/
public static function extractVersionFromFilename(string $filename): ?string
{
// Remove extension
$basename = pathinfo($filename, PATHINFO_FILENAME);
// Try various patterns
$patterns = [
'/[_-]v?(\d+\.\d+\.\d+)$/i', // ends with -v1.2.3 or -1.2.3
'/[_-]v?(\d+\.\d+\.\d+)[_-]/i', // contains -v1.2.3- or -1.2.3-
'/^v?(\d+\.\d+\.\d+)$/i', // just version number
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $basename, $matches)) {
return $matches[1];
}
}
return null;
}
public function getId(): int
{
return $this->id;
@@ -100,6 +132,36 @@ class ProductVersion
return $this->downloadUrl;
}
public function getAttachmentId(): ?int
{
return $this->attachmentId;
}
/**
* Get the effective download URL (from attachment or direct URL)
*/
public function getEffectiveDownloadUrl(): ?string
{
if ($this->attachmentId) {
return wp_get_attachment_url($this->attachmentId) ?: null;
}
return $this->downloadUrl;
}
/**
* Get the filename for download
*/
public function getDownloadFilename(): ?string
{
if ($this->attachmentId) {
return wp_basename(get_attached_file($this->attachmentId) ?: '');
}
if ($this->downloadUrl) {
return wp_basename($this->downloadUrl);
}
return null;
}
public function isActive(): bool
{
return $this->isActive;
@@ -129,6 +191,7 @@ class ProductVersion
'patch_version' => $this->patchVersion,
'release_notes' => $this->releaseNotes,
'download_url' => $this->downloadUrl,
'attachment_id' => $this->attachmentId,
'is_active' => $this->isActive,
'released_at' => $this->releasedAt->format('Y-m-d H:i:s'),
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),