Storefront Widgets
PromoSync populates rich metafield data that you can display on your storefront. This guide shows how to implement common widgets using Liquid templates.
Overview
PromoSync stores product data in Shopify metafields under the psrestful namespace. These metafields can be accessed in your theme’s Liquid templates to create dynamic displays.
Tier Pricing Block
Display quantity-based pricing tiers on product pages.
The Data Structure
// product.metafields.psrestful.tier_pricing
{
"value": {
"tiers": [
{"minQuantity": 24, "maxQuantity": 47, "price": 12.50},
{"minQuantity": 48, "maxQuantity": 143, "price": 11.00},
{"minQuantity": 144, "maxQuantity": 287, "price": 9.50},
{"minQuantity": 288, "maxQuantity": null, "price": 8.25}
],
"currency": "USD",
"pricingMethod": "MQ"
}
}Basic Implementation
Add to your product template (sections/main-product.liquid or similar):
{% raw %}{% assign tier_pricing = product.metafields.psrestful.tier_pricing.value %}
{% if tier_pricing and tier_pricing.tiers.size > 0 %}
<div class="tier-pricing-widget">
<h3>Volume Pricing</h3>
<table class="tier-pricing-table">
<thead>
<tr>
<th>Quantity</th>
<th>Price Each</th>
<th>You Save</th>
</tr>
</thead>
<tbody>
{% assign base_price = tier_pricing.tiers.first.price %}
{% for tier in tier_pricing.tiers %}
{% assign savings = base_price | minus: tier.price | times: 100 | divided_by: base_price %}
<tr>
<td>
{{ tier.minQuantity }}{% if tier.maxQuantity %} - {{ tier.maxQuantity }}{% else %}+{% endif %}
</td>
<td>{{ tier.price | money }}</td>
<td>
{% if savings > 0 %}{{ savings }}%{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}{% endraw %}Styling the Tier Pricing Table
.tier-pricing-widget {
margin: 20px 0;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
}
.tier-pricing-widget h3 {
margin: 0 0 15px 0;
font-size: 16px;
font-weight: 600;
}
.tier-pricing-table {
width: 100%;
border-collapse: collapse;
}
.tier-pricing-table th,
.tier-pricing-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #eee;
}
.tier-pricing-table th {
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
color: #666;
}
.tier-pricing-table td:last-child {
color: #2a9d3a;
font-weight: 500;
}Compact Tier Display
For a more compact display:
{% raw %}{% assign tiers = product.metafields.psrestful.tier_pricing.value.tiers %}
{% if tiers.size > 0 %}
<div class="tier-badges">
{% for tier in tiers %}
<span class="tier-badge">
<strong>{{ tier.minQuantity }}+</strong>
<span>{{ tier.price | money }}</span>
</span>
{% endfor %}
</div>
{% endif %}{% endraw %}Location Decorations Block
Display available decoration methods and locations.
The Data Structure
// product.metafields.psrestful.location_decorations
[
{
"locationId": 70,
"decorations": [
{
"default": false,
"decorationId": 950,
"priceIncludes": false,
"decorationName": "Embroidery",
"maxImprintColors": 99
},
{
"default": true,
"decorationId": 951,
"priceIncludes": false,
"decorationName": "Screen Print",
"maxImprintColors": 6
}
],
"locationName": "FRONT",
"locationRank": 2,
"maxDecoration": 0,
"minDecoration": 0,
"defaultLocation": false,
"decorationsIncluded": 0
},
{
"locationId": 71,
"decorations": [
{
"default": false,
"decorationId": 950,
"priceIncludes": false,
"decorationName": "Embroidery",
"maxImprintColors": 99
}
],
"locationName": "LEFT CHEST",
"locationRank": 1,
"maxDecoration": 0,
"minDecoration": 0,
"defaultLocation": true,
"decorationsIncluded": 0
}
]Implementation
{% raw %}{% assign locations = product.metafields.psrestful.location_decorations.value %}
{% if locations and locations.size > 0 %}
<div class="decoration-options">
<h3>Decoration Options</h3>
<div class="decoration-locations">
{% for location in locations %}
<div class="decoration-location">
<h4>{{ location.locationName }}{% if location.defaultLocation %} (Default){% endif %}</h4>
<ul class="decoration-methods">
{% for dec in location.decorations %}
<li>
<span class="method-name">{{ dec.decorationName }}</span>
{% if dec.maxImprintColors %}
<span class="colors">Up to {{ dec.maxImprintColors }} colors</span>
{% endif %}
{% if dec.default %}
<span class="default-badge">Default</span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
{% endif %}{% endraw %}Styling Decorations
.decoration-options {
margin: 20px 0;
}
.decoration-locations {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.decoration-location {
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.decoration-location h4 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: 600;
}
.location-size {
font-size: 12px;
color: #666;
margin-bottom: 10px;
}
.decoration-methods {
list-style: none;
padding: 0;
margin: 0;
}
.decoration-methods li {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #e0e0e0;
}
.decoration-methods li:last-child {
border-bottom: none;
}
.setup-charge.free {
color: #2a9d3a;
}Product Specifications Block
Display detailed product specifications.
The Data Structure
// product.metafields.psrestful.specifications
{
"value": {
"material": "100% Airlume combed and ring-spun cotton",
"weight": "4.2 oz",
"fit": "Unisex retail fit",
"features": [
"Side-seamed",
"Tear-away label",
"Shoulder-to-shoulder taping"
],
"compliance": ["CPSIA Certified", "WRAP Certified"]
}
}Implementation
{% raw %}{% assign specs = product.metafields.psrestful.specifications.value %}
{% if specs %}
<div class="product-specifications">
<h3>Specifications</h3>
<dl class="specs-list">
{% if specs.material %}
<dt>Material</dt>
<dd>{{ specs.material }}</dd>
{% endif %}
{% if specs.weight %}
<dt>Weight</dt>
<dd>{{ specs.weight }}</dd>
{% endif %}
{% if specs.fit %}
<dt>Fit</dt>
<dd>{{ specs.fit }}</dd>
{% endif %}
</dl>
{% if specs.features.size > 0 %}
<h4>Features</h4>
<ul class="features-list">
{% for feature in specs.features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
{% endif %}
{% if specs.compliance.size > 0 %}
<div class="compliance-badges">
{% for cert in specs.compliance %}
<span class="compliance-badge">{{ cert }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}{% endraw %}Supplier Information Block
Display supplier and brand information.
{% raw %}{% assign supplier = product.metafields.psrestful.supplier.value %}
{% assign brand = product.metafields.psrestful.brand.value %}
<div class="product-source">
{% if brand %}
<span class="product-brand">{{ brand }}</span>
{% endif %}
{% if supplier %}
<span class="product-supplier">from {{ supplier }}</span>
{% endif %}
</div>{% endraw %}Inventory Status Widget
Show inventory availability with more detail.
The Data Structure
// product.metafields.psrestful.inventory_data
{
"value": {
"totalAvailable": 1250,
"lastSync": "2024-01-20T14:30:00Z",
"warehouses": [
{"location": "Dallas", "qty": 500},
{"location": "Phoenix", "qty": 450},
{"location": "Atlanta", "qty": 300}
]
}
}Implementation
{% raw %}{% assign inv = product.metafields.psrestful.inventory_data.value %}
{% if inv %}
<div class="inventory-status">
{% if inv.totalAvailable > 100 %}
<span class="status in-stock">In Stock</span>
<span class="qty">{{ inv.totalAvailable }} available</span>
{% elsif inv.totalAvailable > 0 %}
<span class="status low-stock">Low Stock</span>
<span class="qty">Only {{ inv.totalAvailable }} left</span>
{% else %}
<span class="status out-of-stock">Out of Stock</span>
{% endif %}
{% if inv.warehouses.size > 1 %}
<details class="warehouse-details">
<summary>View warehouse availability</summary>
<ul>
{% for wh in inv.warehouses %}
<li>{{ wh.location }}: {{ wh.qty }} units</li>
{% endfor %}
</ul>
</details>
{% endif %}
</div>
{% endif %}{% endraw %}Theme App Extension (Online Store 2.0)
For Online Store 2.0 themes, create a theme app extension block.
Block Schema
Create blocks/tier-pricing.liquid:
{% raw %}{% assign tier_pricing = product.metafields.psrestful.tier_pricing.value %}
{% if tier_pricing and tier_pricing.tiers.size > 0 %}
<div class="tier-pricing-block" {{ block.shopify_attributes }}>
{% if block.settings.show_title %}
<h3 class="tier-pricing-title">{{ block.settings.title }}</h3>
{% endif %}
<table class="tier-pricing-table">
<thead>
<tr>
<th>{{ block.settings.quantity_label }}</th>
<th>{{ block.settings.price_label }}</th>
</tr>
</thead>
<tbody>
{% for tier in tier_pricing.tiers %}
<tr>
<td>{{ tier.minQuantity }}+</td>
<td>{{ tier.price | money }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% schema %}
{
"name": "Tier Pricing",
"target": "section",
"settings": [
{
"type": "checkbox",
"id": "show_title",
"label": "Show title",
"default": true
},
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Volume Pricing"
},
{
"type": "text",
"id": "quantity_label",
"label": "Quantity column label",
"default": "Quantity"
},
{
"type": "text",
"id": "price_label",
"label": "Price column label",
"default": "Price Each"
}
]
}
{% endschema %}{% endraw %}JavaScript Enhancements
Dynamic Price Calculator
Add a quantity-based price calculator:
// tier-pricing-calculator.js
class TierPricingCalculator {
constructor(element) {
this.element = element;
this.tiers = JSON.parse(element.dataset.tiers);
this.quantityInput = element.querySelector('[data-quantity-input]');
this.priceDisplay = element.querySelector('[data-price-display]');
this.totalDisplay = element.querySelector('[data-total-display]');
this.bindEvents();
}
bindEvents() {
this.quantityInput.addEventListener('input', () => this.calculate());
}
calculate() {
const quantity = parseInt(this.quantityInput.value) || 0;
const tier = this.findTier(quantity);
if (tier) {
const price = tier.price;
const total = price * quantity;
this.priceDisplay.textContent = this.formatMoney(price);
this.totalDisplay.textContent = this.formatMoney(total);
}
}
findTier(quantity) {
for (let i = this.tiers.length - 1; i >= 0; i--) {
if (quantity >= this.tiers[i].minQuantity) {
return this.tiers[i];
}
}
return this.tiers[0];
}
formatMoney(cents) {
return '$' + (cents).toFixed(2);
}
}
// Initialize
document.querySelectorAll('[data-tier-calculator]').forEach(el => {
new TierPricingCalculator(el);
});Usage in Template
{% raw %}<div data-tier-calculator data-tiers='{{ tier_pricing.tiers | json }}'>
<input type="number" data-quantity-input value="24" min="1">
<span>Price: <span data-price-display></span></span>
<span>Total: <span data-total-display></span></span>
</div>{% endraw %}Best Practices
1. Check for Data Existence
Always check if metafield data exists before rendering:
{% raw %}{% if product.metafields.psrestful.tier_pricing.value %}
<!-- Render widget -->
{% endif %}{% endraw %}2. Provide Fallbacks
Show graceful fallbacks when data is missing.
3. Keep It Fast
Avoid complex calculations in Liquid - use JavaScript if needed.
4. Mobile-First Design
Ensure widgets work well on mobile devices.
5. Test with Multiple Products
Test widgets with products that have varying amounts of data.
Troubleshooting
Widget Not Showing
- Verify the product has the metafield data
- Check the metafield namespace (
psrestful) - Ensure the product was synced recently
- Check for Liquid syntax errors
Data Not Updating
- Trigger an inventory sync
- Check last sync timestamp
- Verify supplier connection
Styling Issues
- Check for CSS conflicts with your theme
- Use specific selectors to avoid conflicts
- Test in theme preview mode