Fix Shopify embed theming: modal/cart stuck on stale theme, unreadable quantity stepper

Root cause: the cart drawer and product modal are SDK singletons attached
outside shopifyContainer, not children of it. Destroying and recreating
the collection component on theme change only rebuilt the product grid —
the cart/modal never got torn down, so they stayed frozen on whatever
theme was active the first time they rendered.

- Replace destroy-and-recreate with ui.createComponent() + updateConfig()
  on a persisted component reference, so theme changes push new options
  into the cart/modal singletons in place instead of leaving them stale
- Rename initShopify() -> renderShopify() to reflect create-or-update
  behavior
- Drop the shopifyRenderToken race guard from the previous commit — no
  longer needed since we're not destroying/rebuilding DOM on each change
- Theme quantityInput (background, text, border) in both the product tile
  and modal; it was left on the SDK's static default and stayed
  unreadable regardless of theme

Files changed:
  src/pages/StorePage.vue
This commit is contained in:
Daniel McGee
2026-07-01 22:14:51 -04:00
committed by GitHub
parent 2ea5b6cf06
commit ab571bc67b
+46 -24
View File
@@ -336,7 +336,14 @@ function buildShopifyOptions(dark: boolean) {
'padding-left': '20px',
'padding-right': '20px',
},
quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' },
quantityInput: {
'font-size': '16px',
'padding-top': '16px',
'padding-bottom': '16px',
'background-color': surfaceBg2,
color: modalText,
'border-color': borderColor,
},
price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText },
compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 },
unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 },
@@ -378,7 +385,14 @@ function buildShopifyOptions(dark: boolean) {
'padding-left': '20px',
'padding-right': '20px',
},
quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' },
quantityInput: {
'font-size': '16px',
'padding-top': '16px',
'padding-bottom': '16px',
'background-color': surfaceBg2,
color: modalText,
'border-color': borderColor,
},
title: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', 'font-size': '26px', color: modalText },
price: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '18px', color: modalText },
compareAt: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '15.3px', color: modalText2 },
@@ -502,62 +516,70 @@ watch(() => route.query, (query) => {
});
watch(collectionId, (id) => {
if (window.ShopifyBuy?.UI) initShopify(id);
if (window.ShopifyBuy?.UI) renderShopify(id);
});
declare global {
interface Window { ShopifyBuy: any }
}
// Guards against overlapping initShopify calls. If the collection or theme
// changes again before a previous onReady() promise resolves, the stale
// callback below becomes a no-op instead of racing the newer one and
// leaving the container half-rendered (some product images missing).
let shopifyRenderToken = 0;
// The cart drawer and product modal are singletons the SDK attaches outside
// `shopifyContainer` (not as children of it), tied to the client for the
// lifetime of the page. Destroying and recreating the collection component
// on every theme change only rebuilds the product grid — the cart/modal
// singletons are untouched and stay stuck on whatever theme was active the
// first time they were created. Keeping a reference and calling
// updateConfig() instead pushes new styles into all of them in place.
let shopifyComponent: any = null;
function initShopify(id: string) {
function renderShopify(id: string) {
const container = shopifyContainer.value;
if (!container) return;
if (!container || !window.ShopifyBuy?.UI) return;
const myToken = ++shopifyRenderToken;
const options = buildShopifyOptions(isDark.value);
if (shopifyComponent?.updateConfig) {
shopifyComponent.updateConfig({
id,
node: container,
moneyFormat: '%24%7B%7Bamount%7D%7D',
options,
});
shopifyReady.value = true;
return;
}
shopifyReady.value = false;
container.innerHTML = '';
const client = window.ShopifyBuy.buildClient({
domain: 'agora.markets',
storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd',
});
window.ShopifyBuy.UI.onReady(client).then((ui: any) => {
if (myToken !== shopifyRenderToken) return; // a newer render superseded this one
ui.createComponent('collection', {
shopifyComponent = ui.createComponent('collection', {
id,
node: container,
moneyFormat: '%24%7B%7Bamount%7D%7D',
options: buildShopifyOptions(isDark.value),
options,
});
// The SDK renders synchronously into the node before this callback resolves,
// so flipping the flag here reliably hides the skeleton at the right moment.
shopifyReady.value = true;
});
}
// Re-render the Shopify embed whenever the Vuetify theme flips so the modal
// text colours stay readable in both light and dark mode.
// Push new styles into the existing component whenever the Vuetify theme
// flips, instead of tearing down and rebuilding the whole widget.
watch(isDark, () => {
if (window.ShopifyBuy?.UI) initShopify(collectionId.value);
renderShopify(collectionId.value);
});
function loadShopifySDK() {
if (window.ShopifyBuy?.UI) {
initShopify(collectionId.value);
renderShopify(collectionId.value);
return;
}
const script = document.createElement('script');
script.async = true;
script.src = 'https://sdks.shopifycdn.com/buy-button/latest/buy-button-storefront.min.js';
script.onload = () => initShopify(collectionId.value);
script.onload = () => renderShopify(collectionId.value);
document.head.appendChild(script);
}