diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8503a..73c05a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.12] - 2026-01-27 + +### Fixed + +- **CRITICAL:** Fixed stock indicator ("1 in stock") appearing in cart for licensed variable product variations +- Override `get_children()` with direct SQL query to bypass WooCommerce's `is_type('variable')` check +- Override `get_variation_attributes()` to properly load taxonomy attribute terms +- Override `get_variation_prices()` to prevent fatal error with null `$this->prices_array` +- Override `get_available_variations()` with empty `availability_html` for variations +- Added `is_type()` override to return true for both 'licensed-variable' and 'variable' type checks +- Added multiple stock-related filters: `woocommerce_get_availability_text`, `woocommerce_product_get_stock_quantity`, `woocommerce_product_variation_get_stock_quantity` +- Improved `isLicensedProductOrVariation()` check using `WC_Product_Factory::get_product_type()` for reliable parent type detection + +### Changed + +- `LicensedProductVariation` now includes `get_availability()`, `managing_stock()`, and `is_purchasable()` overrides +- Simplified `isVirtual()` to use shared `isLicensedProductOrVariation()` helper + ## [0.5.11] - 2026-01-27 ### Fixed diff --git a/languages/wc-licensed-product-de_CH.mo b/languages/wc-licensed-product-de_CH.mo index ebc3d66..65e9afc 100644 Binary files a/languages/wc-licensed-product-de_CH.mo and b/languages/wc-licensed-product-de_CH.mo differ diff --git a/languages/wc-licensed-product-de_CH.po b/languages/wc-licensed-product-de_CH.po index 6887a13..6169f12 100644 --- a/languages/wc-licensed-product-de_CH.po +++ b/languages/wc-licensed-product-de_CH.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: WC Licensed Product 0.5.0\n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n" -"POT-Creation-Date: 2026-01-27 13:34+0100\n" +"POT-Creation-Date: 2026-01-27 14:41+0100\n" "PO-Revision-Date: 2026-01-25T18:30:00+00:00\n" "Last-Translator: Marco Graetsch \n" "Language-Team: German (Switzerland) \n" @@ -311,10 +311,10 @@ msgstr "Speichern" #: src/Admin/DashboardWidgetController.php:136 #: src/Admin/OrderLicenseController.php:260 #: src/Admin/SettingsController.php:192 -#: src/Product/LicensedProductVariation.php:139 -#: src/Product/LicensedProductType.php:136 -#: src/Product/LicensedProductType.php:184 -#: src/Product/LicensedProductType.php:403 +#: src/Product/LicensedProductVariation.php:194 +#: src/Product/LicensedProductType.php:164 +#: src/Product/LicensedProductType.php:212 +#: src/Product/LicensedProductType.php:553 #: src/Frontend/AccountController.php:286 msgid "Lifetime" msgstr "Lebenslang" @@ -492,6 +492,7 @@ msgstr "Lizenz erfolgreich verlängert." msgid "License set to lifetime successfully." msgstr "Lizenz erfolgreich auf lebenslang gesetzt." +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1106 #, php-format msgid "%d license activated." @@ -499,6 +500,7 @@ msgid_plural "%d licenses activated." msgstr[0] "%d Lizenz aktiviert." msgstr[1] "%d Lizenzen aktiviert." +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1114 #, php-format msgid "%d license deactivated." @@ -506,6 +508,7 @@ msgid_plural "%d licenses deactivated." msgstr[0] "%d Lizenz deaktiviert." msgstr[1] "%d Lizenzen deaktiviert." +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1122 #, php-format msgid "%d license revoked." @@ -513,6 +516,7 @@ msgid_plural "%d licenses revoked." msgstr[0] "%d Lizenz widerrufen." msgstr[1] "%d Lizenzen widerrufen." +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1130 #, php-format msgid "%d license deleted." @@ -520,6 +524,7 @@ msgid_plural "%d licenses deleted." msgstr[0] "%d Lizenz gelöscht." msgstr[1] "%d Lizenzen gelöscht." +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1138 #, php-format msgid "%d license extended." @@ -541,6 +546,7 @@ msgstr "" msgid "No licenses to export." msgstr "Keine Lizenzen zum Exportieren." +#. translators: %d: number of licenses imported #: src/Admin/AdminController.php:1159 #, php-format msgid "%d license imported." @@ -548,6 +554,7 @@ msgid_plural "%d licenses imported." msgstr[0] "%d Lizenz importiert." msgstr[1] "%d Lizenzen importiert." +#. translators: %d: number of licenses updated #: src/Admin/AdminController.php:1166 #, php-format msgid "%d updated." @@ -555,6 +562,7 @@ msgid_plural "%d updated." msgstr[0] "%d aktualisiert." msgstr[1] "%d aktualisiert." +#. translators: %d: number of licenses skipped #: src/Admin/AdminController.php:1174 #, php-format msgid "%d skipped." @@ -562,6 +570,7 @@ msgid_plural "%d skipped." msgstr[0] "%d übersprungen." msgstr[1] "%d übersprungen." +#. translators: %d: number of errors #: src/Admin/AdminController.php:1182 #, php-format msgid "%d error." @@ -1020,6 +1029,7 @@ msgstr "Domain bearbeiten" msgid "View in Licenses" msgstr "In Lizenzen anzeigen" +#. translators: %s: Link to licenses page #: src/Admin/OrderLicenseController.php:280 #, php-format msgid "For more actions (revoke, extend, delete), go to the %s page." @@ -1166,8 +1176,8 @@ msgstr "" "Diese Einstellungen dienen als Standard für neue lizensierte Produkte. " "Individuelle Produkteinstellungen überschreiben diese Standards." -#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:154 -#: src/Product/LicensedProductType.php:420 +#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:182 +#: src/Product/LicensedProductType.php:570 msgid "Max Activations" msgstr "Max. Aktivierungen" @@ -1175,7 +1185,7 @@ msgstr "Max. Aktivierungen" msgid "Default maximum number of domain activations per license." msgstr "Standard maximale Anzahl der Domain-Aktivierungen pro Lizenz." -#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:172 +#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:200 msgid "License Validity (Days)" msgstr "Lizenz-Gültigkeit (Tage)" @@ -1187,7 +1197,7 @@ msgstr "" "Standard Anzahl Tage, die eine Lizenz gültig ist. Leer lassen oder auf 0 " "setzen für lebenslange Lizenzen." -#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:190 +#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:218 msgid "Bind to Major Version" msgstr "An Hauptversion binden" @@ -1215,6 +1225,7 @@ msgstr "" msgid "Expiration Warning Schedule" msgstr "Ablaufwarnung Zeitplan" +#. translators: %s: URL to WooCommerce email settings #: src/Admin/SettingsController.php:230 #, php-format msgid "" @@ -1346,6 +1357,7 @@ msgstr "Lizenz-Domains" msgid "Each license requires a unique domain." msgstr "Jede Lizenz erfordert eine eindeutige Domain." +#. translators: %d: license number #: src/Checkout/CheckoutBlocksIntegration.php:129 #: src/Checkout/CheckoutController.php:224 #, php-format @@ -1357,6 +1369,16 @@ msgstr "Lizenz %d:" msgid "required" msgstr "erforderlich" +#: src/Checkout/CheckoutController.php:215 +#, php-format +msgid "licensed_domains[%s][%d]" +msgstr "licensed_domains[%s][%d]" + +#: src/Checkout/CheckoutController.php:216 +#, php-format +msgid "licensed_domain_%s_%d" +msgstr "licensed_domain_%s_%d" + #: src/Checkout/CheckoutController.php:323 msgid "Please enter a domain for your license." msgstr "Bitte geben Sie eine Domain für Ihre Lizenz ein." @@ -1365,16 +1387,19 @@ msgstr "Bitte geben Sie eine Domain für Ihre Lizenz ein." msgid "Please enter a valid domain for your license." msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz ein." +#. translators: 1: product name, 2: license number #: src/Checkout/CheckoutController.php:356 #, php-format msgid "Please enter a domain for %1$s (License %2$d)." msgstr "Bitte geben Sie eine Domain für %1$s (Lizenz %2$d) ein." +#. translators: 1: product name, 2: license number #: src/Checkout/CheckoutController.php:371 #, php-format msgid "Please enter a valid domain for %1$s (License %2$d)." msgstr "Bitte geben Sie eine gültige Domain für %1$s (Lizenz %2$d) ein." +#. translators: 1: domain name, 2: product name #: src/Checkout/CheckoutController.php:385 #, php-format msgid "" @@ -1438,68 +1463,74 @@ msgstr "Diese Lizenz ist für diese Domain nicht gültig." msgid "Attachment file not found." msgstr "Anhangs-Datei nicht gefunden." +#. translators: 1: provided hash, 2: calculated hash #: src/Product/VersionManager.php:177 #, php-format msgid "File checksum does not match. Expected: %1$s, Got: %2$s" msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s" -#: src/Product/LicensedProductVariation.php:143 +#: src/Product/LicensedProductVariation.php:198 msgid "Monthly" msgstr "Monatlich" -#: src/Product/LicensedProductVariation.php:147 +#: src/Product/LicensedProductVariation.php:202 msgid "Quarterly" msgstr "Vierteljährlich" -#: src/Product/LicensedProductVariation.php:151 +#: src/Product/LicensedProductVariation.php:206 msgid "Yearly" msgstr "Jährlich" -#: src/Product/LicensedProductVariation.php:156 +#. translators: %d: number of days +#: src/Product/LicensedProductVariation.php:211 #, php-format msgid "%d day" msgid_plural "%d days" msgstr[0] "%d Tag" msgstr[1] "%d Tage" -#: src/Product/LicensedProductType.php:72 +#: src/Product/LicensedProductType.php:82 msgid "Licensed Product" msgstr "Lizensiertes Produkt" -#: src/Product/LicensedProductType.php:73 +#: src/Product/LicensedProductType.php:83 msgid "Licensed Variable Product" msgstr "Lizensiertes variables Produkt" -#: src/Product/LicensedProductType.php:108 +#: src/Product/LicensedProductType.php:136 msgid "License Settings" msgstr "Lizenz-Einstellungen" -#: src/Product/LicensedProductType.php:135 -#: src/Product/LicensedProductType.php:402 +#: src/Product/LicensedProductType.php:163 +#: src/Product/LicensedProductType.php:552 #, php-format msgid "%d days" msgstr "%d Tage" -#: src/Product/LicensedProductType.php:145 +#. translators: %s: URL to settings page +#: src/Product/LicensedProductType.php:173 #, php-format msgid "Leave fields empty to use default settings from %s." msgstr "Felder leer lassen, um Standardeinstellungen von %s zu verwenden." -#: src/Product/LicensedProductType.php:147 +#: src/Product/LicensedProductType.php:175 msgid "WooCommerce > Settings > Licensed Products" msgstr "WooCommerce > Einstellungen > Lizensierte Produkte" -#: src/Product/LicensedProductType.php:157 +#. translators: %d: default max activations value +#: src/Product/LicensedProductType.php:185 #, php-format msgid "Maximum number of domain activations per license. Default: %d" msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d" -#: src/Product/LicensedProductType.php:175 +#. translators: %s: default validity value +#: src/Product/LicensedProductType.php:203 #, php-format msgid "Number of days the license is valid. Leave empty for default (%s)." msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s)." -#: src/Product/LicensedProductType.php:193 +#. translators: %s: default bind to version value (Yes/No) +#: src/Product/LicensedProductType.php:221 #, php-format msgid "" "If enabled, licenses are bound to the major version at purchase time. " @@ -1508,35 +1539,35 @@ msgstr "" "Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt " "gebunden. Standard: %s" -#: src/Product/LicensedProductType.php:194 +#: src/Product/LicensedProductType.php:222 msgid "Yes" msgstr "Ja" -#: src/Product/LicensedProductType.php:194 +#: src/Product/LicensedProductType.php:222 msgid "No" msgstr "Nein" -#: src/Product/LicensedProductType.php:329 +#: src/Product/LicensedProductType.php:447 msgid "Version:" msgstr "Version:" -#: src/Product/LicensedProductType.php:373 +#: src/Product/LicensedProductType.php:523 msgid "Licensed products are always virtual" msgstr "Lizenzierte Produkte sind immer virtuell" -#: src/Product/LicensedProductType.php:375 +#: src/Product/LicensedProductType.php:525 msgid "Virtual" msgstr "Virtuell" -#: src/Product/LicensedProductType.php:408 +#: src/Product/LicensedProductType.php:558 msgid "License Duration (Days)" msgstr "Lizenz-Gültigkeit (Tage)" -#: src/Product/LicensedProductType.php:417 +#: src/Product/LicensedProductType.php:567 msgid "Leave empty for parent default. 0 = Lifetime." msgstr "Leer lassen für übergeordneten Standard. 0 = Lebenslang." -#: src/Product/LicensedProductType.php:429 +#: src/Product/LicensedProductType.php:579 msgid "Leave empty for parent default." msgstr "Leer lassen für übergeordneten Standard." @@ -1599,6 +1630,7 @@ msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen." msgid "You have no licenses yet." msgstr "Sie haben noch keine Lizenzen." +#. translators: %s: order number #: src/Frontend/AccountController.php:245 #, php-format msgid "Order #%s" @@ -1756,6 +1788,7 @@ msgstr "" "Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz vor " "dem Ablaufdatum." +#. translators: %s: list of placeholders #: src/Email/LicenseExpirationEmail.php:301 #: src/Email/LicenseExpiredEmail.php:288 #, php-format @@ -1894,6 +1927,7 @@ msgstr "" msgid "Configure License" msgstr "Lizenz konfigurieren" +#. translators: %s: WooCommerce plugin name #: wc-licensed-product.php:61 #, php-format msgid "%s requires WooCommerce to be installed and active." diff --git a/languages/wc-licensed-product.pot b/languages/wc-licensed-product.pot index aed70bc..239b595 100644 --- a/languages/wc-licensed-product.pot +++ b/languages/wc-licensed-product.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: WC Licensed Product 0.5.8\n" +"Project-Id-Version: WC Licensed Product 0.5.12\n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n" -"POT-Creation-Date: 2026-01-27 13:34+0100\n" +"POT-Creation-Date: 2026-01-27 14:41+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -304,10 +304,10 @@ msgstr "" #: src/Admin/DashboardWidgetController.php:136 #: src/Admin/OrderLicenseController.php:260 #: src/Admin/SettingsController.php:192 -#: src/Product/LicensedProductVariation.php:139 -#: src/Product/LicensedProductType.php:136 -#: src/Product/LicensedProductType.php:184 -#: src/Product/LicensedProductType.php:403 +#: src/Product/LicensedProductVariation.php:194 +#: src/Product/LicensedProductType.php:164 +#: src/Product/LicensedProductType.php:212 +#: src/Product/LicensedProductType.php:553 #: src/Frontend/AccountController.php:286 msgid "Lifetime" msgstr "" @@ -485,6 +485,7 @@ msgstr "" msgid "License set to lifetime successfully." msgstr "" +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1106 #, php-format msgid "%d license activated." @@ -492,6 +493,7 @@ msgid_plural "%d licenses activated." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1114 #, php-format msgid "%d license deactivated." @@ -499,6 +501,7 @@ msgid_plural "%d licenses deactivated." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1122 #, php-format msgid "%d license revoked." @@ -506,6 +509,7 @@ msgid_plural "%d licenses revoked." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1130 #, php-format msgid "%d license deleted." @@ -513,6 +517,7 @@ msgid_plural "%d licenses deleted." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of licenses #: src/Admin/AdminController.php:1138 #, php-format msgid "%d license extended." @@ -532,6 +537,7 @@ msgstr "" msgid "No licenses to export." msgstr "" +#. translators: %d: number of licenses imported #: src/Admin/AdminController.php:1159 #, php-format msgid "%d license imported." @@ -539,6 +545,7 @@ msgid_plural "%d licenses imported." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of licenses updated #: src/Admin/AdminController.php:1166 #, php-format msgid "%d updated." @@ -546,6 +553,7 @@ msgid_plural "%d updated." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of licenses skipped #: src/Admin/AdminController.php:1174 #, php-format msgid "%d skipped." @@ -553,6 +561,7 @@ msgid_plural "%d skipped." msgstr[0] "" msgstr[1] "" +#. translators: %d: number of errors #: src/Admin/AdminController.php:1182 #, php-format msgid "%d error." @@ -997,6 +1006,7 @@ msgstr "" msgid "View in Licenses" msgstr "" +#. translators: %s: Link to licenses page #: src/Admin/OrderLicenseController.php:280 #, php-format msgid "For more actions (revoke, extend, delete), go to the %s page." @@ -1133,8 +1143,8 @@ msgid "" "product settings override these defaults." msgstr "" -#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:154 -#: src/Product/LicensedProductType.php:420 +#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:182 +#: src/Product/LicensedProductType.php:570 msgid "Max Activations" msgstr "" @@ -1142,7 +1152,7 @@ msgstr "" msgid "Default maximum number of domain activations per license." msgstr "" -#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:172 +#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:200 msgid "License Validity (Days)" msgstr "" @@ -1152,7 +1162,7 @@ msgid "" "lifetime licenses." msgstr "" -#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:190 +#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:218 msgid "Bind to Major Version" msgstr "" @@ -1176,6 +1186,7 @@ msgstr "" msgid "Expiration Warning Schedule" msgstr "" +#. translators: %s: URL to WooCommerce email settings #: src/Admin/SettingsController.php:230 #, php-format msgid "" @@ -1299,6 +1310,7 @@ msgstr "" msgid "Each license requires a unique domain." msgstr "" +#. translators: %d: license number #: src/Checkout/CheckoutBlocksIntegration.php:129 #: src/Checkout/CheckoutController.php:224 #, php-format @@ -1310,6 +1322,16 @@ msgstr "" msgid "required" msgstr "" +#: src/Checkout/CheckoutController.php:215 +#, php-format +msgid "licensed_domains[%s][%d]" +msgstr "" + +#: src/Checkout/CheckoutController.php:216 +#, php-format +msgid "licensed_domain_%s_%d" +msgstr "" + #: src/Checkout/CheckoutController.php:323 msgid "Please enter a domain for your license." msgstr "" @@ -1318,16 +1340,19 @@ msgstr "" msgid "Please enter a valid domain for your license." msgstr "" +#. translators: 1: product name, 2: license number #: src/Checkout/CheckoutController.php:356 #, php-format msgid "Please enter a domain for %1$s (License %2$d)." msgstr "" +#. translators: 1: product name, 2: license number #: src/Checkout/CheckoutController.php:371 #, php-format msgid "Please enter a valid domain for %1$s (License %2$d)." msgstr "" +#. translators: 1: domain name, 2: product name #: src/Checkout/CheckoutController.php:385 #, php-format msgid "" @@ -1389,103 +1414,109 @@ msgstr "" msgid "Attachment file not found." msgstr "" +#. translators: 1: provided hash, 2: calculated hash #: src/Product/VersionManager.php:177 #, php-format msgid "File checksum does not match. Expected: %1$s, Got: %2$s" msgstr "" -#: src/Product/LicensedProductVariation.php:143 +#: src/Product/LicensedProductVariation.php:198 msgid "Monthly" msgstr "" -#: src/Product/LicensedProductVariation.php:147 +#: src/Product/LicensedProductVariation.php:202 msgid "Quarterly" msgstr "" -#: src/Product/LicensedProductVariation.php:151 +#: src/Product/LicensedProductVariation.php:206 msgid "Yearly" msgstr "" -#: src/Product/LicensedProductVariation.php:156 +#. translators: %d: number of days +#: src/Product/LicensedProductVariation.php:211 #, php-format msgid "%d day" msgid_plural "%d days" msgstr[0] "" msgstr[1] "" -#: src/Product/LicensedProductType.php:72 +#: src/Product/LicensedProductType.php:82 msgid "Licensed Product" msgstr "" -#: src/Product/LicensedProductType.php:73 +#: src/Product/LicensedProductType.php:83 msgid "Licensed Variable Product" msgstr "" -#: src/Product/LicensedProductType.php:108 +#: src/Product/LicensedProductType.php:136 msgid "License Settings" msgstr "" -#: src/Product/LicensedProductType.php:135 -#: src/Product/LicensedProductType.php:402 +#: src/Product/LicensedProductType.php:163 +#: src/Product/LicensedProductType.php:552 #, php-format msgid "%d days" msgstr "" -#: src/Product/LicensedProductType.php:145 +#. translators: %s: URL to settings page +#: src/Product/LicensedProductType.php:173 #, php-format msgid "Leave fields empty to use default settings from %s." msgstr "" -#: src/Product/LicensedProductType.php:147 +#: src/Product/LicensedProductType.php:175 msgid "WooCommerce > Settings > Licensed Products" msgstr "" -#: src/Product/LicensedProductType.php:157 +#. translators: %d: default max activations value +#: src/Product/LicensedProductType.php:185 #, php-format msgid "Maximum number of domain activations per license. Default: %d" msgstr "" -#: src/Product/LicensedProductType.php:175 +#. translators: %s: default validity value +#: src/Product/LicensedProductType.php:203 #, php-format msgid "Number of days the license is valid. Leave empty for default (%s)." msgstr "" -#: src/Product/LicensedProductType.php:193 +#. translators: %s: default bind to version value (Yes/No) +#: src/Product/LicensedProductType.php:221 #, php-format msgid "" "If enabled, licenses are bound to the major version at purchase time. " "Default: %s" msgstr "" -#: src/Product/LicensedProductType.php:194 +#: src/Product/LicensedProductType.php:222 msgid "Yes" msgstr "" -#: src/Product/LicensedProductType.php:194 +#: src/Product/LicensedProductType.php:222 msgid "No" msgstr "" -#: src/Product/LicensedProductType.php:329 +#: src/Product/LicensedProductType.php:447 msgid "Version:" msgstr "" -#: src/Product/LicensedProductType.php:373 +#: src/Product/LicensedProductType.php:523 msgid "Licensed products are always virtual" msgstr "" -#: src/Product/LicensedProductType.php:375 +#: src/Product/LicensedProductType.php:525 msgid "Virtual" msgstr "" -#: src/Product/LicensedProductType.php:408 +#: src/Product/LicensedProductType.php:558 msgid "License Duration (Days)" msgstr "" -#: src/Product/LicensedProductType.php:417 +#: src/Product/LicensedProductType.php:567 msgid "Leave empty for parent default. 0 = Lifetime." msgstr "" -#: src/Product/LicensedProductType.php:429 +#: src/Product/LicensedProductType.php:579 msgid "Leave empty for parent default." msgstr "" @@ -1548,6 +1579,7 @@ msgstr "" msgid "You have no licenses yet." msgstr "" +#. translators: %s: order number #: src/Frontend/AccountController.php:245 #, php-format msgid "Order #%s" @@ -1697,6 +1729,7 @@ msgid "" "expiration date." msgstr "" +#. translators: %s: list of placeholders #: src/Email/LicenseExpirationEmail.php:301 #: src/Email/LicenseExpiredEmail.php:288 #, php-format @@ -1827,6 +1860,7 @@ msgstr "" msgid "Configure License" msgstr "" +#. translators: %s: WooCommerce plugin name #: wc-licensed-product.php:61 #, php-format msgid "%s requires WooCommerce to be installed and active." diff --git a/releases/wc-licensed-product-0.5.12.sha256 b/releases/wc-licensed-product-0.5.12.sha256 new file mode 100644 index 0000000..65e046b --- /dev/null +++ b/releases/wc-licensed-product-0.5.12.sha256 @@ -0,0 +1 @@ +20bb5cd453de9bca781864430ebd152c82f660b6f9fc3f09107ba03489a71d75 /home/magdev/workspaces/php/wordpress/wp-content/plugins/wc-licensed-product/releases/wc-licensed-product-0.5.12.zip diff --git a/releases/wc-licensed-product-0.5.12.zip b/releases/wc-licensed-product-0.5.12.zip new file mode 100644 index 0000000..2e71941 Binary files /dev/null and b/releases/wc-licensed-product-0.5.12.zip differ diff --git a/src/Product/LicensedProduct.php b/src/Product/LicensedProduct.php index d628f80..33768a6 100644 --- a/src/Product/LicensedProduct.php +++ b/src/Product/LicensedProduct.php @@ -55,6 +55,14 @@ class LicensedProduct extends WC_Product return $this->exists() && $this->get_price() !== ''; } + /** + * Licensed products are always in stock (virtual, no inventory) + */ + public function is_in_stock(): bool + { + return true; + } + /** * Get max activations for this product * Falls back to default settings if not set on product diff --git a/src/Product/LicensedProductType.php b/src/Product/LicensedProductType.php index ff9fe12..57e6665 100644 --- a/src/Product/LicensedProductType.php +++ b/src/Product/LicensedProductType.php @@ -46,9 +46,19 @@ final class LicensedProductType add_action('woocommerce_licensed_add_to_cart', [$this, 'addToCartTemplate']); add_action('woocommerce_licensed-variable_add_to_cart', [$this, 'variableAddToCartTemplate']); + // Use variable product add-to-cart handler for licensed-variable products + add_filter('woocommerce_add_to_cart_handler', [$this, 'addToCartHandler'], 10, 2); + // Make product virtual by default add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2); + // Hide stock HTML for licensed products + add_filter('woocommerce_get_stock_html', [$this, 'hideStockHtml'], 10, 2); + add_filter('woocommerce_get_availability', [$this, 'hideAvailability'], 10, 2); + add_filter('woocommerce_get_availability_text', [$this, 'hideAvailabilityText'], 10, 2); + add_filter('woocommerce_product_get_stock_quantity', [$this, 'hideStockQuantity'], 10, 2); + add_filter('woocommerce_product_variation_get_stock_quantity', [$this, 'hideStockQuantity'], 10, 2); + // Display current version under product title on single product page add_action('woocommerce_single_product_summary', [$this, 'displayCurrentVersion'], 6); @@ -80,9 +90,9 @@ final class LicensedProductType * @param string $className Default class name * @param string $productType Product type * @param string $postType Post type (usually 'product' or 'product_variation') - * @param int $productId Product ID + * @param mixed $productId Product ID (can be int or string) */ - public function getProductClass(string $className, string $productType, string $postType = '', int $productId = 0): string + public function getProductClass(string $className, string $productType, string $postType = '', $productId = 0): string { if ($productType === 'licensed') { return LicensedProduct::class; @@ -91,17 +101,19 @@ final class LicensedProductType return LicensedVariableProduct::class; } // Handle variations of licensed-variable products - if ($productType === 'variation') { + // Check both by product type and by post type for variations + if ($productType === 'variation' || $postType === 'product_variation') { // Get parent ID from the product post $parentId = 0; - if ($productId > 0) { - $parentId = wp_get_post_parent_id($productId); + $productIdInt = (int) $productId; + if ($productIdInt > 0) { + $parentId = wp_get_post_parent_id($productIdInt); } // Fallback to global $post if product ID not available if (!$parentId) { global $post; if ($post && $post->post_parent) { - $parentId = $post->post_parent; + $parentId = (int) $post->post_parent; } } @@ -281,26 +293,111 @@ final class LicensedProductType wc_get_template('single-product/add-to-cart/simple.php'); } + /** + * Use the variable product add-to-cart handler for licensed-variable products + * WooCommerce uses product type to determine which handler to use + */ + public function addToCartHandler(string $handler, \WC_Product $product): string + { + if ($product->is_type('licensed-variable')) { + return 'variable'; + } + return $handler; + } + + /** + * Hide stock HTML for licensed products (they're always virtual/in-stock) + */ + public function hideStockHtml(string $html, \WC_Product $product): string + { + if ($this->isLicensedProductOrVariation($product)) { + return ''; + } + return $html; + } + + /** + * Hide availability data for licensed products (they're always virtual/in-stock) + */ + public function hideAvailability(array $availability, \WC_Product $product): array + { + if ($this->isLicensedProductOrVariation($product)) { + return [ + 'availability' => '', + 'class' => '', + ]; + } + return $availability; + } + + /** + * Hide availability text for licensed products + */ + public function hideAvailabilityText(string $availability, \WC_Product $product): string + { + if ($this->isLicensedProductOrVariation($product)) { + return ''; + } + return $availability; + } + + /** + * Hide stock quantity for licensed products (return null = no stock display) + * + * @param int|null $quantity + * @param \WC_Product $product + * @return int|null + */ + public function hideStockQuantity($quantity, \WC_Product $product) + { + if ($this->isLicensedProductOrVariation($product)) { + return null; + } + return $quantity; + } + + /** + * Check if product is a licensed product or variation of one + */ + private function isLicensedProductOrVariation(\WC_Product $product): bool + { + // Direct licensed products + if ($product->is_type('licensed') || $product->is_type('licensed-variable')) { + return true; + } + + // Check by class name for our custom variation class + if ($product instanceof LicensedProductVariation) { + return true; + } + + // Check if this is a variation with a licensed-variable parent + // Use WC_Product_Factory::get_product_type() to get parent type directly from DB + // This is more reliable than loading the full product object + $parentId = $product->get_parent_id(); + if ($parentId) { + $parentType = \WC_Product_Factory::get_product_type($parentId); + if ($parentType === 'licensed-variable') { + return true; + } + } + + return false; + } + /** * Make licensed products virtual by default */ public function isVirtual(bool $isVirtual, \WC_Product $product): bool { - if ($product->is_type('licensed') || $product->is_type('licensed-variable')) { + if ($this->isLicensedProductOrVariation($product)) { return true; } - // Also handle variations of licensed-variable products - if ($product->is_type('variation') && $product->get_parent_id()) { - $parent = wc_get_product($product->get_parent_id()); - if ($parent && $parent->is_type('licensed-variable')) { - return true; - } - } return $isVirtual; } /** - * Enqueue frontend styles for licensed products on single product pages + * Enqueue frontend styles and scripts for licensed products on single product pages */ public function enqueueFrontendStyles(): void { @@ -320,6 +417,11 @@ final class LicensedProductType [], WC_LICENSED_PRODUCT_VERSION ); + + // For licensed-variable products, enqueue WooCommerce variation scripts + if ($product->is_type('licensed-variable')) { + wp_enqueue_script('wc-add-to-cart-variation'); + } } /** @@ -372,8 +474,13 @@ final class LicensedProductType return; } + // Update global $product to use the correctly loaded instance + // This ensures the template has the right product type + $product = $variableProduct; + // Get variations count to determine if we should load them via AJAX - $getVariations = count($variableProduct->get_children()) <= apply_filters('woocommerce_ajax_variation_threshold', 30, $variableProduct); + $children = $variableProduct->get_children(); + $getVariations = count($children) <= apply_filters('woocommerce_ajax_variation_threshold', 30, $variableProduct); // Get template variables - WooCommerce expects these to be set $availableVariations = $getVariations ? $variableProduct->get_available_variations() : false; diff --git a/src/Product/LicensedProductVariation.php b/src/Product/LicensedProductVariation.php index cda9b35..221d7a6 100644 --- a/src/Product/LicensedProductVariation.php +++ b/src/Product/LicensedProductVariation.php @@ -35,6 +35,61 @@ class LicensedProductVariation extends WC_Product_Variation return true; } + /** + * Licensed products are always in stock (virtual, no inventory) + */ + public function is_in_stock(): bool + { + return true; + } + + /** + * Get availability - empty for licensed products (no stock indicator) + */ + public function get_availability(): array + { + return [ + 'availability' => '', + 'class' => '', + ]; + } + + /** + * Don't manage stock for licensed products + */ + public function managing_stock(): bool + { + return false; + } + + /** + * Check if variation is purchasable + * Override to handle custom parent product type + */ + public function is_purchasable(): bool + { + // Check if variation exists + if (!$this->exists()) { + return false; + } + + // Check parent product status + $parentId = $this->get_parent_id(); + $parentStatus = get_post_status($parentId); + + if ($parentStatus !== 'publish' && !current_user_can('edit_post', $parentId)) { + return false; + } + + // Check if variation has a price + $price = $this->get_price(); + if ($price === '' || $price === null) { + return false; + } + + return apply_filters('woocommerce_variation_is_purchasable', true, $this); + } + /** * Get max activations for this variation * Falls back to parent product, then to default settings diff --git a/src/Product/LicensedVariableProduct.php b/src/Product/LicensedVariableProduct.php index d1eb233..f8dd6e5 100644 --- a/src/Product/LicensedVariableProduct.php +++ b/src/Product/LicensedVariableProduct.php @@ -41,6 +41,19 @@ class LicensedVariableProduct extends WC_Product_Variable return 'licensed-variable'; } + /** + * Check if product is of a certain type + * Override to return true for 'variable' as well, so WooCommerce internal + * checks pass (many methods in WC_Product_Variable check is_type('variable')) + */ + public function is_type($type): bool + { + if (is_array($type)) { + return in_array($this->get_type(), $type, true) || in_array('variable', $type, true); + } + return $this->get_type() === $type || 'variable' === $type; + } + /** * Licensed products are always virtual */ @@ -60,6 +73,189 @@ class LicensedVariableProduct extends WC_Product_Variable return parent::is_purchasable(); } + /** + * Licensed products are always in stock (virtual, no inventory) + */ + public function is_in_stock(): bool + { + return true; + } + + /** + * Get children (variations) for this product + * Override because WC_Product_Variable::get_children() checks is_type('variable') + * which fails for our 'licensed-variable' type + */ + public function get_children($context = 'view'): array + { + if (!$this->get_id()) { + return []; + } + + // Query variations directly from database since WooCommerce's data store + // doesn't work properly with custom variable product types + global $wpdb; + $children = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_parent = %d + AND post_type = 'product_variation' + AND post_status IN ('publish', 'private') + ORDER BY menu_order ASC, ID ASC", + $this->get_id() + )); + + $children = array_map('intval', $children); + + if ('view' === $context) { + $children = apply_filters('woocommerce_get_children', $children, $this, false); + } + + return is_array($children) ? $children : []; + } + + /** + * Get variation attributes for this product + * Override because WC_Product_Variable uses data_store which doesn't work + * properly with custom variable product types + */ + public function get_variation_attributes(): array + { + $attributes = $this->get_attributes(); + + if (!$attributes || !is_array($attributes)) { + return []; + } + + $variation_attributes = []; + + foreach ($attributes as $attribute) { + // For WC_Product_Attribute objects + if ($attribute instanceof \WC_Product_Attribute) { + if ($attribute->get_variation()) { + $attribute_name = $attribute->get_name(); + + // For taxonomy attributes, get term slugs + if ($attribute->is_taxonomy()) { + $attribute_terms = wc_get_product_terms( + $this->get_id(), + $attribute_name, + ['fields' => 'slugs'] + ); + $variation_attributes[$attribute_name] = $attribute_terms; + } else { + // For custom attributes, get options directly + $variation_attributes[$attribute_name] = $attribute->get_options(); + } + } + } + // For array-based attributes (older format) + elseif (is_array($attribute) && !empty($attribute['is_variation'])) { + $attribute_name = $attribute['name']; + $values = isset($attribute['value']) ? explode('|', $attribute['value']) : []; + $variation_attributes[$attribute_name] = array_map('trim', $values); + } + } + + return $variation_attributes; + } + + /** + * Get variation prices (regular, sale, and final prices) + * Override because WC_Product_Variable uses data_store which doesn't work + * properly with custom variable product types + */ + public function get_variation_prices($for_display = false): array + { + $children = $this->get_children(); + + if (empty($children)) { + return [ + 'price' => [], + 'regular_price' => [], + 'sale_price' => [], + ]; + } + + $prices = [ + 'price' => [], + 'regular_price' => [], + 'sale_price' => [], + ]; + + foreach ($children as $child_id) { + $variation = wc_get_product($child_id); + if ($variation) { + $price = $variation->get_price(); + $regular_price = $variation->get_regular_price(); + $sale_price = $variation->get_sale_price(); + + if ('' !== $price) { + $prices['price'][$child_id] = $price; + } + if ('' !== $regular_price) { + $prices['regular_price'][$child_id] = $regular_price; + } + if ('' !== $sale_price) { + $prices['sale_price'][$child_id] = $sale_price; + } + } + } + + // Sort prices + asort($prices['price']); + asort($prices['regular_price']); + asort($prices['sale_price']); + + $this->prices_array = $prices; + + return $this->prices_array; + } + + /** + * Get available variations for this product + * Override because WC_Product_Variable uses data_store which doesn't work + * properly with custom variable product types + */ + public function get_available_variations($return = 'array') + { + $children = $this->get_children(); + $available_variations = []; + + foreach ($children as $child_id) { + $variation = wc_get_product($child_id); + + if (!$variation) { + continue; + } + + // Check if variation should be available + if (!$variation->exists()) { + continue; + } + + // Check if purchasable (has price) + if (!$variation->is_purchasable()) { + continue; + } + + // Build variation data + if ($return === 'array') { + $variationData = $this->get_available_variation($variation); + // Override availability_html to be empty for licensed products + $variationData['availability_html'] = ''; + $available_variations[] = $variationData; + } else { + $available_variations[] = $variation; + } + } + + if ($return === 'array') { + $available_variations = array_values(array_filter($available_variations)); + } + + return $available_variations; + } + /** * Get max activations for this product (parent default) * Falls back to default settings if not set on product diff --git a/wc-licensed-product.php b/wc-licensed-product.php index 2600266..78a1c47 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce 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. - * Version: 0.5.11 + * Version: 0.5.12 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * License: GPL-2.0-or-later @@ -28,7 +28,7 @@ if (!defined('ABSPATH')) { } // Plugin constants -define('WC_LICENSED_PRODUCT_VERSION', '0.5.11'); +define('WC_LICENSED_PRODUCT_VERSION', '0.5.12'); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));