From ab571bc67be67c42511c81d65f8b028f3e3f393a Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:14:51 -0400 Subject: [PATCH] Fix Shopify embed theming: modal/cart stuck on stale theme, unreadable quantity stepper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- webapp/src/views/Store.vue | 70 +++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 9d45fa7..7fc488b 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -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); }