You've already forked wc-composable-product
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed66c96d3d | |||
| ea64dbfb33 | |||
| 6507f4d8bb | |||
| 29a68b0be4 | |||
| fb8ddf903e | |||
| 33d2836de0 | |||
| c036a37602 | |||
| efedd1bf29 | |||
| 12388af5a0 | |||
| c39c13ffed | |||
| 7931dbeef9 | |||
| ee81de86c2 | |||
| 669888817b | |||
| 5564b888fc | |||
| 91aca25169 | |||
| 4195fb2651 | |||
| ba28ae174f | |||
| 755108a7d3 | |||
| 85983d5473 | |||
| 252b187600 | |||
| 8185a77697 | |||
| 6c2e317230 | |||
| 58f5329bc4 | |||
| 0767016370 | |||
| fa7ec0e422 | |||
| f4d2543d4e | |||
| 9e4513f911 | |||
| 4dc7b767a8 | |||
| f763e35d19 | |||
| 8b271c90c0 | |||
| 0dd4408b23 | |||
| 7a4a0a0135 | |||
| c6a48d6404 | |||
| ac1cb9b135 | |||
| f5bc0d0335 | |||
| 88a907c4dd | |||
| 03a7624564 | |||
| 1c3f44f3c2 | |||
| 287f8b778b | |||
| 63d8f9ed52 | |||
| 601570d724 | |||
| e9b2d1c79b | |||
| d27dd4b7bd |
205
.gitea/workflows/release.yml
Normal file
205
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,205 @@
|
||||
name: Create Release Package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip, intl, gettext
|
||||
tools: composer:v2
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Validate composer.json
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Install Composer dependencies (production)
|
||||
run: |
|
||||
composer config platform.php 8.3.0
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
- name: Install gettext
|
||||
run: apt-get update && apt-get install -y gettext
|
||||
|
||||
- name: Compile translations
|
||||
run: |
|
||||
for po in languages/*.po; do
|
||||
if [ -f "$po" ]; then
|
||||
mo="${po%.po}.mo"
|
||||
echo "Compiling $po to $mo"
|
||||
msgfmt -o "$mo" "$po"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Verify plugin version matches tag
|
||||
run: |
|
||||
PLUGIN_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" wc-composable-product.php | head -1)
|
||||
TAG_VERSION=${{ steps.version.outputs.version }}
|
||||
if [ "$PLUGIN_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "Error: Plugin version ($PLUGIN_VERSION) does not match tag version ($TAG_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Version verified: $PLUGIN_VERSION"
|
||||
|
||||
- name: Create release directory
|
||||
run: mkdir -p releases
|
||||
|
||||
- name: Build release package
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
PLUGIN_NAME="wc-composable-product"
|
||||
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
|
||||
|
||||
# Move to parent directory for proper zip structure
|
||||
cd ..
|
||||
|
||||
# Create zip with proper WordPress plugin structure
|
||||
zip -r "${PLUGIN_NAME}/${RELEASE_FILE}" "${PLUGIN_NAME}" \
|
||||
-x "${PLUGIN_NAME}/.git/*" \
|
||||
-x "${PLUGIN_NAME}/.gitea/*" \
|
||||
-x "${PLUGIN_NAME}/.github/*" \
|
||||
-x "${PLUGIN_NAME}/.vscode/*" \
|
||||
-x "${PLUGIN_NAME}/.claude/*" \
|
||||
-x "${PLUGIN_NAME}/CLAUDE.md" \
|
||||
-x "${PLUGIN_NAME}/wp-core" \
|
||||
-x "${PLUGIN_NAME}/wp-core/*" \
|
||||
-x "${PLUGIN_NAME}/wp-plugins" \
|
||||
-x "${PLUGIN_NAME}/wp-plugins/*" \
|
||||
-x "${PLUGIN_NAME}/releases/*" \
|
||||
-x "${PLUGIN_NAME}/cache/*" \
|
||||
-x "${PLUGIN_NAME}/composer.lock" \
|
||||
-x "${PLUGIN_NAME}/*.log" \
|
||||
-x "${PLUGIN_NAME}/.gitignore" \
|
||||
-x "${PLUGIN_NAME}/.editorconfig" \
|
||||
-x "${PLUGIN_NAME}/phpcs.xml*" \
|
||||
-x "${PLUGIN_NAME}/phpunit.xml*" \
|
||||
-x "${PLUGIN_NAME}/tests/*" \
|
||||
-x "${PLUGIN_NAME}/*.po~" \
|
||||
-x "${PLUGIN_NAME}/*.bak" \
|
||||
-x "*.DS_Store"
|
||||
|
||||
cd "${PLUGIN_NAME}"
|
||||
echo "Created: ${RELEASE_FILE}"
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
|
||||
cd releases
|
||||
sha256sum "wc-composable-product-${VERSION}.zip" > "wc-composable-product-${VERSION}.zip.sha256"
|
||||
|
||||
echo "SHA256:"
|
||||
cat "wc-composable-product-${VERSION}.zip.sha256"
|
||||
|
||||
- name: Verify package structure
|
||||
run: |
|
||||
set +o pipefail
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
echo "Package contents:"
|
||||
unzip -l "releases/wc-composable-product-${VERSION}.zip" | head -50 || true
|
||||
|
||||
# Verify main file is at correct location
|
||||
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/wc-composable-product.php"; then
|
||||
echo "✓ Main plugin file at correct location"
|
||||
else
|
||||
echo "✗ Error: Main plugin file not found at wc-composable-product/wc-composable-product.php"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify vendor directory is included
|
||||
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/vendor/"; then
|
||||
echo "✓ Vendor directory included"
|
||||
else
|
||||
echo "✗ Error: Vendor directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Extract changelog for release notes
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
# Extract changelog section for this version
|
||||
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
|
||||
if [ -z "$NOTES" ]; then
|
||||
NOTES="Release version ${VERSION}"
|
||||
fi
|
||||
# Save to file for multi-line output
|
||||
echo "$NOTES" > release_notes.txt
|
||||
echo "Release notes extracted"
|
||||
|
||||
- name: Create Gitea Release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
TAG_NAME=${{ github.ref_name }}
|
||||
PRERELEASE="false"
|
||||
if [[ "$TAG_NAME" == *-* ]]; then
|
||||
PRERELEASE="true"
|
||||
fi
|
||||
|
||||
# Read release notes
|
||||
BODY=$(cat release_notes.txt)
|
||||
|
||||
# Check if release already exists for this tag and delete it
|
||||
EXISTING_RELEASE=$(curl -s \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}")
|
||||
|
||||
EXISTING_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
||||
|
||||
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
||||
echo "Deleting existing release ID: $EXISTING_ID"
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${EXISTING_ID}"
|
||||
fi
|
||||
|
||||
# Create release via Gitea API
|
||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||
|
||||
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||
echo "Failed to create release:"
|
||||
echo "$RELEASE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created release ID: $RELEASE_ID"
|
||||
|
||||
# Upload attachments
|
||||
for file in "releases/wc-composable-product-${VERSION}.zip" "releases/wc-composable-product-${VERSION}.zip.sha256"; do
|
||||
if [ -f "$file" ]; then
|
||||
FILENAME=$(basename "$file")
|
||||
echo "Uploading $FILENAME..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$file" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||
echo "Uploaded $FILENAME"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Release created successfully: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||
11
.gitignore
vendored
Normal file → Executable file
11
.gitignore
vendored
Normal file → Executable file
@@ -1,14 +1,8 @@
|
||||
# Linked sources
|
||||
wp-core
|
||||
wp-plugins
|
||||
tpp
|
||||
|
||||
# Editor swap files
|
||||
*.*swp
|
||||
|
||||
# Composer
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
@@ -16,9 +10,14 @@ cache/
|
||||
# Development files
|
||||
.vscode/
|
||||
.idea/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.directory
|
||||
|
||||
# Binary files
|
||||
languages/*.mo
|
||||
|
||||
|
||||
294
CHANGELOG.md
294
CHANGELOG.md
@@ -5,6 +5,300 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.2.1] - 2026-03-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Consolidated documentation: merged INSTALL.md into README.md, merged IMPLEMENTATION.md into CLAUDE.md
|
||||
- Condensed CLAUDE.md from ~1960 lines to ~160 lines, keeping only essential architecture and lessons learned
|
||||
- README.md now includes full installation guide, usage tutorial, and troubleshooting section
|
||||
- Cleaned up .gitignore
|
||||
|
||||
### Removed
|
||||
|
||||
- INSTALL.md (content merged into README.md)
|
||||
- IMPLEMENTATION.md (content merged into CLAUDE.md)
|
||||
|
||||
## [1.2.0] - 2026-03-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Product selection always empty regardless of configuration (categories, tags, or SKUs)
|
||||
- Root cause: Product query used a `meta_query` checking `_product_type` in `wp_postmeta`, but WooCommerce stores product types in the `product_type` **taxonomy** — the `!=` comparison with a non-existent meta key caused an INNER JOIN returning zero results
|
||||
- Fix: Replaced broken `meta_query` with correct `tax_query` using `product_type` taxonomy to exclude composable products
|
||||
- **CRITICAL**: Cart price always 0.00 despite correct frontend price calculation
|
||||
- Root cause: `composable_price_calculated` flag was persisted to the cart session, preventing price recalculation on subsequent page loads — but `set_price()` only modifies the in-memory product object and is lost between requests
|
||||
- Fix: Removed per-item session flag; the existing static `$already_calculated` flag already prevents duplicate calculation within a single request
|
||||
- **Admin tab rendering**: Both General and Composable Options panels visible on initial page load
|
||||
- Root cause: JavaScript manually showed `#composable_product_data` via `.show()` without hiding the General panel
|
||||
- Fix: Trigger WooCommerce's native tab click instead, so the tab system handles panel visibility correctly
|
||||
|
||||
### Added
|
||||
|
||||
- **Gitea CI/CD release workflow** (`.gitea/workflows/release.yml`)
|
||||
- Triggered on `v*` tags
|
||||
- Installs PHP 8.3 with production Composer dependencies
|
||||
- Compiles `.po` → `.mo` translations
|
||||
- Verifies plugin version matches tag
|
||||
- Builds release ZIP with proper WordPress directory structure
|
||||
- Generates SHA-256 checksums
|
||||
- Verifies package contains main plugin file and vendor directory
|
||||
- Extracts changelog for release notes
|
||||
- Creates Gitea release with attachments via API
|
||||
|
||||
### Removed
|
||||
|
||||
- Debug logging from v1.1.14 (no longer needed after root cause identified)
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified files: includes/Product_Type.php, includes/Cart_Handler.php, assets/js/admin.js
|
||||
- New file: .gitea/workflows/release.yml
|
||||
- Product query now correctly uses `tax_query` with `product_type` taxonomy (`NOT IN` operator)
|
||||
- Cart price recalculated on every request via `woocommerce_before_calculate_totals` hook
|
||||
- Admin JS uses `$('ul.product_data_tabs li.composable_options a').trigger('click')` for native WooCommerce tab handling
|
||||
|
||||
## [1.1.14] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
- **DEBUG**: Comprehensive debug logging to troubleshoot product retrieval issues
|
||||
- Error log output shows selection criteria, query arguments, and results
|
||||
- Logs each product/variation being added to help identify filtering issues
|
||||
- Enable by setting WP_DEBUG to true in wp-config.php
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (added error_log statements throughout get_available_products())
|
||||
- Logs criteria array (categories, tags, SKUs)
|
||||
- Logs WP_Query arguments before execution
|
||||
- Logs number of posts found by query
|
||||
- Logs each variable product's variation count
|
||||
- Logs each variation/simple product being added with name
|
||||
- Logs total products available at end
|
||||
- All logging wrapped in WP_DEBUG checks (no performance impact in production)
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a debug release to help diagnose why products aren't showing
|
||||
- No functional changes from v1.1.13
|
||||
- User should enable WP_DEBUG and check debug.log or error.log
|
||||
- Log output will show exactly where products are being filtered out
|
||||
- All translation files remain at 100% completion (57/57 strings)
|
||||
|
||||
## [1.1.13] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: "No products available for selection" message showing even when products are configured
|
||||
- Removed overly strict `is_in_stock()` requirement that was filtering out all products
|
||||
- Products now show regardless of stock status (out-of-stock items are displayed but disabled)
|
||||
- Added `'relation' => 'AND'` to meta_query for proper handling of multiple meta conditions
|
||||
|
||||
### Changed
|
||||
|
||||
- Product retrieval now shows all purchasable products, not just in-stock ones
|
||||
- Stock status still displayed on frontend with appropriate styling
|
||||
- Out-of-stock items shown but disabled via checkbox and visual indicators
|
||||
- Frontend stock management from v1.1.0 still fully functional
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (lines 117-124, 177, 181)
|
||||
- Changed from `$variation->is_in_stock() && $variation->is_purchasable()` to just `$variation->is_purchasable()`
|
||||
- Changed from `$product->is_in_stock() && $product->is_purchasable()` to just `$product->is_purchasable()`
|
||||
- Added `'relation' => 'AND'` to meta_query array for WordPress query compatibility
|
||||
|
||||
### Notes
|
||||
|
||||
- This fixes the issue where NO products were showing in the selector
|
||||
- Stock validation still occurs at add-to-cart time (Stock_Manager class)
|
||||
- Frontend still displays stock badges (in stock, low stock, out of stock)
|
||||
- Out-of-stock items remain non-selectable via disabled checkboxes
|
||||
- All translation files remain at 100% completion (57/57 strings)
|
||||
|
||||
## [1.1.12] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Variable product variations still not appearing in product selector after v1.1.11 release
|
||||
- Changed variation retrieval method from `get_available_variations()` to `get_children()` for more reliable variation ID retrieval
|
||||
- `get_available_variations()` returns complex data arrays which may not work in all contexts
|
||||
- `get_children()` returns simple array of variation IDs directly, ensuring consistent results
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (lines 171-184)
|
||||
- Changed from `$product->get_available_variations()` to `$product->get_children()`
|
||||
- More direct and reliable method for retrieving variation IDs
|
||||
- Each variation ID passed to `wc_get_product()` for full product object
|
||||
- Maintains all stock and purchasability checks from v1.1.11
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a patch release fixing the variable product support introduced in v1.1.11
|
||||
- User reported "nope, still no product selectable" after v1.1.11
|
||||
- Root cause: `get_available_variations()` returns variation data arrays instead of clean IDs
|
||||
- `get_children()` is the standard WooCommerce method for retrieving variation IDs
|
||||
- All translation files remain at 100% completion (57/57 strings - no changes needed)
|
||||
|
||||
## [1.1.11] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
- **FEATURE**: Variable product support - composable products can now include variable products and their variations
|
||||
- Variable products automatically expand to show all available variations as selectable items
|
||||
- Each variation displays with full attribute information (e.g., "Product - Color: Red, Size: Large")
|
||||
|
||||
### Fixed
|
||||
|
||||
- Products not showing in selector when all available products were variable products
|
||||
- Variable products were being filtered out because parent products aren't directly purchasable
|
||||
|
||||
### Changed
|
||||
|
||||
- Modified `get_available_products()` to detect and handle variable products
|
||||
- Variable products now expand into their individual variations
|
||||
- Each variation checked individually for stock status and purchasability
|
||||
- Simple products continue to work exactly as before
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified file: includes/Product_Type.php (lines 160-188)
|
||||
- Added logic to detect `is_type('variable')` products
|
||||
- Uses `get_available_variations()` to retrieve all variations
|
||||
- Each variation validated with `is_in_stock()` and `is_purchasable()`
|
||||
- Maintains backward compatibility with simple products
|
||||
|
||||
### Notes
|
||||
|
||||
- This is a feature enhancement release, not a bug fix
|
||||
- Resolves the issue where categories containing only variable products showed no selections
|
||||
- Variations display with their parent product name plus selected attributes
|
||||
- Stock management works correctly for both simple products and variations
|
||||
- All translation files remain at 100% completion (57/57 strings - no new strings added)
|
||||
|
||||
## [1.1.10] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Admin panel - Both General and Composable tabs visible simultaneously on initial page load
|
||||
- **CRITICAL**: Frontend - No products showing in product selector, only cart button and pricing visible
|
||||
- Empty product grid now shows helpful message instead of blank space
|
||||
|
||||
### Changed
|
||||
|
||||
- Added explicit `display: none` to `#composable_product_data` panel for proper initial hiding
|
||||
- Panel now only shows when `body.product-type-composable` class is present
|
||||
- Added empty state message in product selector template when no products are configured
|
||||
- Cleared Twig cache to ensure template changes take effect
|
||||
|
||||
### Added
|
||||
|
||||
- Empty state message: "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
- Translations for empty state message in all 6 supported locales (de_DE, de_DE_informal, de_CH, de_CH_informal, fr_CH, it_CH)
|
||||
- Recompiled .mo translation files
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified files: assets/css/admin.css (lines 7-16), templates/product-selector.twig (lines 12-15)
|
||||
- Root cause (admin): Panel lacked explicit CSS hiding rule, relied only on `hidden` class
|
||||
- Root cause (frontend): No feedback when products array is empty
|
||||
- Solution: CSS specificity + empty state conditional in Twig template
|
||||
|
||||
### Notes
|
||||
|
||||
- This release fixes two critical bugs discovered immediately after v1.1.9
|
||||
- Admin interface now correctly hides composable panel until product type is selected
|
||||
- Frontend provides clear user feedback when product selection is unavailable
|
||||
- All translation files now 100% complete (57/57 strings)
|
||||
|
||||
## [1.1.9] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Admin rendering completely broken - tabs disappeared and fields appeared out of context after v1.1.8 release
|
||||
- CSS selectors were too broad, hiding tab navigation along with field groups
|
||||
- Removed `!important` flags that caused overly aggressive hiding
|
||||
|
||||
### Changed
|
||||
|
||||
- Made CSS selectors more specific: `.options_group.show_if_composable` for field groups only
|
||||
- Added separate rule for tab links: `.product_data_tabs li.composable_options`
|
||||
- Tab navigation now works correctly without hiding itself
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified files: assets/css/admin.css (lines 22-40)
|
||||
- Root cause: `.show_if_composable` class used by WooCommerce for both tab links AND field groups
|
||||
- Solution: Separate selectors for each use case to prevent unintended hiding
|
||||
|
||||
### Notes
|
||||
|
||||
- This release fixes critical regression introduced in v1.1.8
|
||||
- Admin interface now renders correctly with visible tabs and properly positioned fields
|
||||
- No `!important` flags needed with specific selectors
|
||||
|
||||
## [1.1.8] - 2025-12-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Admin rendering bug where both General and Composable Options tabs showed simultaneously on initial page load
|
||||
- **CRITICAL**: Frontend product selector not appearing on product pages - WooCommerce's default add-to-cart button now hidden for composable products
|
||||
- **CRITICAL**: Price formatting not localized - prices now display with proper currency symbols, decimal separators, and thousand separators for all locales
|
||||
|
||||
### Added
|
||||
|
||||
- `wc_price()` Twig function for proper price formatting in templates
|
||||
- `formatPrice()` JavaScript method with full WooCommerce locale support
|
||||
- Price format localization data passed to frontend JavaScript (decimal/thousand separators, currency position, number of decimals)
|
||||
- `hide_default_add_to_cart()` method to prevent WooCommerce's default purchase UI for composable products
|
||||
|
||||
### Changed
|
||||
|
||||
- Enhanced CSS specificity with `!important` flags for proper tab visibility control
|
||||
- Template now uses `{{ fixed_price_html|raw }}` instead of raw currency concatenation
|
||||
- Product selector passes pre-formatted price HTML from `wc_price()` function
|
||||
- Frontend JavaScript updates prices dynamically using WooCommerce format settings
|
||||
|
||||
### Technical
|
||||
|
||||
- Modified files: assets/css/admin.css (+24 lines), includes/Cart_Handler.php (+14 lines), includes/Plugin.php (+7 lines), includes/Product_Selector.php (+2 lines), templates/product-selector.twig, assets/js/frontend.js (+28 lines)
|
||||
- All PHP files pass syntax validation
|
||||
- Supports Swiss format (CHF 50.-), European format (50,00 €), US format ($50.00), and all other WooCommerce locales
|
||||
- Thousand separator support: comma (1,000), dot (1.000), apostrophe (1'000), space (1 000)
|
||||
|
||||
### Notes
|
||||
|
||||
- This release fixes all three critical UI bugs reported in CLAUDE.md
|
||||
- Admin tabs now display correctly on initial page load without JavaScript flicker
|
||||
- Frontend product selector is now the only purchase interface (no WooCommerce default button)
|
||||
- All prices maintain proper locale formatting during dynamic updates
|
||||
|
||||
## [1.1.7] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
- Compiled .mo translation files for all 6 supported locales (de_DE, de_DE_informal, de_CH, de_CH_informal, fr_CH, it_CH)
|
||||
- WordPress can now load translations in admin and frontend areas
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL**: Missing translations in WordPress admin when using non-English locales
|
||||
- Settings page ("Composable Products", "Default Selection Limit", etc.) now properly translated
|
||||
- Product settings ("Composable Options", "Selection Criteria", etc.) now properly translated
|
||||
|
||||
### Technical
|
||||
|
||||
- Compiled .mo files from .po sources using msgfmt
|
||||
- All 6 locales now have complete translation coverage (56/56 strings translated and compiled)
|
||||
- .mo files required for WordPress i18n system to display translations
|
||||
|
||||
### Notes
|
||||
|
||||
- Previous versions included .po translation files but WordPress requires compiled .mo files
|
||||
- This release makes all existing translations actually visible to users
|
||||
|
||||
## [1.1.6] - 2025-12-31
|
||||
|
||||
### Added
|
||||
|
||||
861
CLAUDE.md
861
CLAUDE.md
@@ -1,107 +1,31 @@
|
||||
# WooCommerce plugin for user composable products - AI Context Document
|
||||
# WooCommerce Composable Products - AI Context Document
|
||||
|
||||
**Author:** Marco Graetsch
|
||||
|
||||
## Project Overview
|
||||
|
||||
This plugin implements a special product type, for which users can select a limited number of product from a configurable set of simple or variable products. The limit of selectable products should be a global and per-product setting, for which global is the fallback. The set of selectable products can be defined per category, tag or SKU. The price is either a fixed price or the sum of the prices of the selected products. Think of a package of stickers as composable product, where each package can contain $limit number of stickers.
|
||||
This plugin implements a custom WooCommerce product type where customers select a limited number of products from a configurable set of simple or variable products. The limit is configurable globally and per-product. The selectable products are defined by category, tag, or SKU. Pricing is either fixed or the sum of selected products. Think of a sticker pack where each package contains N stickers chosen by the customer.
|
||||
|
||||
### Key Fact: 100% AI-Generated
|
||||
|
||||
This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance.
|
||||
This project is 100% AI-generated ("vibe-coded") using Claude.AI.
|
||||
|
||||
## Technical Stack
|
||||
|
||||
- **Language:** PHP 8.3+
|
||||
- **Framework:** Latest WordPress Plugin API
|
||||
- **Framework:** WordPress Plugin API
|
||||
- **E-commerce:** WooCommerce 10.0+
|
||||
- **Template Engine:** Twig 3.0 (via Composer)
|
||||
- **Frontend:** Vanilla JavaScript + jQuery
|
||||
- **Styling:** Custom CSS
|
||||
- **Dependency Management:** Composer
|
||||
- **Internationalization:** WordPress i18n (.pot/.po/.mo files)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- All user inputs are sanitized (integers for quantities/prices)
|
||||
- Nonce verification on form submissions
|
||||
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
|
||||
- Direct file access prevention via `ABSPATH` check
|
||||
|
||||
### Translation Ready
|
||||
|
||||
All user-facing strings use:
|
||||
|
||||
```php
|
||||
__('Text to translate', 'wc-composable-product')
|
||||
_e('Text to translate', 'wc-composable-product')
|
||||
```
|
||||
|
||||
Text domain: `wc-composable-product`
|
||||
|
||||
**Translation Template:**
|
||||
|
||||
- Base `.pot` file created: `languages/wc-composable-product.pot`
|
||||
- Ready for translation to any locale
|
||||
- All translatable strings properly marked with text domain
|
||||
|
||||
**Available Translations:**
|
||||
|
||||
- `en_US` - English (United States) [base language - .pot template]
|
||||
- `de_DE` - German (Germany, formal) ✓ Complete
|
||||
- `de_DE_informal` - German (Germany, informal "du") ✓ Complete
|
||||
- `de_CH` - German (Switzerland, formal "Sie") ✓ Complete
|
||||
- `de_CH_informal` - German (Switzerland, informal "du") ✓ Complete
|
||||
- `fr_CH` - French (Switzerland) ✓ Complete
|
||||
- `it_CH` - Italian (Switzerland) ✓ Complete
|
||||
|
||||
All .po files created with 40+ translated strings. Swiss locales include CHF currency formatting in examples (e.g., "CHF 50.-").
|
||||
|
||||
To compile translations to .mo files for production:
|
||||
|
||||
```bash
|
||||
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||
```
|
||||
|
||||
### Create releases
|
||||
|
||||
- The `vendor/` directory MUST be included in releases (Twig dependency required for runtime)
|
||||
- Running zip from wrong directory creates empty or malformed archives
|
||||
- Exclusion patterns must match the relative path structure used in zip command
|
||||
- Always verify the package with `unzip -l` and test extraction before committing
|
||||
- The `wp-core/` and `wp-plugins/` directories MUST NOT be included in releases
|
||||
- Releases are stored in `releases/` including checksums
|
||||
|
||||
**Important Git Notes:**
|
||||
|
||||
- Always commit from `dev` branch first
|
||||
- Tags should use format `vX.X.X` (e.g., `v1.1.22`)
|
||||
- Use annotated tags (`-a`) not lightweight tags
|
||||
- Commit messages should follow the established format with Claude Code attribution
|
||||
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
|
||||
|
||||
#### What Gets Released
|
||||
|
||||
- All plugin source files
|
||||
- Compiled vendor dependencies
|
||||
- Translation files (.mo compiled from .po)
|
||||
- Assets (CSS, JS)
|
||||
- Documentation (README, CHANGELOG, etc.)
|
||||
|
||||
#### What's Excluded
|
||||
|
||||
- Git metadata (`.git/`)
|
||||
- Development files (`.vscode/`, `.claude/`, `CLAUDE.md`)
|
||||
- Logs and cache files
|
||||
- Previous releases
|
||||
- `composer.lock` (but `vendor/` is included)
|
||||
- **Dependencies:** Composer
|
||||
- **i18n:** WordPress i18n (.pot/.po/.mo), text domain: `wc-composable-product`
|
||||
- **CI/CD:** Gitea Actions (`.gitea/workflows/release.yml`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```txt
|
||||
wc-composable-product/
|
||||
├── .gitea/workflows/
|
||||
│ └── release.yml # CI/CD release workflow
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin panel styling
|
||||
@@ -109,7 +33,7 @@ wc-composable-product/
|
||||
│ └── js/
|
||||
│ ├── admin.js # Product edit interface logic
|
||||
│ └── frontend.js # AJAX cart & selection UI
|
||||
├── cache/ # Twig template cache (writable)
|
||||
├── cache/ # Twig template cache (writable, gitignored)
|
||||
├── includes/
|
||||
│ ├── Admin/
|
||||
│ │ ├── Product_Data.php # Product data tab & meta boxes
|
||||
@@ -118,726 +42,145 @@ wc-composable-product/
|
||||
│ ├── Plugin.php # Main plugin class (Singleton)
|
||||
│ ├── Product_Selector.php # Frontend product selector renderer (with stock info)
|
||||
│ ├── Product_Type.php # Custom WC_Product extension
|
||||
│ └── Stock_Manager.php # Stock management & inventory tracking (v1.1.0+)
|
||||
├── languages/
|
||||
│ └── wc-composable-product.pot # Translation template
|
||||
├── releases/ # Releases files
|
||||
│ └── Stock_Manager.php # Stock management & inventory tracking
|
||||
├── languages/ # Translation files (.pot, .po, .mo)
|
||||
├── releases/ # Release packages (gitignored)
|
||||
├── templates/
|
||||
│ └── product-selector.twig # Frontend selection interface (with stock display)
|
||||
├── vendor/ # Composer dependencies (gitignored)
|
||||
├── composer.json # Dependency configuration
|
||||
├── wc-composable-product.php # Main plugin file
|
||||
└── [Documentation files] # README, INSTALL, IMPLEMENTATION, etc.
|
||||
│ └── product-selector.twig # Frontend selection interface
|
||||
├── vendor/ # Composer dependencies (gitignored, included in releases)
|
||||
├── composer.json
|
||||
└── wc-composable-product.php # Main plugin file
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
## Architecture
|
||||
|
||||
### Core Classes
|
||||
|
||||
1. **Plugin.php** - Main singleton class
|
||||
- Initializes Twig template engine
|
||||
- Registers hooks and filters
|
||||
- Manages asset enqueuing
|
||||
- Provides template rendering API
|
||||
1. **Plugin.php** — Main singleton class
|
||||
- Initializes Twig with WordPress functions registered as both Twig functions AND filters
|
||||
- Registers hooks, manages asset enqueuing, provides template rendering API
|
||||
- Settings.php is lazy-loaded via `woocommerce_get_settings_pages` filter (not in `includes()`) to avoid "Class WC_Settings_Page not found" errors
|
||||
|
||||
2. **Product_Type.php** - Custom WooCommerce product type
|
||||
2. **Product_Type.php** — Custom WooCommerce product type (`composable`)
|
||||
- Extends `WC_Product`
|
||||
- Handles selection criteria (category/tag/SKU)
|
||||
- Manages pricing modes (fixed/sum)
|
||||
- Queries available products dynamically
|
||||
- Queries available products via `get_available_products()` using `WP_Query`
|
||||
- **Critical**: Uses `tax_query` with `product_type` taxonomy to exclude composable products (NOT `meta_query` — WooCommerce stores product types as taxonomy terms)
|
||||
- Handles variable products by expanding them into individual variations via `get_children()`
|
||||
- Products are filtered by `is_purchasable()` only (not `is_in_stock()` — stock is shown visually and validated at add-to-cart)
|
||||
|
||||
3. **Cart_Handler.php** - Cart integration
|
||||
- Validates product selections
|
||||
- Stores selected products in cart meta
|
||||
- Calculates dynamic pricing
|
||||
- Displays selections in cart/checkout
|
||||
3. **Cart_Handler.php** — Cart integration
|
||||
- Validates selections, stores selected products in cart meta, calculates pricing
|
||||
- Uses `woocommerce_is_purchasable` filter to hide default add-to-cart button for composable products
|
||||
- Price recalculation uses a static `$already_calculated` flag per request (no persistent session flags — `set_price()` is in-memory only)
|
||||
|
||||
4. **Product_Selector.php** - Frontend renderer
|
||||
- Renders Twig template with product data
|
||||
- Applies display settings (images/prices/total)
|
||||
4. **Product_Selector.php** — Frontend renderer
|
||||
- Renders Twig template with product data, stock info, and pre-formatted price HTML via `wc_price()`
|
||||
|
||||
5. **Admin/Product_Data.php** - Product edit interface
|
||||
- Adds "Composable Options" tab
|
||||
- Category/tag/SKU selection fields
|
||||
- Saves product metadata
|
||||
5. **Admin/Product_Data.php** — Product edit interface
|
||||
- Adds "Composable Options" tab with category/tag/SKU selection fields
|
||||
- Saved meta: `_composable_selection_limit`, `_composable_pricing_mode`, `_composable_criteria_type`, `_composable_categories`, `_composable_tags`, `_composable_skus`
|
||||
|
||||
6. **Admin/Settings.php** - Global settings
|
||||
- WooCommerce settings tab integration
|
||||
- Default limits and pricing mode
|
||||
- Display preferences
|
||||
6. **Admin/Settings.php** — Global settings (extends `WC_Settings_Page`)
|
||||
- Default selection limit, pricing mode, display preferences
|
||||
|
||||
7. **Stock_Manager.php** - Inventory management (v1.1.0+)
|
||||
- Stock validation for selected products
|
||||
- Automatic stock deduction on order completion
|
||||
- Stock restoration on order cancellation/refund
|
||||
- Order notes for audit trail
|
||||
- Backorder support detection
|
||||
7. **Stock_Manager.php** — Inventory management
|
||||
- Stock validation, automatic deduction on order completion, restoration on cancellation
|
||||
- Prevents WooCommerce double-deduction via `woocommerce_can_reduce_order_stock`
|
||||
|
||||
### Data Flow
|
||||
|
||||
**Product Creation:**
|
||||
**Product Creation:** Admin selects "Composable product" type → configures criteria/limits/pricing → metadata saved as `_composable_*` fields
|
||||
|
||||
1. Admin selects "Composable product" type
|
||||
2. Configures criteria, limits, pricing in product data tab
|
||||
3. Metadata saved: `_composable_*` fields
|
||||
**Frontend Display:** `Cart_Handler::render_product_selector()` → `Product_Type::get_available_products()` queries products via taxonomy/SKU → `Product_Selector::render()` passes data to Twig template → JavaScript handles selection UI
|
||||
|
||||
**Frontend Display:**
|
||||
**Add to Cart:** Customer selects products → JS validates limit → AJAX request with `composable_products[]` → server-side validation (selection + stock) → selections stored in cart item data → price calculated per pricing mode
|
||||
|
||||
1. `Cart_Handler::render_product_selector()` called on product page
|
||||
2. `Product_Type::get_available_products()` queries matching products
|
||||
3. `Product_Selector::render()` passes data to Twig template
|
||||
4. JavaScript handles selection UI and AJAX
|
||||
**Order Processing:** Order completed → `Stock_Manager` deducts inventory → order notes added for audit → on cancellation/refund: stock restored
|
||||
|
||||
**Add to Cart:**
|
||||
### Key Hooks
|
||||
|
||||
1. Customer selects products (JS validates limit)
|
||||
2. AJAX request with `composable_products[]` array
|
||||
3. `Cart_Handler::validate_add_to_cart()` server-side validation
|
||||
4. `Stock_Manager::validate_stock_availability()` checks stock levels (v1.1.0+)
|
||||
5. `Cart_Handler::add_cart_item_data()` stores selections
|
||||
6. `Cart_Handler::calculate_cart_item_price()` applies pricing
|
||||
- `woocommerce_add_to_cart_validation` — validate selections
|
||||
- `woocommerce_add_cart_item_data` — store selections
|
||||
- `woocommerce_before_calculate_totals` — update prices
|
||||
- `woocommerce_get_item_data` — display in cart
|
||||
- `woocommerce_order_status_completed/processing` — deduct stock
|
||||
- `woocommerce_order_status_cancelled/refunded` — restore stock
|
||||
|
||||
**Order Processing (v1.1.0+):**
|
||||
### Security
|
||||
|
||||
1. Order status changes to completed/processing
|
||||
2. `Stock_Manager::reduce_stock_on_order_complete()` deducts inventory
|
||||
3. Selected product IDs stored in order meta: `_composable_products`
|
||||
4. Order notes added documenting stock changes
|
||||
5. On cancellation/refund: `Stock_Manager::restore_stock_on_order_cancel()` reverses deduction
|
||||
- Input: `absint()` for IDs/limits, `sanitize_text_field()` for modes, `sanitize_textarea_field()` for SKUs
|
||||
- Output: `esc_html()`, `esc_attr()`, `esc_url()` (registered as both Twig functions and filters)
|
||||
- Nonce verification via WooCommerce
|
||||
|
||||
## Development Workflow
|
||||
### Developer API
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```php
|
||||
$product = wc_get_product($product_id);
|
||||
if ($product->get_type() === 'composable') {
|
||||
$products = $product->get_available_products();
|
||||
$limit = $product->get_selection_limit();
|
||||
$price = $product->calculate_composed_price($selected_ids);
|
||||
}
|
||||
```
|
||||
|
||||
### Making Changes
|
||||
## Translations
|
||||
|
||||
1. **PHP Classes:** Edit files in `includes/` or `includes/Admin/`
|
||||
2. **Templates:** Modify `templates/*.twig` (cache clears on auto-reload)
|
||||
3. **Styles:** Update `assets/css/*.css`
|
||||
4. **JavaScript:** Edit `assets/js/*.js`
|
||||
5. **Translations:** Update `.pot` file, create `.po` translations
|
||||
All strings use text domain `wc-composable-product`. Available locales:
|
||||
|
||||
### Testing Checklist
|
||||
- `en_US` (base), `de_DE`, `de_DE_informal`, `de_CH`, `de_CH_informal`, `fr_CH`, `it_CH`
|
||||
|
||||
- [ ] Create composable product in admin
|
||||
- [ ] Test each selection criteria type (category/tag/SKU)
|
||||
- [ ] Verify selection limit enforcement
|
||||
- [ ] Test both pricing modes (fixed/sum)
|
||||
- [ ] Check AJAX add-to-cart functionality
|
||||
- [ ] Verify cart display shows selected products
|
||||
- [ ] Test checkout process
|
||||
- [ ] Check responsive design on mobile
|
||||
- [ ] Validate all strings are translatable
|
||||
Compile .po to .mo: `for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done`
|
||||
|
||||
### Creating Releases
|
||||
WordPress requires compiled .mo files — .po files alone are insufficient.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
### Automated (Gitea CI/CD)
|
||||
|
||||
Push an annotated tag (`v*`) to trigger the workflow. It installs PHP 8.3, production Composer deps, compiles translations, verifies version matches tag, creates ZIP with checksums, and publishes a Gitea release.
|
||||
|
||||
### Manual
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
zip -r wc-composable-product-vX.X.X.zip . \
|
||||
zip -r releases/wc-composable-product-vX.X.X.zip . \
|
||||
-x "*.git*" "*.vscode*" "*.claude*" "CLAUDE.md" \
|
||||
"wp-core" "wp-plugins" "*.log" "composer.lock" \
|
||||
"cache/*" "releases/*" "*.zip"
|
||||
|
||||
# Verify contents
|
||||
unzip -l wc-composable-product-vX.X.X.zip
|
||||
|
||||
# IMPORTANT: Ensure vendor/ is included!
|
||||
"wp-core/*" "wp-plugins/*" "*.log" "composer.lock" \
|
||||
"cache/*" "releases/*" "*.zip" "logs/*"
|
||||
```
|
||||
|
||||
## Bugs found
|
||||
The `vendor/` directory MUST be included in releases (Twig dependency required at runtime).
|
||||
|
||||
- ✅ ~~There is a bug related to twig in the frontend area. Documented in `logs/fatal-errors*.log`~~ **FIXED in v1.1.5**
|
||||
- ✅ ~~Translate the admin area, too~~ **COMPLETED in v1.1.6** - All admin strings now translated to 6 locales
|
||||
### Git Workflow
|
||||
|
||||
## Session History
|
||||
- Develop on `dev` branch, merge to `main` for releases
|
||||
- Tags: annotated, format `vX.X.X` (e.g., `v1.2.0`)
|
||||
- Commit messages include `Co-Authored-By: Claude` attribution
|
||||
|
||||
### v1.0.0 - Initial Implementation & Release (2024-12-31)
|
||||
## Critical Lessons Learned
|
||||
|
||||
#### Session 1: Core Implementation
|
||||
1. **WooCommerce stores product types as taxonomy terms** (`product_type` taxonomy), NOT as postmeta. Using `meta_query` on `_product_type` silently returns zero results because the meta key doesn't exist.
|
||||
|
||||
- Complete plugin implementation from scratch
|
||||
- All 6 core PHP classes with PSR-4 autoloading
|
||||
- Twig template system integration
|
||||
- Responsive frontend with AJAX functionality
|
||||
- Admin interface with WooCommerce integration
|
||||
- Full i18n support with .pot template
|
||||
- Comprehensive documentation (README, INSTALL, IMPLEMENTATION)
|
||||
- Initial commit to `main` branch (1edb0be)
|
||||
2. **`WC_Product::set_price()` is in-memory only** — changes are lost between HTTP requests. Never persist a "price already calculated" flag to cart session; use a static per-request flag instead.
|
||||
|
||||
#### Session 2: Documentation & Translations
|
||||
3. **Settings.php must be lazy-loaded** — `require_once` in `Plugin::includes()` causes "Class WC_Settings_Page not found" because WooCommerce hasn't loaded that class yet. Load it inside the `woocommerce_get_settings_pages` filter callback instead.
|
||||
|
||||
- Enhanced CLAUDE.md with complete architecture documentation
|
||||
- Created 6 complete translation files (.po):
|
||||
- German (Germany - formal & informal)
|
||||
- German (Switzerland - formal & informal)
|
||||
- French (Switzerland)
|
||||
- Italian (Switzerland)
|
||||
- All 40+ strings translated with locale-specific terminology
|
||||
- Swiss locales include CHF currency formatting examples
|
||||
4. **Register WordPress functions as both Twig functions AND filters** — other plugins may bundle their own Twig instance that parses our templates. Both `{{ esc_attr(value) }}` and `{{ value|esc_attr }}` syntax must work.
|
||||
|
||||
#### Session 3: Release Creation
|
||||
5. **HPOS compatibility declaration is required** — without it, WooCommerce shows incompatibility warnings.
|
||||
|
||||
- Created annotated git tag `v1.0.0`
|
||||
- Generated release package: `wc-composable-product-v1.0.0.zip` (371 KB)
|
||||
- Verified vendor/ directory inclusion (336 Twig files)
|
||||
- Created SHA-256 and MD5 checksums
|
||||
- Stored in `releases/` directory (gitignored)
|
||||
6. **WordPress i18n requires compiled .mo files** — .po files are source only; WordPress cannot use them directly.
|
||||
|
||||
**Key decisions made:**
|
||||
7. **Don't filter by `is_in_stock()` during product retrieval** — it's too strict (excludes backorder-enabled products, products without stock management). Show all purchasable products; let the frontend display stock status and validate at add-to-cart time.
|
||||
|
||||
- Used Singleton pattern for main Plugin class
|
||||
- Twig for templating (per requirements)
|
||||
- Vanilla JS + jQuery for frontend (WooCommerce standard)
|
||||
- Grid layout for product selector (responsive)
|
||||
- AJAX add-to-cart for better UX
|
||||
- Meta-based configuration storage
|
||||
## For AI Assistants
|
||||
|
||||
**Files created:** 28 files total (21 PHP/templates + 7 translations), 3,842 lines of code
|
||||
When starting a new session:
|
||||
|
||||
**Git workflow:**
|
||||
|
||||
- Main branch: Initial implementation (1edb0be)
|
||||
- Dev branch: +2 commits for documentation and translations
|
||||
- Tagged: v1.0.0 on dev branch (8c17734)
|
||||
|
||||
**What works:**
|
||||
|
||||
- Product type registration ✓
|
||||
- Admin product data tab ✓
|
||||
- Category/tag/SKU selection ✓
|
||||
- Frontend product selector ✓
|
||||
- AJAX add-to-cart ✓
|
||||
- Cart integration ✓
|
||||
- Pricing calculation (both modes) ✓
|
||||
- Full multilingual support (6 locales) ✓
|
||||
- Production-ready release package ✓
|
||||
|
||||
**Known limitations:**
|
||||
|
||||
- Currently only simple products in selection (not variable)
|
||||
- No grouped product support
|
||||
- Template cache requires manual clearing after updates
|
||||
- Translations are .po files only (not compiled to .mo yet)
|
||||
|
||||
**Release details:**
|
||||
|
||||
- Package size: 371 KB
|
||||
- Includes: All source + vendor dependencies + translations
|
||||
- Checksums: SHA-256 and MD5 provided
|
||||
- Ready for WordPress installation (no composer install needed)
|
||||
|
||||
**Future enhancements to consider:**
|
||||
|
||||
- Variable product support
|
||||
- Quantity selection per item
|
||||
- Visual bundle preview
|
||||
- Product recommendations
|
||||
- Selection presets/templates
|
||||
- Compile .mo translation files
|
||||
|
||||
---
|
||||
|
||||
### v1.0.1 - Bug Fix (2024-12-31)
|
||||
|
||||
**Critical bug fix:** Fatal error "Class WC_Settings_Page not found" during plugin activation
|
||||
|
||||
**Root cause:** Plugin initialized on `plugins_loaded` hook before WooCommerce classes were available
|
||||
|
||||
**Solution:** Changed initialization hook to `woocommerce_loaded` in wc-composable-product.php:65
|
||||
|
||||
**Impact:** Settings page now correctly integrates as tab in WooCommerce > Settings
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- wc-composable-product.php (version bump to 1.0.1, hook change)
|
||||
- CHANGELOG.md (documented fix)
|
||||
|
||||
**Commit:** a581ef4
|
||||
|
||||
---
|
||||
|
||||
### v1.1.0 - Stock Management Integration (2024-12-31)
|
||||
|
||||
#### Session 4: Stock Management Implementation
|
||||
|
||||
**Major feature release** adding comprehensive inventory tracking for composable products.
|
||||
|
||||
**What was built:**
|
||||
|
||||
1. **New Stock_Manager class** (includes/Stock_Manager.php - 7.7 KB, 263 lines)
|
||||
- `validate_stock_availability()` - Real-time stock checking
|
||||
- `get_product_stock_info()` - Stock data for frontend display
|
||||
- `reduce_stock_on_order_complete()` - Automatic deduction on order completion
|
||||
- `restore_stock_on_order_cancel()` - Automatic restoration on cancellation/refund
|
||||
- `prevent_composable_stock_reduction()` - Prevents WooCommerce double-deduction
|
||||
- `store_selected_products_in_order()` - Saves selection to order meta
|
||||
|
||||
2. **Enhanced existing classes:**
|
||||
- Cart_Handler.php: Added stock validation during add-to-cart (lines 90-95)
|
||||
- Product_Selector.php: Passes stock data to template (lines 36-56)
|
||||
- Plugin.php: Includes Stock_Manager in autoload (line 96)
|
||||
|
||||
3. **Frontend enhancements:**
|
||||
- templates/product-selector.twig: Stock status display (lines 39-47)
|
||||
- assets/css/frontend.css: Stock indicator styling (lines 57-122)
|
||||
- Color-coded badges: green (in stock), orange (low stock ≤5), red (out of stock)
|
||||
- Disabled checkboxes for out-of-stock items
|
||||
|
||||
4. **Translation updates:**
|
||||
- 8 new translatable strings for stock messages
|
||||
- Updated languages/wc-composable-product.pot
|
||||
- Updated languages/wc-composable-product-it_CH.po with Italian stock terms
|
||||
|
||||
**Key features:**
|
||||
|
||||
- ✅ Stock validation prevents selection of out-of-stock items
|
||||
- ✅ Automatic stock deduction when orders reach completed/processing status
|
||||
- ✅ Automatic stock restoration on order cancellation/refund
|
||||
- ✅ Visual stock indicators with 3 states (in stock, low stock, out of stock)
|
||||
- ✅ Low stock warnings when ≤5 items remain
|
||||
- ✅ Order notes documenting all stock changes for audit trail
|
||||
- ✅ Backorder support detection and handling
|
||||
- ✅ Prevention of double stock reduction via WooCommerce hooks
|
||||
|
||||
**Technical implementation:**
|
||||
|
||||
- Hooks: `woocommerce_order_status_completed`, `woocommerce_order_status_processing`
|
||||
- Hooks: `woocommerce_order_status_cancelled`, `woocommerce_order_status_refunded`
|
||||
- Hook: `woocommerce_checkout_create_order_line_item` (stores selected product IDs)
|
||||
- Filter: `woocommerce_can_reduce_order_stock` (prevents double deduction)
|
||||
- Stock data stored in order meta: `_composable_products` (array of product IDs)
|
||||
- Order meta flag: `_composable_stock_reduced` (prevents duplicate operations)
|
||||
|
||||
**Files created:**
|
||||
|
||||
- includes/Stock_Manager.php (new, 263 lines)
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- includes/Cart_Handler.php (+13 lines: stock manager integration)
|
||||
- includes/Product_Selector.php (+17 lines: stock info retrieval)
|
||||
- includes/Plugin.php (+1 line: Stock_Manager require)
|
||||
- templates/product-selector.twig (+8 lines: stock status display)
|
||||
- assets/css/frontend.css (+40 lines: stock indicator styles)
|
||||
- languages/wc-composable-product.pot (+32 lines: 8 new strings)
|
||||
- languages/wc-composable-product-it_CH.po (+32 lines: Italian translations)
|
||||
- wc-composable-product.php (version bump to 1.1.0)
|
||||
- CHANGELOG.md (v1.1.0 release notes)
|
||||
|
||||
**Release details:**
|
||||
|
||||
- Package size: 375 KB (+4 KB from v1.0.0)
|
||||
- Git tag: v1.1.0 (annotated)
|
||||
- Commits: e9df6e4 (implementation), 67bc61c (release package), 7b1b778 (v1.0.0 package), 91f44b0 (.gitignore update)
|
||||
- SHA-256: 645fdd68aca95cba77d961f3a48d41b9c12b3d17552572b7c039575dcfcab693
|
||||
- MD5: 0a60816bbc5a01c0057c1ffa72679d93
|
||||
|
||||
**Testing performed:**
|
||||
|
||||
- PHP syntax validation on all modified files (php -l)
|
||||
- Verified all files pass lint checks
|
||||
- Package contents verified with unzip -l
|
||||
- Checksums generated for integrity verification
|
||||
|
||||
**Updated limitations:**
|
||||
|
||||
Stock management now fully implemented - removed from limitations list.
|
||||
|
||||
Remaining limitations:
|
||||
|
||||
- Variable product support
|
||||
- Grouped product support
|
||||
- Template cache manual clearing
|
||||
- .mo compilation
|
||||
|
||||
**What works (v1.1.0):**
|
||||
|
||||
Everything from v1.0.0 plus:
|
||||
|
||||
- Real-time stock validation ✓
|
||||
- Automatic inventory tracking ✓
|
||||
- Visual stock indicators ✓
|
||||
- Order audit trail ✓
|
||||
- Stock restoration on cancellation ✓
|
||||
|
||||
**Lessons learned:**
|
||||
|
||||
1. **Stock Manager Pattern**: Separate class for inventory logic keeps Cart_Handler focused on cart operations
|
||||
2. **Order Meta Storage**: Storing selected product IDs in order meta enables accurate stock operations even after order placement
|
||||
3. **Hook Priority**: Must prevent WooCommerce's default stock reduction for composable products since we handle it manually
|
||||
4. **Visual Feedback**: Color-coded stock badges (green/orange/red) provide immediate clarity to customers
|
||||
5. **Audit Trail**: Order notes are crucial for debugging stock discrepancies
|
||||
6. **Defensive Programming**: Check for `_composable_stock_reduced` flag to prevent duplicate operations on order status changes
|
||||
|
||||
---
|
||||
|
||||
### v1.1.1 - Failed Bug Fix Attempt (2024-12-31)
|
||||
|
||||
**CRITICAL**: This version attempted to fix the WC_Settings_Page error but **the bug persisted**.
|
||||
|
||||
**Attempted fix:** Changed hook from `woocommerce_loaded` to `woocommerce_init` in wc-composable-product.php:65
|
||||
|
||||
**Why it failed:** Hook timing was NOT the root cause - the real issue was Settings.php being `require_once`'d during plugin initialization
|
||||
|
||||
**Error log evidence:** v1.1.1 continued to crash with "Class WC_Settings_Page not found" after release
|
||||
|
||||
**Lesson learned:** Always check error logs after deployment - don't assume a fix worked without verification
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- wc-composable-product.php (version bump to 1.1.1, hook change)
|
||||
- CHANGELOG.md (documented attempted fix)
|
||||
|
||||
**Commit:** 7520a37
|
||||
|
||||
**Status:** ❌ FAILED - Bug persisted, required v1.1.2
|
||||
|
||||
---
|
||||
|
||||
### v1.1.2 - CRITICAL Bug Fix (2024-12-31)
|
||||
|
||||
#### Session 5: Fixing Persistent Settings.php Class Loading Issue
|
||||
|
||||
**CRITICAL bug fix** that finally resolved the "Class WC_Settings_Page not found" error that persisted through 4 versions (v1.0.0, v1.0.1, v1.1.0, v1.1.1).
|
||||
|
||||
**The Journey to the Fix:**
|
||||
|
||||
1. v1.0.0: Used `plugins_loaded` hook → Fatal error
|
||||
2. v1.0.1: Changed to `woocommerce_loaded` → Still failed
|
||||
3. v1.1.0: Kept `woocommerce_loaded` → Bug continued
|
||||
4. v1.1.1: Changed to `woocommerce_init` → **STILL FAILING!**
|
||||
5. v1.1.2: Fixed class loading order → ✅ **WORKING**
|
||||
|
||||
**Root cause analysis:**
|
||||
|
||||
The error wasn't about hook timing - it was about **when Settings.php was being parsed**:
|
||||
|
||||
- `Plugin::includes()` was doing `require_once Settings.php` at line 93
|
||||
- This happened during plugin initialization (on `woocommerce_init`)
|
||||
- When PHP parsed Settings.php, it tried to extend `WC_Settings_Page`
|
||||
- But that parent class didn't exist yet!
|
||||
- Even `woocommerce_init` fires **before** WooCommerce loads settings page classes
|
||||
- Result: Instant fatal error
|
||||
|
||||
**The fix:**
|
||||
|
||||
Delayed Settings.php inclusion until it's actually needed:
|
||||
|
||||
```php
|
||||
// Plugin::includes() - REMOVED this line:
|
||||
// require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||
|
||||
// Plugin::add_settings_page() - ADDED this:
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||
$settings[] = new Admin\Settings();
|
||||
```
|
||||
|
||||
The `add_settings_page()` method is called via the `woocommerce_get_settings_pages` filter, which fires when WooCommerce has already loaded all its settings classes, guaranteeing `WC_Settings_Page` exists.
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- includes/Plugin.php:
|
||||
- Line 93: Removed `require_once Settings.php`, added explanatory comment
|
||||
- Line 196: Added `require_once Settings.php` in `add_settings_page()` method
|
||||
- wc-composable-product.php (version bump to 1.1.2)
|
||||
- CHANGELOG.md (documented the fix and v1.1.1's failure)
|
||||
|
||||
**Release details:**
|
||||
|
||||
- Package size: 375 KB (383,194 bytes)
|
||||
- Git tag: v1.1.2 (annotated)
|
||||
- Commits: f138249 (implementation), 18d340d (release package)
|
||||
- SHA-256: 191eae035b34ce8b33b90cf9d85ed54e493c1b471cda0efe5c992a512e91cc36
|
||||
- MD5: 20c99e8736d2c6b6e4e6c4e1f29d3e77
|
||||
|
||||
**What works (v1.1.2):**
|
||||
|
||||
Everything from v1.1.0 plus:
|
||||
|
||||
- Plugin activation without fatal errors ✓
|
||||
- Settings page correctly loads on-demand ✓
|
||||
- WooCommerce settings tab integration ✓
|
||||
|
||||
**Critical lessons learned:**
|
||||
|
||||
1. **Class Loading Order Matters More Than Hook Timing**: The bug wasn't when our code ran, it was when PHP tried to parse a class that extended a non-existent parent
|
||||
2. **Always Verify Fixes**: v1.1.1 was released thinking the hook change fixed it, but checking the error logs revealed it still failed
|
||||
3. **Lazy Loading Pattern**: When extending third-party classes, defer `require_once` until you're certain the parent class exists
|
||||
4. **Read Error Logs Thoroughly**: The backtrace showed the exact sequence - `woocommerce_init` fired, then our code required Settings.php, then PHP crashed trying to parse the `extends` statement
|
||||
5. **Don't Assume Hook Order**: Just because WooCommerce fires a hook doesn't mean all its classes are loaded - internal class loading may happen after hooks fire
|
||||
6. **Test After Each Release**: If this had been tested immediately after v1.1.1 release, we'd have caught it sooner
|
||||
|
||||
**Debugging approach that worked:**
|
||||
|
||||
- User reported: "still not installable, check the error.log again"
|
||||
- Checked error log and found v1.1.1 still failing at 15:56:50
|
||||
- Analyzed backtrace to see Settings.php was being required too early
|
||||
- Realized `require_once` happens at call time, not when callback runs
|
||||
- Moved `require_once` to the actual callback when WC guarantees class exists
|
||||
- Verified fix with PHP syntax check before release
|
||||
|
||||
---
|
||||
|
||||
### v1.1.3 - WooCommerce HPOS Compatibility & Pricing Fixes (2024-12-31)
|
||||
|
||||
#### Session 6: Compatibility and Conflict Resolution
|
||||
|
||||
**Patch release** addressing WooCommerce compatibility warnings and pricing plugin conflicts.
|
||||
|
||||
**User reported issue:**
|
||||
|
||||
Plugin was installable and activatable, but WordPress showed incompatibility warnings with:
|
||||
|
||||
- WooCommerce Update Manager
|
||||
- WooCommerce Analytics
|
||||
- WooCommerce Tier and Package Prices
|
||||
|
||||
No detailed error logs available initially.
|
||||
|
||||
**Root cause analysis:**
|
||||
|
||||
1. **Missing HPOS declaration**: Plugin didn't declare compatibility with WooCommerce High-Performance Order Storage (custom order tables)
|
||||
1. **Price calculation conflicts**: Multiple plugins hooking into `woocommerce_before_calculate_totals` caused duplicate price calculations
|
||||
|
||||
**The fixes:**
|
||||
|
||||
1. **HPOS Compatibility Declaration** (wc-composable-product.php lines 67-74):
|
||||
|
||||
```php
|
||||
add_action('before_woocommerce_init', function() {
|
||||
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
|
||||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
1. **Price Calculation Protection** (includes/Cart_Handler.php lines 188-207):
|
||||
|
||||
- Added static flag to prevent multiple executions
|
||||
- Added `composable_price_calculated` cart item flag to prevent re-calculation by other plugins
|
||||
- Ensures our pricing runs once and other plugins respect it
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- wc-composable-product.php:
|
||||
- Line 6, 22: Version bump to 1.1.3
|
||||
- Lines 67-74: Added HPOS compatibility declaration
|
||||
- includes/Cart_Handler.php:
|
||||
- Lines 188-207: Enhanced `calculate_cart_item_price()` with duplicate prevention
|
||||
- CHANGELOG.md: Added v1.1.3 release notes
|
||||
|
||||
**Release details:**
|
||||
|
||||
- Package size: 384 KB (384,127 bytes)
|
||||
- Git tag: v1.1.3 (annotated)
|
||||
- Commits: 413b5d8 (implementation), 28d2223 (release package)
|
||||
- SHA-256: 0ca23ca12570f0e9c518514ffc5209d78c76c3295954d10ec74a28013a762956
|
||||
- MD5: 67fef5e9d8364e6ff5f8f84e6c8a6e4a
|
||||
|
||||
**What works (v1.1.3):**
|
||||
|
||||
Everything from v1.1.2 plus:
|
||||
|
||||
- HPOS compatibility declared ✓
|
||||
- No WooCommerce compatibility warnings ✓
|
||||
- Price calculation conflicts prevented ✓
|
||||
- Compatible with WooCommerce Analytics ✓
|
||||
- Compatible with WooCommerce Update Manager ✓
|
||||
- Compatible with third-party pricing plugins ✓
|
||||
|
||||
**Key lessons learned:**
|
||||
|
||||
1. **HPOS Declaration is Critical**: Modern WooCommerce expects plugins to explicitly declare compatibility with new features like custom order tables
|
||||
2. **Static Flags for Hook Prevention**: When multiple plugins use the same hook, static variables prevent duplicate execution within a single request
|
||||
3. **Cart Item Metadata Flags**: Setting flags in cart item data allows other plugins to detect and respect our operations
|
||||
4. **Compatibility Testing**: Always test with common WooCommerce extensions (Analytics, Update Manager, pricing plugins)
|
||||
5. **Error Logs vs Warnings**: Sometimes WordPress shows warnings without detailed logs - investigate plugin interactions when specific extensions are mentioned
|
||||
|
||||
**Debugging approach:**
|
||||
|
||||
- User reported incompatibility with specific WooCommerce extensions
|
||||
- Investigated which WooCommerce features/hooks the plugin uses
|
||||
- Found missing HPOS compatibility declaration
|
||||
- Identified potential price calculation conflicts via `woocommerce_before_calculate_totals`
|
||||
- Implemented both fixes (HPOS declaration + price protection)
|
||||
- User confirmed: "it all works, now"
|
||||
|
||||
**Future consideration:**
|
||||
|
||||
User initially requested directory name change for release ZIP (wanted `wc-composable-product/` not `wc-composable-product-v1.1.3/`). Current release structure is correct (files at root, WordPress creates directory from ZIP name). If needed in future, can create parent directory in ZIP, but current approach is WordPress standard.
|
||||
|
||||
**Post-release updates:**
|
||||
|
||||
Translation files updated (392559d) to include all 8 stock-related strings across all 5 locales that were missing them (de_DE, de_DE_informal, de_CH, de_CH_informal, fr_CH). All translation files now 100% complete with 55/55 strings.
|
||||
|
||||
---
|
||||
|
||||
### v1.1.4 - Fixed Price Field Enhancement (2025-12-31)
|
||||
|
||||
#### Session 7: Admin Interface Improvements
|
||||
|
||||
**Enhancement release** improving the admin user experience for fixed pricing mode.
|
||||
|
||||
**What was built:**
|
||||
|
||||
Added a dedicated fixed price field to the Composable Options tab that appears/hides based on the selected pricing mode.
|
||||
|
||||
**Implementation details:**
|
||||
|
||||
1. **Admin UI Enhancement** (includes/Admin/Product_Data.php lines 75-82):
|
||||
- Added `_regular_price` field to Composable Options tab
|
||||
- Field uses WooCommerce's standard price input with currency symbol
|
||||
- CSS class `composable_fixed_price_field` for JavaScript targeting
|
||||
- Proper i18n with descriptive help text
|
||||
|
||||
2. **JavaScript Toggle Logic** (assets/js/admin.js lines 39-54):
|
||||
- Added `toggleFixedPriceField()` function
|
||||
- Shows field only when pricing mode is "fixed"
|
||||
- Hides field for "sum" mode or when using global default
|
||||
- Triggers on page load and when pricing mode changes
|
||||
|
||||
3. **UX Improvements:**
|
||||
- Field appears/disappears dynamically without page reload
|
||||
- Clear visual feedback for which pricing mode is active
|
||||
- Uses WooCommerce's native price input styling
|
||||
- Consistent with WooCommerce admin patterns
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- includes/Admin/Product_Data.php:
|
||||
- Line 66: Simplified pricing mode description text
|
||||
- Lines 75-82: Added fixed price field with wrapper class
|
||||
- assets/js/admin.js:
|
||||
- Lines 39-54: Added price field toggle functionality
|
||||
|
||||
**User experience improvements:**
|
||||
|
||||
- ✅ Fixed price field now visible in Composable Options tab
|
||||
- ✅ Field automatically shows/hides based on pricing mode selection
|
||||
- ✅ Eliminates confusion about where to set the fixed price
|
||||
- ✅ Follows WooCommerce UI/UX conventions
|
||||
|
||||
**Key lessons learned:**
|
||||
|
||||
1. **Reuse Standard Fields**: Using `_regular_price` instead of custom meta leverages WooCommerce's existing price handling
|
||||
2. **Progressive Disclosure**: Show/hide fields based on context reduces cognitive load
|
||||
3. **JavaScript + CSS Classes**: Using semantic class names (`composable_fixed_price_field`) makes JS targeting clean
|
||||
4. **Trigger on Load**: Always call toggle functions on page load to set initial state
|
||||
5. **Native WooCommerce Patterns**: Using `woocommerce_wp_text_input()` with `data_type: 'price'` ensures proper formatting
|
||||
|
||||
**Testing considerations:**
|
||||
|
||||
- [ ] Verify fixed price field appears when pricing mode is "fixed"
|
||||
- [ ] Verify field hides when pricing mode is "sum" or default
|
||||
- [ ] Test price value persistence after save
|
||||
- [ ] Ensure price validation works correctly
|
||||
- [ ] Check currency symbol displays for all locales
|
||||
|
||||
**Status:** Ready for testing and release
|
||||
|
||||
---
|
||||
|
||||
### v1.1.5 - Critical Twig Filter Bug Fix (2025-12-31)
|
||||
|
||||
#### Session 8: Twig Template Compatibility Fix
|
||||
|
||||
**Critical bug fix** resolving template rendering errors when other plugins use Twig.
|
||||
|
||||
**The bug:**
|
||||
|
||||
Plugin crashed with `Twig\Error\SyntaxError: Unknown "esc_attr" filter` when rendering the product selector template on the frontend.
|
||||
|
||||
**Root cause analysis:**
|
||||
|
||||
1. **Filter vs Function mismatch**: The template used filter syntax (`{{ product.name|esc_attr }}`), but WordPress escaping functions were only registered as Twig **functions**, not **filters**
|
||||
2. **Plugin conflict**: When another plugin (e.g., WooCommerce Tier and Package Prices) bundles its own Twig installation, it may parse our templates with its Twig instance
|
||||
3. **Missing registrations**: That external Twig instance didn't have our custom filters registered, causing the "Unknown filter" error
|
||||
|
||||
**Error log evidence:**
|
||||
|
||||
From [logs/fatal-errors-2025-12-31.log:5](logs/fatal-errors-2025-12-31.log#L5):
|
||||
|
||||
```text
|
||||
Uncaught Twig\Error\SyntaxError: Unknown "esc_attr" filter in "product-selector.twig" at line 26
|
||||
```
|
||||
|
||||
The backtrace showed the error originated from `/wp-content/plugins/wc-tier-and-package-prices/vendor/twig/twig/`, proving another plugin's Twig instance was parsing our template.
|
||||
|
||||
**The fix:**
|
||||
|
||||
Added Twig filter registrations alongside existing function registrations in [includes/Plugin.php:88-91](includes/Plugin.php#L88-L91):
|
||||
|
||||
```php
|
||||
// Add WordPress escaping functions as Twig filters
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_html', 'esc_html'));
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_attr', 'esc_attr'));
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_url', 'esc_url'));
|
||||
```
|
||||
|
||||
This allows both syntaxes to work:
|
||||
|
||||
- Filter syntax: `{{ product.name|esc_attr }}` ✅
|
||||
- Function syntax: `{{ esc_attr(product.name) }}` ✅
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- includes/Plugin.php:
|
||||
- Lines 88-91: Added TwigFilter registrations for WordPress escaping functions
|
||||
- wc-composable-product.php:
|
||||
- Lines 6, 22: Version bump to 1.1.5
|
||||
- CHANGELOG.md: Added v1.1.5 release notes with technical details
|
||||
|
||||
**What works (v1.1.5):**
|
||||
|
||||
Everything from v1.1.4 plus:
|
||||
|
||||
- Product selector template renders without errors ✅
|
||||
- Compatible with plugins that bundle Twig (e.g., pricing plugins) ✅
|
||||
- WordPress escaping works with both filter and function syntax ✅
|
||||
- No more "Unknown filter" errors ✅
|
||||
|
||||
**Key lessons learned:**
|
||||
|
||||
1. **Filter vs Function Registration**: In Twig, `{{ value|filter }}` requires `TwigFilter`, while `{{ function(value) }}` requires `TwigFunction` - they're not interchangeable
|
||||
2. **Multiple Twig Instances**: WordPress plugins may bundle their own Twig installations that can parse other plugins' templates
|
||||
3. **Template Syntax Matters**: Using filter syntax in templates requires filter registration, even if function registration exists
|
||||
4. **Defensive Compatibility**: Register WordPress functions as BOTH filters and functions for maximum compatibility
|
||||
5. **Error Log Investigation**: Backtrace reveals which Twig instance is parsing the template, crucial for diagnosing multi-plugin conflicts
|
||||
6. **Template Location Doesn't Matter**: Even though our template is in our plugin directory, other Twig instances can still parse it during rendering
|
||||
|
||||
**Debugging approach:**
|
||||
|
||||
1. User mentioned Twig bug in CLAUDE.md "Bugs found" section
|
||||
2. Checked `logs/fatal-errors-2025-12-31.log` and found the exact error
|
||||
3. Analyzed backtrace showing external Twig instance from another plugin
|
||||
4. Examined template and found filter syntax (`|esc_attr`)
|
||||
5. Checked Plugin.php and discovered only function registrations existed
|
||||
6. Added filter registrations alongside function registrations
|
||||
7. Committed fix with detailed explanation
|
||||
|
||||
**Impact:**
|
||||
|
||||
This was a **critical bug** preventing the plugin from working on sites with certain other WooCommerce extensions installed. Users would see a blank page or error when viewing composable products.
|
||||
|
||||
**Status:** Fixed and committed to dev branch (8fc0614)
|
||||
|
||||
---
|
||||
|
||||
**For AI Assistants:**
|
||||
|
||||
When starting a new session on this project:
|
||||
|
||||
1. Read this CLAUDE.md file first
|
||||
2. Review IMPLEMENTATION.md for technical details
|
||||
3. Check git log for recent changes
|
||||
4. Verify you're on the `dev` branch before making changes
|
||||
5. Run `composer install` if vendor/ is missing
|
||||
6. Test changes before committing
|
||||
7. Follow commit message format with Claude Code attribution
|
||||
8. Update this session history section with learnings
|
||||
|
||||
Always refer to this document when starting work on this project. Good luck!
|
||||
1. Read this CLAUDE.md first
|
||||
2. Check git log for recent changes
|
||||
3. Verify you're on the `dev` branch before making changes
|
||||
4. Run `composer install` if vendor/ is missing
|
||||
5. Test changes before committing
|
||||
6. Follow commit message format with Claude Code attribution
|
||||
7. Always use `tax_query` (not `meta_query`) for product type filtering
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
# WooCommerce Composable Products - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a technical overview of the WooCommerce Composable Products plugin implementation.
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Created:** 2024-12-31
|
||||
**AI-Generated:** 100% created with Claude.AI assistance
|
||||
|
||||
## Architecture
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```txt
|
||||
wc-composable-product/
|
||||
├── assets/ # Frontend assets
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin styles
|
||||
│ │ └── frontend.css # Frontend styles
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin JavaScript
|
||||
│ └── frontend.js # Frontend JavaScript
|
||||
├── cache/ # Twig template cache
|
||||
├── includes/ # PHP classes
|
||||
│ ├── Admin/
|
||||
│ │ ├── Product_Data.php # Product data tab
|
||||
│ │ └── Settings.php # Settings page
|
||||
│ ├── Cart_Handler.php # Cart integration
|
||||
│ ├── Plugin.php # Main plugin class
|
||||
│ ├── Product_Selector.php # Frontend selector
|
||||
│ └── Product_Type.php # Custom product type
|
||||
├── languages/ # Translation files
|
||||
│ └── wc-composable-product.pot
|
||||
├── templates/ # Twig templates
|
||||
│ └── product-selector.twig
|
||||
└── wc-composable-product.php # Main plugin file
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Main Plugin Class (`Plugin.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Singleton pattern implementation
|
||||
- Twig template engine initialization
|
||||
- Hook registration
|
||||
- Component initialization
|
||||
- Asset enqueuing
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `instance()`: Get singleton instance
|
||||
- `init_twig()`: Initialize Twig with WordPress functions
|
||||
- `render_template()`: Render Twig templates
|
||||
- `add_product_type()`: Register composable product type
|
||||
|
||||
### 2. Product Type (`Product_Type.php`)
|
||||
|
||||
**Extends:** `WC_Product`
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Custom product type: `composable`
|
||||
- Selection limit management (per-product or global)
|
||||
- Pricing mode (fixed or sum)
|
||||
- Product selection criteria (category/tag/SKU)
|
||||
- Dynamic product availability
|
||||
- Price calculation
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `get_selection_limit()`: Get max selectable items
|
||||
- `get_pricing_mode()`: Get pricing calculation mode
|
||||
- `get_available_products()`: Query available products
|
||||
- `calculate_composed_price()`: Calculate final price
|
||||
|
||||
### 3. Admin Settings (`Admin/Settings.php`)
|
||||
|
||||
**Extends:** `WC_Settings_Page`
|
||||
|
||||
**Global Settings:**
|
||||
|
||||
- Default selection limit
|
||||
- Default pricing mode
|
||||
- Display options (images, prices, total)
|
||||
|
||||
**Integration:** Adds tab to WooCommerce Settings
|
||||
|
||||
### 4. Product Data Tab (`Admin/Product_Data.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Add "Composable Options" tab to product edit page
|
||||
- Render selection criteria fields
|
||||
- Save product meta data
|
||||
- Dynamic field visibility based on criteria type
|
||||
|
||||
**Saved Meta:**
|
||||
|
||||
- `_composable_selection_limit`: Item limit
|
||||
- `_composable_pricing_mode`: Pricing calculation
|
||||
- `_composable_criteria_type`: Selection method
|
||||
- `_composable_categories`: Selected categories
|
||||
- `_composable_tags`: Selected tags
|
||||
- `_composable_skus`: SKU list
|
||||
|
||||
### 5. Product Selector (`Product_Selector.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Render frontend product selection interface
|
||||
- Prepare data for Twig template
|
||||
- Apply display settings
|
||||
|
||||
**Template Variables:**
|
||||
|
||||
- `products`: Available products array
|
||||
- `selection_limit`: Max selections
|
||||
- `pricing_mode`: Pricing calculation
|
||||
- `show_images/prices/total`: Display flags
|
||||
|
||||
### 6. Cart Handler (`Cart_Handler.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- Validate product selection
|
||||
- Add selected products to cart data
|
||||
- Calculate dynamic pricing
|
||||
- Display selected products in cart
|
||||
|
||||
**Hooks:**
|
||||
|
||||
- `woocommerce_add_to_cart_validation`: Validate selections
|
||||
- `woocommerce_add_cart_item_data`: Store selections
|
||||
- `woocommerce_before_calculate_totals`: Update prices
|
||||
- `woocommerce_get_item_data`: Display in cart
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Product Selector Template (`product-selector.twig`)
|
||||
|
||||
**Features:**
|
||||
|
||||
- Responsive grid layout
|
||||
- Checkbox-based selection
|
||||
- Product images and prices
|
||||
- Real-time total calculation
|
||||
- AJAX add-to-cart
|
||||
|
||||
**Data Attributes:**
|
||||
|
||||
- `data-product-id`: Composable product ID
|
||||
- `data-selection-limit`: Max selections
|
||||
- `data-pricing-mode`: Pricing mode
|
||||
- `data-price`: Individual product prices
|
||||
|
||||
### JavaScript (`frontend.js`)
|
||||
|
||||
**Functionality:**
|
||||
|
||||
- Selection limit enforcement
|
||||
- Visual feedback on selection
|
||||
- Real-time price updates (sum mode)
|
||||
- AJAX cart operations
|
||||
- Error/success messages
|
||||
|
||||
**Key Functions:**
|
||||
|
||||
- `handleCheckboxChange()`: Selection logic
|
||||
- `updateTotalPrice()`: Calculate total
|
||||
- `addToCart()`: AJAX add-to-cart
|
||||
- `showMessage()`: User feedback
|
||||
|
||||
### CSS Styling
|
||||
|
||||
**Approach:**
|
||||
|
||||
- Grid-based layout (responsive)
|
||||
- Card-style product items
|
||||
- Visual selection states
|
||||
- Mobile-first design
|
||||
- Breakpoints: 768px, 480px
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Admin selects "Composable product" type
|
||||
2. Configure selection limit and pricing mode
|
||||
3. Choose selection criteria (category/tag/SKU)
|
||||
4. Save product metadata
|
||||
5. WooCommerce registers product with custom type
|
||||
|
||||
### Frontend Display
|
||||
|
||||
1. Customer visits product page
|
||||
2. `Cart_Handler` renders `Product_Selector`
|
||||
3. `Product_Type::get_available_products()` queries products
|
||||
4. Twig template renders grid with products
|
||||
5. JavaScript handles interactions
|
||||
|
||||
### Adding to Cart
|
||||
|
||||
1. Customer selects products (JavaScript validation)
|
||||
2. Click "Add to Cart" button
|
||||
3. AJAX request with selected product IDs
|
||||
4. `Cart_Handler::validate_add_to_cart()` validates
|
||||
5. `Cart_Handler::add_cart_item_data()` stores selections
|
||||
6. `Cart_Handler::calculate_cart_item_price()` updates price
|
||||
7. Product added to cart with custom data
|
||||
|
||||
### Cart Display
|
||||
|
||||
1. WooCommerce loads cart
|
||||
2. `Cart_Handler::get_cart_item_from_session()` restores data
|
||||
3. `Cart_Handler::display_cart_item_data()` shows selections
|
||||
4. Price calculated dynamically on each cart load
|
||||
|
||||
## Security Implementation
|
||||
|
||||
### Input Sanitization
|
||||
|
||||
- **Integers:** `absint()` for IDs and limits
|
||||
- **Text:** `sanitize_text_field()` for modes and types
|
||||
- **Textarea:** `sanitize_textarea_field()` for SKUs
|
||||
- **Arrays:** `array_map()` with sanitization functions
|
||||
|
||||
### Output Escaping
|
||||
|
||||
- **HTML:** `esc_html()`, `esc_html_e()`
|
||||
- **Attributes:** `esc_attr()`
|
||||
- **URLs:** `esc_url()`
|
||||
- **JavaScript:** Localized scripts with escaped data
|
||||
|
||||
### Validation
|
||||
|
||||
- Selection limit enforcement
|
||||
- Product availability verification
|
||||
- Cart data validation
|
||||
- Nonce verification (via WooCommerce)
|
||||
|
||||
## Internationalization
|
||||
|
||||
### Text Domain
|
||||
|
||||
`wc-composable-product`
|
||||
|
||||
### Translation Functions
|
||||
|
||||
- `__()`: Return translated string
|
||||
- `_e()`: Echo translated string
|
||||
- `sprintf()` with `__()`: Variable substitution
|
||||
|
||||
### POT File
|
||||
|
||||
Generated template: `languages/wc-composable-product.pot`
|
||||
|
||||
**Supported Locales (per CLAUDE.md):**
|
||||
|
||||
- en_US (English)
|
||||
- de_DE, de_DE_informal (German - Germany)
|
||||
- de_CH, de_CH_informal (German - Switzerland)
|
||||
- fr_CH (French - Switzerland)
|
||||
- it_CH (Italian - Switzerland)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching
|
||||
|
||||
- Twig templates cached in `cache/` directory
|
||||
- Auto-reload enabled in debug mode
|
||||
- Optimized Composer autoloader
|
||||
|
||||
### Database Queries
|
||||
|
||||
- Efficient `WP_Query` for product selection
|
||||
- Meta queries for SKU filtering
|
||||
- Taxonomy queries for category/tag filtering
|
||||
|
||||
### Asset Loading
|
||||
|
||||
- Scripts only on relevant pages
|
||||
- Minification ready (use build tools)
|
||||
- Conditional enqueuing
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Hooks & Filters
|
||||
|
||||
**Available Filters:**
|
||||
|
||||
- `wc_composable_settings`: Modify settings array
|
||||
- `woocommerce_product_class`: Custom product class
|
||||
- `product_type_selector`: Product type registration
|
||||
|
||||
**Customization Points:**
|
||||
|
||||
- Twig templates (override in theme)
|
||||
- CSS styling (enqueue custom styles)
|
||||
- JavaScript behavior (extend object)
|
||||
|
||||
### Developer API
|
||||
|
||||
```php
|
||||
// Get composable product
|
||||
$product = wc_get_product($product_id);
|
||||
|
||||
// Check if composable
|
||||
if ($product->get_type() === 'composable') {
|
||||
// Get available products
|
||||
$products = $product->get_available_products();
|
||||
|
||||
// Get selection limit
|
||||
$limit = $product->get_selection_limit();
|
||||
|
||||
// Calculate price
|
||||
$price = $product->calculate_composed_price($selected_ids);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Admin Testing
|
||||
|
||||
- [ ] Product type appears in dropdown
|
||||
- [ ] Composable Options tab displays
|
||||
- [ ] Selection criteria toggle works
|
||||
- [ ] Meta data saves correctly
|
||||
- [ ] Settings page accessible
|
||||
- [ ] Global defaults apply
|
||||
|
||||
### Frontend Testing
|
||||
|
||||
- [ ] Product selector renders
|
||||
- [ ] Selection limit enforced
|
||||
- [ ] Price calculation accurate (both modes)
|
||||
- [ ] AJAX add-to-cart works
|
||||
- [ ] Cart displays selections
|
||||
- [ ] Checkout processes correctly
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- [ ] Empty criteria (no products)
|
||||
- [ ] Out of stock products excluded
|
||||
- [ ] Invalid product selections rejected
|
||||
- [ ] Multiple cart items unique
|
||||
- [ ] Session persistence
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Variable Products:** Currently supports simple products in selection
|
||||
2. **Grouped Products:** Cannot be used as selectable items
|
||||
3. **Stock Management:** No automatic stock reduction for selected items
|
||||
4. **Caching:** Template cache needs manual clearing after updates
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential features for future versions:
|
||||
|
||||
- Variable product support in selection
|
||||
- Quantity selection per item (not just presence)
|
||||
- Visual bundle previews
|
||||
- Advanced pricing rules
|
||||
- Stock management integration
|
||||
- Product recommendations
|
||||
- Selection templates/presets
|
||||
- Multi-currency support enhancements
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
|
||||
- PHP 8.3+
|
||||
- WordPress 6.0+
|
||||
- WooCommerce 8.0+
|
||||
- Twig 3.0 (via Composer)
|
||||
|
||||
### Development
|
||||
|
||||
- Composer for dependency management
|
||||
- WP-CLI for i18n operations (optional)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Checklist
|
||||
|
||||
1. Run `composer install --no-dev --optimize-autoloader`
|
||||
2. Ensure `vendor/` directory is included
|
||||
3. Ensure `cache/` directory is writable
|
||||
4. Test on staging environment
|
||||
5. Clear all caches after activation
|
||||
6. Verify WooCommerce compatibility
|
||||
|
||||
### Release Package
|
||||
|
||||
Must include:
|
||||
|
||||
- All PHP files
|
||||
- `vendor/` directory
|
||||
- Assets (CSS, JS)
|
||||
- Templates
|
||||
- Language files
|
||||
- Documentation
|
||||
|
||||
Must exclude:
|
||||
|
||||
- `.git/` directory
|
||||
- `composer.lock`
|
||||
- Development files
|
||||
- `wp-core/`, `wp-plugins/` symlinks
|
||||
|
||||
## Support & Maintenance
|
||||
|
||||
### Code Standards
|
||||
|
||||
- WordPress Coding Standards
|
||||
- WooCommerce best practices
|
||||
- PSR-4 autoloading
|
||||
- Inline documentation
|
||||
|
||||
### Version Control
|
||||
|
||||
- Semantic versioning (MAJOR.MINOR.PATCH)
|
||||
- Changelog maintained
|
||||
- Annotated git tags
|
||||
- Development on `dev` branch
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2024-12-31
|
||||
**Maintainer:** Marco Graetsch
|
||||
**AI Assistant:** Claude.AI (Anthropic)
|
||||
150
INSTALL.md
150
INSTALL.md
@@ -1,150 +0,0 @@
|
||||
# Installation Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
Before installing the WooCommerce Composable Products plugin, ensure your system meets these requirements:
|
||||
|
||||
- **PHP**: 8.3 or higher
|
||||
- **WordPress**: 6.0 or higher
|
||||
- **WooCommerce**: 8.0 or higher
|
||||
- **Composer**: For dependency management
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Upload Plugin Files
|
||||
|
||||
Upload the plugin directory to your WordPress installation:
|
||||
|
||||
```bash
|
||||
/wp-content/plugins/wc-composable-product/
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Navigate to the plugin directory and install dependencies:
|
||||
|
||||
```bash
|
||||
cd /wp-content/plugins/wc-composable-product/
|
||||
composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
### 3. Activate Plugin
|
||||
|
||||
1. Log in to your WordPress admin panel
|
||||
2. Navigate to **Plugins > Installed Plugins**
|
||||
3. Find "WooCommerce Composable Products"
|
||||
4. Click **Activate**
|
||||
|
||||
### 4. Configure Settings
|
||||
|
||||
After activation, configure the plugin:
|
||||
|
||||
1. Navigate to **WooCommerce > Settings**
|
||||
2. Click on the **Composable Products** tab
|
||||
3. Configure default settings:
|
||||
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
||||
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
||||
- **Display Options**: Toggle product images, prices, and totals
|
||||
|
||||
## Creating Your First Composable Product
|
||||
|
||||
### Step 1: Create a New Product
|
||||
|
||||
1. Go to **Products > Add New**
|
||||
2. Enter a product name (e.g., "Custom Sticker Pack")
|
||||
|
||||
### Step 2: Set Product Type
|
||||
|
||||
1. In the **Product Data** panel, select **Composable product** from the dropdown
|
||||
|
||||
### Step 3: Configure General Settings
|
||||
|
||||
In the **General** tab:
|
||||
- Set a **Regular price** (used if pricing mode is "Fixed")
|
||||
- Configure **Selection Limit** (leave empty to use global default)
|
||||
- Choose **Pricing Mode** (leave empty to use global default)
|
||||
|
||||
### Step 4: Configure Composable Options
|
||||
|
||||
Click on the **Composable Options** tab:
|
||||
|
||||
1. **Selection Criteria**: Choose how to select available products
|
||||
- **By Category**: Select product categories
|
||||
- **By Tag**: Select product tags
|
||||
- **By SKU**: Enter comma-separated SKUs
|
||||
|
||||
2. Based on your selection:
|
||||
- **Categories**: Select one or more categories from the dropdown
|
||||
- **Tags**: Select one or more tags from the dropdown
|
||||
- **SKUs**: Enter SKUs like: `STICKER-01, STICKER-02, STICKER-03`
|
||||
|
||||
### Step 5: Publish
|
||||
|
||||
Click **Publish** to make your composable product live.
|
||||
|
||||
## Frontend Usage
|
||||
|
||||
When customers visit your composable product:
|
||||
|
||||
1. They see a grid of available products based on your criteria
|
||||
2. They can select up to the configured limit
|
||||
3. The total price updates in real-time (if using sum pricing mode)
|
||||
4. Click "Add to Cart" to add the composition to their cart
|
||||
5. Selected products are displayed in the cart
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Activate
|
||||
|
||||
- Ensure WooCommerce is installed and activated first
|
||||
- Check PHP version (must be 8.3+)
|
||||
- Verify Composer dependencies are installed
|
||||
|
||||
### Products Not Showing in Selector
|
||||
|
||||
- Check that products are published and in stock
|
||||
- Verify the selection criteria (category/tag/SKU) is correct
|
||||
- Ensure products match the criteria you configured
|
||||
|
||||
### Twig Template Errors
|
||||
|
||||
- Ensure the `vendor/` directory exists and contains Twig
|
||||
- Run `composer install` again
|
||||
- Check that the `cache/` directory is writable
|
||||
|
||||
### JavaScript Not Working
|
||||
|
||||
- Clear browser cache
|
||||
- Check browser console for errors
|
||||
- Ensure jQuery is loaded (WooCommerce includes it)
|
||||
|
||||
## Updating
|
||||
|
||||
When updating the plugin:
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Replace plugin files
|
||||
3. Run `composer install --no-dev --optimize-autoloader`
|
||||
4. Reactivate the plugin
|
||||
5. Clear all caches (WordPress, browser, CDN)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To completely remove the plugin:
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Delete the plugin from the Plugins page
|
||||
3. Optionally clean up database entries (WooCommerce will handle this automatically)
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests:
|
||||
- GitHub: https://github.com/magdev/wc-composable-product/issues
|
||||
- Documentation: See README.md
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Customize the template by editing `templates/product-selector.twig`
|
||||
- Modify styles in `assets/css/frontend.css`
|
||||
- Translate the plugin using the provided `.pot` file
|
||||
- Create categories/tags for easier product organization
|
||||
133
README.md
133
README.md
@@ -1,51 +1,112 @@
|
||||
# WooCommerce Composable Products
|
||||
|
||||
Create composable products where customers can select a limited number of items from a configurable set of products.
|
||||
Create composable products where customers can select a limited number of items from a configurable set of products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
||||
|
||||
## Description
|
||||
|
||||
This plugin adds a new product type to WooCommerce that allows customers to build their own product bundles by selecting from a predefined set of simple or variable products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
||||
|
||||
### Key Features
|
||||
## Key Features
|
||||
|
||||
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
||||
- **Flexible Selection**: Define available products by category, tag, or SKU
|
||||
- **Variable Product Support**: Automatically expands variable products into selectable variations
|
||||
- **Stock Management**: Real-time stock validation, visual indicators, and automatic inventory tracking
|
||||
- **Configurable Limits**: Set global or per-product selection limits
|
||||
- **Pricing Options**: Fixed price or sum of selected products
|
||||
- **Multi-language Support**: Fully translatable with i18n support
|
||||
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
||||
- **Multi-language Support**: Fully translated in 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
||||
- **CI/CD**: Automated release workflow for Gitea
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.3 or higher
|
||||
- WordPress 6.0 or higher
|
||||
- WooCommerce 8.0 or higher
|
||||
- Composer (for dependency management)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the plugin files to `/wp-content/plugins/wc-composable-product/`
|
||||
2. Run `composer install --no-dev` in the plugin directory
|
||||
3. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
4. Configure global settings under WooCommerce > Settings > Composable Products
|
||||
### From Release Package
|
||||
|
||||
1. Download the latest release ZIP from the releases page
|
||||
2. In WordPress admin, go to **Plugins > Add New > Upload Plugin**
|
||||
3. Upload the ZIP file and click **Install Now**
|
||||
4. Activate the plugin through the **Plugins** menu
|
||||
5. Configure global settings under **WooCommerce > Settings > Composable Products**
|
||||
|
||||
### From Source
|
||||
|
||||
1. Upload the plugin directory to `/wp-content/plugins/wc-composable-product/`
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd /wp-content/plugins/wc-composable-product/
|
||||
composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
3. Activate the plugin through the **Plugins** menu in WordPress
|
||||
4. Configure global settings under **WooCommerce > Settings > Composable Products**
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Go to Products > Add New
|
||||
2. Select "Composable Product" as the product type
|
||||
3. Configure product details:
|
||||
- Set the selection limit (or use global default)
|
||||
- Choose pricing mode (fixed or sum)
|
||||
- Define available products by category, tag, or SKU
|
||||
4. Publish the product
|
||||
|
||||
### Global Settings
|
||||
|
||||
Navigate to WooCommerce > Settings > Composable Products to configure:
|
||||
- Default selection limit
|
||||
- Default pricing mode
|
||||
- Display options
|
||||
Navigate to **WooCommerce > Settings > Composable Products** to configure:
|
||||
|
||||
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
||||
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
||||
- **Display Options**: Toggle product images, prices, and totals
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Go to **Products > Add New**
|
||||
2. Select **Composable product** from the Product Data dropdown
|
||||
3. In the **General** tab:
|
||||
- Set a **Regular price** (used when pricing mode is "Fixed")
|
||||
- Configure **Selection Limit** (leave empty to use global default)
|
||||
- Choose **Pricing Mode** (leave empty to use global default)
|
||||
4. Click the **Composable Options** tab:
|
||||
- **Selection Criteria**: Choose how to define available products
|
||||
- **By Category**: Select one or more product categories
|
||||
- **By Tag**: Select one or more product tags
|
||||
- **By SKU**: Enter comma-separated SKUs (e.g., `STICKER-01, STICKER-02`)
|
||||
5. Click **Publish**
|
||||
|
||||
### Frontend Behavior
|
||||
|
||||
When customers visit a composable product page:
|
||||
|
||||
1. A grid of available products is displayed based on configured criteria
|
||||
2. Customers select up to the configured limit via checkboxes
|
||||
3. Total price updates in real-time (in sum pricing mode)
|
||||
4. Stock indicators show availability (green/orange/red badges)
|
||||
5. Click "Add to Cart" to add the composition to cart
|
||||
6. Selected products are listed in the cart and checkout
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Activate
|
||||
|
||||
- Ensure WooCommerce is installed and activated first
|
||||
- Check PHP version (must be 8.3+)
|
||||
- Verify Composer dependencies are installed (`vendor/` directory exists)
|
||||
|
||||
### Products Not Showing in Selector
|
||||
|
||||
- Check that products are published
|
||||
- Verify the selection criteria (category/tag/SKU) matches existing products
|
||||
- Ensure the criteria type and values are saved in the Composable Options tab
|
||||
|
||||
### Twig Template Errors
|
||||
|
||||
- Ensure the `vendor/` directory exists and contains Twig
|
||||
- Run `composer install` again
|
||||
- Check that the `cache/` directory is writable
|
||||
|
||||
### Updating
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Replace plugin files (or upload new release ZIP)
|
||||
3. If installed from source: run `composer install --no-dev --optimize-autoloader`
|
||||
4. Reactivate the plugin
|
||||
5. Clear all caches (WordPress, browser, CDN)
|
||||
|
||||
## Development
|
||||
|
||||
@@ -60,10 +121,28 @@ composer install
|
||||
### Translation
|
||||
|
||||
Generate POT file:
|
||||
|
||||
```bash
|
||||
wp i18n make-pot . languages/wc-composable-product.pot
|
||||
```
|
||||
|
||||
Compile translations:
|
||||
|
||||
```bash
|
||||
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||
```
|
||||
|
||||
### Creating Releases
|
||||
|
||||
Releases are automated via Gitea CI/CD. Push an annotated tag to trigger:
|
||||
|
||||
```bash
|
||||
git tag -a v1.2.0 -m "Release v1.2.0"
|
||||
git push origin v1.2.0
|
||||
```
|
||||
|
||||
The workflow builds the release ZIP, compiles translations, generates checksums, and creates a Gitea release with attachments.
|
||||
|
||||
## License
|
||||
|
||||
GPL v3 or later - see LICENSE file for details
|
||||
@@ -74,4 +153,4 @@ Marco Graetsch
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please use the GitHub issue tracker.
|
||||
For issues and feature requests, please use the issue tracker.
|
||||
|
||||
23
assets/css/admin.css
Normal file → Executable file
23
assets/css/admin.css
Normal file → Executable file
@@ -4,10 +4,17 @@
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
/* Hide composable panel by default */
|
||||
#composable_product_data {
|
||||
display: none;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Show composable panel when composable type is selected */
|
||||
body.product-type-composable #composable_product_data {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.composable_criteria_group {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 12px;
|
||||
@@ -19,11 +26,23 @@
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.show_if_composable {
|
||||
/* Hide composable-specific elements by default (but not tabs) */
|
||||
.options_group.show_if_composable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-type-composable .show_if_composable {
|
||||
/* Show composable elements when composable product type is selected */
|
||||
body.product-type-composable .options_group.show_if_composable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide the Composable Options tab link by default */
|
||||
.product_data_tabs li.composable_options {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the Composable Options tab when composable type selected */
|
||||
body.product-type-composable .product_data_tabs li.composable_options {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
0
assets/css/frontend.css
Normal file → Executable file
0
assets/css/frontend.css
Normal file → Executable file
8
assets/js/admin.js
Normal file → Executable file
8
assets/js/admin.js
Normal file → Executable file
@@ -17,12 +17,14 @@
|
||||
if (productType === 'composable') {
|
||||
$('.show_if_composable').show();
|
||||
$('.hide_if_composable').hide();
|
||||
$('#composable_product_data').show();
|
||||
$('.product_data_tabs .composable_options a').show();
|
||||
// Show the composable tab, then click it so WooCommerce's
|
||||
// native tab system hides all other panels properly
|
||||
$('.product_data_tabs li.composable_options').show();
|
||||
$('ul.product_data_tabs li.composable_options a').trigger('click');
|
||||
} else {
|
||||
$('.show_if_composable').hide();
|
||||
$('.product_data_tabs li.composable_options').hide();
|
||||
$('#composable_product_data').hide();
|
||||
$('.product_data_tabs .composable_options a').hide();
|
||||
}
|
||||
}).trigger('change');
|
||||
|
||||
|
||||
34
assets/js/frontend.js
Normal file → Executable file
34
assets/js/frontend.js
Normal file → Executable file
@@ -63,6 +63,36 @@
|
||||
this.clearMessages($container);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format price using WooCommerce settings
|
||||
*
|
||||
* @param {number} price Price amount
|
||||
* @return {string} Formatted price HTML
|
||||
*/
|
||||
formatPrice: function(price) {
|
||||
if (typeof wcComposableProduct === 'undefined' || !wcComposableProduct.price_format) {
|
||||
return price.toFixed(2);
|
||||
}
|
||||
|
||||
const format = wcComposableProduct.price_format;
|
||||
const decimals = parseInt(format.decimals, 10);
|
||||
const decimalSep = format.decimal_separator;
|
||||
const thousandSep = format.thousand_separator;
|
||||
|
||||
// Format number
|
||||
let priceStr = price.toFixed(decimals);
|
||||
const parts = priceStr.split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
|
||||
priceStr = parts.join(decimalSep);
|
||||
|
||||
// Apply price format (e.g., "%1$s%2$s" for symbol+price or "%2$s%1$s" for price+symbol)
|
||||
let formatted = format.price_format
|
||||
.replace('%1$s', '<span class="woocommerce-Price-currencySymbol">' + format.currency_symbol + '</span>')
|
||||
.replace('%2$s', priceStr);
|
||||
|
||||
return '<span class="woocommerce-Price-amount amount">' + formatted + '</span>';
|
||||
},
|
||||
|
||||
/**
|
||||
* Update total price
|
||||
*
|
||||
@@ -79,8 +109,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
const currencySymbol = $container.find('.total-price').data('currency');
|
||||
$container.find('.calculated-total').text(currencySymbol + total.toFixed(2));
|
||||
const formattedPrice = this.formatPrice(total);
|
||||
$container.find('.calculated-total').html(formattedPrice);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Graetsch",
|
||||
"email": "marco@example.com"
|
||||
"email": "magdev3.0@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
|
||||
@@ -35,6 +35,21 @@ class Cart_Handler {
|
||||
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
||||
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
||||
add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
||||
add_filter('woocommerce_is_purchasable', [$this, 'hide_default_add_to_cart'], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide default WooCommerce add to cart button for composable products
|
||||
*
|
||||
* @param bool $is_purchasable Is purchasable status
|
||||
* @param \WC_Product $product Product object
|
||||
* @return bool
|
||||
*/
|
||||
public function hide_default_add_to_cart($is_purchasable, $product) {
|
||||
if ($product && $product->get_type() === 'composable') {
|
||||
return false;
|
||||
}
|
||||
return $is_purchasable;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +200,7 @@ class Cart_Handler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use static flag to prevent multiple executions
|
||||
// Use static flag to prevent multiple executions within the same request
|
||||
static $already_calculated = false;
|
||||
if ($already_calculated) {
|
||||
return;
|
||||
@@ -193,13 +208,10 @@ class Cart_Handler {
|
||||
|
||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
||||
if (isset($cart_item['composable_products']) && !isset($cart_item['composable_price_calculated'])) {
|
||||
if (isset($cart_item['composable_products'])) {
|
||||
$product = $cart_item['data'];
|
||||
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
||||
$cart_item['data']->set_price($price);
|
||||
|
||||
// Mark as calculated to prevent re-calculation by other plugins
|
||||
$cart->cart_contents[$cart_item_key]['composable_price_calculated'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ class Plugin {
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_attr', 'esc_attr'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('wc_price', 'wc_price'));
|
||||
|
||||
// Add WordPress escaping functions as Twig filters
|
||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_html', 'esc_html'));
|
||||
@@ -161,6 +162,13 @@ class Plugin {
|
||||
'max_items' => __('Maximum items selected', 'wc-composable-product'),
|
||||
'min_items' => __('Please select at least one item', 'wc-composable-product'),
|
||||
],
|
||||
'price_format' => [
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
'decimal_separator' => wc_get_price_decimal_separator(),
|
||||
'thousand_separator' => wc_get_price_thousand_separator(),
|
||||
'decimals' => wc_get_price_decimals(),
|
||||
'price_format' => get_woocommerce_price_format(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ class Product_Selector {
|
||||
'show_prices' => $show_prices,
|
||||
'show_total' => $show_total,
|
||||
'fixed_price' => $product->get_price(),
|
||||
'fixed_price_html' => wc_price($product->get_price()),
|
||||
'zero_price_html' => wc_price(0),
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
];
|
||||
|
||||
|
||||
@@ -110,15 +110,17 @@ class Product_Type extends \WC_Product {
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'tax_query' => [],
|
||||
];
|
||||
|
||||
// Exclude composable products from selection
|
||||
$args['meta_query'] = [
|
||||
// Exclude composable products using the product_type taxonomy
|
||||
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||
$args['tax_query'] = [
|
||||
'relation' => 'AND',
|
||||
[
|
||||
'key' => '_product_type',
|
||||
'value' => 'composable',
|
||||
'compare' => '!=',
|
||||
'taxonomy' => 'product_type',
|
||||
'field' => 'slug',
|
||||
'terms' => ['composable'],
|
||||
'operator' => 'NOT IN',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -148,10 +150,12 @@ class Product_Type extends \WC_Product {
|
||||
case 'sku':
|
||||
if (!empty($criteria['skus'])) {
|
||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
||||
$args['meta_query'][] = [
|
||||
$args['meta_query'] = [
|
||||
[
|
||||
'key' => '_sku',
|
||||
'value' => $skus,
|
||||
'compare' => 'IN',
|
||||
],
|
||||
];
|
||||
}
|
||||
break;
|
||||
@@ -163,13 +167,28 @@ class Product_Type extends \WC_Product {
|
||||
if ($query->have_posts()) {
|
||||
foreach ($query->posts as $post) {
|
||||
$product = wc_get_product($post->ID);
|
||||
if ($product && $product->is_in_stock() && $product->is_purchasable()) {
|
||||
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle variable products by including their variations
|
||||
if ($product->is_type('variable')) {
|
||||
$variation_ids = $product->get_children();
|
||||
foreach ($variation_ids as $variation_id) {
|
||||
$variation = wc_get_product($variation_id);
|
||||
if ($variation && $variation->is_purchasable()) {
|
||||
$products[] = $variation;
|
||||
}
|
||||
}
|
||||
} elseif ($product->is_purchasable()) {
|
||||
$products[] = $product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
|
||||
4
languages/wc-composable-product-de_CH.po
Normal file → Executable file
4
languages/wc-composable-product-de_CH.po
Normal file → Executable file
@@ -238,3 +238,7 @@ msgstr "übrig"
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr "An Lager"
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die Produktkriterien im Admin-Bereich."
|
||||
|
||||
4
languages/wc-composable-product-de_CH_informal.po
Normal file → Executable file
4
languages/wc-composable-product-de_CH_informal.po
Normal file → Executable file
@@ -238,3 +238,7 @@ msgstr "übrig"
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr "An Lager"
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die Produktkriterien im Admin-Bereich."
|
||||
|
||||
4
languages/wc-composable-product-de_DE.po
Normal file → Executable file
4
languages/wc-composable-product-de_DE.po
Normal file → Executable file
@@ -238,3 +238,7 @@ msgstr "übrig"
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr "Auf Lager"
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die Produktkriterien im Admin-Bereich."
|
||||
|
||||
4
languages/wc-composable-product-de_DE_informal.po
Normal file → Executable file
4
languages/wc-composable-product-de_DE_informal.po
Normal file → Executable file
@@ -238,3 +238,7 @@ msgstr "übrig"
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr "Auf Lager"
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die Produktkriterien im Admin-Bereich."
|
||||
|
||||
4
languages/wc-composable-product-fr_CH.po
Normal file → Executable file
4
languages/wc-composable-product-fr_CH.po
Normal file → Executable file
@@ -238,3 +238,7 @@ msgstr "restant"
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr "En stock"
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Aucun produit disponible pour la sélection. Veuillez configurer les critères de produit dans le panneau d'administration."
|
||||
|
||||
4
languages/wc-composable-product-it_CH.po
Normal file → Executable file
4
languages/wc-composable-product-it_CH.po
Normal file → Executable file
@@ -238,3 +238,7 @@ msgstr "rimasti"
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr "Disponibile"
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr "Nessun prodotto disponibile per la selezione. Si prega di configurare i criteri del prodotto nel pannello di amministrazione."
|
||||
|
||||
4
languages/wc-composable-product.pot
Normal file → Executable file
4
languages/wc-composable-product.pot
Normal file → Executable file
@@ -237,3 +237,7 @@ msgstr ""
|
||||
#: templates/product-selector.twig
|
||||
msgid "In stock"
|
||||
msgstr ""
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
aec3bae001f0013322a73fa941169688 wc-composable-product-v1.0.0.zip
|
||||
@@ -1 +0,0 @@
|
||||
4a0f7ec2171aeabfdfe155419fd6124f35f3e14501ee2ca324bbab447259a8bb wc-composable-product-v1.0.0.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
0a60816bbc5a01c0057c1ffa72679d93 releases/wc-composable-product-v1.1.0.zip
|
||||
@@ -1 +0,0 @@
|
||||
645fdd68aca95cba77d961f3a48d41b9c12b3d17552572b7c039575dcfcab693 releases/wc-composable-product-v1.1.0.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
db09928aea6fffbf9c2e754d2264f2bc wc-composable-product-v1.1.1.zip
|
||||
@@ -1 +0,0 @@
|
||||
761eef69da910ecfdb20ceeed70b5d0381c7cab895e81a040d132cb0f88d749b wc-composable-product-v1.1.1.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
37cef191778b448dcbd2ae10141f64c6 wc-composable-product-v1.1.2.zip
|
||||
@@ -1 +0,0 @@
|
||||
191eae035b34ce8b33b90cf9d85ed54e493c1b471cda0efe5c992a512e91cc36 wc-composable-product-v1.1.2.zip
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
9bbed416019a796b4d4a5ef72e016e1f wc-composable-product-v1.1.3.zip
|
||||
@@ -1 +0,0 @@
|
||||
0ca23ca12570f0e9c518514ffc5209d78c76c3295954d10ec74a28013a762956 wc-composable-product-v1.1.3.zip
|
||||
12
templates/product-selector.twig
Normal file → Executable file
12
templates/product-selector.twig
Normal file → Executable file
@@ -9,6 +9,11 @@
|
||||
</div>
|
||||
|
||||
<div class="composable-products-grid">
|
||||
{% if products is empty %}
|
||||
<div class="composable-no-products">
|
||||
<p>{{ __('No products available for selection. Please configure the product criteria in the admin panel.') }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{% for product in products %}
|
||||
<div class="composable-product-item{% if not product.in_stock %} out-of-stock{% endif %}" data-product-id="{{ product.id }}" data-price="{{ product.price }}" data-stock-status="{{ product.stock_status }}">
|
||||
<div class="product-item-inner">
|
||||
@@ -52,16 +57,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if show_total %}
|
||||
<div class="composable-total">
|
||||
<div class="total-label">{{ __('Total Price:') }}</div>
|
||||
<div class="total-price" data-currency="{{ currency_symbol }}">
|
||||
<div class="total-price" data-currency="{{ currency_symbol }}" data-fixed-price="{{ fixed_price }}">
|
||||
{% if pricing_mode == 'fixed' %}
|
||||
{{ currency_symbol }}{{ fixed_price }}
|
||||
{{ fixed_price_html|raw }}
|
||||
{% else %}
|
||||
<span class="calculated-total">{{ currency_symbol }}0.00</span>
|
||||
<span class="calculated-total">{{ zero_price_html|raw }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Plugin Name: WooCommerce Composable Products
|
||||
* Plugin URI: https://github.com/magdev/wc-composable-product
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-composable-product
|
||||
* Description: Create composable products where customers select a limited number of items from a configurable set
|
||||
* Version: 1.1.6
|
||||
* Version: 1.2.1
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://example.com
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL v3 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: wc-composable-product
|
||||
@@ -19,7 +20,7 @@
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// Define plugin constants
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.1.6');
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.2.1');
|
||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user