14 Commits

Author SHA1 Message Date
41e46fc7b8 Bump version to 0.5.2 and update changelog
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:31:53 +01:00
549a58dc5d Add per-license customer secrets for API response verification
- Add static methods to ResponseSigner for deriving customer-specific secrets
- Display "API Verification Secret" in customer account licenses page
- Add collapsible secret section with copy button
- Update server-implementation.md with per-license secret documentation
- Update translations with new strings

Each customer now gets a unique verification secret derived from their
license key, eliminating the need to share the master server secret.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:29:57 +01:00
7d02105284 Update CLAUDE.md with v0.5.1 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:20:35 +01:00
2207efbc52 Add release package v0.5.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:12:43 +01:00
3fe173686b Bump version to 0.5.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:10:23 +01:00
86b5bdb075 Fix version sorting and license actions visibility
- Sort product versions by version DESC when adding via AJAX
- Make license actions always visible in admin overview

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:09:42 +01:00
c6d6269ee3 Update translations for v0.5.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 15:09:25 +01:00
75f1dabdb4 Add roadmap placeholder sections for next versions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 18:38:31 +01:00
8acde7cadd Update CLAUDE.md with v0.5.0 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 18:37:28 +01:00
c45816b491 Add release package v0.5.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 18:35:26 +01:00
bcabf8feb2 Bump version to 0.5.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 18:32:24 +01:00
83836d69af Implement multi-domain licensing for v0.5.0
- Add multi-domain checkout support for WooCommerce Blocks
- Fix domain field rendering using ExperimentalOrderMeta slot
- Add DOM injection fallback for checkout field rendering
- Update translations with new multi-domain strings (de_CH)
- Update email templates for grouped license display
- Refactor account page to group licenses by product/order

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 18:31:36 +01:00
550a84beb9 Update CLAUDE.md with v0.4.0 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 17:01:50 +01:00
7d48028f62 Add release package v0.4.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 16:45:33 +01:00
30 changed files with 3022 additions and 834 deletions

View File

@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.5.2] - 2026-01-26
### Added
- Per-license customer secrets for API response verification
- "API Verification Secret" section in customer account licenses page (collapsible)
- Copy button for customer secrets with clipboard support
- Documentation for per-license secret derivation and usage
### Security
- Customers no longer need the master server secret for signature verification
- Each license key has a unique derived secret using HKDF-like key derivation
- If one customer's secret is compromised, other customers remain unaffected
### Changed
- Updated `ResponseSigner` with static methods for secret derivation
- Updated `server-implementation.md` with per-license secret documentation
- Added new translation strings for secret-related UI
## [0.5.1] - 2026-01-26
### Fixed
- Product versions now sort correctly by version DESC when added via AJAX in admin
- License actions in admin overview are now always visible instead of only on hover
### Changed
- Added `compareVersions()` JavaScript function for proper semantic version comparison
- Updated CSS with `!important` to override WordPress default hover-only behavior for row actions
## [0.5.0] - 2026-01-25
### Added
- Multi-domain licensing support: Customers can now purchase multiple licenses for different domains in a single order
- Each cart item quantity requires a unique domain at checkout
- New "Enable Multi-Domain Licensing" setting in WooCommerce > Settings > Licensed Products
- Multi-domain checkout UI for WooCommerce Blocks checkout
- DOM injection fallback for checkout domain fields when React component fails to render
- Grouped license display in customer account page by product/order
- "Older versions" collapsible section in customer download area
- Updated email templates to show licenses grouped by product
### Changed
- Customer account licenses page now shows licenses grouped by product package
- Order meta now stores `_licensed_product_domains` array for multi-domain orders
- Updated translations with 19 new strings for multi-domain functionality (de_CH)
- Refactored checkout blocks JavaScript to use ExperimentalOrderMeta slot pattern
### Technical Details
- `CheckoutBlocksIntegration` now uses `registerPlugin` with `woocommerce-checkout` scope
- `StoreApiExtension` handles both single-domain and multi-domain data formats
- `CheckoutController` validates unique domains per product in multi-domain mode
- `AccountController` groups licenses by product for package-style display
- Backward compatible: existing single-domain orders continue to work
## [0.4.0] - 2026-01-24 ## [0.4.0] - 2026-01-24
### Added ### Added

142
CLAUDE.md
View File

@@ -32,13 +32,13 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**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. **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 ### Version 0.5.2
No known bugs at the moment. *No planned bugfixes yet.*
### Version 0.4.0 ### Version 0.6.0
- Self-licensing prevention: Plugin automatically bypasses license validation when the configured license server URL points to the same WordPress installation (prevents circular dependency) *No planned features yet.*
## Technical Stack ## Technical Stack
@@ -1159,3 +1159,137 @@ Fixed a critical bug where licenses were not generated for orders created manual
- Created release package: `releases/wc-licensed-product-0.3.9.zip` (851 KB) - Created release package: `releases/wc-licensed-product-0.3.9.zip` (851 KB)
- SHA256: `fdb65200c368da380df0cabb3c6ac6419d5b4731cd528f630f9b432a3ba5c586` - SHA256: `fdb65200c368da380df0cabb3c6ac6419d5b4731cd528f630f9b432a3ba5c586`
- Tagged as `v0.3.9` and pushed to `main` branch - 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

View File

@@ -201,7 +201,8 @@ code.file-hash {
} }
.licenses-table .row-actions { .licenses-table .row-actions {
visibility: visible; visibility: visible !important;
position: static !important;
padding: 2px 0 0; padding: 2px 0 0;
} }

View File

@@ -37,13 +37,196 @@
color: #383d41; color: #383d41;
} }
/* License Cards */ /* License Packages */
.woocommerce-licenses { .woocommerce-licenses {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5em; gap: 1.5em;
} }
.license-package {
border: 1px solid #e5e5e5;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.package-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1em 1.5em;
background: #f8f9fa;
border-bottom: 1px solid #e5e5e5;
}
.package-title {
display: flex;
flex-direction: column;
gap: 0.25em;
}
.package-title h3 {
margin: 0;
font-size: 1.1em;
}
.package-title h3 a {
color: inherit;
text-decoration: none;
}
.package-title h3 a:hover {
text-decoration: underline;
}
.package-order {
font-size: 0.85em;
color: #666;
}
.package-order a {
color: #2271b1;
text-decoration: none;
}
.package-order a:hover {
text-decoration: underline;
}
.package-license-count {
font-size: 0.9em;
color: #666;
background: #e9ecef;
padding: 0.3em 0.8em;
border-radius: 12px;
}
/* Package Licenses - Two Row Layout */
.package-licenses {
padding: 0;
}
.license-entry {
padding: 1em 1.5em;
border-bottom: 1px solid #e5e5e5;
}
.license-entry:last-child {
border-bottom: none;
}
.license-entry:hover {
background-color: #fafafa;
}
.license-row-primary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1em;
margin-bottom: 0.5em;
}
.license-key-group {
display: flex;
align-items: center;
gap: 0.75em;
flex-shrink: 1;
min-width: 0;
}
.license-entry code.license-key {
font-family: 'SF Mono', Monaco, Consolas, monospace;
background-color: #f5f5f5;
padding: 0.4em 0.75em;
border-radius: 4px;
font-size: 0.95em;
letter-spacing: 0.03em;
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.license-key-group .license-status {
flex-shrink: 0;
}
.license-actions {
display: flex;
align-items: center;
gap: 0.5em;
flex-shrink: 0;
}
.license-row-secondary {
display: flex;
align-items: center;
gap: 1.5em;
font-size: 0.9em;
color: #666;
flex-wrap: wrap;
}
.license-meta-item {
display: inline-flex;
align-items: center;
gap: 0.35em;
}
.license-meta-item .dashicons {
font-size: 14px;
width: 14px;
height: 14px;
color: #999;
}
.license-domain {
color: #333;
}
.license-expiry .lifetime {
color: #28a745;
font-weight: 500;
}
/* Legacy table styles (kept for backwards compatibility) */
.licenses-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95em;
}
.licenses-table th,
.licenses-table td {
padding: 0.75em 1em;
text-align: left;
border-bottom: 1px solid #e5e5e5;
}
.licenses-table th {
font-weight: 600;
background-color: #fafafa;
font-size: 0.9em;
color: #555;
}
.licenses-table code.license-key {
font-family: 'SF Mono', Monaco, Consolas, monospace;
background-color: #f5f5f5;
padding: 0.3em 0.6em;
border-radius: 4px;
font-size: 0.9em;
letter-spacing: 0.03em;
}
.licenses-table .lifetime {
color: #28a745;
font-weight: 500;
}
/* Legacy single card styles (kept for backwards compatibility) */
.license-card { .license-card {
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
border-radius: 8px; border-radius: 8px;
@@ -184,12 +367,14 @@
} }
/* Download Section */ /* Download Section */
.package-downloads,
.license-downloads { .license-downloads {
padding: 1em 1.5em; padding: 1em 1.5em;
background: #f8f9fa; background: #f8f9fa;
border-top: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5;
} }
.package-downloads h4,
.license-downloads h4 { .license-downloads h4 {
margin: 0 0 0.75em 0; margin: 0 0 0.75em 0;
font-size: 0.95em; font-size: 0.95em;
@@ -282,6 +467,71 @@
color: #666; color: #666;
} }
/* Latest version badge */
.download-version-badge {
display: inline-block;
padding: 0.15em 0.5em;
margin-left: 0.5em;
font-size: 0.75em;
font-weight: 600;
text-transform: uppercase;
background: #d4edda;
color: #155724;
border-radius: 3px;
vertical-align: middle;
}
/* Older versions collapsible */
.older-versions-section {
margin-top: 0.75em;
padding-top: 0.75em;
border-top: 1px dashed #ddd;
}
.older-versions-toggle {
display: inline-flex;
align-items: center;
gap: 0.35em;
padding: 0.4em 0.75em;
background: transparent;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.85em;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.older-versions-toggle:hover {
background: #f5f5f5;
border-color: #ccc;
color: #333;
}
.older-versions-toggle .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
transition: transform 0.2s ease;
}
.older-versions-toggle[aria-expanded="true"] .dashicons {
transform: rotate(180deg);
}
.older-versions-list {
margin-top: 0.75em;
padding-left: 0;
}
.older-versions-list .download-item {
opacity: 0.85;
}
.older-versions-list .download-item:hover {
opacity: 1;
}
/* Domain Field */ /* Domain Field */
#licensed-product-domain-field { #licensed-product-domain-field {
margin-top: 2em; margin-top: 2em;
@@ -333,6 +583,52 @@
/* Responsive */ /* Responsive */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
/* Package header responsive */
.package-header {
flex-direction: column;
align-items: flex-start;
gap: 0.75em;
}
.package-license-count {
align-self: flex-start;
}
/* License entry responsive */
.license-entry {
padding: 1em;
}
.license-row-primary {
flex-direction: column;
align-items: flex-start;
gap: 0.75em;
}
.license-key-group {
flex-direction: column;
align-items: flex-start;
gap: 0.5em;
width: 100%;
}
.license-entry code.license-key {
font-size: 0.85em;
word-break: break-all;
white-space: normal;
}
.license-actions {
align-self: flex-start;
}
.license-row-secondary {
flex-direction: column;
align-items: flex-start;
gap: 0.5em;
}
/* Legacy card layout responsive */
.license-header { .license-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -354,33 +650,44 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Legacy table responsive */
.woocommerce-licenses-table, .woocommerce-licenses-table,
.woocommerce-licenses-table thead, .woocommerce-licenses-table thead,
.woocommerce-licenses-table tbody, .woocommerce-licenses-table tbody,
.woocommerce-licenses-table th, .woocommerce-licenses-table th,
.woocommerce-licenses-table td, .woocommerce-licenses-table td,
.woocommerce-licenses-table tr { .woocommerce-licenses-table tr,
.licenses-table,
.licenses-table thead,
.licenses-table tbody,
.licenses-table th,
.licenses-table td,
.licenses-table tr {
display: block; display: block;
} }
.woocommerce-licenses-table thead tr { .woocommerce-licenses-table thead tr,
.licenses-table thead tr {
position: absolute; position: absolute;
top: -9999px; top: -9999px;
left: -9999px; left: -9999px;
} }
.woocommerce-licenses-table tr { .woocommerce-licenses-table tr,
.licenses-table tr {
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
margin-bottom: 1em; margin-bottom: 1em;
} }
.woocommerce-licenses-table td { .woocommerce-licenses-table td,
.licenses-table td {
border: none; border: none;
position: relative; position: relative;
padding-left: 50%; padding-left: 50%;
} }
.woocommerce-licenses-table td:before { .woocommerce-licenses-table td:before,
.licenses-table td:before {
content: attr(data-title); content: attr(data-title);
position: absolute; position: absolute;
left: 0.75em; left: 0.75em;
@@ -556,3 +863,118 @@
color: #2271b1; color: #2271b1;
font-weight: 500; font-weight: 500;
} }
/* Customer Secret Section */
.license-row-secret {
margin-top: 0.75em;
padding-top: 0.75em;
border-top: 1px dashed #e5e5e5;
}
.secret-toggle {
display: inline-flex;
align-items: center;
gap: 0.35em;
padding: 0.4em 0.75em;
background: transparent;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.85em;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.secret-toggle:hover {
background: #f5f5f5;
border-color: #ccc;
color: #333;
}
.secret-toggle .dashicons {
font-size: 14px;
width: 14px;
height: 14px;
}
.secret-toggle .toggle-arrow {
transition: transform 0.2s ease;
}
.secret-toggle[aria-expanded="true"] .toggle-arrow {
transform: rotate(180deg);
}
.secret-content {
margin-top: 0.75em;
padding: 1em;
background: #f8f9fa;
border-radius: 4px;
border: 1px solid #e5e5e5;
}
.secret-description {
margin: 0 0 0.75em 0;
font-size: 0.85em;
color: #666;
}
.secret-value-wrapper {
display: flex;
align-items: center;
gap: 0.5em;
}
.secret-value {
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 0.75em;
background: #fff;
padding: 0.5em 0.75em;
border: 1px solid #ddd;
border-radius: 4px;
word-break: break-all;
flex: 1;
min-width: 0;
overflow-x: auto;
}
.copy-secret-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.copy-secret-btn:hover {
background: #e5e5e5;
border-color: #ccc;
}
.copy-secret-btn .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
}
@media screen and (max-width: 768px) {
.secret-value-wrapper {
flex-direction: column;
align-items: stretch;
}
.secret-value {
font-size: 0.7em;
}
.copy-secret-btn {
align-self: flex-start;
}
}

View File

@@ -1,7 +1,8 @@
/** /**
* WooCommerce Checkout Blocks Integration * WooCommerce Checkout Blocks Integration
* *
* Adds a domain field to the checkout block for licensed products. * Adds domain fields to the checkout block for licensed products.
* Supports single domain mode (legacy) and multi-domain mode (per quantity).
* *
* @package WcLicensedProduct * @package WcLicensedProduct
*/ */
@@ -9,92 +10,333 @@
(function () { (function () {
'use strict'; 'use strict';
const { registerCheckoutBlock } = wc.blocksCheckout; // Check dependencies
const { createElement, useState, useEffect } = wp.element; if (typeof wc === 'undefined' ||
typeof wc.blocksCheckout === 'undefined' ||
typeof wc.wcSettings === 'undefined') {
return;
}
const { getSetting } = wc.wcSettings;
const { createElement, useState } = wp.element;
const { TextControl } = wp.components; const { TextControl } = wp.components;
const { __ } = wp.i18n; const { __ } = wp.i18n;
const { extensionCartUpdate } = wc.blocksCheckout;
const { getSetting } = wc.wcSettings;
// Get settings passed from PHP // Get available exports from blocksCheckout
const { ExperimentalOrderMeta } = wc.blocksCheckout;
// Get settings from PHP
const settings = getSetting('wc-licensed-product_data', {}); const settings = getSetting('wc-licensed-product_data', {});
// Check if we have licensed products
if (!settings.hasLicensedProducts) {
return;
}
/** /**
* Validate domain format * Validate domain format
*/ */
function isValidDomain(domain) { function isValidDomain(domain) {
if (!domain || domain.length > 255) { if (!domain || domain.length > 255) return false;
return false;
}
const pattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; const pattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
return pattern.test(domain); return pattern.test(domain);
} }
/** /**
* Normalize domain (remove protocol and www) * Normalize domain
*/ */
function normalizeDomain(domain) { function normalizeDomain(domain) {
let normalized = domain.toLowerCase().trim(); return domain.toLowerCase().trim()
normalized = normalized.replace(/^https?:\/\//, ''); .replace(/^https?:\/\//, '')
normalized = normalized.replace(/^www\./, ''); .replace(/^www\./, '')
normalized = normalized.replace(/\/.*$/, ''); .replace(/\/.*$/, '');
return normalized;
} }
/** /**
* License Domain Block Component * Single Domain Component
*/ */
const LicenseDomainBlock = ({ checkoutExtensionData, extensions }) => { const SingleDomainField = () => {
const [domain, setDomain] = useState(''); const [domain, setDomain] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const { setExtensionData } = checkoutExtensionData;
// Only show if cart has licensed products
if (!settings.hasLicensedProducts) {
return null;
}
const handleChange = (value) => { const handleChange = (value) => {
const normalized = normalizeDomain(value); const normalized = normalizeDomain(value);
setDomain(normalized); setDomain(normalized);
// Validate
if (normalized && !isValidDomain(normalized)) { if (normalized && !isValidDomain(normalized)) {
setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product')); setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'));
} else { } else {
setError(''); setError('');
} }
// Update extension data for server-side processing // Store in hidden input for form submission
setExtensionData('wc-licensed-product', 'licensed_product_domain', normalized); const hiddenInput = document.getElementById('wclp-domain-hidden');
if (hiddenInput) {
hiddenInput.value = normalized;
}
}; };
return createElement( return createElement(
'div', 'div',
{ className: 'wc-block-components-licensed-product-domain' }, {
createElement( className: 'wc-block-components-licensed-product-domain',
'h3', style: {
{ className: 'wc-block-components-title' }, padding: '16px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
marginBottom: '16px',
}
},
createElement('h4', { style: { marginTop: 0, marginBottom: '8px' } },
settings.sectionTitle || __('License Domain', 'wc-licensed-product') settings.sectionTitle || __('License Domain', 'wc-licensed-product')
), ),
createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } },
settings.fieldDescription || __('Enter the domain where you will use the license.', 'wc-licensed-product')
),
createElement(TextControl, { createElement(TextControl, {
label: settings.fieldLabel || __('Domain for License Activation', 'wc-licensed-product'), label: settings.singleDomainLabel || __('Domain', 'wc-licensed-product'),
value: domain, value: domain,
onChange: handleChange, onChange: handleChange,
placeholder: settings.fieldPlaceholder || 'example.com', placeholder: settings.fieldPlaceholder || 'example.com',
help: error || settings.fieldDescription || __('Enter the domain where you will use this license.', 'wc-licensed-product'), help: error || '',
className: error ? 'has-error' : '', className: error ? 'has-error' : '',
required: true, }),
createElement('input', {
type: 'hidden',
id: 'wclp-domain-hidden',
name: 'wclp_license_domain',
value: domain,
}) })
); );
}; };
// Register the checkout block /**
registerCheckoutBlock({ * Multi-Domain Component
metadata: { */
name: 'wc-licensed-product/domain-field', const MultiDomainFields = () => {
parent: ['woocommerce/checkout-contact-information-block'], const products = settings.licensedProducts || [];
}, const [domains, setDomains] = useState(() => {
component: LicenseDomainBlock, const init = {};
}); products.forEach(p => {
init[p.product_id] = Array(p.quantity).fill('');
});
return init;
});
const [errors, setErrors] = useState({});
if (!products.length) {
return null;
}
const handleChange = (productId, index, value) => {
const normalized = normalizeDomain(value);
const newDomains = { ...domains };
if (!newDomains[productId]) newDomains[productId] = [];
newDomains[productId] = [...newDomains[productId]];
newDomains[productId][index] = normalized;
setDomains(newDomains);
// Validate
const key = `${productId}_${index}`;
const newErrors = { ...errors };
if (normalized && !isValidDomain(normalized)) {
newErrors[key] = settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product');
} else {
delete newErrors[key];
}
// Check for duplicates within same product
const productDomains = newDomains[productId].filter(d => d);
const uniqueDomains = new Set(productDomains.map(d => normalizeDomain(d)));
if (productDomains.length !== uniqueDomains.size) {
const seen = new Set();
newDomains[productId].forEach((d, idx) => {
const normalizedD = normalizeDomain(d);
const dupKey = `${productId}_${idx}`;
if (normalizedD && seen.has(normalizedD)) {
newErrors[dupKey] = settings.duplicateError || __('Each license requires a unique domain.', 'wc-licensed-product');
} else if (normalizedD) {
seen.add(normalizedD);
}
});
}
setErrors(newErrors);
// Update hidden field
const data = Object.entries(newDomains).map(([pid, doms]) => ({
product_id: parseInt(pid, 10),
domains: doms.filter(d => d),
})).filter(item => item.domains.length > 0);
const hiddenInput = document.getElementById('wclp-domains-hidden');
if (hiddenInput) {
hiddenInput.value = JSON.stringify(data);
}
};
return createElement(
'div',
{
className: 'wc-block-components-licensed-product-domains',
style: {
padding: '16px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
marginBottom: '16px',
}
},
createElement('h4', { style: { marginTop: 0, marginBottom: '8px' } },
settings.sectionTitle || __('License Domains', 'wc-licensed-product')
),
createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } },
settings.fieldDescription || __('Enter a unique domain for each license.', 'wc-licensed-product')
),
products.map(product => createElement(
'div',
{
key: product.product_id,
style: {
marginBottom: '16px',
padding: '12px',
backgroundColor: '#fff',
borderRadius: '4px',
}
},
createElement('strong', { style: { display: 'block', marginBottom: '8px' } },
product.name + (product.quantity > 1 ? ` (×${product.quantity})` : '')
),
Array.from({ length: product.quantity }, (_, i) => {
const key = `${product.product_id}_${i}`;
return createElement(
'div',
{ key: i, style: { marginBottom: '8px' } },
createElement(TextControl, {
label: (settings.licenseLabel || __('License %d:', 'wc-licensed-product')).replace('%d', i + 1),
value: domains[product.product_id]?.[i] || '',
onChange: (val) => handleChange(product.product_id, i, val),
placeholder: settings.fieldPlaceholder || 'example.com',
help: errors[key] || '',
})
);
})
)),
createElement('input', {
type: 'hidden',
id: 'wclp-domains-hidden',
name: 'wclp_license_domains',
value: '',
})
);
};
/**
* Main License Domains Block
*/
const LicenseDomainsBlock = () => {
if (settings.isMultiDomainEnabled) {
return createElement(MultiDomainFields);
}
return createElement(SingleDomainField);
};
// Register using ExperimentalOrderMeta slot
if (ExperimentalOrderMeta) {
const { registerPlugin } = wp.plugins || {};
if (registerPlugin) {
registerPlugin('wc-licensed-product-domain-fields', {
render: () => createElement(
ExperimentalOrderMeta,
{},
createElement(LicenseDomainsBlock)
),
scope: 'woocommerce-checkout',
});
}
}
// Fallback: inject into DOM directly if React approach fails
setTimeout(function() {
const existingComponent = document.querySelector('.wc-block-components-licensed-product-domain, .wc-block-components-licensed-product-domains');
if (existingComponent) {
return;
}
const checkoutForm = document.querySelector('.wc-block-checkout, .wc-block-checkout__form, form.checkout');
if (!checkoutForm) {
return;
}
const contactInfo = document.querySelector('.wc-block-checkout__contact-fields, .wp-block-woocommerce-checkout-contact-information-block');
const paymentMethods = document.querySelector('.wc-block-checkout__payment-method, .wp-block-woocommerce-checkout-payment-block');
let insertionPoint = contactInfo || paymentMethods;
if (!insertionPoint) {
insertionPoint = checkoutForm.querySelector('.wc-block-components-form');
}
if (!insertionPoint) {
return;
}
const container = document.createElement('div');
container.id = 'wclp-domain-fields-container';
container.className = 'wc-block-components-licensed-product-wrapper';
container.style.cssText = 'margin: 20px 0; padding: 16px; background: #f0f0f0; border-radius: 4px;';
if (settings.isMultiDomainEnabled && settings.licensedProducts) {
container.innerHTML = `
<h4 style="margin: 0 0 8px 0;">${settings.sectionTitle || 'License Domains'}</h4>
<p style="margin-bottom: 12px; color: #666; font-size: 0.9em;">
${settings.fieldDescription || 'Enter a unique domain for each license.'}
</p>
${settings.licensedProducts.map(product => `
<div style="margin-bottom: 16px; padding: 12px; background: #fff; border-radius: 4px;">
<strong style="display: block; margin-bottom: 8px;">
${product.name}${product.quantity > 1 ? ` (×${product.quantity})` : ''}
</strong>
${Array.from({ length: product.quantity }, (_, i) => `
<div style="margin-bottom: 8px;">
<label style="display: block; margin-bottom: 4px;">
${(settings.licenseLabel || 'License %d:').replace('%d', i + 1)}
</label>
<input type="text"
name="licensed_domains[${product.product_id}][${i}]"
placeholder="${settings.fieldPlaceholder || 'example.com'}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
/>
</div>
`).join('')}
</div>
`).join('')}
`;
} else {
container.innerHTML = `
<h4 style="margin: 0 0 8px 0;">${settings.sectionTitle || 'License Domain'}</h4>
<p style="margin-bottom: 12px; color: #666; font-size: 0.9em;">
${settings.fieldDescription || 'Enter the domain where you will use the license.'}
</p>
<div style="margin-bottom: 8px;">
<label style="display: block; margin-bottom: 4px;">
${settings.singleDomainLabel || 'Domain'}
</label>
<input type="text"
name="licensed_product_domain"
placeholder="${settings.fieldPlaceholder || 'example.com'}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
/>
</div>
`;
}
if (contactInfo) {
contactInfo.parentNode.insertBefore(container, contactInfo.nextSibling);
} else if (paymentMethods) {
paymentMethods.parentNode.insertBefore(container, paymentMethods);
} else {
insertionPoint.appendChild(container);
}
}, 2000);
})(); })();

View File

@@ -19,12 +19,19 @@
bindEvents: function() { bindEvents: function() {
$(document).on('click', '.copy-license-btn', this.copyLicenseKey); $(document).on('click', '.copy-license-btn', this.copyLicenseKey);
$(document).on('click', '.copy-secret-btn', this.copySecret);
// Transfer modal events // Transfer modal events
$(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this)); $(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this));
$(document).on('click', '.wclp-modal-close, .wclp-modal-cancel, .wclp-modal-overlay', this.closeTransferModal.bind(this)); $(document).on('click', '.wclp-modal-close, .wclp-modal-cancel, .wclp-modal-overlay', this.closeTransferModal.bind(this));
$(document).on('submit', '#wclp-transfer-form', this.submitTransfer.bind(this)); $(document).on('submit', '#wclp-transfer-form', this.submitTransfer.bind(this));
// Older versions toggle
$(document).on('click', '.older-versions-toggle', this.toggleOlderVersions);
// Secret toggle
$(document).on('click', '.secret-toggle', this.toggleSecret);
// Close modal on escape key // Close modal on escape key
$(document).on('keyup', function(e) { $(document).on('keyup', function(e) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
@@ -33,6 +40,61 @@
}); });
}, },
/**
* Toggle older versions visibility
*/
toggleOlderVersions: function(e) {
e.preventDefault();
var $btn = $(this);
var $list = $btn.siblings('.older-versions-list');
var isExpanded = $btn.attr('aria-expanded') === 'true';
$btn.attr('aria-expanded', !isExpanded);
$list.slideToggle(200);
},
/**
* Toggle secret visibility
*/
toggleSecret: function(e) {
e.preventDefault();
var $btn = $(this);
var $content = $btn.siblings('.secret-content');
var isExpanded = $btn.attr('aria-expanded') === 'true';
$btn.attr('aria-expanded', !isExpanded);
$content.slideToggle(200);
},
/**
* Copy secret to clipboard
*/
copySecret: function(e) {
e.preventDefault();
var $btn = $(this);
var secret = $btn.data('secret');
if (!secret) {
return;
}
// Use modern clipboard API if available
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(secret)
.then(function() {
WCLicensedProductFrontend.showCopyFeedback($btn, true);
})
.catch(function() {
WCLicensedProductFrontend.fallbackCopy(secret, $btn);
});
} else {
WCLicensedProductFrontend.fallbackCopy(secret, $btn);
}
},
/** /**
* Copy license key to clipboard * Copy license key to clipboard
*/ */

View File

@@ -174,6 +174,24 @@
}); });
}, },
/**
* Compare two semantic version strings
* Returns: positive if a > b, negative if a < b, 0 if equal
*/
compareVersions: function(a, b) {
var partsA = a.split('.').map(Number);
var partsB = b.split('.').map(Number);
for (var i = 0; i < 3; i++) {
var numA = partsA[i] || 0;
var numB = partsB[i] || 0;
if (numA !== numB) {
return numA - numB;
}
}
return 0;
},
/** /**
* Extract version from filename * Extract version from filename
* Supports patterns like: plugin-v1.2.3.zip, plugin-1.2.3.zip, v1.2.3.zip * Supports patterns like: plugin-v1.2.3.zip, plugin-1.2.3.zip, v1.2.3.zip
@@ -244,8 +262,23 @@
// Remove "no versions" row if present // Remove "no versions" row if present
$('#versions-table tbody .no-versions').remove(); $('#versions-table tbody .no-versions').remove();
// Add new row to table // Add new row in sorted position (by version DESC)
$('#versions-table tbody').prepend(response.data.html); var $newRow = $(response.data.html);
var newVersion = (response.data.version && response.data.version.version) || version;
var inserted = false;
$('#versions-table tbody tr').each(function() {
var rowVersion = $(this).find('td:first strong').text();
if (self.compareVersions(newVersion, rowVersion) > 0) {
$newRow.insertBefore($(this));
inserted = true;
return false; // break
}
});
if (!inserted) {
$('#versions-table tbody').append($newRow);
}
// Clear form // Clear form
$('#new_version').val(''); $('#new_version').val('');

12
composer.lock generated
View File

@@ -380,16 +380,16 @@
}, },
{ {
"name": "symfony/http-client", "name": "symfony/http-client",
"version": "v7.4.3", "version": "v7.4.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-client.git", "url": "https://github.com/symfony/http-client.git",
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616" "reference": "d63c23357d74715a589454c141c843f0172bec6c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616", "url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c",
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616", "reference": "d63c23357d74715a589454c141c843f0172bec6c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -457,7 +457,7 @@
"http" "http"
], ],
"support": { "support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.3" "source": "https://github.com/symfony/http-client/tree/v7.4.4"
}, },
"funding": [ "funding": [
{ {
@@ -477,7 +477,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-23T14:50:43+00:00" "time": "2026-01-23T16:34:22+00:00"
}, },
{ {
"name": "symfony/http-client-contracts", "name": "symfony/http-client-contracts",

View File

@@ -8,14 +8,16 @@ The security model works as follows:
1. Server generates a unique signature for each response using HMAC-SHA256 1. Server generates a unique signature for each response using HMAC-SHA256
2. Signature includes a timestamp to prevent replay attacks 2. Signature includes a timestamp to prevent replay attacks
3. Client verifies the signature using a shared secret 3. Each license key has a unique derived secret (not the master secret)
4. Invalid signatures cause the client to reject the response 4. Client verifies the signature using their per-license secret
5. Invalid signatures cause the client to reject the response
This prevents attackers from: This prevents attackers from:
- Faking valid license responses - Faking valid license responses
- Replaying old responses - Replaying old responses
- Tampering with response data - Tampering with response data
- Using one customer's secret to verify another customer's responses
## Requirements ## Requirements
@@ -323,13 +325,49 @@ Adjust if needed:
$signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes $signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes
``` ```
### Per-License Secrets
Each customer receives a unique secret derived from their license key. This means:
- Customers only know their own secret, not the master server secret
- If one customer's secret is leaked, other customers are not affected
- The server uses HKDF-like derivation to create unique secrets
#### How Customers Get Their Secret
Customers can find their per-license verification secret in their account:
1. Log in to the store
2. Go to My Account > Licenses
3. Click "API Verification Secret" under any license
4. Copy the 64-character hex string
This secret is automatically derived from the customer's license key and the server's master secret.
#### Using the Customer Secret
```php
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Symfony\Component\HttpClient\HttpClient;
// Customer uses their per-license secret (from account page)
$client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: 'https://shop.example.com',
serverSecret: 'customer-secret-from-account-page', // 64 hex chars
);
$info = $client->validate('XXXX-XXXX-XXXX-XXXX', 'example.com');
```
### Secret Key Rotation ### Secret Key Rotation
To rotate the server secret: To rotate the server secret:
1. Deploy new secret to server 1. Deploy new secret to server
2. Update client configurations 2. All per-license secrets change automatically (they're derived)
3. Old signatures become invalid immediately 3. Customers must copy their new secret from their account page
4. Old signatures become invalid immediately
For zero-downtime rotation, implement versioned secrets: For zero-downtime rotation, implement versioned secrets:

View File

@@ -3,10 +3,10 @@
# This file is distributed under the GPL-2.0-or-later. # This file is distributed under the GPL-2.0-or-later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WC Licensed Product 0.3.1\n" "Project-Id-Version: WC Licensed Product 0.5.0\n"
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
"POT-Creation-Date: 2026-01-24 16:39+0100\n" "POT-Creation-Date: 2026-01-26 15:26+0100\n"
"PO-Revision-Date: 2026-01-22T17:15:00+00:00\n" "PO-Revision-Date: 2026-01-25T18:30:00+00:00\n"
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n" "Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
"Language-Team: German (Switzerland) <de_CH@li.org>\n" "Language-Team: German (Switzerland) <de_CH@li.org>\n"
"Language: de_CH\n" "Language: de_CH\n"
@@ -17,9 +17,9 @@ msgstr ""
#: src/Admin/AdminController.php:76 src/Admin/AdminController.php:77 #: src/Admin/AdminController.php:76 src/Admin/AdminController.php:77
#: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200 #: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200
#: src/Admin/OrderLicenseController.php:128 #: src/Admin/OrderLicenseController.php:149
#: src/Admin/OrderLicenseController.php:249 #: src/Admin/OrderLicenseController.php:281
#: src/Frontend/AccountController.php:90 #: src/Frontend/AccountController.php:91
msgid "Licenses" msgid "Licenses"
msgstr "Lizenzen" msgstr "Lizenzen"
@@ -39,7 +39,7 @@ msgstr "Suche..."
msgid "Search failed" msgid "Search failed"
msgstr "Suche fehlgeschlagen" msgstr "Suche fehlgeschlagen"
#: src/Admin/AdminController.php:144 src/Admin/OrderLicenseController.php:338 #: src/Admin/AdminController.php:144 src/Admin/OrderLicenseController.php:370
msgid "Saving..." msgid "Saving..."
msgstr "Speichere..." msgstr "Speichere..."
@@ -65,32 +65,33 @@ msgstr "Bearbeiten"
#: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341 #: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341
#: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382 #: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382
#: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:212 #: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:244
#: src/Frontend/AccountController.php:271 #: src/Frontend/AccountController.php:387
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: src/Admin/AdminController.php:150 src/Admin/AdminController.php:1340 #: src/Admin/AdminController.php:150 src/Admin/AdminController.php:1340
#: src/Admin/AdminController.php:1360 src/Admin/AdminController.php:1381 #: src/Admin/AdminController.php:1360 src/Admin/AdminController.php:1381
#: src/Admin/OrderLicenseController.php:119 #: src/Admin/OrderLicenseController.php:139
#: src/Admin/OrderLicenseController.php:209 #: src/Admin/OrderLicenseController.php:241
msgid "Save" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
#: src/Admin/AdminController.php:151 src/Admin/AdminController.php:266 #: src/Admin/AdminController.php:151 src/Admin/AdminController.php:266
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613 #: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
#: src/Admin/DashboardWidgetController.php:136 #: src/Admin/DashboardWidgetController.php:136
#: src/Admin/OrderLicenseController.php:228 #: src/Admin/OrderLicenseController.php:260
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:110 #: src/Admin/SettingsController.php:192 src/Frontend/AccountController.php:286
#: src/Product/LicensedProductType.php:110
#: src/Product/LicensedProductType.php:158 #: src/Product/LicensedProductType.php:158
msgid "Lifetime" msgid "Lifetime"
msgstr "Lebenslang" msgstr "Lebenslang"
#: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:309 #: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:425
msgid "Copied!" msgid "Copied!"
msgstr "Kopiert!" msgstr "Kopiert!"
#: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:310 #: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:426
msgid "Copy failed" msgid "Copy failed"
msgstr "Kopieren fehlgeschlagen" msgstr "Kopieren fehlgeschlagen"
@@ -124,9 +125,9 @@ msgstr "Widerrufen"
#: src/Admin/AdminController.php:173 src/Admin/AdminController.php:213 #: src/Admin/AdminController.php:173 src/Admin/AdminController.php:213
#: src/Admin/AdminController.php:249 src/Admin/AdminController.php:301 #: src/Admin/AdminController.php:249 src/Admin/AdminController.php:301
#: src/Admin/AdminController.php:339 src/Admin/AdminController.php:369 #: src/Admin/AdminController.php:339 src/Admin/AdminController.php:369
#: src/Admin/OrderLicenseController.php:355 #: src/Admin/OrderLicenseController.php:387
#: src/Admin/OrderLicenseController.php:394 #: src/Admin/OrderLicenseController.php:426
#: src/Admin/OrderLicenseController.php:458 #: src/Admin/OrderLicenseController.php:490
#: src/Admin/VersionAdminController.php:259 #: src/Admin/VersionAdminController.php:259
#: src/Admin/VersionAdminController.php:328 #: src/Admin/VersionAdminController.php:328
#: src/Admin/VersionAdminController.php:354 #: src/Admin/VersionAdminController.php:354
@@ -134,7 +135,7 @@ msgid "Permission denied."
msgstr "Zugriff verweigert." msgstr "Zugriff verweigert."
#: src/Admin/AdminController.php:195 src/Admin/AdminController.php:1019 #: src/Admin/AdminController.php:195 src/Admin/AdminController.php:1019
#: src/Admin/OrderLicenseController.php:195 #: src/Admin/OrderLicenseController.php:227
msgid "Unknown" msgid "Unknown"
msgstr "Unbekannt" msgstr "Unbekannt"
@@ -144,7 +145,7 @@ msgstr "Gast"
#: src/Admin/AdminController.php:220 src/Admin/AdminController.php:256 #: src/Admin/AdminController.php:220 src/Admin/AdminController.php:256
#: src/Admin/AdminController.php:308 src/Admin/AdminController.php:345 #: src/Admin/AdminController.php:308 src/Admin/AdminController.php:345
#: src/Admin/OrderLicenseController.php:401 #: src/Admin/OrderLicenseController.php:433
msgid "Invalid license ID." msgid "Invalid license ID."
msgstr "Ungültige Lizenz-ID." msgstr "Ungültige Lizenz-ID."
@@ -176,7 +177,7 @@ msgstr "Ablaufdatum erfolgreich aktualisiert."
msgid "Invalid date format." msgid "Invalid date format."
msgstr "Ungültiges Datumsformat." msgstr "Ungültiges Datumsformat."
#: src/Admin/AdminController.php:312 src/Admin/OrderLicenseController.php:405 #: src/Admin/AdminController.php:312 src/Admin/OrderLicenseController.php:437
msgid "Domain cannot be empty." msgid "Domain cannot be empty."
msgstr "Domain darf nicht leer sein." msgstr "Domain darf nicht leer sein."
@@ -204,8 +205,8 @@ msgstr "Lizenzschlüssel und Domain sind erforderlich."
#: src/Admin/AdminController.php:493 src/Admin/AdminController.php:511 #: src/Admin/AdminController.php:493 src/Admin/AdminController.php:511
#: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549 #: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549
#: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621 #: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621
#: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:454 #: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:469
#: src/Frontend/AccountController.php:326 #: src/Frontend/AccountController.php:442
msgid "Security check failed." msgid "Security check failed."
msgstr "Sicherheitsüberprüfung fehlgeschlagen." msgstr "Sicherheitsüberprüfung fehlgeschlagen."
@@ -266,7 +267,7 @@ msgstr "Lizenzen verwalten"
msgid "Export to CSV" msgid "Export to CSV"
msgstr "Als CSV exportieren" msgstr "Als CSV exportieren"
#: src/Admin/AdminController.php:968 wc-licensed-product.php:137 #: src/Admin/AdminController.php:968
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
@@ -411,9 +412,11 @@ msgstr "Einträge"
msgid "Showing" msgid "Showing"
msgstr "Anzeige" msgstr "Anzeige"
#: src/Admin/AdminController.php:1259 #: src/Admin/AdminController.php:1259 src/Email/LicenseEmailController.php:338
msgid "license" msgid "license"
msgstr "Lizenz" msgid_plural "licenses"
msgstr[0] "Lizenz"
msgstr[1] "Lizenzen"
#: src/Admin/AdminController.php:1259 #: src/Admin/AdminController.php:1259
msgid "licenses" msgid "licenses"
@@ -471,15 +474,14 @@ msgid "Apply"
msgstr "Anwenden" msgstr "Anwenden"
#: src/Admin/AdminController.php:1291 src/Admin/AdminController.php:1442 #: src/Admin/AdminController.php:1291 src/Admin/AdminController.php:1442
#: src/Admin/AdminController.php:1491 src/Admin/OrderLicenseController.php:171 #: src/Admin/AdminController.php:1491 src/Admin/OrderLicenseController.php:203
#: src/Admin/SettingsController.php:142 #: src/Admin/SettingsController.php:142
#: src/Email/LicenseEmailController.php:269 #: src/Email/LicenseEmailController.php:287
msgid "License Key" msgid "License Key"
msgstr "Lizenzschlüssel" msgstr "Lizenzschlüssel"
#: src/Admin/AdminController.php:1292 src/Admin/AdminController.php:1443 #: src/Admin/AdminController.php:1292 src/Admin/AdminController.php:1443
#: src/Admin/AdminController.php:1608 src/Admin/OrderLicenseController.php:172 #: src/Admin/AdminController.php:1608 src/Admin/OrderLicenseController.php:204
#: src/Email/LicenseEmailController.php:268
msgid "Product" msgid "Product"
msgstr "Produkt" msgstr "Produkt"
@@ -490,12 +492,15 @@ msgid "Customer"
msgstr "Kunde" msgstr "Kunde"
#: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445 #: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445
#: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:173 #: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:205
#: src/Checkout/CheckoutBlocksIntegration.php:129
#: src/Checkout/CheckoutController.php:122
#: src/Email/LicenseEmailController.php:288
msgid "Domain" msgid "Domain"
msgstr "Domain" msgstr "Domain"
#: src/Admin/AdminController.php:1295 src/Admin/AdminController.php:1446 #: src/Admin/AdminController.php:1295 src/Admin/AdminController.php:1446
#: src/Admin/OrderLicenseController.php:174 #: src/Admin/OrderLicenseController.php:206
#: src/Admin/VersionAdminController.php:140 #: src/Admin/VersionAdminController.php:140
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -506,13 +511,13 @@ msgstr "Erstellt"
#: src/Admin/AdminController.php:1297 src/Admin/AdminController.php:1448 #: src/Admin/AdminController.php:1297 src/Admin/AdminController.php:1448
#: src/Admin/AdminController.php:1611 src/Admin/AdminController.php:1613 #: src/Admin/AdminController.php:1611 src/Admin/AdminController.php:1613
#: src/Admin/OrderLicenseController.php:175 #: src/Admin/OrderLicenseController.php:207
#: src/Email/LicenseEmailController.php:270 #: src/Email/LicenseEmailController.php:289
msgid "Expires" msgid "Expires"
msgstr "Läuft ab" msgstr "Läuft ab"
#: src/Admin/AdminController.php:1298 src/Admin/AdminController.php:1449 #: src/Admin/AdminController.php:1298 src/Admin/AdminController.php:1449
#: src/Admin/OrderLicenseController.php:176 #: src/Admin/OrderLicenseController.php:208
#: src/Admin/VersionAdminController.php:142 #: src/Admin/VersionAdminController.php:142
msgid "Actions" msgid "Actions"
msgstr "Aktionen" msgstr "Aktionen"
@@ -521,7 +526,7 @@ msgstr "Aktionen"
msgid "No licenses found." msgid "No licenses found."
msgstr "Keine Lizenzen gefunden." msgstr "Keine Lizenzen gefunden."
#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:194 #: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:263
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "In Zwischenablage kopieren" msgstr "In Zwischenablage kopieren"
@@ -541,11 +546,11 @@ msgstr "Lizenz gegen API testen"
msgid "Test" msgid "Test"
msgstr "Testen" msgstr "Testen"
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:207 #: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:270
msgid "Transfer to new domain" msgid "Transfer to new domain"
msgstr "Auf neue Domain übertragen" msgstr "Auf neue Domain übertragen"
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:209 #: src/Admin/AdminController.php:1400
msgid "Transfer" msgid "Transfer"
msgstr "Übertragen" msgstr "Übertragen"
@@ -569,27 +574,27 @@ msgstr "Lizenzvalidierungstest"
msgid "Testing license..." msgid "Testing license..."
msgstr "Lizenz wird geprüft..." msgstr "Lizenz wird geprüft..."
#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:249 #: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:365
msgid "Close" msgid "Close"
msgstr "Schliessen" msgstr "Schliessen"
#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:250 #: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:366
msgid "Transfer License to New Domain" msgid "Transfer License to New Domain"
msgstr "Lizenz auf neue Domain übertragen" msgstr "Lizenz auf neue Domain übertragen"
#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:255 #: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:371
msgid "Current Domain" msgid "Current Domain"
msgstr "Aktuelle Domain" msgstr "Aktuelle Domain"
#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:260 #: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:376
msgid "New Domain" msgid "New Domain"
msgstr "Neue Domain" msgstr "Neue Domain"
#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:264 #: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:380
msgid "Enter the new domain without http:// or www." msgid "Enter the new domain without http:// or www."
msgstr "Geben Sie die neue Domain ohne http:// oder www ein." msgstr "Geben Sie die neue Domain ohne http:// oder www ein."
#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:269 #: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:385
msgid "Transfer License" msgid "Transfer License"
msgstr "Lizenz übertragen" msgstr "Lizenz übertragen"
@@ -764,8 +769,8 @@ msgid "Product Licenses"
msgstr "Produktlizenzen" msgstr "Produktlizenzen"
#: src/Admin/OrderLicenseController.php:78 #: src/Admin/OrderLicenseController.php:78
#: src/Admin/OrderLicenseController.php:367 #: src/Admin/OrderLicenseController.php:399
#: src/Admin/OrderLicenseController.php:469 #: src/Admin/OrderLicenseController.php:501
msgid "Order not found." msgid "Order not found."
msgstr "Bestellung nicht gefunden." msgstr "Bestellung nicht gefunden."
@@ -773,11 +778,26 @@ msgstr "Bestellung nicht gefunden."
msgid "This order does not contain licensed products." msgid "This order does not contain licensed products."
msgstr "Diese Bestellung enthält keine lizensierten Produkte." msgstr "Diese Bestellung enthält keine lizensierten Produkte."
#: src/Admin/OrderLicenseController.php:107
msgid "Order Domain"
msgstr "Bestellungs-Domain"
#: src/Admin/OrderLicenseController.php:109 #: src/Admin/OrderLicenseController.php:109
msgid "Order Domains"
msgstr "Bestellungs-Domains"
#: src/Admin/OrderLicenseController.php:113
msgid "Domains specified during checkout (multi-domain order)."
msgstr "Bei der Bestellung angegebene Domains (Multi-Domain-Bestellung)."
#: src/Admin/OrderLicenseController.php:119
#: src/Checkout/CheckoutController.php:436
#: src/Checkout/CheckoutController.php:486
#: src/Checkout/CheckoutController.php:496
#: src/Email/LicenseExpirationEmail.php:107
#: src/Email/LicenseExpiredEmail.php:99 src/Frontend/AccountController.php:148
#: src/License/LicenseManager.php:806 src/Product/VersionManager.php:349
#: src/Product/VersionManager.php:361
msgid "Unknown Product"
msgstr "Unbekanntes Produkt"
#: src/Admin/OrderLicenseController.php:129
msgid "" msgid ""
"The domain specified during checkout. Changing this will not automatically " "The domain specified during checkout. Changing this will not automatically "
"update existing license domains." "update existing license domains."
@@ -785,17 +805,19 @@ msgstr ""
"Die bei der Bestellung angegebene Domain. Eine Änderung aktualisiert nicht " "Die bei der Bestellung angegebene Domain. Eine Änderung aktualisiert nicht "
"automatisch bestehende Lizenz-Domains." "automatisch bestehende Lizenz-Domains."
#: src/Admin/OrderLicenseController.php:117 #: src/Admin/OrderLicenseController.php:137
#: src/Checkout/CheckoutBlocksIntegration.php:102 #: src/Checkout/CheckoutBlocksIntegration.php:83
#: src/Checkout/CheckoutController.php:89 #: src/Checkout/CheckoutBlocksIntegration.php:119
#: src/Checkout/CheckoutController.php:130
#: src/Checkout/CheckoutController.php:186
msgid "example.com" msgid "example.com"
msgstr "beispiel.ch" msgstr "beispiel.ch"
#: src/Admin/OrderLicenseController.php:144 #: src/Admin/OrderLicenseController.php:176
msgid "No licenses have been generated for this order yet." msgid "No licenses have been generated for this order yet."
msgstr "Für diese Bestellung wurden noch keine Lizenzen generiert." msgstr "Für diese Bestellung wurden noch keine Lizenzen generiert."
#: src/Admin/OrderLicenseController.php:147 #: src/Admin/OrderLicenseController.php:179
msgid "" msgid ""
"Licenses should be generated automatically when an order is paid. If " "Licenses should be generated automatically when an order is paid. If "
"missing, check that a domain was specified during checkout." "missing, check that a domain was specified during checkout."
@@ -804,112 +826,113 @@ msgstr ""
"wird. Falls fehlend, prüfen Sie, ob bei der Bestellung eine Domain angegeben " "wird. Falls fehlend, prüfen Sie, ob bei der Bestellung eine Domain angegeben "
"wurde." "wurde."
#: src/Admin/OrderLicenseController.php:150 #: src/Admin/OrderLicenseController.php:182
msgid "Licenses will be generated when the order is marked as paid/completed." msgid "Licenses will be generated when the order is marked as paid/completed."
msgstr "" msgstr ""
"Lizenzen werden generiert, sobald die Bestellung als bezahlt/abgeschlossen " "Lizenzen werden generiert, sobald die Bestellung als bezahlt/abgeschlossen "
"markiert wird." "markiert wird."
#: src/Admin/OrderLicenseController.php:156 #: src/Admin/OrderLicenseController.php:188
msgid "Generate Licenses" msgid "Generate Licenses"
msgstr "Lizenzen generieren" msgstr "Lizenzen generieren"
#: src/Admin/OrderLicenseController.php:164 #: src/Admin/OrderLicenseController.php:196
msgid "Please set the order domain above before generating licenses." msgid "Please set the order domain above before generating licenses."
msgstr "" msgstr ""
"Bitte legen Sie zuerst die Bestellungs-Domain oben fest, bevor Sie Lizenzen " "Bitte legen Sie zuerst die Bestellungs-Domain oben fest, bevor Sie Lizenzen "
"generieren." "generieren."
#: src/Admin/OrderLicenseController.php:205 #: src/Admin/OrderLicenseController.php:237
msgid "Edit domain" msgid "Edit domain"
msgstr "Domain bearbeiten" msgstr "Domain bearbeiten"
#: src/Admin/OrderLicenseController.php:235 #: src/Admin/OrderLicenseController.php:267
msgid "View in Licenses" msgid "View in Licenses"
msgstr "In Lizenzen anzeigen" msgstr "In Lizenzen anzeigen"
#: src/Admin/OrderLicenseController.php:248 #: src/Admin/OrderLicenseController.php:280
#, php-format #, php-format
msgid "For more actions (revoke, extend, delete), go to the %s page." msgid "For more actions (revoke, extend, delete), go to the %s page."
msgstr "" msgstr ""
"Für weitere Aktionen (widerrufen, verlängern, löschen), gehen Sie zur Seite " "Für weitere Aktionen (widerrufen, verlängern, löschen), gehen Sie zur Seite "
"%s." "%s."
#: src/Admin/OrderLicenseController.php:261 #: src/Admin/OrderLicenseController.php:293
#, php-format #, php-format
msgid "%d licensed product is missing a license." msgid "%d licensed product is missing a license."
msgid_plural "%d licensed products are missing licenses." msgid_plural "%d licensed products are missing licenses."
msgstr[0] "%d lizenziertes Produkt hat keine Lizenz." msgstr[0] "%d lizenziertes Produkt hat keine Lizenz."
msgstr[1] "%d lizenzierte Produkte haben keine Lizenzen." msgstr[1] "%d lizenzierte Produkte haben keine Lizenzen."
#: src/Admin/OrderLicenseController.php:270 #: src/Admin/OrderLicenseController.php:302
msgid "Generate Missing Licenses" msgid "Generate Missing Licenses"
msgstr "Fehlende Lizenzen generieren" msgstr "Fehlende Lizenzen generieren"
#: src/Admin/OrderLicenseController.php:339 #: src/Admin/OrderLicenseController.php:371
msgid "Saved!" msgid "Saved!"
msgstr "Gespeichert!" msgstr "Gespeichert!"
#: src/Admin/OrderLicenseController.php:340 #: src/Admin/OrderLicenseController.php:372
msgid "Error. Please try again." msgid "Error. Please try again."
msgstr "Fehler. Bitte versuchen Sie es erneut." msgstr "Fehler. Bitte versuchen Sie es erneut."
#: src/Admin/OrderLicenseController.php:341 #: src/Admin/OrderLicenseController.php:373
#: src/Frontend/AccountController.php:314 #: src/Checkout/CheckoutBlocksIntegration.php:126
#: src/Frontend/AccountController.php:346 #: src/Frontend/AccountController.php:430
#: src/Frontend/AccountController.php:462
msgid "Please enter a valid domain." msgid "Please enter a valid domain."
msgstr "Bitte geben Sie eine gültige Domain ein." msgstr "Bitte geben Sie eine gültige Domain ein."
#: src/Admin/OrderLicenseController.php:342 #: src/Admin/OrderLicenseController.php:374
msgid "Generating..." msgid "Generating..."
msgstr "Generiere..." msgstr "Generiere..."
#: src/Admin/OrderLicenseController.php:362 #: src/Admin/OrderLicenseController.php:394
#: src/Admin/OrderLicenseController.php:464 #: src/Admin/OrderLicenseController.php:496
msgid "Invalid order ID." msgid "Invalid order ID."
msgstr "Ungültige Bestellungs-ID." msgstr "Ungültige Bestellungs-ID."
#: src/Admin/OrderLicenseController.php:373 #: src/Admin/OrderLicenseController.php:405
#: src/Admin/OrderLicenseController.php:411 #: src/Admin/OrderLicenseController.php:443
msgid "Invalid domain format." msgid "Invalid domain format."
msgstr "Ungültiges Domain-Format." msgstr "Ungültiges Domain-Format."
#: src/Admin/OrderLicenseController.php:381 #: src/Admin/OrderLicenseController.php:413
msgid "Order domain updated." msgid "Order domain updated."
msgstr "Bestellungs-Domain aktualisiert." msgstr "Bestellungs-Domain aktualisiert."
#: src/Admin/OrderLicenseController.php:417 #: src/Admin/OrderLicenseController.php:449
#: src/Frontend/AccountController.php:352 #: src/Frontend/AccountController.php:468
#: src/Frontend/DownloadController.php:117 #: src/Frontend/DownloadController.php:117
msgid "License not found." msgid "License not found."
msgstr "Lizenz nicht gefunden." msgstr "Lizenz nicht gefunden."
#: src/Admin/OrderLicenseController.php:425 #: src/Admin/OrderLicenseController.php:457
msgid "License domain updated." msgid "License domain updated."
msgstr "Lizenz-Domain aktualisiert." msgstr "Lizenz-Domain aktualisiert."
#: src/Admin/OrderLicenseController.php:429 #: src/Admin/OrderLicenseController.php:461
msgid "Failed to update license domain." msgid "Failed to update license domain."
msgstr "Lizenz-Domain konnte nicht aktualisiert werden." msgstr "Lizenz-Domain konnte nicht aktualisiert werden."
#: src/Admin/OrderLicenseController.php:474 #: src/Admin/OrderLicenseController.php:506
msgid "Order must be paid before licenses can be generated." msgid "Order must be paid before licenses can be generated."
msgstr "" msgstr ""
"Die Bestellung muss bezahlt sein, bevor Lizenzen generiert werden können." "Die Bestellung muss bezahlt sein, bevor Lizenzen generiert werden können."
#: src/Admin/OrderLicenseController.php:480 #: src/Admin/OrderLicenseController.php:520
msgid "Please set the order domain before generating licenses." msgid "Please set the order domain before generating licenses."
msgstr "" msgstr ""
"Bitte legen Sie die Bestellungs-Domain fest, bevor Sie Lizenzen generieren." "Bitte legen Sie die Bestellungs-Domain fest, bevor Sie Lizenzen generieren."
#: src/Admin/OrderLicenseController.php:521 #: src/Admin/OrderLicenseController.php:529
#, php-format #, php-format
msgid "%d license generated successfully." msgid "%d license generated successfully."
msgid_plural "%d licenses generated successfully." msgid_plural "%d licenses generated successfully."
msgstr[0] "%d Lizenz erfolgreich generiert." msgstr[0] "%d Lizenz erfolgreich generiert."
msgstr[1] "%d Lizenzen erfolgreich generiert." msgstr[1] "%d Lizenzen erfolgreich generiert."
#: src/Admin/OrderLicenseController.php:534 #: src/Admin/OrderLicenseController.php:542
msgid "All licenses already exist for this order." msgid "All licenses already exist for this order."
msgstr "Alle Lizenzen für diese Bestellung existieren bereits." msgstr "Alle Lizenzen für diese Bestellung existieren bereits."
@@ -1005,11 +1028,23 @@ msgstr ""
"Falls aktiviert, werden Lizenzen standardmässig an die Hauptversion zum " "Falls aktiviert, werden Lizenzen standardmässig an die Hauptversion zum "
"Kaufzeitpunkt gebunden." "Kaufzeitpunkt gebunden."
#: src/Admin/SettingsController.php:219 #: src/Admin/SettingsController.php:206
msgid "Enable Multi-Domain Licensing"
msgstr "Multi-Domain-Lizenzierung aktivieren"
#: src/Admin/SettingsController.php:208
msgid ""
"Allow customers to purchase multiple licenses for different domains at once. "
"Each unit in cart quantity requires a unique domain."
msgstr ""
"Ermöglicht Kunden, mehrere Lizenzen für verschiedene Domains auf einmal zu "
"kaufen. Jede Einheit in der Warenkorbmenge erfordert eine eindeutige Domain."
#: src/Admin/SettingsController.php:226
msgid "Expiration Warning Schedule" msgid "Expiration Warning Schedule"
msgstr "Ablaufwarnung Zeitplan" msgstr "Ablaufwarnung Zeitplan"
#: src/Admin/SettingsController.php:223 #: src/Admin/SettingsController.php:230
#, php-format #, php-format
msgid "" msgid ""
"Configure when expiration warning emails are sent. To customize the email " "Configure when expiration warning emails are sent. To customize the email "
@@ -1019,62 +1054,62 @@ msgstr ""
"Mail-Vorlage anzupassen, zu aktivieren/deaktivieren oder den Betreff zu " "Mail-Vorlage anzupassen, zu aktivieren/deaktivieren oder den Betreff zu "
"ändern, gehen Sie zu %s." "ändern, gehen Sie zu %s."
#: src/Admin/SettingsController.php:225 #: src/Admin/SettingsController.php:232
msgid "WooCommerce > Settings > Emails > License Expiration Warning" msgid "WooCommerce > Settings > Emails > License Expiration Warning"
msgstr "WooCommerce > Einstellungen > E-Mails > Lizenzablauf-Warnung" msgstr "WooCommerce > Einstellungen > E-Mails > Lizenzablauf-Warnung"
#: src/Admin/SettingsController.php:230 #: src/Admin/SettingsController.php:237
msgid "First Warning (Days Before)" msgid "First Warning (Days Before)"
msgstr "Erste Warnung (Tage vorher)" msgstr "Erste Warnung (Tage vorher)"
#: src/Admin/SettingsController.php:232 #: src/Admin/SettingsController.php:239
msgid "Days before expiration to send the first warning email." msgid "Days before expiration to send the first warning email."
msgstr "Tage vor Ablauf, um die erste Warn-E-Mail zu senden." msgstr "Tage vor Ablauf, um die erste Warn-E-Mail zu senden."
#: src/Admin/SettingsController.php:241 #: src/Admin/SettingsController.php:248
msgid "Second Warning (Days Before)" msgid "Second Warning (Days Before)"
msgstr "Zweite Warnung (Tage vorher)" msgstr "Zweite Warnung (Tage vorher)"
#: src/Admin/SettingsController.php:243 #: src/Admin/SettingsController.php:250
msgid "" msgid ""
"Days before expiration to send the second warning email. Set to 0 to disable." "Days before expiration to send the second warning email. Set to 0 to disable."
msgstr "" msgstr ""
"Tage vor Ablauf, um die zweite Warn-E-Mail zu senden. Setzen Sie auf 0, um " "Tage vor Ablauf, um die zweite Warn-E-Mail zu senden. Setzen Sie auf 0, um "
"sie zu deaktivieren." "sie zu deaktivieren."
#: src/Admin/SettingsController.php:283 #: src/Admin/SettingsController.php:290
msgid "Running on localhost - license validation bypassed." msgid "Running on localhost - license validation bypassed."
msgstr "Läuft auf localhost - Lizenzvalidierung übersprungen." msgstr "Läuft auf localhost - Lizenzvalidierung übersprungen."
#: src/Admin/SettingsController.php:291 #: src/Admin/SettingsController.php:298
msgid "License is valid and active." msgid "License is valid and active."
msgstr "Lizenz ist gültig und aktiv." msgstr "Lizenz ist gültig und aktiv."
#: src/Admin/SettingsController.php:297 #: src/Admin/SettingsController.php:304
msgid "License is not valid. Frontend features are disabled." msgid "License is not valid. Frontend features are disabled."
msgstr "Lizenz ist ungültig. Frontend-Funktionen sind deaktiviert." msgstr "Lizenz ist ungültig. Frontend-Funktionen sind deaktiviert."
#: src/Admin/SettingsController.php:308 src/Admin/SettingsController.php:344 #: src/Admin/SettingsController.php:315 src/Admin/SettingsController.php:351
msgid "Verify License" msgid "Verify License"
msgstr "Lizenz überprüfen" msgstr "Lizenz überprüfen"
#: src/Admin/SettingsController.php:322 #: src/Admin/SettingsController.php:329
msgid "Verifying..." msgid "Verifying..."
msgstr "Überprüfe..." msgstr "Überprüfe..."
#: src/Admin/SettingsController.php:341 #: src/Admin/SettingsController.php:348
msgid "Request failed." msgid "Request failed."
msgstr "Anfrage fehlgeschlagen." msgstr "Anfrage fehlgeschlagen."
#: src/Admin/SettingsController.php:458 #: src/Admin/SettingsController.php:473
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen." msgstr "Unzureichende Berechtigungen."
#: src/Admin/SettingsController.php:467 #: src/Admin/SettingsController.php:482
msgid "License verified successfully!" msgid "License verified successfully!"
msgstr "Lizenz erfolgreich überprüft!" msgstr "Lizenz erfolgreich überprüft!"
#: src/Admin/SettingsController.php:469 #: src/Admin/SettingsController.php:484
msgid "License validation failed." msgid "License validation failed."
msgstr "Lizenzvalidierung fehlgeschlagen." msgstr "Lizenzvalidierung fehlgeschlagen."
@@ -1253,7 +1288,7 @@ msgid "Too many requests. Please try again later."
msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut." msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378 #: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
#: src/License/LicenseManager.php:357 #: src/License/LicenseManager.php:403
msgid "License key not found." msgid "License key not found."
msgstr "Lizenzschlüssel nicht gefunden." msgstr "Lizenzschlüssel nicht gefunden."
@@ -1277,89 +1312,145 @@ msgstr "Lizenz konnte nicht aktiviert werden."
msgid "License activated successfully." msgid "License activated successfully."
msgstr "Lizenz erfolgreich aktiviert." msgstr "Lizenz erfolgreich aktiviert."
#: src/Checkout/CheckoutBlocksIntegration.php:101 #: src/Checkout/CheckoutBlocksIntegration.php:78
#: src/Checkout/CheckoutController.php:81 #: src/Checkout/CheckoutBlocksIntegration.php:125
msgid "Domain for License Activation" #: src/Checkout/CheckoutController.php:119
msgstr "Domain für Lizenz-Aktivierung"
#: src/Checkout/CheckoutBlocksIntegration.php:103
#: src/Checkout/CheckoutController.php:93
msgid ""
"Enter the domain where you will use this license (without http:// or www)."
msgstr ""
"Geben Sie die Domain ein, auf der Sie diese Lizenz verwenden möchten (ohne "
"http:// oder www)."
#: src/Checkout/CheckoutBlocksIntegration.php:104
#: src/Checkout/CheckoutController.php:78
msgid "License Domain" msgid "License Domain"
msgstr "Lizenz-Domain" msgstr "Lizenz-Domain"
#: src/Checkout/CheckoutBlocksIntegration.php:105 #: src/Checkout/CheckoutBlocksIntegration.php:85
msgid "Please enter a valid domain for your license activation." msgid "Enter a valid domain (without http:// or www)"
msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz-Aktivierung ein." msgstr "Geben Sie eine gültige Domain ein (ohne http:// oder www)"
#: src/Checkout/CheckoutController.php:82 #: src/Checkout/CheckoutBlocksIntegration.php:121
#: src/Checkout/CheckoutController.php:150
msgid "Enter a unique domain for each license (without http:// or www)."
msgstr ""
"Geben Sie für jede Lizenz eine eindeutige Domain ein (ohne http:// oder www)."
#: src/Checkout/CheckoutBlocksIntegration.php:122
#: src/Checkout/CheckoutController.php:134
msgid ""
"Enter the domain where you will use the license (without http:// or www)."
msgstr ""
"Geben Sie die Domain ein, auf der Sie die Lizenz verwenden möchten (ohne "
"http:// oder www)."
#: src/Checkout/CheckoutBlocksIntegration.php:124
#: src/Checkout/CheckoutController.php:148
msgid "License Domains"
msgstr "Lizenz-Domains"
#: src/Checkout/CheckoutBlocksIntegration.php:127
msgid "Each license requires a unique domain."
msgstr "Jede Lizenz erfordert eine eindeutige Domain."
#: src/Checkout/CheckoutBlocksIntegration.php:128
#: src/Checkout/CheckoutController.php:175
#, php-format
msgid "License %d:"
msgstr "Lizenz %d:"
#: src/Checkout/CheckoutController.php:123
#: src/Checkout/CheckoutController.php:179
msgid "required" msgid "required"
msgstr "erforderlich" msgstr "erforderlich"
#: src/Checkout/CheckoutController.php:115 #: src/Checkout/CheckoutController.php:258
msgid "Please enter a domain for your license activation." msgid "Please enter a domain for your license."
msgstr "Bitte geben Sie eine Domain für Ihre Lizenz-Aktivierung ein." msgstr "Bitte geben Sie eine Domain für Ihre Lizenz ein."
#: src/Checkout/CheckoutController.php:125 #: src/Checkout/CheckoutController.php:264
msgid "Please enter a valid domain name." msgid "Please enter a valid domain for your license."
msgstr "Bitte geben Sie einen gültigen Domain-Namen ein." msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz ein."
#: src/Checkout/CheckoutController.php:164 #: src/Checkout/CheckoutController.php:287
#: src/Checkout/CheckoutController.php:181 #, php-format
#: src/Checkout/CheckoutController.php:185 msgid "Please enter a domain for %1$s (License %2$d)."
msgstr "Bitte geben Sie eine Domain für %1$s (Lizenz %2$d) ein."
#: src/Checkout/CheckoutController.php:302
#, php-format
msgid "Please enter a valid domain for %1$s (License %2$d)."
msgstr "Bitte geben Sie eine gültige Domain für %1$s (Lizenz %2$d) ein."
#: src/Checkout/CheckoutController.php:316
#, php-format
msgid ""
"The domain \"%1$s\" is used multiple times for %2$s. Each license requires a "
"unique domain."
msgstr ""
"Die Domain \"%1$s\" wird mehrfach für %2$s verwendet. Jede Lizenz erfordert "
"eine eindeutige Domain."
#: src/Checkout/CheckoutController.php:419
#: src/Checkout/CheckoutController.php:466
#: src/Checkout/CheckoutController.php:470
msgid "License Domain:" msgid "License Domain:"
msgstr "Lizenz-Domain:" msgstr "Lizenz-Domain:"
#: src/Checkout/StoreApiExtension.php:85 #: src/Checkout/CheckoutController.php:432
#: src/Checkout/CheckoutController.php:483
#: src/Checkout/CheckoutController.php:492
msgid "License Domains:"
msgstr "Lizenz-Domains:"
#: src/Checkout/StoreApiExtension.php:93
msgid "Domains for license activation by product"
msgstr "Domains für Lizenz-Aktivierung nach Produkt"
#: src/Checkout/StoreApiExtension.php:117
msgid "Domain for license activation" msgid "Domain for license activation"
msgstr "Domain für Lizenz-Aktivierung" msgstr "Domain für Lizenz-Aktivierung"
#: src/Email/LicenseEmailController.php:212 #: src/Email/LicenseEmailController.php:212
#: src/Email/LicenseEmailController.php:216 #: src/Email/LicenseEmailController.php:220
#: src/Email/LicenseEmailController.php:320 msgid "License Keys:"
#: src/Email/LicenseExpirationEmail.php:207
#: src/Email/LicenseExpirationEmail.php:270
#: src/Email/LicenseExpiredEmail.php:191 src/Email/LicenseExpiredEmail.php:256
#: src/Frontend/AccountController.php:190
msgid "License Key:"
msgstr "Lizenzschlüssel:" msgstr "Lizenzschlüssel:"
#: src/Email/LicenseEmailController.php:256 #: src/Email/LicenseEmailController.php:268
msgid "Your License Keys" msgid "Your License Keys"
msgstr "Ihre Lizenzschlüssel" msgstr "Ihre Lizenzschlüssel"
#: src/Email/LicenseEmailController.php:260 #: src/Email/LicenseEmailController.php:277
#: src/Email/LicenseEmailController.php:315 #, php-format
msgid "Licensed Domain:" msgid "%d license"
msgstr "Lizensierte Domain:" msgid_plural "%d licenses"
msgstr[0] "%d Lizenz"
msgstr[1] "%d Lizenzen"
#: src/Email/LicenseEmailController.php:287 #: src/Email/LicenseEmailController.php:308
#: src/Email/LicenseEmailController.php:326 #: src/Email/LicenseEmailController.php:352
#: src/Frontend/AccountController.php:218
msgid "Never" msgid "Never"
msgstr "Nie" msgstr "Nie"
#: src/Email/LicenseEmailController.php:296 #: src/Email/LicenseEmailController.php:319
#: src/Email/LicenseEmailController.php:330 #: src/Email/LicenseEmailController.php:357
msgid "You can also view your licenses in your account under \"Licenses\"." msgid "You can also view your licenses in your account under \"Licenses\"."
msgstr "" msgstr ""
"Sie können Ihre Lizenzen auch in Ihrem Konto unter \"Lizenzen\" einsehen." "Sie können Ihre Lizenzen auch in Ihrem Konto unter \"Lizenzen\" einsehen."
#: src/Email/LicenseEmailController.php:311 #: src/Email/LicenseEmailController.php:332
msgid "YOUR LICENSE KEYS" msgid "YOUR LICENSE KEYS"
msgstr "IHRE LIZENZSCHLÜSSEL" msgstr "IHRE LIZENZSCHLÜSSEL"
#: src/Email/LicenseEmailController.php:323 #: src/Email/LicenseEmailController.php:343
#: src/Email/LicenseExpirationEmail.php:207
#: src/Email/LicenseExpirationEmail.php:270
#: src/Email/LicenseExpiredEmail.php:191 src/Email/LicenseExpiredEmail.php:256
msgid "License Key:"
msgstr "Lizenzschlüssel:"
#: src/Email/LicenseEmailController.php:345
#: src/Email/LicenseExpirationEmail.php:215
#: src/Email/LicenseExpirationEmail.php:271
#: src/Email/LicenseExpiredEmail.php:199 src/Email/LicenseExpiredEmail.php:257
msgid "Domain:"
msgstr "Domain:"
#: src/Email/LicenseEmailController.php:347
#: src/Email/LicenseExpirationEmail.php:219 #: src/Email/LicenseExpirationEmail.php:219
#: src/Email/LicenseExpirationEmail.php:272 #: src/Email/LicenseExpirationEmail.php:272
#: src/Frontend/AccountController.php:213
msgid "Expires:" msgid "Expires:"
msgstr "Läuft ab:" msgstr "Läuft ab:"
@@ -1387,13 +1478,6 @@ msgstr ""
msgid "License Expiration Notice" msgid "License Expiration Notice"
msgstr "Lizenzablauf-Benachrichtigung" msgstr "Lizenzablauf-Benachrichtigung"
#: src/Email/LicenseExpirationEmail.php:107
#: src/Email/LicenseExpiredEmail.php:99 src/Frontend/AccountController.php:140
#: src/License/LicenseManager.php:760 src/Product/VersionManager.php:349
#: src/Product/VersionManager.php:361
msgid "Unknown Product"
msgstr "Unbekanntes Produkt"
#: src/Email/LicenseExpirationEmail.php:176 #: src/Email/LicenseExpirationEmail.php:176
#: src/Email/LicenseExpirationEmail.php:246 #: src/Email/LicenseExpirationEmail.php:246
#: src/Email/LicenseExpiredEmail.php:167 src/Email/LicenseExpiredEmail.php:238 #: src/Email/LicenseExpiredEmail.php:167 src/Email/LicenseExpiredEmail.php:238
@@ -1424,13 +1508,6 @@ msgstr "Lizenzdetails"
msgid "Product:" msgid "Product:"
msgstr "Produkt:" msgstr "Produkt:"
#: src/Email/LicenseExpirationEmail.php:215
#: src/Email/LicenseExpirationEmail.php:271
#: src/Email/LicenseExpiredEmail.php:199 src/Email/LicenseExpiredEmail.php:257
#: src/Frontend/AccountController.php:201
msgid "Domain:"
msgstr "Domain:"
#: src/Email/LicenseExpirationEmail.php:235 #: src/Email/LicenseExpirationEmail.php:235
#: src/Email/LicenseExpirationEmail.php:281 #: src/Email/LicenseExpirationEmail.php:281
#: src/Email/LicenseExpiredEmail.php:227 src/Email/LicenseExpiredEmail.php:268 #: src/Email/LicenseExpiredEmail.php:227 src/Email/LicenseExpiredEmail.php:268
@@ -1537,33 +1614,48 @@ msgid "To continue using this product, please renew your license."
msgstr "" msgstr ""
"Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz." "Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz."
#: src/Frontend/AccountController.php:104 #: src/Frontend/AccountController.php:105
msgid "Please log in to view your licenses." msgid "Please log in to view your licenses."
msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen." msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen."
#: src/Frontend/AccountController.php:165 #: src/Frontend/AccountController.php:223
msgid "You have no licenses yet." msgid "You have no licenses yet."
msgstr "Sie haben noch keine Lizenzen." msgstr "Sie haben noch keine Lizenzen."
#: src/Frontend/AccountController.php:226 #: src/Frontend/AccountController.php:245
#, php-format
msgid "Order #%s"
msgstr "Bestellung #%s"
#: src/Frontend/AccountController.php:296
msgid "Available Downloads" msgid "Available Downloads"
msgstr "Verfügbare Downloads" msgstr "Verfügbare Downloads"
#: src/Frontend/AccountController.php:232 #: src/Frontend/AccountController.php:305
#: src/Frontend/AccountController.php:338
#, php-format #, php-format
msgid "Version %s" msgid "Version %s"
msgstr "Version %s" msgstr "Version %s"
#: src/Frontend/AccountController.php:311 #: src/Frontend/AccountController.php:307
#: src/Frontend/AccountController.php:378 msgid "Latest"
msgstr "Neueste"
#: src/Frontend/AccountController.php:327
#, php-format
msgid "Older versions (%d)"
msgstr "Ältere Versionen (%d)"
#: src/Frontend/AccountController.php:427
#: src/Frontend/AccountController.php:494
msgid "License transferred successfully!" msgid "License transferred successfully!"
msgstr "Lizenz erfolgreich übertragen!" msgstr "Lizenz erfolgreich übertragen!"
#: src/Frontend/AccountController.php:312 #: src/Frontend/AccountController.php:428
msgid "Transfer failed. Please try again." msgid "Transfer failed. Please try again."
msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut." msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
#: src/Frontend/AccountController.php:313 #: src/Frontend/AccountController.php:429
msgid "" msgid ""
"Are you sure you want to transfer this license to a new domain? This action " "Are you sure you want to transfer this license to a new domain? This action "
"cannot be undone." "cannot be undone."
@@ -1571,31 +1663,31 @@ msgstr ""
"Sind Sie sicher, dass Sie diese Lizenz auf eine neue Domain übertragen " "Sind Sie sicher, dass Sie diese Lizenz auf eine neue Domain übertragen "
"möchten? Diese Aktion kann nicht rückgängig gemacht werden." "möchten? Diese Aktion kann nicht rückgängig gemacht werden."
#: src/Frontend/AccountController.php:332 #: src/Frontend/AccountController.php:448
msgid "Please log in to transfer a license." msgid "Please log in to transfer a license."
msgstr "Bitte melden Sie sich an, um eine Lizenz zu übertragen." msgstr "Bitte melden Sie sich an, um eine Lizenz zu übertragen."
#: src/Frontend/AccountController.php:338 #: src/Frontend/AccountController.php:454
msgid "Invalid license." msgid "Invalid license."
msgstr "Ungültige Lizenz." msgstr "Ungültige Lizenz."
#: src/Frontend/AccountController.php:356 #: src/Frontend/AccountController.php:472
msgid "You do not have permission to transfer this license." msgid "You do not have permission to transfer this license."
msgstr "Sie haben keine Berechtigung, diese Lizenz zu übertragen." msgstr "Sie haben keine Berechtigung, diese Lizenz zu übertragen."
#: src/Frontend/AccountController.php:361 #: src/Frontend/AccountController.php:477
msgid "Revoked licenses cannot be transferred." msgid "Revoked licenses cannot be transferred."
msgstr "Widerrufene Lizenzen können nicht übertragen werden." msgstr "Widerrufene Lizenzen können nicht übertragen werden."
#: src/Frontend/AccountController.php:365 #: src/Frontend/AccountController.php:481
msgid "Expired licenses cannot be transferred." msgid "Expired licenses cannot be transferred."
msgstr "Abgelaufene Lizenzen können nicht übertragen werden." msgstr "Abgelaufene Lizenzen können nicht übertragen werden."
#: src/Frontend/AccountController.php:370 #: src/Frontend/AccountController.php:486
msgid "The new domain is the same as the current domain." msgid "The new domain is the same as the current domain."
msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain." msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain."
#: src/Frontend/AccountController.php:382 #: src/Frontend/AccountController.php:498
msgid "Failed to transfer license. Please try again." msgid "Failed to transfer license. Please try again."
msgstr "Lizenzübertragung fehlgeschlagen. Bitte versuchen Sie es erneut." msgstr "Lizenzübertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
@@ -1650,19 +1742,19 @@ msgstr "Keine Download-Datei für diese Version verfügbar."
msgid "Download file not found." msgid "Download file not found."
msgstr "Download-Datei nicht gefunden." msgstr "Download-Datei nicht gefunden."
#: src/License/LicenseManager.php:366 #: src/License/LicenseManager.php:412
msgid "This license has been revoked." msgid "This license has been revoked."
msgstr "Diese Lizenz wurde widerrufen." msgstr "Diese Lizenz wurde widerrufen."
#: src/License/LicenseManager.php:376 #: src/License/LicenseManager.php:422
msgid "This license has expired." msgid "This license has expired."
msgstr "Diese Lizenz ist abgelaufen." msgstr "Diese Lizenz ist abgelaufen."
#: src/License/LicenseManager.php:384 #: src/License/LicenseManager.php:430
msgid "This license is inactive." msgid "This license is inactive."
msgstr "Diese Lizenz ist inaktiv." msgstr "Diese Lizenz ist inaktiv."
#: src/License/LicenseManager.php:394 #: src/License/LicenseManager.php:440
msgid "This license is not valid for this domain." msgid "This license is not valid for this domain."
msgstr "Diese Lizenz ist für diese Domain nicht gültig." msgstr "Diese Lizenz ist für diese Domain nicht gültig."
@@ -1674,18 +1766,18 @@ msgstr "Lizenzeinstellungen nicht konfiguriert."
msgid "Could not connect to license server." msgid "Could not connect to license server."
msgstr "Verbindung zum Lizenzserver konnte nicht hergestellt werden." msgstr "Verbindung zum Lizenzserver konnte nicht hergestellt werden."
#: src/Plugin.php:260 #: src/Plugin.php:318
msgid "WC Licensed Product" msgid "WC Licensed Product"
msgstr "WC Licensed Product" msgstr "WC Licensed Product"
#: src/Plugin.php:261 #: src/Plugin.php:319
msgid "" msgid ""
"Plugin license is not configured or invalid. Frontend features are disabled." "Plugin license is not configured or invalid. Frontend features are disabled."
msgstr "" msgstr ""
"Plugin-Lizenz ist nicht konfiguriert oder ungültig. Frontend-Funktionen sind " "Plugin-Lizenz ist nicht konfiguriert oder ungültig. Frontend-Funktionen sind "
"deaktiviert." "deaktiviert."
#: src/Plugin.php:262 #: src/Plugin.php:320
msgid "Configure License" msgid "Configure License"
msgstr "Lizenz konfigurieren" msgstr "Lizenz konfigurieren"
@@ -1763,13 +1855,29 @@ msgstr "Anhangs-Datei nicht gefunden."
msgid "File checksum does not match. Expected: %1$s, Got: %2$s" msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s" msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s"
#: wc-licensed-product.php:61 #: templates/frontend/licenses.html.twig:72
#, php-format msgid "API Verification Secret"
msgid "%s requires WooCommerce to be installed and active." msgstr "API-Verifizierungs-Secret"
msgstr "%s benötigt WooCommerce als installierte und aktivierte Erweiterung."
#: wc-licensed-product.php:119 #: templates/frontend/licenses.html.twig:77
msgid "WC Licensed Product requires WooCommerce to be installed and active." msgid "Use this secret to verify signed API responses. Keep it secure."
msgstr "" msgstr "Verwenden Sie dieses Secret, um signierte API-Antworten zu verifizieren. Bewahren Sie es sicher auf."
"WC Licensed Product benötigt WooCommerce als installierte und aktivierte "
"Erweiterung." #, php-format
#~ msgid "%s requires WooCommerce to be installed and active."
#~ msgstr ""
#~ "%s benötigt WooCommerce als installierte und aktivierte Erweiterung."
#~ msgid "WC Licensed Product requires WooCommerce to be installed and active."
#~ msgstr ""
#~ "WC Licensed Product benötigt WooCommerce als installierte und aktivierte "
#~ "Erweiterung."
#~ msgid "Domain for License Activation"
#~ msgstr "Domain für Lizenz-Aktivierung"
#~ msgid "Please enter a valid domain name."
#~ msgstr "Bitte geben Sie einen gültigen Domain-Namen ein."
#~ msgid "Licensed Domain:"
#~ msgstr "Lizensierte Domain:"

View File

@@ -6,9 +6,9 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WC Licensed Product 0.4.0\n" "Project-Id-Version: WC Licensed Product 0.5.2\n"
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
"POT-Creation-Date: 2026-01-24 16:39+0100\n" "POT-Creation-Date: 2026-01-26 15:29+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -20,9 +20,9 @@ msgstr ""
#: src/Admin/AdminController.php:76 src/Admin/AdminController.php:77 #: src/Admin/AdminController.php:76 src/Admin/AdminController.php:77
#: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200 #: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200
#: src/Admin/OrderLicenseController.php:128 #: src/Admin/OrderLicenseController.php:149
#: src/Admin/OrderLicenseController.php:249 #: src/Admin/OrderLicenseController.php:281
#: src/Frontend/AccountController.php:90 #: src/Frontend/AccountController.php:91
msgid "Licenses" msgid "Licenses"
msgstr "" msgstr ""
@@ -42,7 +42,7 @@ msgstr ""
msgid "Search failed" msgid "Search failed"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:144 src/Admin/OrderLicenseController.php:338 #: src/Admin/AdminController.php:144 src/Admin/OrderLicenseController.php:370
msgid "Saving..." msgid "Saving..."
msgstr "" msgstr ""
@@ -66,32 +66,33 @@ msgstr ""
#: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341 #: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341
#: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382 #: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382
#: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:212 #: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:244
#: src/Frontend/AccountController.php:271 #: src/Frontend/AccountController.php:387
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:150 src/Admin/AdminController.php:1340 #: src/Admin/AdminController.php:150 src/Admin/AdminController.php:1340
#: src/Admin/AdminController.php:1360 src/Admin/AdminController.php:1381 #: src/Admin/AdminController.php:1360 src/Admin/AdminController.php:1381
#: src/Admin/OrderLicenseController.php:119 #: src/Admin/OrderLicenseController.php:139
#: src/Admin/OrderLicenseController.php:209 #: src/Admin/OrderLicenseController.php:241
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:151 src/Admin/AdminController.php:266 #: src/Admin/AdminController.php:151 src/Admin/AdminController.php:266
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613 #: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
#: src/Admin/DashboardWidgetController.php:136 #: src/Admin/DashboardWidgetController.php:136
#: src/Admin/OrderLicenseController.php:228 #: src/Admin/OrderLicenseController.php:260
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:110 #: src/Admin/SettingsController.php:192 src/Frontend/AccountController.php:286
#: src/Product/LicensedProductType.php:110
#: src/Product/LicensedProductType.php:158 #: src/Product/LicensedProductType.php:158
msgid "Lifetime" msgid "Lifetime"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:309 #: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:425
msgid "Copied!" msgid "Copied!"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:310 #: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:426
msgid "Copy failed" msgid "Copy failed"
msgstr "" msgstr ""
@@ -125,9 +126,9 @@ msgstr ""
#: src/Admin/AdminController.php:173 src/Admin/AdminController.php:213 #: src/Admin/AdminController.php:173 src/Admin/AdminController.php:213
#: src/Admin/AdminController.php:249 src/Admin/AdminController.php:301 #: src/Admin/AdminController.php:249 src/Admin/AdminController.php:301
#: src/Admin/AdminController.php:339 src/Admin/AdminController.php:369 #: src/Admin/AdminController.php:339 src/Admin/AdminController.php:369
#: src/Admin/OrderLicenseController.php:355 #: src/Admin/OrderLicenseController.php:387
#: src/Admin/OrderLicenseController.php:394 #: src/Admin/OrderLicenseController.php:426
#: src/Admin/OrderLicenseController.php:458 #: src/Admin/OrderLicenseController.php:490
#: src/Admin/VersionAdminController.php:259 #: src/Admin/VersionAdminController.php:259
#: src/Admin/VersionAdminController.php:328 #: src/Admin/VersionAdminController.php:328
#: src/Admin/VersionAdminController.php:354 #: src/Admin/VersionAdminController.php:354
@@ -135,7 +136,7 @@ msgid "Permission denied."
msgstr "" msgstr ""
#: src/Admin/AdminController.php:195 src/Admin/AdminController.php:1019 #: src/Admin/AdminController.php:195 src/Admin/AdminController.php:1019
#: src/Admin/OrderLicenseController.php:195 #: src/Admin/OrderLicenseController.php:227
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr ""
@@ -145,7 +146,7 @@ msgstr ""
#: src/Admin/AdminController.php:220 src/Admin/AdminController.php:256 #: src/Admin/AdminController.php:220 src/Admin/AdminController.php:256
#: src/Admin/AdminController.php:308 src/Admin/AdminController.php:345 #: src/Admin/AdminController.php:308 src/Admin/AdminController.php:345
#: src/Admin/OrderLicenseController.php:401 #: src/Admin/OrderLicenseController.php:433
msgid "Invalid license ID." msgid "Invalid license ID."
msgstr "" msgstr ""
@@ -177,7 +178,7 @@ msgstr ""
msgid "Invalid date format." msgid "Invalid date format."
msgstr "" msgstr ""
#: src/Admin/AdminController.php:312 src/Admin/OrderLicenseController.php:405 #: src/Admin/AdminController.php:312 src/Admin/OrderLicenseController.php:437
msgid "Domain cannot be empty." msgid "Domain cannot be empty."
msgstr "" msgstr ""
@@ -205,8 +206,8 @@ msgstr ""
#: src/Admin/AdminController.php:493 src/Admin/AdminController.php:511 #: src/Admin/AdminController.php:493 src/Admin/AdminController.php:511
#: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549 #: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549
#: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621 #: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621
#: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:454 #: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:469
#: src/Frontend/AccountController.php:326 #: src/Frontend/AccountController.php:442
msgid "Security check failed." msgid "Security check failed."
msgstr "" msgstr ""
@@ -267,7 +268,7 @@ msgstr ""
msgid "Export to CSV" msgid "Export to CSV"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:968 wc-licensed-product.php:137 #: src/Admin/AdminController.php:968
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@@ -410,9 +411,11 @@ msgstr ""
msgid "Showing" msgid "Showing"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1259 #: src/Admin/AdminController.php:1259 src/Email/LicenseEmailController.php:338
msgid "license" msgid "license"
msgstr "" msgid_plural "licenses"
msgstr[0] ""
msgstr[1] ""
#: src/Admin/AdminController.php:1259 #: src/Admin/AdminController.php:1259
msgid "licenses" msgid "licenses"
@@ -470,15 +473,14 @@ msgid "Apply"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1291 src/Admin/AdminController.php:1442 #: src/Admin/AdminController.php:1291 src/Admin/AdminController.php:1442
#: src/Admin/AdminController.php:1491 src/Admin/OrderLicenseController.php:171 #: src/Admin/AdminController.php:1491 src/Admin/OrderLicenseController.php:203
#: src/Admin/SettingsController.php:142 #: src/Admin/SettingsController.php:142
#: src/Email/LicenseEmailController.php:269 #: src/Email/LicenseEmailController.php:287
msgid "License Key" msgid "License Key"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1292 src/Admin/AdminController.php:1443 #: src/Admin/AdminController.php:1292 src/Admin/AdminController.php:1443
#: src/Admin/AdminController.php:1608 src/Admin/OrderLicenseController.php:172 #: src/Admin/AdminController.php:1608 src/Admin/OrderLicenseController.php:204
#: src/Email/LicenseEmailController.php:268
msgid "Product" msgid "Product"
msgstr "" msgstr ""
@@ -489,12 +491,15 @@ msgid "Customer"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445 #: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445
#: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:173 #: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:205
#: src/Checkout/CheckoutBlocksIntegration.php:129
#: src/Checkout/CheckoutController.php:122
#: src/Email/LicenseEmailController.php:288
msgid "Domain" msgid "Domain"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1295 src/Admin/AdminController.php:1446 #: src/Admin/AdminController.php:1295 src/Admin/AdminController.php:1446
#: src/Admin/OrderLicenseController.php:174 #: src/Admin/OrderLicenseController.php:206
#: src/Admin/VersionAdminController.php:140 #: src/Admin/VersionAdminController.php:140
msgid "Status" msgid "Status"
msgstr "" msgstr ""
@@ -505,13 +510,13 @@ msgstr ""
#: src/Admin/AdminController.php:1297 src/Admin/AdminController.php:1448 #: src/Admin/AdminController.php:1297 src/Admin/AdminController.php:1448
#: src/Admin/AdminController.php:1611 src/Admin/AdminController.php:1613 #: src/Admin/AdminController.php:1611 src/Admin/AdminController.php:1613
#: src/Admin/OrderLicenseController.php:175 #: src/Admin/OrderLicenseController.php:207
#: src/Email/LicenseEmailController.php:270 #: src/Email/LicenseEmailController.php:289
msgid "Expires" msgid "Expires"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1298 src/Admin/AdminController.php:1449 #: src/Admin/AdminController.php:1298 src/Admin/AdminController.php:1449
#: src/Admin/OrderLicenseController.php:176 #: src/Admin/OrderLicenseController.php:208
#: src/Admin/VersionAdminController.php:142 #: src/Admin/VersionAdminController.php:142
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@@ -520,7 +525,7 @@ msgstr ""
msgid "No licenses found." msgid "No licenses found."
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:194 #: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:263
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "" msgstr ""
@@ -540,11 +545,11 @@ msgstr ""
msgid "Test" msgid "Test"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:207 #: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:270
msgid "Transfer to new domain" msgid "Transfer to new domain"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:209 #: src/Admin/AdminController.php:1400
msgid "Transfer" msgid "Transfer"
msgstr "" msgstr ""
@@ -568,27 +573,27 @@ msgstr ""
msgid "Testing license..." msgid "Testing license..."
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:249 #: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:365
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:250 #: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:366
msgid "Transfer License to New Domain" msgid "Transfer License to New Domain"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:255 #: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:371
msgid "Current Domain" msgid "Current Domain"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:260 #: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:376
msgid "New Domain" msgid "New Domain"
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:264 #: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:380
msgid "Enter the new domain without http:// or www." msgid "Enter the new domain without http:// or www."
msgstr "" msgstr ""
#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:269 #: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:385
msgid "Transfer License" msgid "Transfer License"
msgstr "" msgstr ""
@@ -758,8 +763,8 @@ msgid "Product Licenses"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:78 #: src/Admin/OrderLicenseController.php:78
#: src/Admin/OrderLicenseController.php:367 #: src/Admin/OrderLicenseController.php:399
#: src/Admin/OrderLicenseController.php:469 #: src/Admin/OrderLicenseController.php:501
msgid "Order not found." msgid "Order not found."
msgstr "" msgstr ""
@@ -767,130 +772,148 @@ msgstr ""
msgid "This order does not contain licensed products." msgid "This order does not contain licensed products."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:107 #: src/Admin/OrderLicenseController.php:109
msgid "Order Domain" msgid "Order Domains"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:109 #: src/Admin/OrderLicenseController.php:113
msgid "Domains specified during checkout (multi-domain order)."
msgstr ""
#: src/Admin/OrderLicenseController.php:119
#: src/Checkout/CheckoutController.php:436
#: src/Checkout/CheckoutController.php:486
#: src/Checkout/CheckoutController.php:496
#: src/Email/LicenseExpirationEmail.php:107
#: src/Email/LicenseExpiredEmail.php:99 src/Frontend/AccountController.php:148
#: src/License/LicenseManager.php:806 src/Product/VersionManager.php:349
#: src/Product/VersionManager.php:361
msgid "Unknown Product"
msgstr ""
#: src/Admin/OrderLicenseController.php:129
msgid "" msgid ""
"The domain specified during checkout. Changing this will not automatically " "The domain specified during checkout. Changing this will not automatically "
"update existing license domains." "update existing license domains."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:117 #: src/Admin/OrderLicenseController.php:137
#: src/Checkout/CheckoutBlocksIntegration.php:102 #: src/Checkout/CheckoutBlocksIntegration.php:83
#: src/Checkout/CheckoutController.php:89 #: src/Checkout/CheckoutBlocksIntegration.php:119
#: src/Checkout/CheckoutController.php:130
#: src/Checkout/CheckoutController.php:186
msgid "example.com" msgid "example.com"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:144 #: src/Admin/OrderLicenseController.php:176
msgid "No licenses have been generated for this order yet." msgid "No licenses have been generated for this order yet."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:147 #: src/Admin/OrderLicenseController.php:179
msgid "" msgid ""
"Licenses should be generated automatically when an order is paid. If " "Licenses should be generated automatically when an order is paid. If "
"missing, check that a domain was specified during checkout." "missing, check that a domain was specified during checkout."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:150 #: src/Admin/OrderLicenseController.php:182
msgid "Licenses will be generated when the order is marked as paid/completed." msgid "Licenses will be generated when the order is marked as paid/completed."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:156 #: src/Admin/OrderLicenseController.php:188
msgid "Generate Licenses" msgid "Generate Licenses"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:164 #: src/Admin/OrderLicenseController.php:196
msgid "Please set the order domain above before generating licenses." msgid "Please set the order domain above before generating licenses."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:205 #: src/Admin/OrderLicenseController.php:237
msgid "Edit domain" msgid "Edit domain"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:235 #: src/Admin/OrderLicenseController.php:267
msgid "View in Licenses" msgid "View in Licenses"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:248 #: src/Admin/OrderLicenseController.php:280
#, php-format #, php-format
msgid "For more actions (revoke, extend, delete), go to the %s page." msgid "For more actions (revoke, extend, delete), go to the %s page."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:261 #: src/Admin/OrderLicenseController.php:293
#, php-format #, php-format
msgid "%d licensed product is missing a license." msgid "%d licensed product is missing a license."
msgid_plural "%d licensed products are missing licenses." msgid_plural "%d licensed products are missing licenses."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: src/Admin/OrderLicenseController.php:270 #: src/Admin/OrderLicenseController.php:302
msgid "Generate Missing Licenses" msgid "Generate Missing Licenses"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:339 #: src/Admin/OrderLicenseController.php:371
msgid "Saved!" msgid "Saved!"
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:340 #: src/Admin/OrderLicenseController.php:372
msgid "Error. Please try again." msgid "Error. Please try again."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:341 #: src/Admin/OrderLicenseController.php:373
#: src/Frontend/AccountController.php:314 #: src/Checkout/CheckoutBlocksIntegration.php:126
#: src/Frontend/AccountController.php:346 #: src/Frontend/AccountController.php:430
#: src/Frontend/AccountController.php:462
msgid "Please enter a valid domain." msgid "Please enter a valid domain."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:342 #: src/Admin/OrderLicenseController.php:374
msgid "Generating..." msgid "Generating..."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:362 #: src/Admin/OrderLicenseController.php:394
#: src/Admin/OrderLicenseController.php:464 #: src/Admin/OrderLicenseController.php:496
msgid "Invalid order ID." msgid "Invalid order ID."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:373 #: src/Admin/OrderLicenseController.php:405
#: src/Admin/OrderLicenseController.php:411 #: src/Admin/OrderLicenseController.php:443
msgid "Invalid domain format." msgid "Invalid domain format."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:381 #: src/Admin/OrderLicenseController.php:413
msgid "Order domain updated." msgid "Order domain updated."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:417 #: src/Admin/OrderLicenseController.php:449
#: src/Frontend/AccountController.php:352 #: src/Frontend/AccountController.php:468
#: src/Frontend/DownloadController.php:117 #: src/Frontend/DownloadController.php:117
msgid "License not found." msgid "License not found."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:425 #: src/Admin/OrderLicenseController.php:457
msgid "License domain updated." msgid "License domain updated."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:429 #: src/Admin/OrderLicenseController.php:461
msgid "Failed to update license domain." msgid "Failed to update license domain."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:474 #: src/Admin/OrderLicenseController.php:506
msgid "Order must be paid before licenses can be generated." msgid "Order must be paid before licenses can be generated."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:480 #: src/Admin/OrderLicenseController.php:520
msgid "Please set the order domain before generating licenses." msgid "Please set the order domain before generating licenses."
msgstr "" msgstr ""
#: src/Admin/OrderLicenseController.php:521 #: src/Admin/OrderLicenseController.php:529
#, php-format #, php-format
msgid "%d license generated successfully." msgid "%d license generated successfully."
msgid_plural "%d licenses generated successfully." msgid_plural "%d licenses generated successfully."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: src/Admin/OrderLicenseController.php:534 #: src/Admin/OrderLicenseController.php:542
msgid "All licenses already exist for this order." msgid "All licenses already exist for this order."
msgstr "" msgstr ""
@@ -976,71 +999,81 @@ msgid ""
"default." "default."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:219 #: src/Admin/SettingsController.php:206
msgid "Enable Multi-Domain Licensing"
msgstr ""
#: src/Admin/SettingsController.php:208
msgid ""
"Allow customers to purchase multiple licenses for different domains at once. "
"Each unit in cart quantity requires a unique domain."
msgstr ""
#: src/Admin/SettingsController.php:226
msgid "Expiration Warning Schedule" msgid "Expiration Warning Schedule"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:223 #: src/Admin/SettingsController.php:230
#, php-format #, php-format
msgid "" msgid ""
"Configure when expiration warning emails are sent. To customize the email " "Configure when expiration warning emails are sent. To customize the email "
"template, enable/disable, or change the subject, go to %s." "template, enable/disable, or change the subject, go to %s."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:225 #: src/Admin/SettingsController.php:232
msgid "WooCommerce > Settings > Emails > License Expiration Warning" msgid "WooCommerce > Settings > Emails > License Expiration Warning"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:230 #: src/Admin/SettingsController.php:237
msgid "First Warning (Days Before)" msgid "First Warning (Days Before)"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:232 #: src/Admin/SettingsController.php:239
msgid "Days before expiration to send the first warning email." msgid "Days before expiration to send the first warning email."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:241 #: src/Admin/SettingsController.php:248
msgid "Second Warning (Days Before)" msgid "Second Warning (Days Before)"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:243 #: src/Admin/SettingsController.php:250
msgid "" msgid ""
"Days before expiration to send the second warning email. Set to 0 to disable." "Days before expiration to send the second warning email. Set to 0 to disable."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:283 #: src/Admin/SettingsController.php:290
msgid "Running on localhost - license validation bypassed." msgid "Running on localhost - license validation bypassed."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:291 #: src/Admin/SettingsController.php:298
msgid "License is valid and active." msgid "License is valid and active."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:297 #: src/Admin/SettingsController.php:304
msgid "License is not valid. Frontend features are disabled." msgid "License is not valid. Frontend features are disabled."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:308 src/Admin/SettingsController.php:344 #: src/Admin/SettingsController.php:315 src/Admin/SettingsController.php:351
msgid "Verify License" msgid "Verify License"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:322 #: src/Admin/SettingsController.php:329
msgid "Verifying..." msgid "Verifying..."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:341 #: src/Admin/SettingsController.php:348
msgid "Request failed." msgid "Request failed."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:458 #: src/Admin/SettingsController.php:473
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:467 #: src/Admin/SettingsController.php:482
msgid "License verified successfully!" msgid "License verified successfully!"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:469 #: src/Admin/SettingsController.php:484
msgid "License validation failed." msgid "License validation failed."
msgstr "" msgstr ""
@@ -1211,7 +1244,7 @@ msgid "Too many requests. Please try again later."
msgstr "" msgstr ""
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378 #: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
#: src/License/LicenseManager.php:357 #: src/License/LicenseManager.php:403
msgid "License key not found." msgid "License key not found."
msgstr "" msgstr ""
@@ -1235,86 +1268,139 @@ msgstr ""
msgid "License activated successfully." msgid "License activated successfully."
msgstr "" msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:101 #: src/Checkout/CheckoutBlocksIntegration.php:78
#: src/Checkout/CheckoutController.php:81 #: src/Checkout/CheckoutBlocksIntegration.php:125
msgid "Domain for License Activation" #: src/Checkout/CheckoutController.php:119
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:103
#: src/Checkout/CheckoutController.php:93
msgid ""
"Enter the domain where you will use this license (without http:// or www)."
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:104
#: src/Checkout/CheckoutController.php:78
msgid "License Domain" msgid "License Domain"
msgstr "" msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:105 #: src/Checkout/CheckoutBlocksIntegration.php:85
msgid "Please enter a valid domain for your license activation." msgid "Enter a valid domain (without http:// or www)"
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:82 #: src/Checkout/CheckoutBlocksIntegration.php:121
#: src/Checkout/CheckoutController.php:150
msgid "Enter a unique domain for each license (without http:// or www)."
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:122
#: src/Checkout/CheckoutController.php:134
msgid ""
"Enter the domain where you will use the license (without http:// or www)."
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:124
#: src/Checkout/CheckoutController.php:148
msgid "License Domains"
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:127
msgid "Each license requires a unique domain."
msgstr ""
#: src/Checkout/CheckoutBlocksIntegration.php:128
#: src/Checkout/CheckoutController.php:175
#, php-format
msgid "License %d:"
msgstr ""
#: src/Checkout/CheckoutController.php:123
#: src/Checkout/CheckoutController.php:179
msgid "required" msgid "required"
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:115 #: src/Checkout/CheckoutController.php:258
msgid "Please enter a domain for your license activation." msgid "Please enter a domain for your license."
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:125 #: src/Checkout/CheckoutController.php:264
msgid "Please enter a valid domain name." msgid "Please enter a valid domain for your license."
msgstr "" msgstr ""
#: src/Checkout/CheckoutController.php:164 #: src/Checkout/CheckoutController.php:287
#: src/Checkout/CheckoutController.php:181 #, php-format
#: src/Checkout/CheckoutController.php:185 msgid "Please enter a domain for %1$s (License %2$d)."
msgstr ""
#: src/Checkout/CheckoutController.php:302
#, php-format
msgid "Please enter a valid domain for %1$s (License %2$d)."
msgstr ""
#: src/Checkout/CheckoutController.php:316
#, php-format
msgid ""
"The domain \"%1$s\" is used multiple times for %2$s. Each license requires a "
"unique domain."
msgstr ""
#: src/Checkout/CheckoutController.php:419
#: src/Checkout/CheckoutController.php:466
#: src/Checkout/CheckoutController.php:470
msgid "License Domain:" msgid "License Domain:"
msgstr "" msgstr ""
#: src/Checkout/StoreApiExtension.php:85 #: src/Checkout/CheckoutController.php:432
#: src/Checkout/CheckoutController.php:483
#: src/Checkout/CheckoutController.php:492
msgid "License Domains:"
msgstr ""
#: src/Checkout/StoreApiExtension.php:93
msgid "Domains for license activation by product"
msgstr ""
#: src/Checkout/StoreApiExtension.php:117
msgid "Domain for license activation" msgid "Domain for license activation"
msgstr "" msgstr ""
#: src/Email/LicenseEmailController.php:212 #: src/Email/LicenseEmailController.php:212
#: src/Email/LicenseEmailController.php:216 #: src/Email/LicenseEmailController.php:220
#: src/Email/LicenseEmailController.php:320 msgid "License Keys:"
#: src/Email/LicenseExpirationEmail.php:207
#: src/Email/LicenseExpirationEmail.php:270
#: src/Email/LicenseExpiredEmail.php:191 src/Email/LicenseExpiredEmail.php:256
#: src/Frontend/AccountController.php:190
msgid "License Key:"
msgstr "" msgstr ""
#: src/Email/LicenseEmailController.php:256 #: src/Email/LicenseEmailController.php:268
msgid "Your License Keys" msgid "Your License Keys"
msgstr "" msgstr ""
#: src/Email/LicenseEmailController.php:260 #: src/Email/LicenseEmailController.php:277
#: src/Email/LicenseEmailController.php:315 #, php-format
msgid "Licensed Domain:" msgid "%d license"
msgstr "" msgid_plural "%d licenses"
msgstr[0] ""
msgstr[1] ""
#: src/Email/LicenseEmailController.php:287 #: src/Email/LicenseEmailController.php:308
#: src/Email/LicenseEmailController.php:326 #: src/Email/LicenseEmailController.php:352
#: src/Frontend/AccountController.php:218
msgid "Never" msgid "Never"
msgstr "" msgstr ""
#: src/Email/LicenseEmailController.php:296 #: src/Email/LicenseEmailController.php:319
#: src/Email/LicenseEmailController.php:330 #: src/Email/LicenseEmailController.php:357
msgid "You can also view your licenses in your account under \"Licenses\"." msgid "You can also view your licenses in your account under \"Licenses\"."
msgstr "" msgstr ""
#: src/Email/LicenseEmailController.php:311 #: src/Email/LicenseEmailController.php:332
msgid "YOUR LICENSE KEYS" msgid "YOUR LICENSE KEYS"
msgstr "" msgstr ""
#: src/Email/LicenseEmailController.php:323 #: src/Email/LicenseEmailController.php:343
#: src/Email/LicenseExpirationEmail.php:207
#: src/Email/LicenseExpirationEmail.php:270
#: src/Email/LicenseExpiredEmail.php:191 src/Email/LicenseExpiredEmail.php:256
msgid "License Key:"
msgstr ""
#: src/Email/LicenseEmailController.php:345
#: src/Email/LicenseExpirationEmail.php:215
#: src/Email/LicenseExpirationEmail.php:271
#: src/Email/LicenseExpiredEmail.php:199 src/Email/LicenseExpiredEmail.php:257
msgid "Domain:"
msgstr ""
#: src/Email/LicenseEmailController.php:347
#: src/Email/LicenseExpirationEmail.php:219 #: src/Email/LicenseExpirationEmail.php:219
#: src/Email/LicenseExpirationEmail.php:272 #: src/Email/LicenseExpirationEmail.php:272
#: src/Frontend/AccountController.php:213
msgid "Expires:" msgid "Expires:"
msgstr "" msgstr ""
@@ -1338,13 +1424,6 @@ msgstr ""
msgid "License Expiration Notice" msgid "License Expiration Notice"
msgstr "" msgstr ""
#: src/Email/LicenseExpirationEmail.php:107
#: src/Email/LicenseExpiredEmail.php:99 src/Frontend/AccountController.php:140
#: src/License/LicenseManager.php:760 src/Product/VersionManager.php:349
#: src/Product/VersionManager.php:361
msgid "Unknown Product"
msgstr ""
#: src/Email/LicenseExpirationEmail.php:176 #: src/Email/LicenseExpirationEmail.php:176
#: src/Email/LicenseExpirationEmail.php:246 #: src/Email/LicenseExpirationEmail.php:246
#: src/Email/LicenseExpiredEmail.php:167 src/Email/LicenseExpiredEmail.php:238 #: src/Email/LicenseExpiredEmail.php:167 src/Email/LicenseExpiredEmail.php:238
@@ -1375,13 +1454,6 @@ msgstr ""
msgid "Product:" msgid "Product:"
msgstr "" msgstr ""
#: src/Email/LicenseExpirationEmail.php:215
#: src/Email/LicenseExpirationEmail.php:271
#: src/Email/LicenseExpiredEmail.php:199 src/Email/LicenseExpiredEmail.php:257
#: src/Frontend/AccountController.php:201
msgid "Domain:"
msgstr ""
#: src/Email/LicenseExpirationEmail.php:235 #: src/Email/LicenseExpirationEmail.php:235
#: src/Email/LicenseExpirationEmail.php:281 #: src/Email/LicenseExpirationEmail.php:281
#: src/Email/LicenseExpiredEmail.php:227 src/Email/LicenseExpiredEmail.php:268 #: src/Email/LicenseExpiredEmail.php:227 src/Email/LicenseExpiredEmail.php:268
@@ -1481,63 +1553,78 @@ msgstr ""
msgid "To continue using this product, please renew your license." msgid "To continue using this product, please renew your license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:104 #: src/Frontend/AccountController.php:105
msgid "Please log in to view your licenses." msgid "Please log in to view your licenses."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:165 #: src/Frontend/AccountController.php:223
msgid "You have no licenses yet." msgid "You have no licenses yet."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:226 #: src/Frontend/AccountController.php:245
#, php-format
msgid "Order #%s"
msgstr ""
#: src/Frontend/AccountController.php:296
msgid "Available Downloads" msgid "Available Downloads"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:232 #: src/Frontend/AccountController.php:305
#: src/Frontend/AccountController.php:338
#, php-format #, php-format
msgid "Version %s" msgid "Version %s"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:311 #: src/Frontend/AccountController.php:307
#: src/Frontend/AccountController.php:378 msgid "Latest"
msgstr ""
#: src/Frontend/AccountController.php:327
#, php-format
msgid "Older versions (%d)"
msgstr ""
#: src/Frontend/AccountController.php:427
#: src/Frontend/AccountController.php:494
msgid "License transferred successfully!" msgid "License transferred successfully!"
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:312 #: src/Frontend/AccountController.php:428
msgid "Transfer failed. Please try again." msgid "Transfer failed. Please try again."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:313 #: src/Frontend/AccountController.php:429
msgid "" msgid ""
"Are you sure you want to transfer this license to a new domain? This action " "Are you sure you want to transfer this license to a new domain? This action "
"cannot be undone." "cannot be undone."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:332 #: src/Frontend/AccountController.php:448
msgid "Please log in to transfer a license." msgid "Please log in to transfer a license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:338 #: src/Frontend/AccountController.php:454
msgid "Invalid license." msgid "Invalid license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:356 #: src/Frontend/AccountController.php:472
msgid "You do not have permission to transfer this license." msgid "You do not have permission to transfer this license."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:361 #: src/Frontend/AccountController.php:477
msgid "Revoked licenses cannot be transferred." msgid "Revoked licenses cannot be transferred."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:365 #: src/Frontend/AccountController.php:481
msgid "Expired licenses cannot be transferred." msgid "Expired licenses cannot be transferred."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:370 #: src/Frontend/AccountController.php:486
msgid "The new domain is the same as the current domain." msgid "The new domain is the same as the current domain."
msgstr "" msgstr ""
#: src/Frontend/AccountController.php:382 #: src/Frontend/AccountController.php:498
msgid "Failed to transfer license. Please try again." msgid "Failed to transfer license. Please try again."
msgstr "" msgstr ""
@@ -1592,19 +1679,19 @@ msgstr ""
msgid "Download file not found." msgid "Download file not found."
msgstr "" msgstr ""
#: src/License/LicenseManager.php:366 #: src/License/LicenseManager.php:412
msgid "This license has been revoked." msgid "This license has been revoked."
msgstr "" msgstr ""
#: src/License/LicenseManager.php:376 #: src/License/LicenseManager.php:422
msgid "This license has expired." msgid "This license has expired."
msgstr "" msgstr ""
#: src/License/LicenseManager.php:384 #: src/License/LicenseManager.php:430
msgid "This license is inactive." msgid "This license is inactive."
msgstr "" msgstr ""
#: src/License/LicenseManager.php:394 #: src/License/LicenseManager.php:440
msgid "This license is not valid for this domain." msgid "This license is not valid for this domain."
msgstr "" msgstr ""
@@ -1616,16 +1703,16 @@ msgstr ""
msgid "Could not connect to license server." msgid "Could not connect to license server."
msgstr "" msgstr ""
#: src/Plugin.php:260 #: src/Plugin.php:318
msgid "WC Licensed Product" msgid "WC Licensed Product"
msgstr "" msgstr ""
#: src/Plugin.php:261 #: src/Plugin.php:319
msgid "" msgid ""
"Plugin license is not configured or invalid. Frontend features are disabled." "Plugin license is not configured or invalid. Frontend features are disabled."
msgstr "" msgstr ""
#: src/Plugin.php:262 #: src/Plugin.php:320
msgid "Configure License" msgid "Configure License"
msgstr "" msgstr ""
@@ -1701,11 +1788,10 @@ msgstr ""
msgid "File checksum does not match. Expected: %1$s, Got: %2$s" msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
msgstr "" msgstr ""
#: wc-licensed-product.php:61 #: templates/frontend/licenses.html.twig:72
#, php-format msgid "API Verification Secret"
msgid "%s requires WooCommerce to be installed and active."
msgstr "" msgstr ""
#: wc-licensed-product.php:119 #: templates/frontend/licenses.html.twig:77
msgid "WC Licensed Product requires WooCommerce to be installed and active." msgid "Use this secret to verify signed API responses. Keep it secure."
msgstr "" msgstr ""

Binary file not shown.

View File

@@ -0,0 +1 @@
cf8769c861d77c327f178049d5fac0d4e47679cc1a1d35c5b613e4cd3fb8674f wc-licensed-product-0.4.0.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316 wc-licensed-product-0.5.0.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
a489f0b8cfcd7d5d9b2021b7ff581b9f1a56468dfde87bbb06bb4555d11f7556 wc-licensed-product-0.5.1.zip

View File

@@ -94,8 +94,10 @@ final class OrderLicenseController
return; return;
} }
// Get order domain // Check for multi-domain format first, then fall back to legacy single domain
$orderDomain = $order->get_meta('_licensed_product_domain'); $multiDomainData = $order->get_meta('_licensed_product_domains');
$legacyDomain = $order->get_meta('_licensed_product_domain');
$hasMultiDomain = !empty($multiDomainData) && is_array($multiDomainData);
// Get licenses for this order // Get licenses for this order
$licenses = $this->licenseManager->getLicensesByOrder($order->get_id()); $licenses = $this->licenseManager->getLicensesByOrder($order->get_id());
@@ -104,23 +106,42 @@ final class OrderLicenseController
?> ?>
<div class="wclp-order-licenses"> <div class="wclp-order-licenses">
<div class="wclp-order-domain-section"> <div class="wclp-order-domain-section">
<h4><?php esc_html_e('Order Domain', 'wc-licensed-product'); ?></h4> <h4><?php esc_html_e('Order Domains', 'wc-licensed-product'); ?></h4>
<p class="description">
<?php esc_html_e('The domain specified during checkout. Changing this will not automatically update existing license domains.', 'wc-licensed-product'); ?> <?php if ($hasMultiDomain): ?>
</p> <p class="description">
<div class="wclp-inline-edit"> <?php esc_html_e('Domains specified during checkout (multi-domain order).', 'wc-licensed-product'); ?>
<input type="text" </p>
id="wclp-order-domain" <div class="wclp-multi-domain-display" style="margin-top: 10px;">
class="regular-text" <?php foreach ($multiDomainData as $item): ?>
value="<?php echo esc_attr($orderDomain); ?>" <?php
data-order-id="<?php echo esc_attr($order->get_id()); ?>" $product = wc_get_product($item['product_id']);
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>" /> $productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
<button type="button" class="button" id="wclp-save-order-domain"> ?>
<?php esc_html_e('Save', 'wc-licensed-product'); ?> <div class="wclp-product-domains-item" style="margin-bottom: 10px; padding: 10px; background: #f8f8f8; border-radius: 4px;">
</button> <strong><?php echo esc_html($productName); ?>:</strong><br>
<span class="spinner"></span> <code><?php echo esc_html(implode(', ', $item['domains'])); ?></code>
<span class="wclp-status-message"></span> </div>
</div> <?php endforeach; ?>
</div>
<?php else: ?>
<p class="description">
<?php esc_html_e('The domain specified during checkout. Changing this will not automatically update existing license domains.', 'wc-licensed-product'); ?>
</p>
<div class="wclp-inline-edit">
<input type="text"
id="wclp-order-domain"
class="regular-text"
value="<?php echo esc_attr($legacyDomain); ?>"
data-order-id="<?php echo esc_attr($order->get_id()); ?>"
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>" />
<button type="button" class="button" id="wclp-save-order-domain">
<?php esc_html_e('Save', 'wc-licensed-product'); ?>
</button>
<span class="spinner"></span>
<span class="wclp-status-message"></span>
</div>
<?php endif; ?>
</div> </div>
<hr /> <hr />
@@ -128,15 +149,26 @@ final class OrderLicenseController
<h4><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h4> <h4><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h4>
<?php <?php
// Count licensed products to check if all have licenses // Count expected licenses based on domain data
$licensedProductCount = 0; $expectedLicenses = 0;
foreach ($order->get_items() as $item) { if ($hasMultiDomain) {
$product = $item->get_product(); // Multi-domain: count total domains across all products
if ($product && $product->is_type('licensed')) { foreach ($multiDomainData as $item) {
$licensedProductCount++; if (isset($item['domains']) && is_array($item['domains'])) {
$expectedLicenses += count($item['domains']);
}
}
} else {
// Legacy: one license per licensed product
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$expectedLicenses++;
}
} }
} }
$missingLicenses = $licensedProductCount - count($licenses); $missingLicenses = $expectedLicenses - count($licenses);
$hasDomainData = $hasMultiDomain || !empty($legacyDomain);
?> ?>
<?php if (empty($licenses)): ?> <?php if (empty($licenses)): ?>
@@ -150,7 +182,7 @@ final class OrderLicenseController
<em><?php esc_html_e('Licenses will be generated when the order is marked as paid/completed.', 'wc-licensed-product'); ?></em> <em><?php esc_html_e('Licenses will be generated when the order is marked as paid/completed.', 'wc-licensed-product'); ?></em>
<?php endif; ?> <?php endif; ?>
</p> </p>
<?php if ($orderDomain && $order->is_paid()): ?> <?php if ($hasDomainData && $order->is_paid()): ?>
<p style="margin-top: 10px;"> <p style="margin-top: 10px;">
<button type="button" class="button button-primary" id="wclp-generate-licenses" data-order-id="<?php echo esc_attr($order->get_id()); ?>"> <button type="button" class="button button-primary" id="wclp-generate-licenses" data-order-id="<?php echo esc_attr($order->get_id()); ?>">
<?php esc_html_e('Generate Licenses', 'wc-licensed-product'); ?> <?php esc_html_e('Generate Licenses', 'wc-licensed-product'); ?>
@@ -158,7 +190,7 @@ final class OrderLicenseController
<span class="spinner" style="float: none; margin-top: 4px;"></span> <span class="spinner" style="float: none; margin-top: 4px;"></span>
<span class="wclp-generate-status"></span> <span class="wclp-generate-status"></span>
</p> </p>
<?php elseif (!$orderDomain): ?> <?php elseif (!$hasDomainData): ?>
<p class="description" style="margin-top: 10px; color: #d63638;"> <p class="description" style="margin-top: 10px; color: #d63638;">
<span class="dashicons dashicons-warning"></span> <span class="dashicons dashicons-warning"></span>
<?php esc_html_e('Please set the order domain above before generating licenses.', 'wc-licensed-product'); ?> <?php esc_html_e('Please set the order domain above before generating licenses.', 'wc-licensed-product'); ?>
@@ -251,7 +283,7 @@ final class OrderLicenseController
?> ?>
</p> </p>
<?php if ($missingLicenses > 0 && $orderDomain && $order->is_paid()): ?> <?php if ($missingLicenses > 0 && $hasDomainData && $order->is_paid()): ?>
<p style="margin-top: 10px;"> <p style="margin-top: 10px;">
<span class="dashicons dashicons-warning" style="color: #dba617;"></span> <span class="dashicons dashicons-warning" style="color: #dba617;"></span>
<?php <?php
@@ -474,68 +506,138 @@ final class OrderLicenseController
wp_send_json_error(['message' => __('Order must be paid before licenses can be generated.', 'wc-licensed-product')]); wp_send_json_error(['message' => __('Order must be paid before licenses can be generated.', 'wc-licensed-product')]);
} }
// Get domain // Check for multi-domain format first
$domain = $order->get_meta('_licensed_product_domain'); $multiDomainData = $order->get_meta('_licensed_product_domains');
if (empty($domain)) { $legacyDomain = $order->get_meta('_licensed_product_domain');
if (!empty($multiDomainData) && is_array($multiDomainData)) {
// Multi-domain format
$result = $this->generateMultiDomainLicenses($order, $multiDomainData);
} elseif (!empty($legacyDomain)) {
// Legacy single domain format
$result = $this->generateLegacyLicenses($order, $legacyDomain);
} else {
wp_send_json_error(['message' => __('Please set the order domain before generating licenses.', 'wc-licensed-product')]); wp_send_json_error(['message' => __('Please set the order domain before generating licenses.', 'wc-licensed-product')]);
return;
} }
// Generate licenses for each licensed product if ($result['generated'] > 0) {
$generated = 0;
$skipped = 0;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$license = $this->licenseManager->generateLicense(
$orderId,
$product->get_id(),
$order->get_customer_id(),
$domain
);
if ($license) {
// Check if this is a new license or existing
$existingLicenses = $this->licenseManager->getLicensesByOrder($orderId);
$isNew = true;
foreach ($existingLicenses as $existing) {
if ($existing->getProductId() === $product->get_id() && $existing->getId() !== $license->getId()) {
$isNew = false;
break;
}
}
if ($isNew) {
$generated++;
} else {
$skipped++;
}
}
}
}
if ($generated > 0) {
wp_send_json_success([ wp_send_json_success([
'message' => sprintf( 'message' => sprintf(
/* translators: %d: Number of licenses generated */ /* translators: %d: Number of licenses generated */
_n( _n(
'%d license generated successfully.', '%d license generated successfully.',
'%d licenses generated successfully.', '%d licenses generated successfully.',
$generated, $result['generated'],
'wc-licensed-product' 'wc-licensed-product'
), ),
$generated $result['generated']
), ),
'generated' => $generated, 'generated' => $result['generated'],
'skipped' => $skipped, 'skipped' => $result['skipped'],
'reload' => true, 'reload' => true,
]); ]);
} else { } else {
wp_send_json_success([ wp_send_json_success([
'message' => __('All licenses already exist for this order.', 'wc-licensed-product'), 'message' => __('All licenses already exist for this order.', 'wc-licensed-product'),
'generated' => 0, 'generated' => 0,
'skipped' => $skipped, 'skipped' => $result['skipped'],
'reload' => false, 'reload' => false,
]); ]);
} }
} }
/**
* Generate licenses for multi-domain format
*/
private function generateMultiDomainLicenses(\WC_Order $order, array $domainData): array
{
$generated = 0;
$skipped = 0;
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
// Index domains by product ID
$domainsByProduct = [];
foreach ($domainData as $item) {
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
$domainsByProduct[(int) $item['product_id']] = $item['domains'];
}
}
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$product->is_type('licensed')) {
continue;
}
$productId = $product->get_id();
$domains = $domainsByProduct[$productId] ?? [];
// Get existing licenses for this product
$existingLicenses = $this->licenseManager->getLicensesByOrderAndProduct($orderId, $productId);
$existingDomains = array_map(fn($l) => $l->getDomain(), $existingLicenses);
foreach ($domains as $domain) {
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
// Skip if license already exists for this domain
if (in_array($normalizedDomain, $existingDomains, true)) {
$skipped++;
continue;
}
$license = $this->licenseManager->generateLicense(
$orderId,
$productId,
$customerId,
$normalizedDomain
);
if ($license) {
$generated++;
}
}
}
return ['generated' => $generated, 'skipped' => $skipped];
}
/**
* Generate licenses for legacy single domain format
*/
private function generateLegacyLicenses(\WC_Order $order, string $domain): array
{
$generated = 0;
$skipped = 0;
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$product->is_type('licensed')) {
continue;
}
// Check if license already exists
$existing = $this->licenseManager->getLicenseByOrderAndProduct($orderId, $product->get_id());
if ($existing) {
$skipped++;
continue;
}
$license = $this->licenseManager->generateLicense(
$orderId,
$product->get_id(),
$customerId,
$domain
);
if ($license) {
$generated++;
}
}
return ['generated' => $generated, 'skipped' => $skipped];
}
} }

View File

@@ -202,6 +202,13 @@ final class SettingsController
'id' => 'wc_licensed_product_default_bind_to_version', 'id' => 'wc_licensed_product_default_bind_to_version',
'default' => 'no', 'default' => 'no',
], ],
'enable_multi_domain' => [
'name' => __('Enable Multi-Domain Licensing', 'wc-licensed-product'),
'type' => 'checkbox',
'desc' => __('Allow customers to purchase multiple licenses for different domains at once. Each unit in cart quantity requires a unique domain.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_enable_multi_domain',
'default' => 'no',
],
'section_end' => [ 'section_end' => [
'type' => 'sectionend', 'type' => 'sectionend',
'id' => 'wc_licensed_product_section_defaults_end', 'id' => 'wc_licensed_product_section_defaults_end',
@@ -387,6 +394,14 @@ final class SettingsController
return get_option('wc_licensed_product_default_bind_to_version', 'no') === 'yes'; return get_option('wc_licensed_product_default_bind_to_version', 'no') === 'yes';
} }
/**
* Check if multi-domain licensing is enabled
*/
public static function isMultiDomainEnabled(): bool
{
return get_option('wc_licensed_product_enable_multi_domain', 'no') === 'yes';
}
/** /**
* Check if expiration warning emails are enabled * Check if expiration warning emails are enabled
* This checks both the WooCommerce email setting and the old setting for backwards compatibility * This checks both the WooCommerce email setting and the old setting for backwards compatibility

View File

@@ -147,9 +147,52 @@ final class ResponseSigner
*/ */
private function deriveKey(string $licenseKey): string private function deriveKey(string $licenseKey): string
{ {
// HKDF-like key derivation return self::deriveCustomerSecret($licenseKey, $this->serverSecret);
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true); }
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret); /**
* Derive a customer-specific secret from a license key
*
* This secret is unique per license and can be shared with the customer
* to verify signed API responses. Each customer gets their own secret
* derived from their license key.
*
* @param string $licenseKey The customer's license key
* @param string $serverSecret The server's master secret
* @return string The derived secret (64 hex characters)
*/
public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
{
// HKDF-like key derivation
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
}
/**
* Get the customer secret for a license key using the configured server secret
*
* @param string $licenseKey The customer's license key
* @return string|null The derived secret, or null if server secret is not configured
*/
public static function getCustomerSecretForLicense(string $licenseKey): ?string
{
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : '';
if (empty($serverSecret)) {
return null;
}
return self::deriveCustomerSecret($licenseKey, $serverSecret);
}
/**
* Check if response signing is enabled
*
* @return bool True if server secret is configured
*/
public static function isSigningEnabled(): bool
{
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET);
} }
} }

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Checkout; namespace Jeremias\WcLicensedProduct\Checkout;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface; use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
/** /**
* Integration with WooCommerce Checkout Blocks * Integration with WooCommerce Checkout Blocks
@@ -30,7 +31,7 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
public function initialize(): void public function initialize(): void
{ {
$this->registerScripts(); $this->registerScripts();
$this->registerBlockExtensionData(); $this->registerAdditionalCheckoutFields();
} }
/** /**
@@ -45,7 +46,7 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
wp_register_script( wp_register_script(
'wc-licensed-product-checkout-blocks', 'wc-licensed-product-checkout-blocks',
$scriptUrl, $scriptUrl,
['wc-blocks-checkout', 'wp-element', 'wp-components', 'wp-i18n'], ['wc-blocks-checkout', 'wp-element', 'wp-components', 'wp-i18n', 'wp-plugins', 'wp-data'],
WC_LICENSED_PRODUCT_VERSION, WC_LICENSED_PRODUCT_VERSION,
true true
); );
@@ -59,20 +60,33 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
} }
/** /**
* Register block extension data * Register additional checkout fields using WooCommerce Blocks API
*/ */
private function registerBlockExtensionData(): void private function registerAdditionalCheckoutFields(): void
{ {
// Pass data to the checkout block script add_action('woocommerce_blocks_loaded', function (): void {
add_filter( // Check if the function exists (WooCommerce 8.9+)
'woocommerce_blocks_checkout_block_registration_data', if (!function_exists('woocommerce_register_additional_checkout_field')) {
function (array $data): array { return;
$data['wc-licensed-product'] = [
'hasLicensedProducts' => $this->cartHasLicensedProducts(),
];
return $data;
} }
);
// Register the domain field using WooCommerce's checkout fields API
// For single domain mode only (multi-domain uses custom JS component)
if (!SettingsController::isMultiDomainEnabled()) {
woocommerce_register_additional_checkout_field([
'id' => 'wc-licensed-product/domain',
'label' => __('License Domain', 'wc-licensed-product'),
'location' => 'order',
'type' => 'text',
'required' => false,
'attributes' => [
'placeholder' => __('example.com', 'wc-licensed-product'),
'pattern' => '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$',
'title' => __('Enter a valid domain (without http:// or www)', 'wc-licensed-product'),
],
]);
}
});
} }
/** /**
@@ -96,13 +110,23 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
*/ */
public function get_script_data(): array public function get_script_data(): array
{ {
$isMultiDomain = SettingsController::isMultiDomainEnabled();
return [ return [
'hasLicensedProducts' => $this->cartHasLicensedProducts(), 'hasLicensedProducts' => $this->cartHasLicensedProducts(),
'fieldLabel' => __('Domain for License Activation', 'wc-licensed-product'), 'licensedProducts' => $this->getLicensedProductsFromCart(),
'isMultiDomainEnabled' => $isMultiDomain,
'fieldPlaceholder' => __('example.com', 'wc-licensed-product'), 'fieldPlaceholder' => __('example.com', 'wc-licensed-product'),
'fieldDescription' => __('Enter the domain where you will use this license (without http:// or www).', 'wc-licensed-product'), 'fieldDescription' => $isMultiDomain
'sectionTitle' => __('License Domain', 'wc-licensed-product'), ? __('Enter a unique domain for each license (without http:// or www).', 'wc-licensed-product')
'validationError' => __('Please enter a valid domain for your license activation.', 'wc-licensed-product'), : __('Enter the domain where you will use the license (without http:// or www).', 'wc-licensed-product'),
'sectionTitle' => $isMultiDomain
? __('License Domains', 'wc-licensed-product')
: __('License Domain', 'wc-licensed-product'),
'validationError' => __('Please enter a valid domain.', 'wc-licensed-product'),
'duplicateError' => __('Each license requires a unique domain.', 'wc-licensed-product'),
'licenseLabel' => __('License %d:', 'wc-licensed-product'),
'singleDomainLabel' => __('Domain', 'wc-licensed-product'),
]; ];
} }
@@ -110,18 +134,34 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
* Check if cart contains licensed products * Check if cart contains licensed products
*/ */
private function cartHasLicensedProducts(): bool private function cartHasLicensedProducts(): bool
{
return !empty($this->getLicensedProductsFromCart());
}
/**
* Get licensed products from cart with quantities
*
* @return array<int, array{product_id: int, name: string, quantity: int}>
*/
private function getLicensedProductsFromCart(): array
{ {
if (!WC()->cart) { if (!WC()->cart) {
return false; return [];
} }
$licensedProducts = [];
foreach (WC()->cart->get_cart() as $cartItem) { foreach (WC()->cart->get_cart() as $cartItem) {
$product = $cartItem['data']; $product = $cartItem['data'];
if ($product && $product->is_type('licensed')) { if ($product && $product->is_type('licensed')) {
return true; $productId = $product->get_id();
$licensedProducts[] = [
'product_id' => $productId,
'name' => $product->get_name(),
'quantity' => (int) $cartItem['quantity'],
];
} }
} }
return false; return $licensedProducts;
} }
} }

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Checkout; namespace Jeremias\WcLicensedProduct\Checkout;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
/** /**
* Handles checkout modifications for licensed products * Handles checkout modifications for licensed products
@@ -50,35 +51,75 @@ final class CheckoutController
*/ */
private function cartHasLicensedProducts(): bool private function cartHasLicensedProducts(): bool
{ {
if (!WC()->cart) { return !empty($this->getLicensedProductsFromCart());
return false;
}
foreach (WC()->cart->get_cart() as $cartItem) {
$product = $cartItem['data'];
if ($product && $product->is_type('licensed')) {
return true;
}
}
return false;
} }
/** /**
* Add domain field to checkout form * Get licensed products from cart with quantities
*
* @return array<int, array{product_id: int, name: string, quantity: int}>
*/
private function getLicensedProductsFromCart(): array
{
if (!WC()->cart) {
return [];
}
$licensedProducts = [];
foreach (WC()->cart->get_cart() as $cartItem) {
$product = $cartItem['data'];
if ($product && $product->is_type('licensed')) {
$productId = $product->get_id();
$licensedProducts[$productId] = [
'product_id' => $productId,
'name' => $product->get_name(),
'quantity' => (int) $cartItem['quantity'],
];
}
}
return $licensedProducts;
}
/**
* Add domain fields to checkout form
* Shows multiple domain fields if multi-domain is enabled, otherwise single field
*/ */
public function addDomainField(): void public function addDomainField(): void
{ {
if (!$this->cartHasLicensedProducts()) { $licensedProducts = $this->getLicensedProductsFromCart();
if (empty($licensedProducts)) {
return; return;
} }
// Check if multi-domain licensing is enabled
if (SettingsController::isMultiDomainEnabled()) {
$this->renderMultiDomainFields($licensedProducts);
} else {
$this->renderSingleDomainField();
}
}
/**
* Render single domain field (legacy mode)
*/
private function renderSingleDomainField(): void
{
$savedValue = '';
// Check POST data first (validation failure case)
if (isset($_POST['licensed_product_domain'])) {
$savedValue = sanitize_text_field($_POST['licensed_product_domain']);
} elseif (WC()->session) {
$savedValue = WC()->session->get('licensed_product_domain', '');
}
?> ?>
<div id="licensed-product-domain-field"> <div id="licensed-product-domain-field">
<h3><?php esc_html_e('License Domain', 'wc-licensed-product'); ?></h3> <h3><?php esc_html_e('License Domain', 'wc-licensed-product'); ?></h3>
<p class="form-row form-row-wide"> <p class="form-row form-row-wide">
<label for="licensed_product_domain"> <label for="licensed_product_domain">
<?php esc_html_e('Domain for License Activation', 'wc-licensed-product'); ?> <?php esc_html_e('Domain', 'wc-licensed-product'); ?>
<abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr> <abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr>
</label> </label>
<input <input
@@ -87,10 +128,10 @@ final class CheckoutController
name="licensed_product_domain" name="licensed_product_domain"
id="licensed_product_domain" id="licensed_product_domain"
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>" placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
value="<?php echo esc_attr(WC()->checkout->get_value('licensed_product_domain')); ?>" value="<?php echo esc_attr($savedValue); ?>"
/> />
<span class="description"> <span class="description">
<?php esc_html_e('Enter the domain where you will use this license (without http:// or www).', 'wc-licensed-product'); ?> <?php esc_html_e('Enter the domain where you will use the license (without http:// or www).', 'wc-licensed-product'); ?>
</span> </span>
</p> </p>
</div> </div>
@@ -98,62 +139,276 @@ final class CheckoutController
} }
/** /**
* Validate domain field during checkout * Render multi-domain fields (one per quantity)
*/
private function renderMultiDomainFields(array $licensedProducts): void
{
?>
<div id="licensed-product-domain-fields">
<h3><?php esc_html_e('License Domains', 'wc-licensed-product'); ?></h3>
<p class="wclp-domain-description">
<?php esc_html_e('Enter a unique domain for each license (without http:// or www).', 'wc-licensed-product'); ?>
</p>
<?php foreach ($licensedProducts as $productId => $productData): ?>
<div class="wclp-product-domains" data-product-id="<?php echo esc_attr($productId); ?>">
<h4>
<?php
echo esc_html($productData['name']);
if ($productData['quantity'] > 1) {
printf(' (×%d)', $productData['quantity']);
}
?>
</h4>
<?php for ($i = 0; $i < $productData['quantity']; $i++): ?>
<?php
$fieldName = sprintf('licensed_domains[%d][%d]', $productId, $i);
$fieldId = sprintf('licensed_domain_%d_%d', $productId, $i);
$savedValue = $this->getSavedDomainValue($productId, $i);
?>
<p class="form-row form-row-wide wclp-domain-row">
<label for="<?php echo esc_attr($fieldId); ?>">
<?php
printf(
/* translators: %d: license number */
esc_html__('License %d:', 'wc-licensed-product'),
$i + 1
);
?>
<abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr>
</label>
<input
type="text"
class="input-text wclp-domain-input"
name="<?php echo esc_attr($fieldName); ?>"
id="<?php echo esc_attr($fieldId); ?>"
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
value="<?php echo esc_attr($savedValue); ?>"
/>
</p>
<?php endfor; ?>
</div>
<?php endforeach; ?>
</div>
<style>
#licensed-product-domain-fields { margin-bottom: 20px; }
#licensed-product-domain-fields h3 { margin-bottom: 10px; }
.wclp-domain-description { margin-bottom: 15px; color: #666; }
.wclp-product-domains { margin-bottom: 20px; padding: 15px; background: #f8f8f8; border-radius: 4px; }
.wclp-product-domains h4 { margin: 0 0 10px 0; font-size: 1em; }
.wclp-domain-row { margin-bottom: 10px; }
.wclp-domain-row:last-child { margin-bottom: 0; }
.wclp-domain-row label { display: block; margin-bottom: 5px; }
</style>
<?php
}
/**
* Get saved domain value from session/POST
*/
private function getSavedDomainValue(int $productId, int $index): string
{
// Check POST data first (validation failure case)
if (isset($_POST['licensed_domains'][$productId][$index])) {
return sanitize_text_field($_POST['licensed_domains'][$productId][$index]);
}
// Check session for blocks checkout
if (WC()->session) {
$sessionDomains = WC()->session->get('licensed_product_domains', []);
foreach ($sessionDomains as $item) {
if (isset($item['product_id']) && (int) $item['product_id'] === $productId) {
if (isset($item['domains'][$index])) {
return $item['domains'][$index];
}
}
}
}
return '';
}
/**
* Validate domain fields during checkout
*/ */
public function validateDomainField(): void public function validateDomainField(): void
{ {
if (!$this->cartHasLicensedProducts()) { $licensedProducts = $this->getLicensedProductsFromCart();
if (empty($licensedProducts)) {
return; return;
} }
$domain = isset($_POST['licensed_product_domain']) // Check if multi-domain licensing is enabled
? sanitize_text_field($_POST['licensed_product_domain']) if (SettingsController::isMultiDomainEnabled()) {
: ''; $this->validateMultiDomainFields($licensedProducts);
} else {
if (empty($domain)) { $this->validateSingleDomainField();
wc_add_notice(
__('Please enter a domain for your license activation.', 'wc-licensed-product'),
'error'
);
return;
}
// Validate domain format
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
if (!$this->isValidDomain($normalizedDomain)) {
wc_add_notice(
__('Please enter a valid domain name.', 'wc-licensed-product'),
'error'
);
} }
} }
/** /**
* Save domain field to order meta * Validate single domain field
*/ */
public function saveDomainField(int $orderId): void private function validateSingleDomainField(): void
{ {
if (!$this->cartHasLicensedProducts()) { $domain = isset($_POST['licensed_product_domain']) ? sanitize_text_field($_POST['licensed_product_domain']) : '';
if (empty($domain)) {
wc_add_notice(__('Please enter a domain for your license.', 'wc-licensed-product'), 'error');
return; return;
} }
if (isset($_POST['licensed_product_domain']) && !empty($_POST['licensed_product_domain'])) { $normalizedDomain = $this->licenseManager->normalizeDomain($domain);
$domain = sanitize_text_field($_POST['licensed_product_domain']); if (!$this->isValidDomain($normalizedDomain)) {
$normalizedDomain = $this->licenseManager->normalizeDomain($domain); wc_add_notice(__('Please enter a valid domain for your license.', 'wc-licensed-product'), 'error');
}
}
$order = wc_get_order($orderId); /**
if ($order) { * Validate multi-domain fields
$order->update_meta_data('_licensed_product_domain', $normalizedDomain); */
$order->save(); private function validateMultiDomainFields(array $licensedProducts): void
{
$licensedDomains = $_POST['licensed_domains'] ?? [];
foreach ($licensedProducts as $productId => $productData) {
$productDomains = $licensedDomains[$productId] ?? [];
$normalizedDomains = [];
for ($i = 0; $i < $productData['quantity']; $i++) {
$domain = isset($productDomains[$i]) ? sanitize_text_field($productDomains[$i]) : '';
// Check if domain is empty
if (empty($domain)) {
wc_add_notice(
sprintf(
/* translators: 1: product name, 2: license number */
__('Please enter a domain for %1$s (License %2$d).', 'wc-licensed-product'),
$productData['name'],
$i + 1
),
'error'
);
continue;
}
// Validate domain format
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
if (!$this->isValidDomain($normalizedDomain)) {
wc_add_notice(
sprintf(
/* translators: 1: product name, 2: license number */
__('Please enter a valid domain for %1$s (License %2$d).', 'wc-licensed-product'),
$productData['name'],
$i + 1
),
'error'
);
continue;
}
// Check for duplicate domains within same product
if (in_array($normalizedDomain, $normalizedDomains, true)) {
wc_add_notice(
sprintf(
/* translators: 1: domain name, 2: product name */
__('The domain "%1$s" is used multiple times for %2$s. Each license requires a unique domain.', 'wc-licensed-product'),
$normalizedDomain,
$productData['name']
),
'error'
);
} else {
$normalizedDomains[] = $normalizedDomain;
}
} }
} }
} }
/** /**
* Display domain in admin order view * Save domain fields to order meta
*/
public function saveDomainField(int $orderId): void
{
$licensedProducts = $this->getLicensedProductsFromCart();
if (empty($licensedProducts)) {
return;
}
$order = wc_get_order($orderId);
if (!$order) {
return;
}
// Check if multi-domain licensing is enabled
if (SettingsController::isMultiDomainEnabled()) {
$this->saveMultiDomainFields($order, $licensedProducts);
} else {
$this->saveSingleDomainField($order);
}
}
/**
* Save single domain field to order meta (legacy format)
*/
private function saveSingleDomainField(\WC_Order $order): void
{
$domain = isset($_POST['licensed_product_domain']) ? sanitize_text_field($_POST['licensed_product_domain']) : '';
if (!empty($domain)) {
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
$order->update_meta_data('_licensed_product_domain', $normalizedDomain);
$order->save();
}
}
/**
* Save multi-domain fields to order meta
*/
private function saveMultiDomainFields(\WC_Order $order, array $licensedProducts): void
{
$licensedDomains = $_POST['licensed_domains'] ?? [];
$domainData = [];
foreach ($licensedProducts as $productId => $productData) {
$productDomains = $licensedDomains[$productId] ?? [];
$normalizedDomains = [];
for ($i = 0; $i < $productData['quantity']; $i++) {
$domain = isset($productDomains[$i]) ? sanitize_text_field($productDomains[$i]) : '';
if (!empty($domain)) {
$normalizedDomains[] = $this->licenseManager->normalizeDomain($domain);
}
}
if (!empty($normalizedDomains)) {
$domainData[] = [
'product_id' => $productId,
'domains' => $normalizedDomains,
];
}
}
if (!empty($domainData)) {
$order->update_meta_data('_licensed_product_domains', $domainData);
$order->save();
}
}
/**
* Display domains in admin order view
*/ */
public function displayDomainInAdmin(\WC_Order $order): void public function displayDomainInAdmin(\WC_Order $order): void
{ {
// Try new multi-domain format first
$domainData = $order->get_meta('_licensed_product_domains');
if (!empty($domainData) && is_array($domainData)) {
$this->displayMultiDomainsInAdmin($domainData);
return;
}
// Fall back to legacy single domain
$domain = $order->get_meta('_licensed_product_domain'); $domain = $order->get_meta('_licensed_product_domain');
if (!$domain) { if (!$domain) {
return; return;
@@ -168,10 +423,40 @@ final class CheckoutController
} }
/** /**
* Display domain in order emails * Display multi-domain data in admin
*/
private function displayMultiDomainsInAdmin(array $domainData): void
{
?>
<div class="wclp-order-domains">
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
<?php foreach ($domainData as $item): ?>
<?php
$product = wc_get_product($item['product_id']);
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
?>
<p style="margin: 5px 0 5px 15px;">
<em><?php echo esc_html($productName); ?>:</em><br>
<?php echo esc_html(implode(', ', $item['domains'])); ?>
</p>
<?php endforeach; ?>
</div>
<?php
}
/**
* Display domains in order emails
*/ */
public function displayDomainInEmail(\WC_Order $order, bool $sentToAdmin, bool $plainText): void public function displayDomainInEmail(\WC_Order $order, bool $sentToAdmin, bool $plainText): void
{ {
// Try new multi-domain format first
$domainData = $order->get_meta('_licensed_product_domains');
if (!empty($domainData) && is_array($domainData)) {
$this->displayMultiDomainsInEmail($domainData, $plainText);
return;
}
// Fall back to legacy single domain
$domain = $order->get_meta('_licensed_product_domain'); $domain = $order->get_meta('_licensed_product_domain');
if (!$domain) { if (!$domain) {
return; return;
@@ -189,6 +474,37 @@ final class CheckoutController
} }
} }
/**
* Display multi-domain data in email
*/
private function displayMultiDomainsInEmail(array $domainData, bool $plainText): void
{
if ($plainText) {
echo "\n" . esc_html__('License Domains:', 'wc-licensed-product') . "\n";
foreach ($domainData as $item) {
$product = wc_get_product($item['product_id']);
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
echo ' ' . esc_html($productName) . ': ' . esc_html(implode(', ', $item['domains'])) . "\n";
}
} else {
?>
<div style="margin-bottom: 15px;">
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
<?php foreach ($domainData as $item): ?>
<?php
$product = wc_get_product($item['product_id']);
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
?>
<p style="margin: 5px 0 5px 15px;">
<em><?php echo esc_html($productName); ?>:</em><br>
<?php echo esc_html(implode(', ', $item['domains'])); ?>
</p>
<?php endforeach; ?>
</div>
<?php
}
}
/** /**
* Validate domain format * Validate domain format
*/ */

View File

@@ -12,6 +12,7 @@ namespace Jeremias\WcLicensedProduct\Checkout;
use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema; use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema;
use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
/** /**
@@ -70,6 +71,12 @@ final class StoreApiExtension
*/ */
public function getExtensionData(): array public function getExtensionData(): array
{ {
if (SettingsController::isMultiDomainEnabled()) {
return [
'licensed_product_domains' => WC()->session ? WC()->session->get('licensed_product_domains', []) : [],
];
}
return [ return [
'licensed_product_domain' => WC()->session ? WC()->session->get('licensed_product_domain', '') : '', 'licensed_product_domain' => WC()->session ? WC()->session->get('licensed_product_domain', '') : '',
]; ];
@@ -80,6 +87,31 @@ final class StoreApiExtension
*/ */
public function getExtensionSchema(): array public function getExtensionSchema(): array
{ {
if (SettingsController::isMultiDomainEnabled()) {
return [
'licensed_product_domains' => [
'description' => __('Domains for license activation by product', 'wc-licensed-product'),
'type' => 'array',
'context' => ['view', 'edit'],
'readonly' => false,
'items' => [
'type' => 'object',
'properties' => [
'product_id' => [
'type' => 'integer',
],
'domains' => [
'type' => 'array',
'items' => [
'type' => 'string',
],
],
],
],
],
];
}
return [ return [
'licensed_product_domain' => [ 'licensed_product_domain' => [
'description' => __('Domain for license activation', 'wc-licensed-product'), 'description' => __('Domain for license activation', 'wc-licensed-product'),
@@ -95,30 +127,103 @@ final class StoreApiExtension
*/ */
public function handleExtensionUpdate(array $data): void public function handleExtensionUpdate(array $data): void
{ {
if (isset($data['licensed_product_domain'])) { if (SettingsController::isMultiDomainEnabled()) {
$domain = sanitize_text_field($data['licensed_product_domain']); // Multi-domain mode
$normalizedDomain = $this->licenseManager->normalizeDomain($domain); if (isset($data['licensed_product_domains']) && is_array($data['licensed_product_domains'])) {
$normalizedData = $this->normalizeDomainsData($data['licensed_product_domains']);
if (WC()->session) { if (WC()->session) {
WC()->session->set('licensed_product_domain', $normalizedDomain); WC()->session->set('licensed_product_domains', $normalizedData);
}
}
} else {
// Single domain mode
if (isset($data['licensed_product_domain'])) {
$sanitized = sanitize_text_field($data['licensed_product_domain']);
$normalized = $this->licenseManager->normalizeDomain($sanitized);
if (WC()->session) {
WC()->session->set('licensed_product_domain', $normalized);
}
} }
} }
} }
/** /**
* Process the checkout order - save domain to order meta * Normalize domains data from frontend
*/
private function normalizeDomainsData(array $domainsData): array
{
$normalized = [];
foreach ($domainsData as $item) {
if (!isset($item['product_id']) || !isset($item['domains']) || !is_array($item['domains'])) {
continue;
}
$productId = (int) $item['product_id'];
$domains = [];
foreach ($item['domains'] as $domain) {
$sanitized = sanitize_text_field($domain);
if (!empty($sanitized)) {
$domains[] = $this->licenseManager->normalizeDomain($sanitized);
}
}
if (!empty($domains)) {
$normalized[] = [
'product_id' => $productId,
'domains' => $domains,
];
}
}
return $normalized;
}
/**
* Process the checkout order - save domains to order meta
*/ */
public function processCheckoutOrder(\WC_Order $order): void public function processCheckoutOrder(\WC_Order $order): void
{ {
$domain = WC()->session ? WC()->session->get('licensed_product_domain', '') : ''; $requestData = json_decode(file_get_contents('php://input'), true);
// Also check in the request data for block checkout if (SettingsController::isMultiDomainEnabled()) {
if (empty($domain)) { $this->processMultiDomainOrder($order, $requestData);
$requestData = json_decode(file_get_contents('php://input'), true); } else {
if (isset($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain'])) { $this->processSingleDomainOrder($order, $requestData);
$domain = sanitize_text_field($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain']); }
$domain = $this->licenseManager->normalizeDomain($domain); }
}
/**
* Process order in single domain mode (legacy)
*/
private function processSingleDomainOrder(\WC_Order $order, ?array $requestData): void
{
$domain = '';
// Check session first
if (WC()->session) {
$domain = WC()->session->get('licensed_product_domain', '');
}
// Check in the request data for block checkout (extension data)
if (empty($domain) && isset($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain'])) {
$sanitized = sanitize_text_field($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain']);
$domain = $this->licenseManager->normalizeDomain($sanitized);
}
// Check for wclp_license_domain (from our hidden input)
if (empty($domain) && isset($requestData['wclp_license_domain'])) {
$sanitized = sanitize_text_field($requestData['wclp_license_domain']);
$domain = $this->licenseManager->normalizeDomain($sanitized);
}
// Check for additional_fields (WC Blocks API)
if (empty($domain) && isset($requestData['additional_fields']['wc-licensed-product/domain'])) {
$sanitized = sanitize_text_field($requestData['additional_fields']['wc-licensed-product/domain']);
$domain = $this->licenseManager->normalizeDomain($sanitized);
} }
if (!empty($domain)) { if (!empty($domain)) {
@@ -131,4 +236,65 @@ final class StoreApiExtension
} }
} }
} }
/**
* Process order in multi-domain mode
*/
private function processMultiDomainOrder(\WC_Order $order, ?array $requestData): void
{
$domainData = [];
// Check session first
if (WC()->session) {
$domainData = WC()->session->get('licensed_product_domains', []);
}
// Check in the request data for block checkout (extension data)
if (empty($domainData) && isset($requestData['extensions'][self::IDENTIFIER]['licensed_product_domains'])) {
$domainData = $this->normalizeDomainsData(
$requestData['extensions'][self::IDENTIFIER]['licensed_product_domains']
);
}
// Check for wclp_license_domains (from our hidden input - JSON string)
if (empty($domainData) && isset($requestData['wclp_license_domains'])) {
$parsed = json_decode($requestData['wclp_license_domains'], true);
if (is_array($parsed)) {
$domainData = $this->normalizeDomainsData($parsed);
}
}
// Check for licensed_domains in classic format (from DOM injection)
if (empty($domainData) && isset($requestData['licensed_domains']) && is_array($requestData['licensed_domains'])) {
$domainData = [];
foreach ($requestData['licensed_domains'] as $productId => $domains) {
if (!is_array($domains)) {
continue;
}
$normalizedDomains = [];
foreach ($domains as $domain) {
$sanitized = sanitize_text_field($domain);
if (!empty($sanitized)) {
$normalizedDomains[] = $this->licenseManager->normalizeDomain($sanitized);
}
}
if (!empty($normalizedDomains)) {
$domainData[] = [
'product_id' => (int) $productId,
'domains' => $normalizedDomains,
];
}
}
}
if (!empty($domainData)) {
$order->update_meta_data('_licensed_product_domains', $domainData);
$order->save();
// Clear session data
if (WC()->session) {
WC()->session->set('licensed_product_domains', []);
}
}
}
} }

View File

@@ -194,7 +194,7 @@ final class LicenseEmailController
} }
/** /**
* Add license key to order item in email * Add license key(s) to order item in email
*/ */
public function addLicenseToOrderItem(int $itemId, \WC_Order_Item $item, \WC_Order $order, bool $plainText): void public function addLicenseToOrderItem(int $itemId, \WC_Order_Item $item, \WC_Order $order, bool $plainText): void
{ {
@@ -203,94 +203,117 @@ final class LicenseEmailController
return; return;
} }
$license = $this->licenseManager->getLicenseByOrderAndProduct($order->get_id(), $product->get_id()); $licenses = $this->licenseManager->getLicensesByOrderAndProduct($order->get_id(), $product->get_id());
if (!$license) { if (empty($licenses)) {
return; return;
} }
if ($plainText) { if ($plainText) {
echo "\n" . esc_html__('License Key:', 'wc-licensed-product') . ' ' . esc_html($license->getLicenseKey()) . "\n"; echo "\n" . esc_html__('License Keys:', 'wc-licensed-product') . "\n";
foreach ($licenses as $license) {
echo ' - ' . esc_html($license->getLicenseKey());
echo ' (' . esc_html($license->getDomain()) . ')' . "\n";
}
} else { } else {
?> ?>
<div style="margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-left: 3px solid #7f54b3;"> <div style="margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-left: 3px solid #7f54b3;">
<strong><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></strong> <strong><?php esc_html_e('License Keys:', 'wc-licensed-product'); ?></strong>
<code style="display: block; margin-top: 5px; padding: 5px; background: #fff; font-family: monospace;"> <?php foreach ($licenses as $license) : ?>
<?php echo esc_html($license->getLicenseKey()); ?> <div style="margin-top: 5px; padding: 5px; background: #fff;">
</code> <code style="font-family: monospace;">
<?php echo esc_html($license->getLicenseKey()); ?>
</code>
<span style="color: #666; margin-left: 10px;">
<?php echo esc_html($license->getDomain()); ?>
</span>
</div>
<?php endforeach; ?>
</div> </div>
<?php <?php
} }
} }
/** /**
* Get all licenses for an order * Get all licenses for an order grouped by product
*
* @return array Array of products with their licenses
*/ */
private function getLicensesForOrder(\WC_Order $order): array private function getLicensesForOrder(\WC_Order $order): array
{ {
$licenses = []; $products = [];
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if ($product && $product->is_type('licensed')) { if ($product && $product->is_type('licensed')) {
$license = $this->licenseManager->getLicenseByOrderAndProduct($order->get_id(), $product->get_id()); $licenses = $this->licenseManager->getLicensesByOrderAndProduct($order->get_id(), $product->get_id());
if ($license) { if (!empty($licenses)) {
$licenses[] = [ $products[] = [
'license' => $license,
'product_name' => $product->get_name(), 'product_name' => $product->get_name(),
'licenses' => $licenses,
]; ];
} }
} }
} }
return $licenses; return $products;
} }
/** /**
* Render license info in HTML format * Render license info in HTML format
*/ */
private function renderHtmlLicenseInfo(array $licenses, \WC_Order $order): void private function renderHtmlLicenseInfo(array $products, \WC_Order $order): void
{ {
$domain = $order->get_meta('_licensed_product_domain');
?> ?>
<div style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border: 1px solid #e5e5e5; border-radius: 4px;"> <div style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border: 1px solid #e5e5e5; border-radius: 4px;">
<h2 style="margin-top: 0; color: #333;"><?php esc_html_e('Your License Keys', 'wc-licensed-product'); ?></h2> <h2 style="margin-top: 0; color: #333;"><?php esc_html_e('Your License Keys', 'wc-licensed-product'); ?></h2>
<?php if ($domain) : ?> <?php foreach ($products as $product) : ?>
<p style="margin-bottom: 15px;"> <div style="margin-bottom: 20px;">
<strong><?php esc_html_e('Licensed Domain:', 'wc-licensed-product'); ?></strong> <h3 style="margin: 0 0 10px 0; font-size: 1.1em; color: #333;">
<?php echo esc_html($domain); ?> <?php echo esc_html($product['product_name']); ?>
</p> <span style="font-weight: normal; color: #666; font-size: 0.9em;">
<?php endif; ?> (<?php
printf(
esc_html(_n('%d license', '%d licenses', count($product['licenses']), 'wc-licensed-product')),
count($product['licenses'])
);
?>)
</span>
</h3>
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse; background: #fff;">
<thead> <thead>
<tr> <tr>
<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;"><?php esc_html_e('Product', 'wc-licensed-product'); ?></th> <th style="text-align: left; padding: 8px 10px; border-bottom: 2px solid #ddd; font-size: 0.9em;"><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;"><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th> <th style="text-align: left; padding: 8px 10px; border-bottom: 2px solid #ddd; font-size: 0.9em;"><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;"><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th> <th style="text-align: left; padding: 8px 10px; border-bottom: 2px solid #ddd; font-size: 0.9em;"><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($licenses as $item) : ?> <?php foreach ($product['licenses'] as $license) : ?>
<tr> <tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><?php echo esc_html($item['product_name']); ?></td> <td style="padding: 8px 10px; border-bottom: 1px solid #eee;">
<td style="padding: 10px; border-bottom: 1px solid #eee;"> <code style="background: #f5f5f5; padding: 3px 6px; font-family: monospace; font-size: 0.9em;">
<code style="background: #fff; padding: 3px 6px; font-family: monospace;"> <?php echo esc_html($license->getLicenseKey()); ?>
<?php echo esc_html($item['license']->getLicenseKey()); ?> </code>
</code> </td>
</td> <td style="padding: 8px 10px; border-bottom: 1px solid #eee;">
<td style="padding: 10px; border-bottom: 1px solid #eee;"> <?php echo esc_html($license->getDomain()); ?>
<?php </td>
$expiresAt = $item['license']->getExpiresAt(); <td style="padding: 8px 10px; border-bottom: 1px solid #eee;">
echo $expiresAt <?php
? esc_html($expiresAt->format(get_option('date_format'))) $expiresAt = $license->getExpiresAt();
: esc_html__('Never', 'wc-licensed-product'); echo $expiresAt
?> ? esc_html($expiresAt->format(get_option('date_format')))
</td> : esc_html__('Never', 'wc-licensed-product');
</tr> ?>
<?php endforeach; ?> </td>
</tbody> </tr>
</table> <?php endforeach; ?>
</tbody>
</table>
</div>
<?php endforeach; ?>
<p style="margin-top: 15px; margin-bottom: 0; font-size: 0.9em; color: #666;"> <p style="margin-top: 15px; margin-bottom: 0; font-size: 0.9em; color: #666;">
<?php esc_html_e('You can also view your licenses in your account under "Licenses".', 'wc-licensed-product'); ?> <?php esc_html_e('You can also view your licenses in your account under "Licenses".', 'wc-licensed-product'); ?>
@@ -302,29 +325,33 @@ final class LicenseEmailController
/** /**
* Render license info in plain text format * Render license info in plain text format
*/ */
private function renderPlainTextLicenseInfo(array $licenses, \WC_Order $order): void private function renderPlainTextLicenseInfo(array $products, \WC_Order $order): void
{ {
$domain = $order->get_meta('_licensed_product_domain');
echo "\n\n"; echo "\n\n";
echo "==========================================================\n"; echo "==========================================================\n";
echo esc_html__('YOUR LICENSE KEYS', 'wc-licensed-product') . "\n"; echo esc_html__('YOUR LICENSE KEYS', 'wc-licensed-product') . "\n";
echo "==========================================================\n\n"; echo "==========================================================\n\n";
if ($domain) { foreach ($products as $product) {
echo esc_html__('Licensed Domain:', 'wc-licensed-product') . ' ' . esc_html($domain) . "\n\n"; echo esc_html($product['product_name']);
} echo ' (' . count($product['licenses']) . ' ' .
_n('license', 'licenses', count($product['licenses']), 'wc-licensed-product') . ')';
echo "\n";
echo "-----------------------------------------------------------\n";
foreach ($licenses as $item) { foreach ($product['licenses'] as $license) {
echo esc_html($item['product_name']) . "\n"; echo esc_html__('License Key:', 'wc-licensed-product') . ' ';
echo esc_html__('License Key:', 'wc-licensed-product') . ' ' . esc_html($item['license']->getLicenseKey()) . "\n"; echo esc_html($license->getLicenseKey()) . "\n";
echo esc_html__('Domain:', 'wc-licensed-product') . ' ';
echo esc_html($license->getDomain()) . "\n";
echo esc_html__('Expires:', 'wc-licensed-product') . ' ';
$expiresAt = $item['license']->getExpiresAt(); $expiresAt = $license->getExpiresAt();
echo esc_html__('Expires:', 'wc-licensed-product') . ' '; echo $expiresAt
echo $expiresAt ? esc_html($expiresAt->format(get_option('date_format')))
? esc_html($expiresAt->format(get_option('date_format'))) : esc_html__('Never', 'wc-licensed-product');
: esc_html__('Never', 'wc-licensed-product'); echo "\n\n";
echo "\n\n"; }
} }
echo esc_html__('You can also view your licenses in your account under "Licenses".', 'wc-licensed-product') . "\n"; echo esc_html__('You can also view your licenses in your account under "Licenses".', 'wc-licensed-product') . "\n";

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Frontend; namespace Jeremias\WcLicensedProduct\Frontend;
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Product\VersionManager; use Jeremias\WcLicensedProduct\Product\VersionManager;
use Twig\Environment; use Twig\Environment;
@@ -107,135 +108,250 @@ final class AccountController
$licenses = $this->licenseManager->getLicensesByCustomer($customerId); $licenses = $this->licenseManager->getLicensesByCustomer($customerId);
// Enrich licenses with product data and downloads // Group licenses by product+order into "packages"
$enrichedLicenses = []; $packages = $this->groupLicensesIntoPackages($licenses);
foreach ($licenses as $license) {
$product = wc_get_product($license->getProductId());
$order = wc_get_order($license->getOrderId());
// Get available downloads for this license
$downloads = [];
if ($license->getStatus() === 'active') {
$versions = $this->versionManager->getVersionsByProduct($license->getProductId());
foreach ($versions as $version) {
if ($version->isActive() && ($version->getAttachmentId() || $version->getDownloadUrl())) {
$downloads[] = [
'version' => $version->getVersion(),
'version_id' => $version->getId(),
'filename' => $version->getDownloadFilename(),
'download_url' => $this->downloadController->generateDownloadUrl(
$license->getId(),
$version->getId()
),
'release_notes' => $version->getReleaseNotes(),
'released_at' => $version->getReleasedAt()->format(get_option('date_format')),
'file_hash' => $version->getFileHash(),
];
}
}
}
$enrichedLicenses[] = [
'license' => $license,
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
'product_url' => $product ? $product->get_permalink() : '',
'order_number' => $order ? $order->get_order_number() : '',
'order_url' => $order ? $order->get_view_order_url() : '',
'downloads' => $downloads,
];
}
try { try {
echo $this->twig->render('frontend/licenses.html.twig', [ echo $this->twig->render('frontend/licenses.html.twig', [
'licenses' => $enrichedLicenses, 'packages' => $packages,
'has_licenses' => !empty($enrichedLicenses), 'has_packages' => !empty($packages),
'signing_enabled' => ResponseSigner::isSigningEnabled(),
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
// Fallback to PHP template if Twig fails // Fallback to PHP template if Twig fails
$this->displayLicensesFallback($enrichedLicenses); $this->displayLicensesFallback($packages);
} }
} }
/**
* Group licenses into packages by product+order
*
* @param array $licenses Array of License objects
* @return array Array of package data
*/
private function groupLicensesIntoPackages(array $licenses): array
{
$grouped = [];
foreach ($licenses as $license) {
$productId = $license->getProductId();
$orderId = $license->getOrderId();
$key = $productId . '_' . $orderId;
if (!isset($grouped[$key])) {
$product = wc_get_product($productId);
$order = wc_get_order($orderId);
$grouped[$key] = [
'product_id' => $productId,
'order_id' => $orderId,
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
'product_url' => $product ? $product->get_permalink() : '',
'order_number' => $order ? $order->get_order_number() : '',
'order_url' => $order ? $order->get_view_order_url() : '',
'licenses' => [],
'downloads' => [],
'has_active_license' => false,
];
}
// Add license to package
$grouped[$key]['licenses'][] = [
'id' => $license->getId(),
'license_key' => $license->getLicenseKey(),
'domain' => $license->getDomain(),
'status' => $license->getStatus(),
'expires_at' => $license->getExpiresAt(),
'is_transferable' => in_array($license->getStatus(), ['active', 'inactive'], true),
'customer_secret' => ResponseSigner::getCustomerSecretForLicense($license->getLicenseKey()),
];
// Track if package has at least one active license
if ($license->getStatus() === 'active') {
$grouped[$key]['has_active_license'] = true;
}
}
// Add downloads for packages with active licenses
foreach ($grouped as $key => &$package) {
if ($package['has_active_license']) {
$package['downloads'] = $this->getDownloadsForProduct(
$package['product_id'],
$package['licenses'][0]['id'] // Use first license for download URL
);
}
}
// Sort by order date (newest first) - re-index array
return array_values($grouped);
}
/**
* Get downloads for a product
*/
private function getDownloadsForProduct(int $productId, int $licenseId): array
{
$downloads = [];
$versions = $this->versionManager->getVersionsByProduct($productId);
foreach ($versions as $version) {
if ($version->isActive() && ($version->getAttachmentId() || $version->getDownloadUrl())) {
$downloads[] = [
'version' => $version->getVersion(),
'version_id' => $version->getId(),
'filename' => $version->getDownloadFilename(),
'download_url' => $this->downloadController->generateDownloadUrl(
$licenseId,
$version->getId()
),
'release_notes' => $version->getReleaseNotes(),
'released_at' => $version->getReleasedAt()->format(get_option('date_format')),
'file_hash' => $version->getFileHash(),
];
}
}
return $downloads;
}
/** /**
* Fallback display method if Twig is unavailable * Fallback display method if Twig is unavailable
*/ */
private function displayLicensesFallback(array $enrichedLicenses): void private function displayLicensesFallback(array $packages): void
{ {
if (empty($enrichedLicenses)) { if (empty($packages)) {
echo '<p>' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '</p>'; echo '<p>' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '</p>';
return; return;
} }
?> ?>
<div class="woocommerce-licenses"> <div class="woocommerce-licenses">
<?php foreach ($enrichedLicenses as $item): ?> <?php foreach ($packages as $package): ?>
<div class="license-card"> <div class="license-package">
<div class="license-header"> <div class="package-header">
<h3> <h3>
<?php if ($item['product_url']): ?> <?php if ($package['product_url']): ?>
<a href="<?php echo esc_url($item['product_url']); ?>"> <a href="<?php echo esc_url($package['product_url']); ?>">
<?php echo esc_html($item['product_name']); ?> <?php echo esc_html($package['product_name']); ?>
</a> </a>
<?php else: ?> <?php else: ?>
<?php echo esc_html($item['product_name']); ?> <?php echo esc_html($package['product_name']); ?>
<?php endif; ?> <?php endif; ?>
</h3> </h3>
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>"> <span class="package-order">
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?> <?php
printf(
/* translators: %s: order number */
esc_html__('Order #%s', 'wc-licensed-product'),
esc_html($package['order_number'])
);
?>
</span> </span>
</div> </div>
<div class="license-details"> <div class="package-licenses">
<div class="license-key-row"> <?php foreach ($package['licenses'] as $license): ?>
<label><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></label> <div class="license-entry license-entry-<?php echo esc_attr($license['status']); ?>">
<code class="license-key" data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>"> <div class="license-row-primary">
<?php echo esc_html($item['license']->getLicenseKey()); ?> <div class="license-key-group">
</code> <code class="license-key"><?php echo esc_html($license['license_key']); ?></code>
<button type="button" class="copy-license-btn" data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>"> <span class="license-status license-status-<?php echo esc_attr($license['status']); ?>">
<span class="dashicons dashicons-clipboard"></span> <?php echo esc_html(ucfirst($license['status'])); ?>
</button> </span>
</div> </div>
<div class="license-actions">
<div class="license-info-row"> <button type="button" class="copy-license-btn" data-license-key="<?php echo esc_attr($license['license_key']); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>">
<span class="license-domain-display" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"> <span class="dashicons dashicons-clipboard"></span>
<strong><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></strong> </button>
<span class="domain-value"><?php echo esc_html($item['license']->getDomain()); ?></span> <?php if ($license['is_transferable']): ?>
<?php if (in_array($item['license']->getStatus(), ['active', 'inactive'], true)): ?> <button type="button" class="wclp-transfer-btn"
<button type="button" class="wclp-transfer-btn" data-license-id="<?php echo esc_attr($license['id']); ?>"
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>" data-current-domain="<?php echo esc_attr($license['domain']); ?>"
data-current-domain="<?php echo esc_attr($item['license']->getDomain()); ?>" title="<?php esc_attr_e('Transfer to new domain', 'wc-licensed-product'); ?>">
title="<?php esc_attr_e('Transfer to new domain', 'wc-licensed-product'); ?>"> <span class="dashicons dashicons-randomize"></span>
<span class="dashicons dashicons-randomize"></span> </button>
<?php esc_html_e('Transfer', 'wc-licensed-product'); ?> <?php endif; ?>
</button> </div>
<?php endif; ?> </div>
</span> <div class="license-row-secondary">
<span><strong><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></strong> <span class="license-meta-item license-domain">
<?php <span class="dashicons dashicons-admin-site-alt3"></span>
$expiresAt = $item['license']->getExpiresAt(); <?php echo esc_html($license['domain']); ?>
echo $expiresAt </span>
? esc_html($expiresAt->format(get_option('date_format'))) <span class="license-meta-item license-expiry">
: esc_html__('Never', 'wc-licensed-product'); <span class="dashicons dashicons-calendar-alt"></span>
?> <?php
</span> echo $license['expires_at']
</div> ? esc_html($license['expires_at']->format('Y-m-d'))
: '<span class="lifetime">' . esc_html__('Lifetime', 'wc-licensed-product') . '</span>';
?>
</span>
</div>
</div>
<?php endforeach; ?>
</div> </div>
<?php if (!empty($item['downloads'])): ?> <?php if (!empty($package['downloads'])): ?>
<div class="license-downloads"> <div class="package-downloads">
<h4><?php esc_html_e('Available Downloads', 'wc-licensed-product'); ?></h4> <h4><?php esc_html_e('Available Downloads', 'wc-licensed-product'); ?></h4>
<ul class="download-list"> <ul class="download-list">
<?php foreach ($item['downloads'] as $download): ?> <?php
<li> $latest = $package['downloads'][0];
<a href="<?php echo esc_url($download['download_url']); ?>" class="download-link"> ?>
<li class="download-item download-item-latest">
<div class="download-row-file">
<a href="<?php echo esc_url($latest['download_url']); ?>" class="download-link">
<span class="dashicons dashicons-download"></span> <span class="dashicons dashicons-download"></span>
<?php echo esc_html($download['filename'] ?: sprintf(__('Version %s', 'wc-licensed-product'), $download['version'])); ?> <?php echo esc_html($latest['filename'] ?: sprintf(__('Version %s', 'wc-licensed-product'), $latest['version'])); ?>
</a> </a>
<span class="download-version">v<?php echo esc_html($download['version']); ?></span> <span class="download-version-badge"><?php esc_html_e('Latest', 'wc-licensed-product'); ?></span>
<span class="download-date"><?php echo esc_html($download['released_at']); ?></span> </div>
</li> <div class="download-row-meta">
<?php endforeach; ?> <span class="download-date"><?php echo esc_html($latest['released_at']); ?></span>
<?php if (!empty($latest['file_hash'])): ?>
<span class="download-hash" title="<?php echo esc_attr($latest['file_hash']); ?>">
<span class="dashicons dashicons-shield"></span>
<code><?php echo esc_html(substr($latest['file_hash'], 0, 12)); ?>...</code>
</span>
<?php endif; ?>
</div>
</li>
</ul> </ul>
<?php if (count($package['downloads']) > 1): ?>
<div class="older-versions-section">
<button type="button" class="older-versions-toggle" aria-expanded="false">
<span class="dashicons dashicons-arrow-down-alt2"></span>
<?php
printf(
esc_html__('Older versions (%d)', 'wc-licensed-product'),
count($package['downloads']) - 1
);
?>
</button>
<ul class="download-list older-versions-list" style="display: none;">
<?php foreach (array_slice($package['downloads'], 1) as $download): ?>
<li class="download-item">
<div class="download-row-file">
<a href="<?php echo esc_url($download['download_url']); ?>" class="download-link">
<span class="dashicons dashicons-download"></span>
<?php echo esc_html($download['filename'] ?: sprintf(__('Version %s', 'wc-licensed-product'), $download['version'])); ?>
</a>
</div>
<div class="download-row-meta">
<span class="download-date"><?php echo esc_html($download['released_at']); ?></span>
<?php if (!empty($download['file_hash'])): ?>
<span class="download-hash" title="<?php echo esc_attr($download['file_hash']); ?>">
<span class="dashicons dashicons-shield"></span>
<code><?php echo esc_html(substr($download['file_hash'], 0, 12)); ?>...</code>
</span>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -49,8 +49,11 @@ class LicenseManager
): ?License { ): ?License {
global $wpdb; global $wpdb;
// Check if license already exists for this order and product // Normalize domain first for duplicate detection
$existing = $this->getLicenseByOrderAndProduct($orderId, $productId); $normalizedDomain = $this->normalizeDomain($domain);
// Check if license already exists for this order, product, and domain
$existing = $this->getLicenseByOrderProductAndDomain($orderId, $productId, $normalizedDomain);
if ($existing) { if ($existing) {
return $existing; return $existing;
} }
@@ -161,6 +164,49 @@ class LicenseManager
return $row ? License::fromArray($row) : null; return $row ? License::fromArray($row) : null;
} }
/**
* Get all licenses for an order and product
*
* @return License[]
*/
public function getLicensesByOrderAndProduct(int $orderId, int $productId): array
{
global $wpdb;
$tableName = Installer::getLicensesTable();
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d ORDER BY created_at ASC",
$orderId,
$productId
),
ARRAY_A
);
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
}
/**
* Get license by order, product, and domain
*/
public function getLicenseByOrderProductAndDomain(int $orderId, int $productId, string $domain): ?License
{
global $wpdb;
$tableName = Installer::getLicensesTable();
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d AND domain = %s",
$orderId,
$productId,
$domain
),
ARRAY_A
);
return $row ? License::fromArray($row) : null;
}
/** /**
* Get all licenses for an order * Get all licenses for an order
*/ */

View File

@@ -208,15 +208,50 @@ final class Plugin
return; return;
} }
// Try new multi-domain format first
$domainData = $order->get_meta('_licensed_product_domains');
if (!empty($domainData) && is_array($domainData)) {
$this->generateLicensesMultiDomain($order, $domainData);
return;
}
// Fall back to legacy single domain format
$this->generateLicensesSingleDomain($order);
}
/**
* Generate licenses for new multi-domain format
*/
private function generateLicensesMultiDomain(\WC_Order $order, array $domainData): void
{
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
// Index domains by product ID for quick lookup
$domainsByProduct = [];
foreach ($domainData as $item) {
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
$domainsByProduct[(int) $item['product_id']] = $item['domains'];
}
}
// Generate licenses for each licensed product
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if ($product && $product->is_type('licensed')) { if (!$product || !$product->is_type('licensed')) {
$domain = $order->get_meta('_licensed_product_domain'); continue;
if ($domain) { }
$productId = $product->get_id();
$domains = $domainsByProduct[$productId] ?? [];
// Generate a license for each domain
foreach ($domains as $domain) {
if (!empty($domain)) {
$this->licenseManager->generateLicense( $this->licenseManager->generateLicense(
$orderId, $orderId,
$product->get_id(), $productId,
$order->get_customer_id(), $customerId,
$domain $domain
); );
} }
@@ -224,6 +259,29 @@ final class Plugin
} }
} }
/**
* Generate licenses for legacy single domain format
*/
private function generateLicensesSingleDomain(\WC_Order $order): void
{
$domain = $order->get_meta('_licensed_product_domain');
if (empty($domain)) {
return;
}
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$this->licenseManager->generateLicense(
$order->get_id(),
$product->get_id(),
$order->get_customer_id(),
$domain
);
}
}
}
/** /**
* Get Twig environment * Get Twig environment
*/ */

View File

@@ -1,81 +1,150 @@
{% if not has_licenses %} {% if not has_packages %}
<p>{{ __('You have no licenses yet.') }}</p> <p>{{ __('You have no licenses yet.') }}</p>
{% else %} {% else %}
<div class="woocommerce-licenses"> <div class="woocommerce-licenses">
{% for item in licenses %} {% for package in packages %}
<div class="license-card"> <div class="license-package">
<div class="license-header"> <div class="package-header">
<h3> <div class="package-title">
{% if item.product_url %} <h3>
<a href="{{ esc_url(item.product_url) }}">{{ esc_html(item.product_name) }}</a> {% if package.product_url %}
{% else %} <a href="{{ esc_url(package.product_url) }}">{{ esc_html(package.product_name) }}</a>
{{ esc_html(item.product_name) }} {% else %}
{% endif %} {{ esc_html(package.product_name) }}
</h3> {% endif %}
<span class="license-status license-status-{{ esc_attr(item.license.status) }}"> </h3>
{{ esc_html(item.license.status)|capitalize }} <span class="package-order">
{{ __('Order') }}
{% if package.order_url %}
<a href="{{ esc_url(package.order_url) }}">#{{ esc_html(package.order_number) }}</a>
{% else %}
#{{ esc_html(package.order_number) }}
{% endif %}
</span>
</div>
<span class="package-license-count">
{{ package.licenses|length }} {{ package.licenses|length == 1 ? __('License') : __('Licenses') }}
</span> </span>
</div> </div>
<div class="license-details"> <div class="package-licenses">
<div class="license-key-row"> {% for license in package.licenses %}
<label>{{ __('License Key:') }}</label> <div class="license-entry license-entry-{{ esc_attr(license.status) }}">
<code class="license-key" data-license-key="{{ esc_attr(item.license.licenseKey) }}"> <div class="license-row-primary">
{{ esc_html(item.license.licenseKey) }} <div class="license-key-group">
</code> <code class="license-key">{{ esc_html(license.license_key) }}</code>
<button type="button" class="copy-license-btn" data-license-key="{{ esc_attr(item.license.licenseKey) }}" title="{{ __('Copy to clipboard') }}"> <span class="license-status license-status-{{ esc_attr(license.status) }}">
<span class="dashicons dashicons-clipboard"></span> {{ esc_html(license.status)|capitalize }}
</button> </span>
</div> </div>
<div class="license-actions">
<div class="license-info-row"> <button type="button" class="copy-license-btn" data-license-key="{{ esc_attr(license.license_key) }}" title="{{ __('Copy to clipboard') }}">
<span class="license-domain-display" data-license-id="{{ item.license.id }}"> <span class="dashicons dashicons-clipboard"></span>
<strong>{{ __('Domain:') }}</strong> </button>
<span class="domain-value">{{ esc_html(item.license.domain) }}</span> {% if license.is_transferable %}
{% if item.license.status == 'active' or item.license.status == 'inactive' %} <button type="button" class="wclp-transfer-btn"
<button type="button" class="wclp-transfer-btn" data-license-id="{{ license.id }}"
data-license-id="{{ item.license.id }}" data-current-domain="{{ esc_attr(license.domain) }}"
data-current-domain="{{ esc_attr(item.license.domain) }}" title="{{ __('Transfer to new domain') }}">
title="{{ __('Transfer to new domain') }}"> <span class="dashicons dashicons-randomize"></span>
<span class="dashicons dashicons-randomize"></span> </button>
{{ __('Transfer') }} {% endif %}
</button> </div>
</div>
<div class="license-row-secondary">
<span class="license-meta-item license-domain">
<span class="dashicons dashicons-admin-site-alt3"></span>
{{ esc_html(license.domain) }}
</span>
<span class="license-meta-item license-expiry">
<span class="dashicons dashicons-calendar-alt"></span>
{% if license.expires_at %}
{{ license.expires_at|date('Y-m-d') }}
{% else %}
<span class="lifetime">{{ __('Lifetime') }}</span>
{% endif %}
</span>
</div>
{% if signing_enabled and license.customer_secret %}
<div class="license-row-secret">
<button type="button" class="secret-toggle" aria-expanded="false">
<span class="dashicons dashicons-lock"></span>
{{ __('API Verification Secret') }}
<span class="dashicons dashicons-arrow-down-alt2 toggle-arrow"></span>
</button>
<div class="secret-content" style="display: none;">
<p class="secret-description">
{{ __('Use this secret to verify signed API responses. Keep it secure.') }}
</p>
<div class="secret-value-wrapper">
<code class="secret-value">{{ esc_html(license.customer_secret) }}</code>
<button type="button" class="copy-secret-btn" data-secret="{{ esc_attr(license.customer_secret) }}" title="{{ __('Copy to clipboard') }}">
<span class="dashicons dashicons-clipboard"></span>
</button>
</div>
</div>
</div>
{% endif %} {% endif %}
</span> </div>
<span><strong>{{ __('Expires:') }}</strong> {% endfor %}
{% if item.license.expiresAt %}
{{ item.license.expiresAt|date('Y-m-d') }}
{% else %}
{{ __('Never') }}
{% endif %}
</span>
</div>
</div> </div>
{% if item.downloads is defined and item.downloads is not empty %} {% if package.downloads is defined and package.downloads is not empty %}
<div class="license-downloads"> <div class="package-downloads">
<h4>{{ __('Available Downloads') }}</h4> <h4>{{ __('Available Downloads') }}</h4>
<ul class="download-list"> <ul class="download-list">
{% for download in item.downloads %} {# Show only the latest version (first item) #}
<li class="download-item"> {% set latest = package.downloads|first %}
<div class="download-row-file"> <li class="download-item download-item-latest">
<a href="{{ esc_url(download.download_url) }}" class="download-link"> <div class="download-row-file">
<span class="dashicons dashicons-download"></span> <a href="{{ esc_url(latest.download_url) }}" class="download-link">
{{ esc_html(download.filename ?: 'Version ' ~ download.version) }} <span class="dashicons dashicons-download"></span>
</a> {{ esc_html(latest.filename ?: 'Version ' ~ latest.version) }}
</div> </a>
<div class="download-row-meta"> <span class="download-version-badge">{{ __('Latest') }}</span>
<span class="download-date">{{ esc_html(download.released_at) }}</span> </div>
{% if download.file_hash %} <div class="download-row-meta">
<span class="download-hash" title="{{ esc_attr(download.file_hash) }}"> <span class="download-date">{{ esc_html(latest.released_at) }}</span>
<span class="dashicons dashicons-shield"></span> {% if latest.file_hash %}
<code>{{ download.file_hash[:12] }}...</code> <span class="download-hash" title="{{ esc_attr(latest.file_hash) }}">
</span> <span class="dashicons dashicons-shield"></span>
{% endif %} <code>{{ latest.file_hash[:12] }}...</code>
</div> </span>
</li> {% endif %}
{% endfor %} </div>
</li>
</ul> </ul>
{# Show older versions in collapsible if more than one version exists #}
{% if package.downloads|length > 1 %}
<div class="older-versions-section">
<button type="button" class="older-versions-toggle" aria-expanded="false">
<span class="dashicons dashicons-arrow-down-alt2"></span>
{{ __('Older versions') }} ({{ package.downloads|length - 1 }})
</button>
<ul class="download-list older-versions-list" style="display: none;">
{% for download in package.downloads|slice(1) %}
<li class="download-item">
<div class="download-row-file">
<a href="{{ esc_url(download.download_url) }}" class="download-link">
<span class="dashicons dashicons-download"></span>
{{ esc_html(download.filename ?: 'Version ' ~ download.version) }}
</a>
</div>
<div class="download-row-meta">
<span class="download-date">{{ esc_html(download.released_at) }}</span>
{% if download.file_hash %}
<span class="download-hash" title="{{ esc_attr(download.file_hash) }}">
<span class="dashicons dashicons-shield"></span>
<code>{{ download.file_hash[:12] }}...</code>
</span>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -3,7 +3,7 @@
* Plugin Name: WooCommerce Licensed Product * Plugin Name: WooCommerce Licensed Product
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation. * Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
* Version: 0.4.0 * Version: 0.5.2
* Author: Marco Graetsch * Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev * Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
} }
// Plugin constants // Plugin constants
define('WC_LICENSED_PRODUCT_VERSION', '0.4.0'); define('WC_LICENSED_PRODUCT_VERSION', '0.5.2');
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));