From 9c1fdb53d716644933f0b0ed5b322202b097b856 Mon Sep 17 00:00:00 2001 From: Vitor Date: Wed, 5 Aug 2026 11:55:10 -0300 Subject: [PATCH 1/3] fix(storefront): Fix loyalty points not showing at checkout when store has discount preset When a store has discount_option or installments_option configured in modules settings, modulesInfo[list_payments] was already non-empty from the preset, causing fetchInfo to skip the API call entirely. As a result, loyalty_points_programs returned by the loyalty app was never merged into storefront.info, so PointsApplier never received the programs and the toggle was not rendered. Fix: always fetch from the API, emitting preset data immediately as an initial value for fast rendering while the API call completes. Co-Authored-By: Claude Sonnet 4.6 --- packages/storefront/src/lib/state/modules-info.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/storefront/src/lib/state/modules-info.ts b/packages/storefront/src/lib/state/modules-info.ts index 4c3ac3bac..4c6c1d195 100644 --- a/packages/storefront/src/lib/state/modules-info.ts +++ b/packages/storefront/src/lib/state/modules-info.ts @@ -81,11 +81,10 @@ if (!import.meta.env.SSR) { const fetchInfo = () => { const modulesToFetch: { modName: ModuleApiEndpoint, reqOptions?: any }[] = []; (['list_payments', 'calculate_shipping'] as const).forEach((modName) => { - if (!Object.keys(modulesInfo[modName]).length) { - modulesToFetch.push({ modName }); - } else { + if (Object.keys(modulesInfo[modName]).length) { modulesInfoEmitter.emit(modName, modulesInfo[modName]); } + modulesToFetch.push({ modName }); }); if (Object.keys(utm).length || sessionCoupon) { const { apiContext } = globalThis.$storefront; From eda8ad04cc625dcec70a5c508bbac6ec5efd8306 Mon Sep 17 00:00:00 2001 From: Leonardo Matos Date: Fri, 7 Aug 2026 19:37:16 -0300 Subject: [PATCH 2/3] fix(storefront): Prevent discount and free shipping info from vanishing after page load Refreshing modules info cleared every field before parsing the API response, so values set on storefront settings disappeared whenever an app returned an error or the preview request brought no such field. - Merge over the preset instead of wiping: only fields absent from both the response and the preset are removed - Delete and assign are now adjacent, with no `await` in between, so the empty state is never rendered - Copy the preset objects on load, `modulesInfo` was aliasing (and therefore emptying) `window.$storefront.modulesInfoPreset` Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HtZi8sdmSDeQxrEeyzPJeS --- .../storefront/src/lib/state/modules-info.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/storefront/src/lib/state/modules-info.ts b/packages/storefront/src/lib/state/modules-info.ts index 4c6c1d195..04fcbeda2 100644 --- a/packages/storefront/src/lib/state/modules-info.ts +++ b/packages/storefront/src/lib/state/modules-info.ts @@ -33,8 +33,19 @@ const modulesInfo = reactive<{ available_extra_discount?: ApplyDiscountResponse['available_extra_discount'], }, }>(emptyInfo); +const infoPreset: { [modName: string]: Record } = {}; loadingGlobalInfoPreset.then((modulesInfoPreset) => { - Object.assign(modulesInfo, modulesInfoPreset); + Object.keys(modulesInfoPreset).forEach((modName) => { + // Must copy, assigning the preset objects themselves would alias + // `window.$storefront.modulesInfoPreset` and empty it on refresh + infoPreset[modName] = { ...modulesInfoPreset[modName] }; + Object.keys(infoPreset[modName]).forEach((field) => { + // Preset is the baseline only, persisted (API sourced) info wins + if (modulesInfo[modName][field] === undefined) { + modulesInfo[modName][field] = infoPreset[modName][field]; + } + }); + }); }); const modulesInfoEmitter = mitt(); @@ -119,9 +130,6 @@ if (!import.meta.env.SSR) { fetchModule(modName, reqOptions) .then(async (response) => { if (response.ok) { - Object.keys(modulesInfo[modName]).forEach((key) => { - delete modulesInfo[modName][key]; - }); const modInfo = {}; const { result } = await response.json(); if (Array.isArray(result)) { @@ -183,7 +191,15 @@ if (!import.meta.env.SSR) { } }); } - Object.assign(modulesInfo[modName], modInfo); + // Keep preset fields the response didn't bring: an app returning + // error must not erase what's set on storefront settings + const nextInfo = { ...infoPreset[modName], ...modInfo }; + Object.keys(modulesInfo[modName]).forEach((key) => { + if (nextInfo[key] === undefined) { + delete modulesInfo[modName][key]; + } + }); + Object.assign(modulesInfo[modName], nextInfo); sessionStorage.setItem(storageKey, JSON.stringify({ ...modulesInfo, __timestamp: Date.now(), From 1bd5ebaa42596d1462f957cdb76036df6e11668c Mon Sep 17 00:00:00 2001 From: Leonardo Matos Date: Fri, 7 Aug 2026 21:17:02 -0300 Subject: [PATCH 3/3] fix(storefront): Stop preset from skipping the modules API request The request was skipped whenever module info wasn't empty, and the preset read from storefront settings also makes it not empty, so stores with a discount or installments configured never fetched `list_payments`, and fields that only exist on the response, such as `loyalty_points_programs`, never arrived. Skip based on where the info came from, but only for `list_payments`: it has three fields and the preset may fill just some of them. The storefront reads a single field from `calculate_shipping`, so a preset for it is already complete and its cache rules stay exactly as they were. A `__fetched` mark per module on the session payload carries the origin across page views. A response yielding no field is not marked, so it is requested again on the next page view instead of caching empty. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HtZi8sdmSDeQxrEeyzPJeS --- .../storefront/src/lib/state/modules-info.ts | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/storefront/src/lib/state/modules-info.ts b/packages/storefront/src/lib/state/modules-info.ts index 04fcbeda2..2b6319da7 100644 --- a/packages/storefront/src/lib/state/modules-info.ts +++ b/packages/storefront/src/lib/state/modules-info.ts @@ -35,12 +35,11 @@ const modulesInfo = reactive<{ }>(emptyInfo); const infoPreset: { [modName: string]: Record } = {}; loadingGlobalInfoPreset.then((modulesInfoPreset) => { + // Copied and gap filled: must not alias (and later empty) + // `window.$storefront.modulesInfoPreset`, nor override persisted info Object.keys(modulesInfoPreset).forEach((modName) => { - // Must copy, assigning the preset objects themselves would alias - // `window.$storefront.modulesInfoPreset` and empty it on refresh infoPreset[modName] = { ...modulesInfoPreset[modName] }; Object.keys(infoPreset[modName]).forEach((field) => { - // Preset is the baseline only, persisted (API sourced) info wins if (modulesInfo[modName][field] === undefined) { modulesInfo[modName][field] = infoPreset[modName][field]; } @@ -74,12 +73,17 @@ export const fetchModule: FetchModule = (modName, reqOptions) => { if (!import.meta.env.SSR) { const storageKey = 'MODULES_INFO'; + const fetchedModules = new Set(); const sessionJson = sessionStorage.getItem(storageKey); if (sessionJson) { try { const persistedValue = JSON.parse(sessionJson); if (persistedValue.__timestamp >= Date.now() - 1000 * 60 * 5) { delete persistedValue.__timestamp; + if (Array.isArray(persistedValue.__fetched)) { + persistedValue.__fetched.forEach((modName) => fetchedModules.add(modName)); + } + delete persistedValue.__fetched; Object.assign(modulesInfo, persistedValue); } else { sessionStorage.removeItem(storageKey); @@ -92,10 +96,18 @@ if (!import.meta.env.SSR) { const fetchInfo = () => { const modulesToFetch: { modName: ModuleApiEndpoint, reqOptions?: any }[] = []; (['list_payments', 'calculate_shipping'] as const).forEach((modName) => { - if (Object.keys(modulesInfo[modName]).length) { + const isInfoSet = Object.keys(modulesInfo[modName]).length > 0; + if (isInfoSet) { modulesInfoEmitter.emit(modName, modulesInfo[modName]); } - modulesToFetch.push({ modName }); + // Preset covers `calculate_shipping` (single field), but `list_payments` + // may still miss `loyalty_points_programs` and others + const canSkipRequest = modName === 'calculate_shipping' + ? isInfoSet + : fetchedModules.has(modName); + if (!canSkipRequest) { + modulesToFetch.push({ modName }); + } }); if (Object.keys(utm).length || sessionCoupon) { const { apiContext } = globalThis.$storefront; @@ -191,8 +203,7 @@ if (!import.meta.env.SSR) { } }); } - // Keep preset fields the response didn't bring: an app returning - // error must not erase what's set on storefront settings + // Preset must survive a response missing the field (app error) const nextInfo = { ...infoPreset[modName], ...modInfo }; Object.keys(modulesInfo[modName]).forEach((key) => { if (nextInfo[key] === undefined) { @@ -200,9 +211,13 @@ if (!import.meta.env.SSR) { } }); Object.assign(modulesInfo[modName], nextInfo); + if (Object.keys(modInfo).length) { + fetchedModules.add(modName); + } sessionStorage.setItem(storageKey, JSON.stringify({ ...modulesInfo, __timestamp: Date.now(), + __fetched: [...fetchedModules], })); modulesInfoEmitter.emit(modName, modulesInfo[modName]); }