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.
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..0176b6c7c 100644
--- a/apps/frontend/src/app/[locale]/not-found.tsx
+++ b/apps/frontend/src/app/[locale]/not-found.tsx
@@ -65,9 +65,16 @@ export default async function NotFound() {
return (
-
+
-
+
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..0439d82d9
--- /dev/null
+++ b/packages/utils/frontend/src/utils/cart-storage.ts
@@ -0,0 +1,85 @@
+const DEFAULT_KEY = 'guest';
+
+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 'guest' key 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';