From dea83710a773c0fa8d123bccc76e692e65eaf7d1 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Thu, 26 Mar 2026 15:59:20 +0100 Subject: [PATCH 1/5] refactor: replace localStorage cart ID handling with utility functions --- .storybook/preview.tsx | 6 +- .../src/app/[locale]/(auth)/login/page.tsx | 6 +- .../src/app/[locale]/[[...slug]]/page.tsx | 8 +- apps/frontend/src/app/[locale]/not-found.tsx | 2 +- .../containers/Header/CartInfo/CartInfo.tsx | 8 +- .../Header/CartInfo/CartInfo.types.ts | 1 - .../frontend/src/containers/Header/Header.tsx | 10 +-- .../src/containers/Header/Header.types.ts | 1 - .../cart/src/frontend/Cart.client.stories.tsx | 8 +- .../cart/src/frontend/Cart.client.tsx | 11 +-- .../cart/src/frontend/Cart.server.tsx | 1 - .../checkout/cart/src/frontend/Cart.types.ts | 2 +- .../CheckoutBillingPayment.client.stories.tsx | 1 - .../CheckoutBillingPayment.client.tsx | 9 +- .../CheckoutBillingPayment.server.tsx | 9 +- .../frontend/CheckoutBillingPayment.types.ts | 3 +- .../CheckoutCompanyData.client.stories.tsx | 1 - .../frontend/CheckoutCompanyData.client.tsx | 9 +- .../frontend/CheckoutCompanyData.server.tsx | 11 +-- .../src/frontend/CheckoutCompanyData.types.ts | 3 +- ...CheckoutShippingAddress.client.stories.tsx | 1 - .../CheckoutShippingAddress.client.tsx | 9 +- .../CheckoutShippingAddress.server.tsx | 9 +- .../frontend/CheckoutShippingAddress.types.ts | 3 +- .../CheckoutSummary.client.stories.tsx | 1 - .../src/frontend/CheckoutSummary.client.tsx | 9 +- .../src/frontend/CheckoutSummary.server.tsx | 11 +-- .../src/frontend/CheckoutSummary.types.ts | 3 +- .../ProductDetails.client.stories.tsx | 1 - .../src/frontend/ProductDetails.client.tsx | 6 +- .../src/frontend/ProductDetails.server.tsx | 1 - .../src/frontend/ProductDetails.types.ts | 3 +- .../frontend/ProductList.client.stories.tsx | 1 - .../src/frontend/ProductList.client.tsx | 13 +-- .../src/frontend/ProductList.server.tsx | 11 +-- .../src/frontend/ProductList.types.ts | 2 +- .../RecommendedProducts.client.stories.tsx | 1 - .../frontend/RecommendedProducts.client.tsx | 6 +- .../frontend/RecommendedProducts.server.tsx | 1 - .../src/frontend/RecommendedProducts.types.ts | 3 +- .../GlobalProvider/GlobalProvider.tsx | 34 +++++++- .../utils/frontend/src/utils/cart-storage.ts | 85 +++++++++++++++++++ packages/utils/frontend/src/utils/index.ts | 1 + 43 files changed, 181 insertions(+), 144 deletions(-) create mode 100644 packages/utils/frontend/src/utils/cart-storage.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index c1cb329b8..023b3c9e4 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -7,6 +7,8 @@ import { initialize, mswLoader } from 'msw-storybook-addon'; import { NextIntlClientProvider } from 'next-intl'; import React from 'react'; +import { Utils } from '@o2s/utils.frontend'; + import { GlobalProvider } from '@o2s/ui/providers/GlobalProvider'; import { AppSpinner } from '@o2s/ui/components/Feedback/AppSpinner'; @@ -20,8 +22,6 @@ import '../apps/frontend/src/styles/global.css'; import { globalProviderConfig, globalProviderCurrentTheme, globalProviderLabels, globalProviderThemes } from './data'; import { cartAndCheckoutHandlers } from './mocks/handlers/cart-handlers'; -const cartIdLocalStorageKey = process.env.CART_ID_LOCAL_STORAGE_KEY!.trim(); - initialize(); createRouter({}); @@ -78,7 +78,7 @@ const preview: Preview = { (Story) => { // Set cartId for cart/checkout blocks - MSW handlers return mock data if (globalThis.window !== undefined) { - globalThis.window.localStorage.setItem(cartIdLocalStorageKey, 'storybook-cart-1'); + Utils.CartStorage.setCartId('storybook-cart-1'); } return ; }, diff --git a/apps/frontend/src/app/[locale]/(auth)/login/page.tsx b/apps/frontend/src/app/[locale]/(auth)/login/page.tsx index 9e9ab3e72..fd549f7d5 100644 --- a/apps/frontend/src/app/[locale]/(auth)/login/page.tsx +++ b/apps/frontend/src/app/[locale]/(auth)/login/page.tsx @@ -99,11 +99,7 @@ export default async function LoginPage({ params }: Readonly) {
-
+
-
+
diff --git a/apps/frontend/src/app/[locale]/not-found.tsx b/apps/frontend/src/app/[locale]/not-found.tsx index 39efa6ddd..8cd0aee9d 100644 --- a/apps/frontend/src/app/[locale]/not-found.tsx +++ b/apps/frontend/src/app/[locale]/not-found.tsx @@ -67,7 +67,7 @@ export default async function NotFound() {
-
+
diff --git a/apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx b/apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx index 103698269..c1941f66d 100644 --- a/apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx +++ b/apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx @@ -6,6 +6,8 @@ import { useSession } from 'next-auth/react'; import { useLocale } from 'next-intl'; import React, { useCallback, useEffect, useState } from 'react'; +import { Utils } from '@o2s/utils.frontend'; + import { Badge } from '@o2s/ui/elements/badge'; import { Button } from '@o2s/ui/elements/button'; @@ -18,7 +20,7 @@ import { CartInfoProps } from './CartInfo.types'; /** Last known line-item count for this browser session; survives header remounts on navigation (avoids badge flicker). */ let lastKnownCartItemCount = 0; -export const CartInfo = ({ data, cartIdLocalStorageKey }: CartInfoProps) => { +export const CartInfo = ({ data }: CartInfoProps) => { const session = useSession(); const locale = useLocale(); const [itemCount, setItemCount] = useState(() => lastKnownCartItemCount); @@ -28,7 +30,7 @@ export const CartInfo = ({ data, cartIdLocalStorageKey }: CartInfoProps) => { let cancelled = false; void (async () => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { if (!cancelled) { lastKnownCartItemCount = 0; @@ -52,7 +54,7 @@ export const CartInfo = ({ data, cartIdLocalStorageKey }: CartInfoProps) => { return () => { cancelled = true; }; - }, [session.data?.accessToken, locale, cartIdLocalStorageKey]); + }, [session.data?.accessToken, locale]); const onCartChanged = useCallback((payload: O2SEventMap['cart:changed']) => { const next = payload.cart.items.data.length; diff --git a/apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts b/apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts index 6dd88ffb7..1d206a977 100644 --- a/apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts +++ b/apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts @@ -3,5 +3,4 @@ export interface CartInfoProps { url: string; label: string; }; - cartIdLocalStorageKey: string; } diff --git a/apps/frontend/src/containers/Header/Header.tsx b/apps/frontend/src/containers/Header/Header.tsx index ab6d260b3..de74d775a 100644 --- a/apps/frontend/src/containers/Header/Header.tsx +++ b/apps/frontend/src/containers/Header/Header.tsx @@ -26,7 +26,6 @@ export const Header: React.FC = ({ alternativeUrls, children, shouldIncludeSignInButton = true, - cartIdLocalStorageKey, }) => { const session = useSession(); const isSignedIn = !!session.data?.user; @@ -73,13 +72,8 @@ export const Header: React.FC = ({ return null; } - return ( - - ); - }, [data.cart, cartIdLocalStorageKey]); + return ; + }, [data.cart]); const LocaleSlot = useMemo( () => , diff --git a/apps/frontend/src/containers/Header/Header.types.ts b/apps/frontend/src/containers/Header/Header.types.ts index dfde32863..d0f6aee43 100644 --- a/apps/frontend/src/containers/Header/Header.types.ts +++ b/apps/frontend/src/containers/Header/Header.types.ts @@ -9,5 +9,4 @@ export interface HeaderProps { [key: string]: string; }; shouldIncludeSignInButton?: boolean; - cartIdLocalStorageKey?: string; } diff --git a/packages/blocks/checkout/cart/src/frontend/Cart.client.stories.tsx b/packages/blocks/checkout/cart/src/frontend/Cart.client.stories.tsx index 7e59265cd..e7e506b27 100644 --- a/packages/blocks/checkout/cart/src/frontend/Cart.client.stories.tsx +++ b/packages/blocks/checkout/cart/src/frontend/Cart.client.stories.tsx @@ -2,12 +2,12 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { defineRouting } from 'next-intl/routing'; import React from 'react'; +import { Utils } from '@o2s/utils.frontend'; + import readme from '../../README.md?raw'; import { CartPure } from './Cart.client'; -const cartIdLocalStorageKey = process.env.CART_ID_LOCAL_STORAGE_KEY!.trim(); - const routing = defineRouting({ locales: ['en'], defaultLocale: 'en', @@ -76,7 +76,6 @@ export const Default: Story = { id: 'cart-1', locale: 'en', routing, - cartIdLocalStorageKey: cartIdLocalStorageKey, }, }; @@ -88,12 +87,11 @@ export const EmptyCart: Story = { id: 'cart-1', locale: 'en', routing, - cartIdLocalStorageKey: cartIdLocalStorageKey, }, decorators: [ (Story) => { if (typeof window !== 'undefined') { - window.localStorage.setItem(cartIdLocalStorageKey, EMPTY_CART_ID); + Utils.CartStorage.setCartId(EMPTY_CART_ID); } return ; }, diff --git a/packages/blocks/checkout/cart/src/frontend/Cart.client.tsx b/packages/blocks/checkout/cart/src/frontend/Cart.client.tsx index 2a3f4608a..d46e0113e 100644 --- a/packages/blocks/checkout/cart/src/frontend/Cart.client.tsx +++ b/packages/blocks/checkout/cart/src/frontend/Cart.client.tsx @@ -4,6 +4,8 @@ import { eventBus } from '@o2s/ui/event-bus'; import { createNavigation } from 'next-intl/navigation'; import React, { useEffect, useState, useTransition } from 'react'; +import { Utils } from '@o2s/utils.frontend'; + import { Carts } from '@o2s/framework/modules'; import { toast } from '@o2s/ui/hooks/use-toast'; @@ -24,7 +26,6 @@ export const CartPure: React.FC> = ({ locale, accessToken, routing, - cartIdLocalStorageKey, title, subtitle, defaultCurrency, @@ -43,7 +44,7 @@ export const CartPure: React.FC> = ({ const [isMutationPending, startMutationTransition] = useTransition(); useEffect(() => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) return; startInitialLoadTransition(async () => { @@ -54,10 +55,10 @@ export const CartPure: React.FC> = ({ toast({ variant: 'destructive', description: errors?.loadError }); } }); - }, [locale, accessToken, cartIdLocalStorageKey, errors?.loadError]); + }, [locale, accessToken, errors?.loadError]); const updateQuantity = (itemId: string, newQuantity: number) => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) return; startMutationTransition(async () => { @@ -78,7 +79,7 @@ export const CartPure: React.FC> = ({ }; const removeItem = (itemId: string) => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) return; startMutationTransition(async () => { diff --git a/packages/blocks/checkout/cart/src/frontend/Cart.server.tsx b/packages/blocks/checkout/cart/src/frontend/Cart.server.tsx index ed936061f..76f1f1304 100644 --- a/packages/blocks/checkout/cart/src/frontend/Cart.server.tsx +++ b/packages/blocks/checkout/cart/src/frontend/Cart.server.tsx @@ -31,7 +31,6 @@ export const Cart: React.FC = async ({ id, accessToken, locale, routi locale={locale} routing={routing} hasPriority={hasPriority} - cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY!} /> ); }; diff --git a/packages/blocks/checkout/cart/src/frontend/Cart.types.ts b/packages/blocks/checkout/cart/src/frontend/Cart.types.ts index f7acb512d..4085655cd 100644 --- a/packages/blocks/checkout/cart/src/frontend/Cart.types.ts +++ b/packages/blocks/checkout/cart/src/frontend/Cart.types.ts @@ -10,7 +10,7 @@ export interface CartProps { hasPriority?: boolean; } -export type CartPureProps = CartProps & Model.CartBlock & { cartIdLocalStorageKey: string }; +export type CartPureProps = CartProps & Model.CartBlock; export type CartRendererProps = Omit & { slug: string[]; diff --git a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.stories.tsx b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.stories.tsx index b365204a6..413fe262e 100644 --- a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.stories.tsx +++ b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.stories.tsx @@ -76,6 +76,5 @@ export const Default: Story = { id: 'checkout-billing-payment-1', locale: 'en', routing, - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, }, }; diff --git a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.tsx b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.tsx index af91e1e5b..8738a2d4d 100644 --- a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.tsx +++ b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.tsx @@ -5,6 +5,8 @@ import { createNavigation } from 'next-intl/navigation'; import React, { useEffect, useState, useTransition } from 'react'; import { object as YupObject, string as YupString } from 'yup'; +import { Utils } from '@o2s/utils.frontend'; + import { Carts, Models, Payments } from '@o2s/framework/modules'; import { useToast } from '@o2s/ui/hooks/use-toast'; @@ -25,7 +27,6 @@ export const CheckoutBillingPaymentPure: React.FC { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { toast({ description: errors?.cartNotFound, variant: 'destructive' }); router.replace(cartPath ?? '/'); @@ -94,14 +95,14 @@ export const CheckoutBillingPaymentPure: React.FC { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) return; startSubmitTransition(async () => { diff --git a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.server.tsx b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.server.tsx index f7d8e95f9..f7a217ebf 100644 --- a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.server.tsx +++ b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.server.tsx @@ -31,13 +31,6 @@ export const CheckoutBillingPayment: React.FC = asy } return ( - + ); }; diff --git a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.types.ts b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.types.ts index a8f2f70f3..ac121c4e3 100644 --- a/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.types.ts +++ b/packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.types.ts @@ -9,8 +9,7 @@ export interface CheckoutBillingPaymentProps { routing: ReturnType; } -export type CheckoutBillingPaymentPureProps = CheckoutBillingPaymentProps & - Model.CheckoutBillingPaymentBlock & { cartIdLocalStorageKey: string }; +export type CheckoutBillingPaymentPureProps = CheckoutBillingPaymentProps & Model.CheckoutBillingPaymentBlock; export type CheckoutBillingPaymentRendererProps = Omit & { slug: string[]; diff --git a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.stories.tsx b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.stories.tsx index 9e30a929d..f76558883 100644 --- a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.stories.tsx +++ b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.stories.tsx @@ -135,6 +135,5 @@ export const Default: Story = { id: 'checkout-company-data-1', locale: 'en', routing, - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, }, }; diff --git a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.tsx b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.tsx index a6fe01bf0..6fbc473ee 100644 --- a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.tsx +++ b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.tsx @@ -5,6 +5,8 @@ import { createNavigation } from 'next-intl/navigation'; import React, { useEffect, useState, useTransition } from 'react'; import { object as YupObject, string as YupString } from 'yup'; +import { Utils } from '@o2s/utils.frontend'; + import { Carts, Models } from '@o2s/framework/modules'; import { useToast } from '@o2s/ui/hooks/use-toast'; @@ -30,7 +32,6 @@ export const CheckoutCompanyDataPure: React.FC { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { toast({ description: errors.cartNotFound, variant: 'destructive' }); router.replace(cartPath); @@ -121,10 +122,10 @@ export const CheckoutCompanyDataPure: React.FC { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { toast({ description: errors.cartNotFound, variant: 'destructive' }); return; diff --git a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.server.tsx b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.server.tsx index 5a7fe55b0..510c77305 100644 --- a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.server.tsx +++ b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.server.tsx @@ -25,14 +25,5 @@ export const CheckoutCompanyData: React.FC = async ({ return null; } - return ( - - ); + return ; }; diff --git a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.types.ts b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.types.ts index a11b4f8be..77fcbc168 100644 --- a/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.types.ts +++ b/packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.types.ts @@ -9,8 +9,7 @@ export interface CheckoutCompanyDataProps { routing: ReturnType; } -export type CheckoutCompanyDataPureProps = CheckoutCompanyDataProps & - Model.CheckoutCompanyDataBlock & { cartIdLocalStorageKey: string }; +export type CheckoutCompanyDataPureProps = CheckoutCompanyDataProps & Model.CheckoutCompanyDataBlock; export type CheckoutCompanyDataRendererProps = Omit & { slug: string[]; diff --git a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.stories.tsx b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.stories.tsx index b00a0ef0b..5e4bd85a5 100644 --- a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.stories.tsx +++ b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.stories.tsx @@ -121,6 +121,5 @@ export const Default: Story = { id: 'checkout-shipping-address-1', locale: 'en', routing, - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, }, }; diff --git a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.tsx b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.tsx index f38c465c7..75063dde6 100644 --- a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.tsx +++ b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.tsx @@ -5,6 +5,8 @@ import { createNavigation } from 'next-intl/navigation'; import React, { useEffect, useState, useTransition } from 'react'; import { boolean as YupBoolean, object as YupObject, string as YupString } from 'yup'; +import { Utils } from '@o2s/utils.frontend'; + import { Carts, Models, Orders } from '@o2s/framework/modules'; import { useToast } from '@o2s/ui/hooks/use-toast'; @@ -31,7 +33,6 @@ export const CheckoutShippingAddressPure: React.FC { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { toast({ description: errors.cartNotFound, variant: 'destructive' }); router.replace(cartPath); @@ -123,11 +124,11 @@ export const CheckoutShippingAddressPure: React.FC { startSubmitTransition(async () => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) return; try { await sdk.checkout.setAddresses( diff --git a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.server.tsx b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.server.tsx index 2c2515560..2ba1e34fa 100644 --- a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.server.tsx +++ b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.server.tsx @@ -31,13 +31,6 @@ export const CheckoutShippingAddress: React.FC = a } return ( - + ); }; diff --git a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.types.ts b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.types.ts index 5d09eceeb..cc3a07515 100644 --- a/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.types.ts +++ b/packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.types.ts @@ -9,8 +9,7 @@ export interface CheckoutShippingAddressProps { routing: ReturnType; } -export type CheckoutShippingAddressPureProps = CheckoutShippingAddressProps & - Model.CheckoutShippingAddressBlock & { cartIdLocalStorageKey: string }; +export type CheckoutShippingAddressPureProps = CheckoutShippingAddressProps & Model.CheckoutShippingAddressBlock; export type CheckoutShippingAddressRendererProps = Omit & { slug: string[]; diff --git a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.stories.tsx b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.stories.tsx index 2faab84b2..40d0974b3 100644 --- a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.stories.tsx +++ b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.stories.tsx @@ -89,6 +89,5 @@ export const Default: Story = { id: 'checkout-summary-1', locale: 'en', routing, - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, }, }; diff --git a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx index d9eea9172..565cbc52e 100644 --- a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx +++ b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx @@ -25,7 +25,6 @@ export const CheckoutSummaryPure: React.FC> = locale, accessToken, routing, - cartIdLocalStorageKey, title, subtitle, stepIndicator, @@ -45,7 +44,7 @@ export const CheckoutSummaryPure: React.FC> = const [isSubmitPending, startSubmitTransition] = useTransition(); useEffect(() => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { toast({ description: errors.cartNotFound, variant: 'destructive' }); router.replace(cartPath); @@ -67,10 +66,10 @@ export const CheckoutSummaryPure: React.FC> = } } }); - }, [locale, accessToken, cartIdLocalStorageKey, toast, errors.cartNotFound, errors.loadError, router, cartPath]); + }, [locale, accessToken, toast, errors.cartNotFound, errors.loadError, router, cartPath]); const handleConfirm = () => { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); if (!cartId) { toast({ description: errors.cartNotFound, variant: 'destructive' }); router.replace(cartPath); @@ -84,7 +83,7 @@ export const CheckoutSummaryPure: React.FC> = if (result.order?.id) { const redirectUrl = result.paymentRedirectUrl || `${buttons.confirm.path}/${result.order.id}`; - localStorage.removeItem(cartIdLocalStorageKey); + Utils.CartStorage.removeCartId(); window.location.href = redirectUrl; return; } diff --git a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.server.tsx b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.server.tsx index 954b2b95e..681c8f098 100644 --- a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.server.tsx +++ b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.server.tsx @@ -25,14 +25,5 @@ export const CheckoutSummary: React.FC = async ({ id, acce return null; } - return ( - - ); + return ; }; diff --git a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts index 2f12e72c8..3d8a33b9d 100644 --- a/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts +++ b/packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts @@ -9,8 +9,7 @@ export interface CheckoutSummaryProps { routing: ReturnType; } -export type CheckoutSummaryPureProps = CheckoutSummaryProps & - Model.CheckoutSummaryBlock & { cartIdLocalStorageKey: string }; +export type CheckoutSummaryPureProps = CheckoutSummaryProps & Model.CheckoutSummaryBlock; export type CheckoutSummaryRendererProps = Omit & { slug: string[]; diff --git a/packages/blocks/products/product-details/src/frontend/ProductDetails.client.stories.tsx b/packages/blocks/products/product-details/src/frontend/ProductDetails.client.stories.tsx index 800dd2da7..eb4933f4e 100644 --- a/packages/blocks/products/product-details/src/frontend/ProductDetails.client.stories.tsx +++ b/packages/blocks/products/product-details/src/frontend/ProductDetails.client.stories.tsx @@ -17,7 +17,6 @@ type Story = StoryObj; export const Default: Story = { args: { - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, __typename: 'ProductDetailsBlock', id: 'product-details-1', productId: 'PRD-015', diff --git a/packages/blocks/products/product-details/src/frontend/ProductDetails.client.tsx b/packages/blocks/products/product-details/src/frontend/ProductDetails.client.tsx index 5f5d1b210..39d98e44d 100644 --- a/packages/blocks/products/product-details/src/frontend/ProductDetails.client.tsx +++ b/packages/blocks/products/product-details/src/frontend/ProductDetails.client.tsx @@ -69,7 +69,6 @@ export const ProductDetailsPure: React.FC = ({ routing, hasPriority, productId, - cartIdLocalStorageKey, ...component }) => { const { product, labels } = component; @@ -137,7 +136,7 @@ export const ProductDetailsPure: React.FC = ({ const handleAddToCart = useCallback(() => { startAddToCartTransition(async () => { try { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); const result = await sdk.cart.addCartItem( { cartId: cartId || undefined, @@ -150,7 +149,7 @@ export const ProductDetailsPure: React.FC = ({ accessToken, ); if (!cartId && result?.id) { - localStorage.setItem(cartIdLocalStorageKey, result.id); + Utils.CartStorage.setCartId(result.id); } eventBus.emit('cart:changed', { cart: result }); toast({ @@ -175,7 +174,6 @@ export const ProductDetailsPure: React.FC = ({ product.name, locale, accessToken, - cartIdLocalStorageKey, labels.addToCartSuccess, labels.addToCartError, labels.viewCart, diff --git a/packages/blocks/products/product-details/src/frontend/ProductDetails.server.tsx b/packages/blocks/products/product-details/src/frontend/ProductDetails.server.tsx index f60a8ad03..399006307 100644 --- a/packages/blocks/products/product-details/src/frontend/ProductDetails.server.tsx +++ b/packages/blocks/products/product-details/src/frontend/ProductDetails.server.tsx @@ -40,7 +40,6 @@ export const ProductDetails: React.FC = async ({ locale={locale} routing={routing} hasPriority={hasPriority} - cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY!} /> ); }; diff --git a/packages/blocks/products/product-details/src/frontend/ProductDetails.types.ts b/packages/blocks/products/product-details/src/frontend/ProductDetails.types.ts index 700105f08..d5cfe813a 100644 --- a/packages/blocks/products/product-details/src/frontend/ProductDetails.types.ts +++ b/packages/blocks/products/product-details/src/frontend/ProductDetails.types.ts @@ -9,8 +9,7 @@ export interface ProductDetailsProps extends Models.BlockProps.BaseBlockProps> & Pick; diff --git a/packages/blocks/products/product-list/src/frontend/ProductList.client.stories.tsx b/packages/blocks/products/product-list/src/frontend/ProductList.client.stories.tsx index 319e8b44f..f6b630a7f 100644 --- a/packages/blocks/products/product-list/src/frontend/ProductList.client.stories.tsx +++ b/packages/blocks/products/product-list/src/frontend/ProductList.client.stories.tsx @@ -31,7 +31,6 @@ export const Default: Story = { }, }, }, - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, __typename: 'ProductListBlock', id: 'product-list-1', title: 'Products', diff --git a/packages/blocks/products/product-list/src/frontend/ProductList.client.tsx b/packages/blocks/products/product-list/src/frontend/ProductList.client.tsx index f0b0181e1..bde8e94bd 100644 --- a/packages/blocks/products/product-list/src/frontend/ProductList.client.tsx +++ b/packages/blocks/products/product-list/src/frontend/ProductList.client.tsx @@ -28,13 +28,7 @@ import { sdk } from '../sdk'; import { ProductListPureProps } from './ProductList.types'; -export const ProductListPure: React.FC = ({ - locale, - accessToken, - routing, - cartIdLocalStorageKey, - ...component -}) => { +export const ProductListPure: React.FC = ({ locale, accessToken, routing, ...component }) => { const { Link: LinkComponent, useRouter } = createNavigation(routing); const router = useRouter(); const initialProducts = component.products?.data ?? []; @@ -64,7 +58,7 @@ export const ProductListPure: React.FC = ({ const productName = data.products.data.find((p) => p.sku === sku)?.name ?? sku; startAddToCartTransition(async () => { try { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); const result = await sdk.cart.addCartItem( { cartId: cartId || undefined, @@ -77,7 +71,7 @@ export const ProductListPure: React.FC = ({ accessToken, ); if (!cartId && result?.id) { - localStorage.setItem(cartIdLocalStorageKey, result.id); + Utils.CartStorage.setCartId(result.id); } eventBus.emit('cart:changed', { cart: result }); toast({ @@ -102,7 +96,6 @@ export const ProductListPure: React.FC = ({ [ locale, accessToken, - cartIdLocalStorageKey, data.labels.addToCartSuccess, data.labels.addToCartError, data.labels.viewCartLabel, diff --git a/packages/blocks/products/product-list/src/frontend/ProductList.server.tsx b/packages/blocks/products/product-list/src/frontend/ProductList.server.tsx index 200a08a95..3b71d18f3 100644 --- a/packages/blocks/products/product-list/src/frontend/ProductList.server.tsx +++ b/packages/blocks/products/product-list/src/frontend/ProductList.server.tsx @@ -31,14 +31,5 @@ export const ProductList: React.FC = async ({ id, accessToken, return null; } - return ( - - ); + return ; }; diff --git a/packages/blocks/products/product-list/src/frontend/ProductList.types.ts b/packages/blocks/products/product-list/src/frontend/ProductList.types.ts index ad27033f4..0081dd281 100644 --- a/packages/blocks/products/product-list/src/frontend/ProductList.types.ts +++ b/packages/blocks/products/product-list/src/frontend/ProductList.types.ts @@ -8,7 +8,7 @@ export interface ProductListProps extends Models.BlockProps.BaseBlockProps> & Pick; diff --git a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.stories.tsx b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.stories.tsx index e4ab96b5e..b561bab5b 100644 --- a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.stories.tsx +++ b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.stories.tsx @@ -17,7 +17,6 @@ type Story = StoryObj; export const Default: Story = { args: { - cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!, __typename: 'RecommendedProductsBlock', id: 'recommended-products-1', locale: 'en', diff --git a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.tsx b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.tsx index 25d367970..6ef013fe0 100644 --- a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.tsx +++ b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.tsx @@ -22,7 +22,6 @@ export const RecommendedProductsPure: React.FC = ( locale, accessToken, routing, - cartIdLocalStorageKey, ...component }) => { const { Link: LinkComponent, useRouter } = createNavigation(routing); @@ -36,7 +35,7 @@ export const RecommendedProductsPure: React.FC = ( const productName = products.find((p) => p.sku === sku)?.name ?? sku; startAddToCartTransition(async () => { try { - const cartId = localStorage.getItem(cartIdLocalStorageKey); + const cartId = Utils.CartStorage.getCartId(); const result = await sdk.cart.addCartItem( { cartId: cartId || undefined, @@ -49,7 +48,7 @@ export const RecommendedProductsPure: React.FC = ( accessToken, ); if (!cartId && result?.id) { - localStorage.setItem(cartIdLocalStorageKey, result.id); + Utils.CartStorage.setCartId(result.id); } eventBus.emit('cart:changed', { cart: result }); toast({ @@ -74,7 +73,6 @@ export const RecommendedProductsPure: React.FC = ( [ locale, accessToken, - cartIdLocalStorageKey, labels.addToCartSuccess, labels.addToCartError, labels.viewCartLabel, diff --git a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.server.tsx b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.server.tsx index 9224df03f..8612832df 100644 --- a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.server.tsx +++ b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.server.tsx @@ -39,7 +39,6 @@ export const RecommendedProducts: React.FC = async ({ accessToken={accessToken} locale={locale} routing={routing} - cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY!} /> ); }; diff --git a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.types.ts b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.types.ts index fd518f16f..69c17f8e3 100644 --- a/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.types.ts +++ b/packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.types.ts @@ -9,8 +9,7 @@ export interface RecommendedProductsProps extends Models.BlockProps.BaseBlockPro limit?: number; } -export type RecommendedProductsPureProps = RecommendedProductsProps & - Model.RecommendedProductsBlock & { cartIdLocalStorageKey: string }; +export type RecommendedProductsPureProps = RecommendedProductsProps & Model.RecommendedProductsBlock; export type RecommendedProductsRendererProps = Omit< Models.BlockProps.BlockWithSlugProps>, diff --git a/packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx b/packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx index adaa359c1..f1de1493d 100644 --- a/packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx +++ b/packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx @@ -1,7 +1,9 @@ 'use client'; import { CMS } from '@o2s/configs.integrations'; -import React, { ReactNode, createContext, useContext, useState } from 'react'; +import React, { ReactNode, createContext, useContext, useEffect, useState } from 'react'; + +import { Utils } from '@o2s/utils.frontend'; import { PriceService, usePriceService } from '@o2s/ui/components/Products/Price'; @@ -23,6 +25,10 @@ export interface GlobalProviderProps { locale: string; themes: CMS.Model.AppConfig.Themes; currentTheme?: string; + user?: { + orgId?: string; + }; + cartStorageKey?: string; children: ReactNode; } @@ -48,15 +54,38 @@ export interface GlobalContextType { available: CMS.Model.AppConfig.Themes; current?: string; }; + user?: { + orgId?: string; + }; } export const GlobalContext = createContext({} as GlobalContextType); -export const GlobalProvider = ({ config, labels, locale, themes, currentTheme, children }: GlobalProviderProps) => { +export const GlobalProvider = ({ + config, + labels, + locale, + themes, + currentTheme, + user, + cartStorageKey, + children, +}: GlobalProviderProps) => { const priceService = usePriceService(locale); const [isSpinnerVisible, setIsSpinnerVisible] = useState(false); + useEffect(() => { + Utils.CartStorage.configureCartStorage({ storageKey: cartStorageKey, orgId: user?.orgId }); + + if (user?.orgId) { + sessionStorage.setItem('wasAuthenticated', '1'); + } else if (sessionStorage.getItem('wasAuthenticated')) { + sessionStorage.removeItem('wasAuthenticated'); + Utils.CartStorage.removeAllCartIds(); + } + }, [cartStorageKey, user?.orgId]); + return ( {children} diff --git a/packages/utils/frontend/src/utils/cart-storage.ts b/packages/utils/frontend/src/utils/cart-storage.ts new file mode 100644 index 000000000..f80eea5ea --- /dev/null +++ b/packages/utils/frontend/src/utils/cart-storage.ts @@ -0,0 +1,85 @@ +const DEFAULT_KEY = '__default__'; + +let _storageKey = 'cartId'; +let _currentOrgId: string | undefined; + +interface CartStorageConfig { + storageKey?: string; + orgId?: string; +} + +interface CartStorageMap { + [orgId: string]: string; +} + +/** + * Configure the cart storage utility. Call once at app initialization. + * + * @param config.storageKey - localStorage key name (defaults to 'cartId') + * @param config.orgId - current organization ID (uses '__default__' when omitted) + */ +export function configureCartStorage(config: CartStorageConfig): void { + if (config.storageKey) { + _storageKey = config.storageKey; + } + _currentOrgId = config.orgId; +} + +/** + * Get the cart ID for the current (or specified) organization. + */ +export function getCartId(orgId?: string): string | null { + const raw = localStorage.getItem(_storageKey); + if (!raw) return null; + + try { + const map: CartStorageMap = JSON.parse(raw); + const key = orgId ?? _currentOrgId ?? DEFAULT_KEY; + return map[key] ?? null; + } catch { + return null; + } +} + +/** + * Set the cart ID for the current (or specified) organization. + */ +export function setCartId(cartId: string, orgId?: string): void { + const key = orgId ?? _currentOrgId ?? DEFAULT_KEY; + const map = readMap(); + map[key] = cartId; + localStorage.setItem(_storageKey, JSON.stringify(map)); +} + +/** + * Remove the cart ID for the current (or specified) organization. + */ +export function removeCartId(orgId?: string): void { + const key = orgId ?? _currentOrgId ?? DEFAULT_KEY; + const map = readMap(); + delete map[key]; + + if (Object.keys(map).length === 0) { + localStorage.removeItem(_storageKey); + } else { + localStorage.setItem(_storageKey, JSON.stringify(map)); + } +} + +/** + * Remove all cart IDs (all organizations). Use on logout. + */ +export function removeAllCartIds(): void { + localStorage.removeItem(_storageKey); +} + +function readMap(): CartStorageMap { + const raw = localStorage.getItem(_storageKey); + if (!raw) return {}; + + try { + return JSON.parse(raw); + } catch { + return {}; + } +} diff --git a/packages/utils/frontend/src/utils/index.ts b/packages/utils/frontend/src/utils/index.ts index ce1ba623a..975776c0b 100644 --- a/packages/utils/frontend/src/utils/index.ts +++ b/packages/utils/frontend/src/utils/index.ts @@ -1,3 +1,4 @@ +export * as CartStorage from './cart-storage'; export * as DownloadFile from './download-file'; export * as FormatAddress from './format-address'; export * as FormatCountry from './format-country'; From 55618ce2236509c9da824b52117e3feb470b2cdd Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 27 Mar 2026 12:04:48 +0100 Subject: [PATCH 2/5] chore: added changeset --- .changeset/clever-ravens-follow.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/clever-ravens-follow.md diff --git a/.changeset/clever-ravens-follow.md b/.changeset/clever-ravens-follow.md new file mode 100644 index 000000000..17654dc53 --- /dev/null +++ b/.changeset/clever-ravens-follow.md @@ -0,0 +1,15 @@ +--- +'@o2s/blocks.checkout-shipping-address': minor +'@o2s/blocks.checkout-billing-payment': minor +'@o2s/blocks.checkout-company-data': minor +'@o2s/blocks.recommended-products': minor +'@o2s/blocks.checkout-summary': minor +'@o2s/blocks.product-details': minor +'@o2s/blocks.product-list': minor +'@o2s/blocks.cart': minor +'@o2s/utils.frontend': minor +'@o2s/frontend': minor +'@o2s/ui': minor +--- + +Add CartStorage utility for org-scoped cart management in localStorage. Replace direct localStorage calls and cartIdLocalStorageKey prop with centralized Utils.CartStorage across all blocks and app components. From ad802e5db372c372528a01ecbaf8ceaab52bed9a Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 27 Mar 2026 12:16:24 +0100 Subject: [PATCH 3/5] refactor: update default storage key for cart from '__default__' to 'guest' --- packages/utils/frontend/src/utils/cart-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/frontend/src/utils/cart-storage.ts b/packages/utils/frontend/src/utils/cart-storage.ts index f80eea5ea..1a8d168d8 100644 --- a/packages/utils/frontend/src/utils/cart-storage.ts +++ b/packages/utils/frontend/src/utils/cart-storage.ts @@ -1,4 +1,4 @@ -const DEFAULT_KEY = '__default__'; +const DEFAULT_KEY = 'guest'; let _storageKey = 'cartId'; let _currentOrgId: string | undefined; From 3f41685d8a6258604f68ca4684bad1ec8abc672b Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 27 Mar 2026 13:26:26 +0100 Subject: [PATCH 4/5] refactor: update default organization ID for cart from '__default__' to 'guest' --- packages/utils/frontend/src/utils/cart-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/frontend/src/utils/cart-storage.ts b/packages/utils/frontend/src/utils/cart-storage.ts index 1a8d168d8..0439d82d9 100644 --- a/packages/utils/frontend/src/utils/cart-storage.ts +++ b/packages/utils/frontend/src/utils/cart-storage.ts @@ -16,7 +16,7 @@ interface CartStorageMap { * Configure the cart storage utility. Call once at app initialization. * * @param config.storageKey - localStorage key name (defaults to 'cartId') - * @param config.orgId - current organization ID (uses '__default__' when omitted) + * @param config.orgId - current organization ID (uses 'guest' key when omitted) */ export function configureCartStorage(config: CartStorageConfig): void { if (config.storageKey) { From 97f86f3e2ee0635e61d116710eb43247f89050fe Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 30 Mar 2026 10:34:59 +0200 Subject: [PATCH 5/5] refactor: update GlobalProvider in NotFound component with user and cartStorageKey props --- apps/frontend/src/app/[locale]/not-found.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/app/[locale]/not-found.tsx b/apps/frontend/src/app/[locale]/not-found.tsx index 8cd0aee9d..0176b6c7c 100644 --- a/apps/frontend/src/app/[locale]/not-found.tsx +++ b/apps/frontend/src/app/[locale]/not-found.tsx @@ -65,7 +65,14 @@ export default async function NotFound() { return ( - +