You've already forked wc-licensed-product
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2207efbc52 | |||
| 3fe173686b | |||
| 86b5bdb075 | |||
| c6d6269ee3 | |||
| 75f1dabdb4 | |||
| 8acde7cadd | |||
| c45816b491 | |||
| bcabf8feb2 | |||
| 83836d69af | |||
| 550a84beb9 | |||
| 7d48028f62 |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|||||||
113
CLAUDE.md
113
CLAUDE.md
@@ -36,9 +36,13 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
No known bugs at the moment.
|
No known bugs at the moment.
|
||||||
|
|
||||||
### 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.*
|
||||||
|
|
||||||
|
### Version 0.5.1
|
||||||
|
|
||||||
|
*No planned bugfixes yet.*
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -1159,3 +1163,108 @@ 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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
$(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);
|
||||||
|
|
||||||
// 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 +36,20 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy license key to clipboard
|
* Copy license key to clipboard
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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('');
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
releases/wc-licensed-product-0.4.0.zip
Normal file
BIN
releases/wc-licensed-product-0.4.0.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.4.0.zip.sha256
Normal file
1
releases/wc-licensed-product-0.4.0.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
cf8769c861d77c327f178049d5fac0d4e47679cc1a1d35c5b613e4cd3fb8674f wc-licensed-product-0.4.0.zip
|
||||||
BIN
releases/wc-licensed-product-0.5.0.zip
Normal file
BIN
releases/wc-licensed-product-0.5.0.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.0.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.0.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316 wc-licensed-product-0.5.0.zip
|
||||||
BIN
releases/wc-licensed-product-0.5.1.zip
Normal file
BIN
releases/wc-licensed-product-0.5.1.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.1.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.1.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
a489f0b8cfcd7d5d9b2021b7ff581b9f1a56468dfde87bbb06bb4555d11f7556 wc-licensed-product-0.5.1.zip
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -107,135 +107,248 @@ 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),
|
||||||
]);
|
]);
|
||||||
} 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),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,81 +1,130 @@
|
|||||||
{% 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>
|
||||||
{% endif %}
|
</div>
|
||||||
</span>
|
<div class="license-row-secondary">
|
||||||
<span><strong>{{ __('Expires:') }}</strong>
|
<span class="license-meta-item license-domain">
|
||||||
{% if item.license.expiresAt %}
|
<span class="dashicons dashicons-admin-site-alt3"></span>
|
||||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
{{ esc_html(license.domain) }}
|
||||||
{% else %}
|
</span>
|
||||||
{{ __('Never') }}
|
<span class="license-meta-item license-expiry">
|
||||||
{% endif %}
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
</span>
|
{% if license.expires_at %}
|
||||||
</div>
|
{{ license.expires_at|date('Y-m-d') }}
|
||||||
|
{% else %}
|
||||||
|
<span class="lifetime">{{ __('Lifetime') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</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>
|
||||||
|
|||||||
@@ -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.1
|
||||||
* 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.1');
|
||||||
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__));
|
||||||
|
|||||||
Reference in New Issue
Block a user