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
15 changes: 15 additions & 0 deletions .changeset/clever-ravens-follow.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({});
Expand Down Expand Up @@ -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 <Story />;
},
Expand Down
6 changes: 1 addition & 5 deletions apps/frontend/src/app/[locale]/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,7 @@ export default async function LoginPage({ params }: Readonly<Props>) {
<body>
<GlobalProvider config={init} labels={init.labels} locale={locale} themes={init.themes}>
<div className="flex flex-col min-h-dvh">
<Header
data={init.common.header}
shouldIncludeSignInButton={false}
cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY}
/>
<Header data={init.common.header} shouldIncludeSignInButton={false} />
<div className="flex flex-col grow">
<AuthLayout>
<SignInForm
Expand Down
8 changes: 3 additions & 5 deletions apps/frontend/src/app/[locale]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,11 @@ export default async function Page({ params }: Props) {
locale={locale}
themes={init.themes}
currentTheme={meta.theme}
user={{ orgId: session?.user?.customer?.id }}
cartStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY}
>
<div className="flex flex-col min-h-dvh">
<Header
data={init.common.header}
alternativeUrls={data.alternativeUrls}
cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY}
/>
<Header data={init.common.header} alternativeUrls={data.alternativeUrls} />
<div className="flex flex-col grow">
<div className="py-6 px-4 md:px-6 ml-auto mr-auto w-full md:max-w-7xl">
<main className="flex flex-col gap-6 row-start-2 items-center sm:items-start">
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default async function NotFound() {
<body>
<GlobalProvider config={init} labels={init.labels} locale={locale} themes={init.themes}>
<div className="flex flex-col min-h-dvh">
<Header data={init.common.header} cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY} />
<Header data={init.common.header} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cart storage is no longer initialized on this route.

After removing the Header prop at Line 70, this page still mounts GlobalProvider without user/cartStorageKey, so cart reads can fall back to defaults instead of the configured storage key.

💡 Suggested fix
-            <GlobalProvider config={init} labels={init.labels} locale={locale} themes={init.themes}>
+            <GlobalProvider
+                config={init}
+                labels={init.labels}
+                locale={locale}
+                themes={init.themes}
+                user={{ orgId: session?.user?.customer?.id }}
+                cartStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY}
+            >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/app/`[locale]/not-found.tsx at line 70, The not-found route
now mounts GlobalProvider without the configured cart storage props because
Header no longer passes them; update the not-found.tsx to ensure cart storage is
initialized by supplying the same user and cartStorageKey (or equivalent storage
init values) to GlobalProvider (the component instance that wraps the page) or
explicitly initialize cart storage before rendering, referencing the Header
component removal and the GlobalProvider symbol so the provider receives the
correct user/cartStorageKey used elsewhere in the app.

<div className="flex flex-col grow">
<div className="py-6 px-4 md:px-6 ml-auto mr-auto w-full md:max-w-7xl">
<main className="flex flex-col items-center justify-center grow">
Expand Down
8 changes: 5 additions & 3 deletions apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ export interface CartInfoProps {
url: string;
label: string;
};
cartIdLocalStorageKey: string;
}
10 changes: 2 additions & 8 deletions apps/frontend/src/containers/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const Header: React.FC<HeaderProps> = ({
alternativeUrls,
children,
shouldIncludeSignInButton = true,
cartIdLocalStorageKey,
}) => {
const session = useSession();
const isSignedIn = !!session.data?.user;
Expand Down Expand Up @@ -73,13 +72,8 @@ export const Header: React.FC<HeaderProps> = ({
return null;
}

return (
<CartInfo
data={{ url: data.cart.url, label: data.cart.label }}
cartIdLocalStorageKey={cartIdLocalStorageKey!}
/>
);
}, [data.cart, cartIdLocalStorageKey]);
return <CartInfo data={{ url: data.cart.url, label: data.cart.label }} />;
}, [data.cart]);

const LocaleSlot = useMemo(
() => <LocaleSwitcher label={data.languageSwitcherLabel} alternativeUrls={alternativeUrls} />,
Expand Down
1 change: 0 additions & 1 deletion apps/frontend/src/containers/Header/Header.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ export interface HeaderProps {
[key: string]: string;
};
shouldIncludeSignInButton?: boolean;
cartIdLocalStorageKey?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -76,7 +76,6 @@ export const Default: Story = {
id: 'cart-1',
locale: 'en',
routing,
cartIdLocalStorageKey: cartIdLocalStorageKey,
},
};

Expand All @@ -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 <Story />;
},
Expand Down
11 changes: 6 additions & 5 deletions packages/blocks/checkout/cart/src/frontend/Cart.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,7 +26,6 @@ export const CartPure: React.FC<Readonly<CartPureProps>> = ({
locale,
accessToken,
routing,
cartIdLocalStorageKey,
title,
subtitle,
defaultCurrency,
Expand All @@ -43,7 +44,7 @@ export const CartPure: React.FC<Readonly<CartPureProps>> = ({
const [isMutationPending, startMutationTransition] = useTransition();

useEffect(() => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
if (!cartId) return;

startInitialLoadTransition(async () => {
Expand All @@ -54,10 +55,10 @@ export const CartPure: React.FC<Readonly<CartPureProps>> = ({
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 () => {
Expand All @@ -78,7 +79,7 @@ export const CartPure: React.FC<Readonly<CartPureProps>> = ({
};

const removeItem = (itemId: string) => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
if (!cartId) return;

startMutationTransition(async () => {
Expand Down
1 change: 0 additions & 1 deletion packages/blocks/checkout/cart/src/frontend/Cart.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const Cart: React.FC<CartProps> = async ({ id, accessToken, locale, routi
locale={locale}
routing={routing}
hasPriority={hasPriority}
cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY!}
/>
);
};
2 changes: 1 addition & 1 deletion packages/blocks/checkout/cart/src/frontend/Cart.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CartProps, 'locale'> & {
slug: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,5 @@ export const Default: Story = {
id: 'checkout-billing-payment-1',
locale: 'en',
routing,
cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,7 +27,6 @@ export const CheckoutBillingPaymentPure: React.FC<Readonly<CheckoutBillingPaymen
locale,
accessToken,
routing,
cartIdLocalStorageKey,
title,
subtitle,
stepIndicator,
Expand Down Expand Up @@ -59,7 +60,7 @@ export const CheckoutBillingPaymentPure: React.FC<Readonly<CheckoutBillingPaymen
});

useEffect(() => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
if (!cartId) {
toast({ description: errors?.cartNotFound, variant: 'destructive' });
router.replace(cartPath ?? '/');
Expand Down Expand Up @@ -94,14 +95,14 @@ export const CheckoutBillingPaymentPure: React.FC<Readonly<CheckoutBillingPaymen
router.replace(cartPath ?? '/');
}
});
}, [locale, accessToken, cartIdLocalStorageKey, toast, errors?.cartNotFound, router, cartPath]);
}, [locale, accessToken, toast, errors?.cartNotFound, router, cartPath]);

const validationSchema = YupObject().shape({
paymentMethod: fields.paymentMethod.required ? YupString().required(errors.required) : YupString(),
});

const handleSubmit = (values: { paymentMethod: string }) => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
if (!cartId) return;

startSubmitTransition(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ export const CheckoutBillingPayment: React.FC<CheckoutBillingPaymentProps> = asy
}

return (
<CheckoutBillingPaymentDynamic
{...data}
id={id}
accessToken={accessToken}
locale={locale}
routing={routing}
cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY!}
/>
<CheckoutBillingPaymentDynamic {...data} id={id} accessToken={accessToken} locale={locale} routing={routing} />
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export interface CheckoutBillingPaymentProps {
routing: ReturnType<typeof defineRouting>;
}

export type CheckoutBillingPaymentPureProps = CheckoutBillingPaymentProps &
Model.CheckoutBillingPaymentBlock & { cartIdLocalStorageKey: string };
export type CheckoutBillingPaymentPureProps = CheckoutBillingPaymentProps & Model.CheckoutBillingPaymentBlock;

export type CheckoutBillingPaymentRendererProps = Omit<CheckoutBillingPaymentProps, ''> & {
slug: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,5 @@ export const Default: Story = {
id: 'checkout-company-data-1',
locale: 'en',
routing,
cartIdLocalStorageKey: process.env.CART_ID_LOCAL_STORAGE_KEY!,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,7 +32,6 @@ export const CheckoutCompanyDataPure: React.FC<Readonly<CheckoutCompanyDataPureP
locale,
accessToken,
routing,
cartIdLocalStorageKey,
title,
subtitle,
stepIndicator,
Expand Down Expand Up @@ -75,7 +76,7 @@ export const CheckoutCompanyDataPure: React.FC<Readonly<CheckoutCompanyDataPureP
});

useEffect(() => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
if (!cartId) {
toast({ description: errors.cartNotFound, variant: 'destructive' });
router.replace(cartPath);
Expand Down Expand Up @@ -121,10 +122,10 @@ export const CheckoutCompanyDataPure: React.FC<Readonly<CheckoutCompanyDataPureP
router.replace(cartPath);
}
});
}, [locale, accessToken, cartIdLocalStorageKey, toast, errors.cartNotFound, router, cartPath]);
}, [locale, accessToken, toast, errors.cartNotFound, router, cartPath]);

const handleSubmit = (values: typeof initialFormValues) => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
if (!cartId) {
toast({ description: errors.cartNotFound, variant: 'destructive' });
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,5 @@ export const CheckoutCompanyData: React.FC<CheckoutCompanyDataProps> = async ({
return null;
}

return (
<CheckoutCompanyDataDynamic
{...data}
id={id}
accessToken={accessToken}
locale={locale}
routing={routing}
cartIdLocalStorageKey={process.env.CART_ID_LOCAL_STORAGE_KEY!}
/>
);
return <CheckoutCompanyDataDynamic {...data} id={id} accessToken={accessToken} locale={locale} routing={routing} />;
};
Loading
Loading