Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scripts/minicart/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,11 +356,12 @@ function getProductID(sku) {
* @param {number} quantity
*/
async function addToCartLegacy(sku, options, quantity) {
const { locale, language } = getLocaleAndLanguage();
const uenc = window.location.href.includes('?')
? window.location.href.split('?').map(btoa).join('_')
: btoa(window.location.href);
const [productId, formKey] = await Promise.all([getProductID(sku), getFormKey()]);
const url = `/us/en_us/checkout/cart/add/uenc/${uenc}/product/${productId}/`;
const url = `/${locale}/${language}/checkout/cart/add/uenc/${uenc}/product/${productId}/`;

const formData = new FormData();
formData.append('product', productId);
Expand Down Expand Up @@ -402,7 +403,6 @@ async function addToCartLegacy(sku, options, quantity) {
if (!resp.ok) {
console.error('Failed to add item to cart', resp);
// Generic error modal
const { locale, language } = getLocaleAndLanguage();
await openModal(`/${locale}/${language}/products/modals/atc-error`);
throw new Error('Failed to add item to cart');
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ function setAffiliateCoupon() {

if (!cjdata || !cjevent || !COUPON) return;

const loginUrl = new URL('https://www.vitamix.com/us/en_us/checkout/cart');
const { locale, language } = getLocaleAndLanguage();
const loginUrl = new URL(`https://www.vitamix.com/${locale}/${language}/checkout/cart`);
Object.entries({ cjdata, cjevent, COUPON }).forEach(([key, value]) => {
loginUrl.searchParams.set(key, value);
});
Expand Down
37 changes: 36 additions & 1 deletion scripts/storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,39 @@ tracking the cache timeout in `mage-cache-timeout`, etc.
This is also where the actual contact with Magento is happening to refresh the local storage cache and
store the information coming directly from Magento.

**Note: this requires that the endpoint `/customer/section/load` is routed to Magento.**
**Note: this requires that the endpoint `/customer/section/load` is routed to Magento.**

## Store-Specific Cache Keys

To support multiple store views (US, Canada, Mexico, etc.) on the same domain, localStorage keys are
store-specific to prevent cart data conflicts between different stores.

### Key Naming Convention

- **US Store (`/us/en_us/`)**: Uses original key names without suffix for backward compatibility
- `mage-cache-storage`
- `mage-cache-timeout`
- `mage-cache-storage-section-invalidation`

- **Other Stores**: Use store-specific suffixes in the format `-{locale}-{language}`
- Example for French Canadian (`/ca/fr_ca/`):
- `mage-cache-storage-ca-fr_ca`
- `mage-cache-timeout-ca-fr_ca`
- `mage-cache-storage-section-invalidation-ca-fr_ca`

### Benefits

- **Cart Isolation**: Each store view maintains its own cart, preventing conflicts when users switch between stores
- **Backward Compatibility**: Existing US customer carts are preserved (no migration required)
- **Multi-Store Support**: Users can have active carts in multiple stores simultaneously

### Helper Functions

The following helper functions generate the appropriate keys based on the current store:

- `getStoreSuffix()` - Returns the store-specific suffix or empty string for US
- `getCacheStorageKey()` - Returns the appropriate `mage-cache-storage` key
- `getCacheTimeoutKey()` - Returns the appropriate `mage-cache-timeout` key
- `getCacheInvalidationKey()` - Returns the appropriate invalidation key

All localStorage operations use these functions to ensure consistent key generation across the application.
83 changes: 61 additions & 22 deletions scripts/storage/util.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
const COMMERCE_CACHE_TIMEOUT_KEY = 'mage-cache-timeout';
const COMMERCE_CACHE_STORAGE_KEY = 'mage-cache-storage';
const COMMERCE_CACHE_INVALIDATION_KEY = 'mage-cache-storage-section-invalidation';
import { getLocaleAndLanguage } from '../scripts.js';

const COMMERCE_CACHE_SESSION_COOKIE = 'mage-cache-sessid';

/**
* Gets the store-specific suffix for localStorage keys.
* Returns empty string for US store to preserve existing carts.
* @returns {string} Store suffix (e.g., '-ca-fr_ca') or empty string for US
*/
function getStoreSuffix() {
const { locale, language } = getLocaleAndLanguage();
if (locale === 'us' && language === 'en_us') {
return '';
}
return `-${locale}-${language}`;
}

/**
* Gets the store-specific cache storage key.
* @returns {string} The localStorage key for mage-cache-storage
*/
function getCacheStorageKey() {
return `mage-cache-storage${getStoreSuffix()}`;
}

/**
* Gets the store-specific cache timeout key.
* @returns {string} The localStorage key for mage-cache-timeout
*/
function getCacheTimeoutKey() {
return `mage-cache-timeout${getStoreSuffix()}`;
}

/**
* Gets the store-specific cache invalidation key.
* @returns {string} The localStorage key for mage-cache-storage-section-invalidation
*/
function getCacheInvalidationKey() {
return `mage-cache-storage-section-invalidation${getStoreSuffix()}`;
}

/**
* Use the Magento systems cache timeout to determine if it's safe to use the
* local storage cache or not.
*
* @returns {boolean} true if local storage is expired
*/
export function isMagentoLocalStorageExpired() {
const localMageCacheTimeout = localStorage.getItem(COMMERCE_CACHE_TIMEOUT_KEY);
const localMageCacheTimeout = localStorage.getItem(getCacheTimeoutKey());

// This cookie will expire around when when the Magento PHP session cookie expires
// see: vendor/magento/module-customer/view/frontend/web/js/customer-data.js:48
Expand Down Expand Up @@ -44,7 +80,8 @@ export function isMagentoLocalStorageExpired() {
* @returns {void}
*/
export function addMagentoCacheInvalidations(sectionsToAdd) {
let invalidations = localStorage.getItem(COMMERCE_CACHE_INVALIDATION_KEY);
const cacheInvalidationKey = getCacheInvalidationKey();
let invalidations = localStorage.getItem(cacheInvalidationKey);

try {
invalidations = JSON.parse(invalidations);
Expand All @@ -57,7 +94,7 @@ export function addMagentoCacheInvalidations(sectionsToAdd) {
return;
}

localStorage.setItem(COMMERCE_CACHE_INVALIDATION_KEY, JSON.stringify(invalidations));
localStorage.setItem(cacheInvalidationKey, JSON.stringify(invalidations));
}

/**
Expand All @@ -68,7 +105,8 @@ export function addMagentoCacheInvalidations(sectionsToAdd) {
*/
function removeMagentoCacheInvalidations(invalidatedSections) {
// update invalidation to remove the cart and customer
let invalidations = localStorage.getItem(COMMERCE_CACHE_INVALIDATION_KEY);
const cacheInvalidationKey = getCacheInvalidationKey();
let invalidations = localStorage.getItem(cacheInvalidationKey);

try {
invalidations = JSON.parse(invalidations);
Expand All @@ -83,7 +121,7 @@ function removeMagentoCacheInvalidations(invalidatedSections) {
}
return true;
}));
localStorage.setItem(COMMERCE_CACHE_INVALIDATION_KEY, JSON.stringify(result));
localStorage.setItem(cacheInvalidationKey, JSON.stringify(result));
}

/**
Expand All @@ -95,7 +133,7 @@ function removeMagentoCacheInvalidations(invalidatedSections) {
* @returns {boolean} true if local storage is expired
*/
export function isMagentoCacheInvalidated(sections) {
const localMageCacheInvalidations = localStorage.getItem(COMMERCE_CACHE_INVALIDATION_KEY);
const localMageCacheInvalidations = localStorage.getItem(getCacheInvalidationKey());

if (!localMageCacheInvalidations) {
return false;
Expand All @@ -121,7 +159,7 @@ export function isMagentoCacheInvalidated(sections) {
* @returns {*} object representing the current Magento cache
*/
export function getMagentoCache() {
const magentoCache = localStorage.getItem(COMMERCE_CACHE_STORAGE_KEY);
const magentoCache = localStorage.getItem(getCacheStorageKey());
if (!magentoCache || isMagentoLocalStorageExpired()) {
return {};
}
Expand Down Expand Up @@ -152,9 +190,9 @@ export function getLoggedInFromLocalStorage() {
* @returns {boolean} true if no commerce state is present
*/
export function isCommerceStatePristine() {
return !localStorage.getItem(COMMERCE_CACHE_INVALIDATION_KEY)
&& !localStorage.getItem(COMMERCE_CACHE_STORAGE_KEY)
&& !localStorage.getItem(COMMERCE_CACHE_TIMEOUT_KEY)
return !localStorage.getItem(getCacheInvalidationKey())
&& !localStorage.getItem(getCacheStorageKey())
&& !localStorage.getItem(getCacheTimeoutKey())
&& (document.cookie.indexOf(`${COMMERCE_CACHE_SESSION_COOKIE}`) === -1);
}

Expand All @@ -165,15 +203,15 @@ export function isCommerceStatePristine() {
* @return {void}
*/
export async function updateMagentoCacheSections(sections) {
const { locale, language } = getLocaleAndLanguage();
const cacheStorageKey = getCacheStorageKey();
const cacheTimeoutKey = getCacheTimeoutKey();

let result = {};
let updatedSections = null;
try {
const loginAbortController = new AbortController();

const pathSegments = window.location.pathname.split('/').filter(Boolean);
const locale = pathSegments[0] || 'us'; // fallback to 'us' if not found
const language = pathSegments[1] || 'en_us'; // fallback to 'en_us' if not found

setTimeout(() => loginAbortController.abort('Section data took too long to respond.'), 10000);
result = await fetch(`/${locale}/${language}/customer/section/load/?sections=${encodeURIComponent(sections.join(','))}&force_new_section_timestamp=false`, {
signal: loginAbortController.signal,
Expand Down Expand Up @@ -201,7 +239,7 @@ export async function updateMagentoCacheSections(sections) {
// to the lifetime of the PHPSESSID but it's unknown here. This could be improved.
const minutesToTimeout = 25;
document.cookie = `${COMMERCE_CACHE_SESSION_COOKIE}=true; path=/; expires=${(new Date(new Date().getTime() + minutesToTimeout * 60000)).toUTCString()}; SameSite=Lax; ${window.location.protocol === 'http:' ? '' : 'Secure'}`;
let magentoCache = localStorage.getItem(COMMERCE_CACHE_STORAGE_KEY);
let magentoCache = localStorage.getItem(cacheStorageKey);

try {
magentoCache = JSON.parse(magentoCache) || {};
Expand All @@ -217,12 +255,12 @@ export async function updateMagentoCacheSections(sections) {
removeMagentoCacheInvalidations(sections);
}
const minutesToExpire = 30;
localStorage.setItem(COMMERCE_CACHE_STORAGE_KEY, JSON.stringify(magentoCache));
localStorage.setItem(cacheStorageKey, JSON.stringify(magentoCache));
localStorage.setItem(
COMMERCE_CACHE_TIMEOUT_KEY,
cacheTimeoutKey,
JSON.stringify((new Date(new Date().getTime() + minutesToExpire * 60000)).toISOString()),
);
window.dispatchEvent(new StorageEvent('storage', { key: COMMERCE_CACHE_STORAGE_KEY }));
window.dispatchEvent(new StorageEvent('storage', { key: cacheStorageKey }));
}

/**
Expand All @@ -232,8 +270,9 @@ export async function updateMagentoCacheSections(sections) {
* @param {function} callback the method to be called with the results of the event
*/
export function addMagentoCacheListener(callback) {
const cacheStorageKey = getCacheStorageKey();
window.addEventListener('storage', (event) => {
if (event.key === COMMERCE_CACHE_STORAGE_KEY) {
if (event.key === cacheStorageKey) {
callback(event);
}
});
Expand Down
Loading