Hi all,
I made a Shopify UI Extension for our retail operation that needs to discount from an MSRP rather than the selling price that Shopify typically discounts from.
I got it working in testing, then deployed to the smart grid. We have a number of locations throughout California, and it seems to work on some but not others.
I put some debugging into the extension so that it would give an actual error code, and I’m getting a 401 Fetch error.
What’s confusing to me is that this works on my personal iPad, and at a couple of the showroom locations iPads. But other ones are getting the 401 fetch error.
Something I can’t do: run webview debugging. All of our devices are iPads but we only have Windows PCs/Laptops.
The only scope this extension has is read_products, which everyone has or they wouldnt be able to look at products at all.
Any suggestions are appreciated!
Best,
Olek
This is my modal.jsx
import {render} from ‘preact’;
import {useState, useEffect} from ‘preact/hooks’;export default async () => {
render(, document.body);
};function Extension() {
const [isLoading, setIsLoading] = useState(true);
const [isApplying, setIsApplying] = useState(false);
const [statusMessage, setStatusMessage] = useState(‘’);
const [eligibleItems, setEligibleItems] = useState();
const [selectedUuids, setSelectedUuids] = useState();useEffect(() => {
async function fetchMsrpData() {
const lineItems = shopify.cart.current.value.lineItems;const catalogItems = lineItems.filter( (item) => item.productId !== undefined && item.productId !== null, ); if (catalogItems.length === 0) { setStatusMessage('No eligible products found in cart.'); setIsLoading(false); return; } const uniqueProductIds = [ ...new Set(catalogItems.map((item) => item.productId)), ]; const productAliases = uniqueProductIds .map( (id, index) => ` product${index}: product(id: "gid://shopify/Product/${id}") { id metafield(namespace: "custom", key: "msrp") { value } } `, ) .join('\n'); const query = `query { ${productAliases} }`; try { const response = await fetch('shopify:admin/api/graphql.json', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({query}), }); const data = await response.json(); if (data.errors) {setStatusMessage(
GraphQL error: ${data.errors[0].message});
setIsLoading(false);
return;
}const msrpByProductId = {}; uniqueProductIds.forEach((id, index) => { const productData = data.data[`product${index}`]; if (productData && productData.metafield) { msrpByProductId[id] = parseFloat(productData.metafield.value); } }); const items = []; for (const item of catalogItems) { const msrp = msrpByProductId[item.productId]; if (msrp === undefined || isNaN(msrp)) continue; const discountedPrice = msrp * 0.6; const discountAmount = item.price - discountedPrice; if (discountAmount <= 0) continue; items.push({ uuid: item.uuid, title: item.title || 'Unknown product', currentPrice: item.price, discountedPrice: discountedPrice, discountAmount: discountAmount, }); } if (items.length === 0) { setStatusMessage( 'No discounts to apply. Items may already be at or below the 40% off MSRP price.', ); setIsLoading(false); return; } setEligibleItems(items); setSelectedUuids(items.map((item) => item.uuid)); } catch (error) {console.error(‘Failed to fetch MSRP metafields:’, error);
setStatusMessage(Fetch error: ${error.message});
}setIsLoading(false); } fetchMsrpData();}, );
const applyDiscounts = async () => {
if (selectedUuids.length === 0) {
setStatusMessage(‘Please select at least one item.’);
return;
}
setIsApplying(true);const discountInputs = eligibleItems .filter((item) => selectedUuids.includes(item.uuid)) .map((item) => ({ lineItemUuid: item.uuid, lineItemDiscount: { type: 'FixedAmount', title: '40% Off MSRP', amount: item.discountAmount.toFixed(2), }, })); try { await shopify.cart.bulkSetLineItemDiscounts(discountInputs); shopify.toast.show('MSRP discounts applied'); } catch (error) { console.error('Failed to apply discounts:', error); setStatusMessage('Failed to apply discounts. Please try again.'); } setIsApplying(false);};
return (
{isLoading ? (
Loading products…
) : statusMessage ? (
{statusMessage}
) : (
<>
Select the items you want to discount:
<s-choice-list
multiple
values={selectedUuids}
onChange={(event) => setSelectedUuids([…event.currentTarget.values])}
>
{eligibleItems.map((item) => (
{item.title}
${item.currentPrice.toFixed(2)} → ${item.discountedPrice.toFixed(2)} (save ${item.discountAmount.toFixed(2)})
))}
<s-button
onClick={applyDiscounts}
disabled={isApplying || selectedUuids.length === 0}
>
{isApplying
? ‘Applying…’
:Apply discount to ${selectedUuids.length} item${selectedUuids.length !== 1 ? 's' : ''}}
</>
)}
);
}
Here’s my tile.jsx
import ‘@shopify/ui-extensions/preact’;
import {render} from ‘preact’;
import {useState, useEffect} from ‘preact/hooks’;export default async () => {
render(, document.body);
};function Extension() {
const [disabled, setDisabled] = useState(
shopify.cart.current.value.lineItems.length === 0,
);useEffect(() => {
const unsubscribe = shopify.cart.current.subscribe((cart) => {
setDisabled(cart.lineItems.length === 0);
});
return unsubscribe;
}, );return (
<s-tile
heading=“Trade Discount”
subheading=“Apply MSRP discount to cart”
onClick={() => shopify.action.presentModal()}
disabled={disabled}
/>
);
}