Skip to content

Feat/handling cart per organisation#840

Open
lukasz-hycom wants to merge 4 commits intomainfrom
feat/handling-cart-per-organisation
Open

Feat/handling cart per organisation#840
lukasz-hycom wants to merge 4 commits intomainfrom
feat/handling-cart-per-organisation

Conversation

@lukasz-hycom
Copy link
Copy Markdown
Contributor

@lukasz-hycom lukasz-hycom commented Mar 27, 2026

Key Changes

  • Added CartStorage utility in @o2s/utils.frontend - centralizes all cart localStorage operations (getCartId, setCartId, removeCartId, removeAllCartIds)
  • Stored cart IDs as JSON map keyed by organization ID, enabling separate carts per org
  • Extended GlobalProvider with user and cartStorageKey props — configures cart storage context on mount and clears cart data on logout
  • Migrated all blocks (8 blocks, ~32 files) and Header/CartInfo from cartIdLocalStorageKey prop + direct localStorage calls to Utils.CartStorage
  • Removed cartIdLocalStorageKey prop from types, server components, client components, and stories across the codebase

Summary by CodeRabbit

  • New Features

    • Centralized CartStorage for persisting cart IDs per organization in localStorage.
  • Refactor

    • Removed legacy cart-storage props across checkout and product components; components now use the centralized cart storage.
    • Global context updated to configure and initialize organization-aware cart storage and session-linked cart cleanup.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
o2s-docs Skipped Skipped Mar 27, 2026 0:26am

Request Review

@lukasz-hycom lukasz-hycom self-assigned this Mar 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ed80c75-4373-4bd9-9be2-3b4451042c08

📥 Commits

Reviewing files that changed from the base of the PR and between ad802e5 and 3f41685.

📒 Files selected for processing (1)
  • packages/utils/frontend/src/utils/cart-storage.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/utils/frontend/src/utils/cart-storage.ts

Walkthrough

Introduces a centralized Utils.CartStorage utility and migrates cart ID persistence from ad-hoc localStorage + cartIdLocalStorageKey props to CartStorage across checkout/product blocks, Header components, Storybook, and GlobalProvider integration.

Changes

Cohort / File(s) Summary
CartStorage Utility
packages/utils/frontend/src/utils/cart-storage.ts, packages/utils/frontend/src/utils/index.ts
Adds new CartStorage API: configureCartStorage, getCartId, setCartId, removeCartId, removeAllCartIds; re-exported under Utils. Stores org-scoped cart IDs as JSON in localStorage.
GlobalProvider Integration
packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx
Adds user (with orgId) and cartStorageKey props; calls Utils.CartStorage.configureCartStorage(...); tracks auth state and clears cart IDs on logout.
Header / CartInfo
apps/frontend/src/containers/Header/Header.tsx, apps/frontend/src/containers/Header/Header.types.ts, apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx, apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts
Removes cartIdLocalStorageKey prop from Header and CartInfo; CartInfo now uses Utils.CartStorage.getCartId() and updated effect dependencies.
Frontend App Pages
apps/frontend/src/app/[locale]/(auth)/login/page.tsx, apps/frontend/src/app/[locale]/[[...slug]]/page.tsx, apps/frontend/src/app/[locale]/not-found.tsx
Removed cartIdLocalStorageKey from Header calls; [[...slug]]/page.tsx now passes user and cartStorageKey into GlobalProvider.
Checkout Blocks
packages/blocks/checkout/*/src/frontend/* (cart, checkout-billing-payment, checkout-company-data, checkout-shipping-address, checkout-summary: .client.tsx, .server.tsx, .types.ts, .client.stories.tsx)
Removed cartIdLocalStorageKey from props/types/stories; replaced direct localStorage reads/writes with Utils.CartStorage.getCartId()/setCartId()/removeCartId(); server components no longer pass env-derived storage key.
Product Blocks
packages/blocks/products/*/src/frontend/* (product-details, product-list, recommended-products: .client.tsx, .server.tsx, .types.ts, .client.stories.tsx)
Removed cartIdLocalStorageKey from props/types/stories; add-to-cart flows now use Utils.CartStorage.getCartId() and setCartId() instead of reading/writing localStorage directly.
Storybook & Release
.storybook/preview.tsx, .changeset/clever-ravens-follow.md
Storybook decorators updated to use Utils.CartStorage.setCartId(); new changeset added documenting CartStorage rollout and prop removals.

Sequence Diagram(s)

sequenceDiagram
  participant GP as GlobalProvider
  participant Comp as UI Component (Cart/Product/Checkout)
  participant Utils as Utils.CartStorage
  participant LS as localStorage

  GP->>Utils: configureCartStorage({storageKey, orgId}) 
  Note right of Utils: stores config in module scope

  Comp->>Utils: getCartId()
  Utils->>LS: read(storageKey) rgba(200,200,255,0.5)
  LS-->>Utils: JSON map
  Utils-->>Comp: cartId | null

  Comp->>Utils: setCartId(cartId)
  Utils->>LS: write(updated JSON map) rgba(200,200,255,0.5)
  LS-->>Utils: ack
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • marcinkrasowski

Poem

🐰 Hopping through code with a carrot and key,

Cart IDs tucked by org, neat as can be.
No more props scattered, no random local writes,
CartStorage hums softly through days and nights. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers key changes and technical implementation details, but omits required sections from the template: Related Ticket(s), How to test, and Media. The description lacks testing instructions and ticket references, which are important for code review and tracking. Add Related Ticket(s), How to test section with detailed steps, and Media (if applicable) to match the repository's PR template structure.
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat/handling cart per organisation' directly addresses the main objective of the PR, which is to implement organization-scoped cart handling. It accurately reflects the primary change from single shared cart to per-organization carts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/handling-cart-per-organisation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Coverage Report for packages/configs/vitest-config

Status Category Percentage Covered / Total
🔵 Lines 78.5% 1734 / 2209
🔵 Statements 77.45% 1824 / 2355
🔵 Functions 74.75% 527 / 705
🔵 Branches 65.93% 1140 / 1729
File CoverageNo changed files found.
Generated in workflow #588 for commit 3f41685 by the Vitest Coverage Report Action

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx (1)

81-86: Session storage key is hardcoded — consider extracting as a constant.

The 'wasAuthenticated' key is used in two places within this effect. For maintainability and to avoid typos, consider extracting it to a named constant.

♻️ Suggested refactor
+'use client';
+
+const WAS_AUTHENTICATED_KEY = 'wasAuthenticated';
+
 // ... existing code ...

 useEffect(() => {
     Utils.CartStorage.configureCartStorage({ storageKey: cartStorageKey, orgId: user?.orgId });

     if (user?.orgId) {
-        sessionStorage.setItem('wasAuthenticated', '1');
-    } else if (sessionStorage.getItem('wasAuthenticated')) {
-        sessionStorage.removeItem('wasAuthenticated');
+        sessionStorage.setItem(WAS_AUTHENTICATED_KEY, '1');
+    } else if (sessionStorage.getItem(WAS_AUTHENTICATED_KEY)) {
+        sessionStorage.removeItem(WAS_AUTHENTICATED_KEY);
         Utils.CartStorage.removeAllCartIds();
     }
 }, [cartStorageKey, user?.orgId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx` around lines 81
- 86, Extract the hardcoded sessionStorage key 'wasAuthenticated' into a
descriptive constant (e.g., WAS_AUTHENTICATED_KEY) and replace both occurrences
in GlobalProvider's effect with that constant; update any imports/exports as
appropriate so the constant is reusable within GlobalProvider (or nearby
constants file) to prevent typos and improve maintainability when
reading/modifying the logic that sets/removes the key and calls
Utils.CartStorage.removeAllCartIds().
packages/utils/frontend/src/utils/cart-storage.ts (1)

31-42: Consider extracting the key resolution logic to reduce duplication.

The pattern orgId ?? _currentOrgId ?? DEFAULT_KEY is repeated in getCartId, setCartId, and removeCartId. Extracting this to a helper function would improve maintainability and ensure consistent behavior.

♻️ Suggested refactor
+function resolveOrgKey(orgId?: string): string {
+    return orgId ?? _currentOrgId ?? DEFAULT_KEY;
+}
+
 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;
+        const key = resolveOrgKey(orgId);
         return map[key] ?? null;
     } catch {
         return null;
     }
 }

 export function setCartId(cartId: string, orgId?: string): void {
-    const key = orgId ?? _currentOrgId ?? DEFAULT_KEY;
+    const key = resolveOrgKey(orgId);
     const map = readMap();
     map[key] = cartId;
     localStorage.setItem(_storageKey, JSON.stringify(map));
 }

 export function removeCartId(orgId?: string): void {
-    const key = orgId ?? _currentOrgId ?? DEFAULT_KEY;
+    const key = resolveOrgKey(orgId);
     const map = readMap();
     delete map[key];
     // ...
 }

Also applies to: 47-52, 57-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/utils/frontend/src/utils/cart-storage.ts` around lines 31 - 42,
Extract the repeated key-resolution logic (orgId ?? _currentOrgId ??
DEFAULT_KEY) into a small helper like resolveCartKey(orgId?: string): string
that returns the resolved storage key, then replace the inline expression in
getCartId, setCartId, and removeCartId with calls to resolveCartKey so all three
functions use the single helper; ensure the helper uses the same DEFAULT_KEY and
_currentOrgId symbols and keeps types compatible with CartStorageMap lookups and
assignments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/frontend/src/app/`[locale]/not-found.tsx:
- 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.

In
`@packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx`:
- Line 47: CartStorage is never initialized so Utils.CartStorage.getCartId() and
removeCartId() in CheckoutSummary.client.tsx always fall back to the guest key;
to fix, either ensure configureCartStorage(...) runs before any CartStorage
usage (e.g., call configureCartStorage(orgId) from the app root/layout or
instantiate GlobalProvider early) or change the calls in
CheckoutSummary.client.tsx to pass the orgId explicitly (use
Utils.CartStorage.getCartId(orgId) and Utils.CartStorage.removeCartId(orgId));
locate the usages in CheckoutSummary.client.tsx (the getCartId and removeCartId
calls) and update them to one of these two approaches so _currentOrgId is set
and org-scoped carts are preserved.

In `@packages/utils/frontend/src/utils/cart-storage.ts`:
- Around line 15-19: The JSDoc for configureCartStorage incorrectly says the
orgId fallback is '__default__' while the code uses the DEFAULT_KEY constant set
to 'guest'; make them consistent by updating the JSDoc to state that the default
orgId is 'guest' (or, if intended behavior was '__default__', change DEFAULT_KEY
to '__default__' and update any references). Ensure the comment for the
config.orgId parameter and any examples reference the same value as the
DEFAULT_KEY constant.

---

Nitpick comments:
In `@packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx`:
- Around line 81-86: Extract the hardcoded sessionStorage key 'wasAuthenticated'
into a descriptive constant (e.g., WAS_AUTHENTICATED_KEY) and replace both
occurrences in GlobalProvider's effect with that constant; update any
imports/exports as appropriate so the constant is reusable within GlobalProvider
(or nearby constants file) to prevent typos and improve maintainability when
reading/modifying the logic that sets/removes the key and calls
Utils.CartStorage.removeAllCartIds().

In `@packages/utils/frontend/src/utils/cart-storage.ts`:
- Around line 31-42: Extract the repeated key-resolution logic (orgId ??
_currentOrgId ?? DEFAULT_KEY) into a small helper like resolveCartKey(orgId?:
string): string that returns the resolved storage key, then replace the inline
expression in getCartId, setCartId, and removeCartId with calls to
resolveCartKey so all three functions use the single helper; ensure the helper
uses the same DEFAULT_KEY and _currentOrgId symbols and keeps types compatible
with CartStorageMap lookups and assignments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72e82aff-03bc-4c45-b4c8-81c973d476c0

📥 Commits

Reviewing files that changed from the base of the PR and between 0b23817 and ad802e5.

📒 Files selected for processing (44)
  • .changeset/clever-ravens-follow.md
  • .storybook/preview.tsx
  • apps/frontend/src/app/[locale]/(auth)/login/page.tsx
  • apps/frontend/src/app/[locale]/[[...slug]]/page.tsx
  • apps/frontend/src/app/[locale]/not-found.tsx
  • apps/frontend/src/containers/Header/CartInfo/CartInfo.tsx
  • apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts
  • apps/frontend/src/containers/Header/Header.tsx
  • apps/frontend/src/containers/Header/Header.types.ts
  • packages/blocks/checkout/cart/src/frontend/Cart.client.stories.tsx
  • packages/blocks/checkout/cart/src/frontend/Cart.client.tsx
  • packages/blocks/checkout/cart/src/frontend/Cart.server.tsx
  • packages/blocks/checkout/cart/src/frontend/Cart.types.ts
  • packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.stories.tsx
  • packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.tsx
  • packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.server.tsx
  • packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.types.ts
  • packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.stories.tsx
  • packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.tsx
  • packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.server.tsx
  • packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.types.ts
  • packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.stories.tsx
  • packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.tsx
  • packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.server.tsx
  • packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.types.ts
  • packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.stories.tsx
  • packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx
  • packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.server.tsx
  • packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts
  • packages/blocks/products/product-details/src/frontend/ProductDetails.client.stories.tsx
  • packages/blocks/products/product-details/src/frontend/ProductDetails.client.tsx
  • packages/blocks/products/product-details/src/frontend/ProductDetails.server.tsx
  • packages/blocks/products/product-details/src/frontend/ProductDetails.types.ts
  • packages/blocks/products/product-list/src/frontend/ProductList.client.stories.tsx
  • packages/blocks/products/product-list/src/frontend/ProductList.client.tsx
  • packages/blocks/products/product-list/src/frontend/ProductList.server.tsx
  • packages/blocks/products/product-list/src/frontend/ProductList.types.ts
  • packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.stories.tsx
  • packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.tsx
  • packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.server.tsx
  • packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.types.ts
  • packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx
  • packages/utils/frontend/src/utils/cart-storage.ts
  • packages/utils/frontend/src/utils/index.ts
💤 Files with no reviewable changes (12)
  • apps/frontend/src/containers/Header/Header.types.ts
  • packages/blocks/checkout/checkout-billing-payment/src/frontend/CheckoutBillingPayment.client.stories.tsx
  • packages/blocks/checkout/cart/src/frontend/Cart.server.tsx
  • packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.stories.tsx
  • apps/frontend/src/containers/Header/CartInfo/CartInfo.types.ts
  • packages/blocks/checkout/checkout-shipping-address/src/frontend/CheckoutShippingAddress.client.stories.tsx
  • packages/blocks/checkout/checkout-company-data/src/frontend/CheckoutCompanyData.client.stories.tsx
  • packages/blocks/products/product-details/src/frontend/ProductDetails.client.stories.tsx
  • packages/blocks/products/product-details/src/frontend/ProductDetails.server.tsx
  • packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.server.tsx
  • packages/blocks/products/product-list/src/frontend/ProductList.client.stories.tsx
  • packages/blocks/products/recommended-products/src/frontend/RecommendedProducts.client.stories.tsx

<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.


useEffect(() => {
const cartId = localStorage.getItem(cartIdLocalStorageKey);
const cartId = Utils.CartStorage.getCartId();
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verify CartStorage configuration timing (GlobalProvider):"
rg -n --type=tsx -C4 'useEffect\(\(\) => \{' packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx
rg -n --type=tsx -C4 'CartStorage\.configureCartStorage\(' packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx

echo
echo "Verify CheckoutSummary cart access points:"
rg -n --type=tsx -C3 'CartStorage\.getCartId\(|CartStorage\.removeCartId\(' \
  packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx

echo
echo "Expected verification result:"
echo "- configureCartStorage is called in an effect (post-mount)"
echo "- CheckoutSummary reads cartId in its own effect/handlers"
echo "If confirmed, initialize CartStorage before descendant effects (or pass explicit orgId to CartStorage calls)."

Repository: o2sdev/openselfservice

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Searching GlobalProvider for CartStorage configuration ==="
find . -name "GlobalProvider.tsx" -type f 2>/dev/null | head -5

echo
echo "=== Searching CheckoutSummary.client.tsx for CartStorage usage ==="
find . -name "CheckoutSummary.client.tsx" -type f 2>/dev/null | head -5

echo
echo "=== Looking for CartStorage configuration in GlobalProvider ==="
rg -n 'configureCartStorage|CartStorage' packages/ui/src/providers/GlobalProvider/ 2>/dev/null || echo "Path not found, searching entire repo..."
rg -n 'configureCartStorage|CartStorage' --type ts --type tsx 2>/dev/null | grep -i globalprovider | head -20

echo
echo "=== Looking for CartStorage usage in CheckoutSummary ==="
rg -n 'CartStorage\.(getCartId|removeCartId)' packages/blocks/checkout/checkout-summary/src/frontend/ 2>/dev/null || echo "Pattern search in path failed, trying broader search..."
rg -n 'CartStorage' packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx 2>/dev/null || echo "File not found"

Repository: o2sdev/openselfservice

Length of output: 683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== GlobalProvider.tsx context around line 79 (CartStorage config) ==="
sed -n '70,95p' packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx

echo
echo "=== CheckoutSummary.client.tsx full content (check lines 47, 72, 86) ==="
wc -l packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx
cat -n packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx

Repository: o2sdev/openselfservice

Length of output: 22050


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Looking for CartStorage implementation ==="
find . -name "*CartStorage*" -type f 2>/dev/null | grep -v node_modules | head -10

echo
echo "=== Searching for CartStorage class/object definition ==="
rg -n "CartStorage\s*[=:]|class CartStorage|export.*CartStorage" --type ts --type tsx --max-count 20 2>/dev/null | head -30

Repository: o2sdev/openselfservice

Length of output: 112


🏁 Script executed:

#!/bin/bash
set -euo pipeflag

echo "=== Searching for where CheckoutSummary is imported/used ==="
rg -n "CheckoutSummary|GlobalProvider" --type ts --type tsx packages/ 2>/dev/null | grep -E "import|export|Provider" | head -30

echo
echo "=== Looking for main layout or root setup files ==="
find packages -name "layout.tsx" -o -name "RootLayout.tsx" -o -name "page.tsx" -o -name "*Layout*" -type f 2>/dev/null | head -15

echo
echo "=== Checking if CheckoutSummary is wrapped by GlobalProvider ==="
rg -B5 "CheckoutSummary" packages/blocks/checkout/checkout-summary/ --type ts --type tsx 2>/dev/null | head -40

Repository: o2sdev/openselfservice

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Searching for where CheckoutSummary is imported/used ==="
rg -n "CheckoutSummary|GlobalProvider" --type ts --type tsx packages/ 2>/dev/null | grep -E "import|export|Provider" | head -30

echo
echo "=== Looking for main layout or root setup files ==="
find packages -name "layout.tsx" -o -name "RootLayout.tsx" -o -name "page.tsx" -o -name "*Layout*" -type f 2>/dev/null | head -15

echo
echo "=== Checking if CheckoutSummary is wrapped by GlobalProvider ==="
rg -B5 "CheckoutSummary" packages/blocks/checkout/checkout-summary/ --type ts --type tsx 2>/dev/null | head -40

Repository: o2sdev/openselfservice

Length of output: 126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find files that import CheckoutSummary (all file types) ==="
rg "import.*CheckoutSummary|from.*CheckoutSummary" --max-count 20 2>/dev/null | head -20

echo
echo "=== Look at the checkout-summary package structure ==="
find packages/blocks/checkout/checkout-summary -type f -name "*.tsx" -o -name "*.ts" -o -name "package.json" 2>/dev/null | sort

echo
echo "=== Check where CheckoutSummaryPure is exported ==="
rg "export.*CheckoutSummary" packages/blocks/checkout/checkout-summary/ 2>/dev/null

Repository: o2sdev/openselfservice

Length of output: 8008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== CheckoutSummary.server.tsx content ==="
cat -n packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.server.tsx

echo
echo "=== CheckoutSummary.renderer.tsx content ==="
cat -n packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.renderer.tsx

echo
echo "=== Search for GlobalProvider usage around checkout pages ==="
rg -n "GlobalProvider" packages/ 2>/dev/null | head -15

Repository: o2sdev/openselfservice

Length of output: 4489


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for app layout or root layout that uses GlobalProvider ==="
find . -path "*/node_modules" -prune -o -name "layout.tsx" -type f -print 2>/dev/null | head -10

echo
echo "=== Check app or root layout content for GlobalProvider ==="
rg -l "GlobalProvider" . --type ts --type tsx 2>/dev/null | grep -E "layout|app|root" | head -5

echo
echo "=== Look for any GlobalProvider usage in actual app setup ==="
rg -B3 -A3 "GlobalProvider" packages/*/src --type tsx --type ts 2>/dev/null | grep -v ".stories\|README" | head -50

Repository: o2sdev/openselfservice

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Root layout ==="
cat -n ./apps/frontend/src/app/layout.tsx

echo
echo "=== Locale layout ==="
cat -n ./apps/frontend/src/app/[locale]/layout.tsx

Repository: o2sdev/openselfservice

Length of output: 3197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for where GlobalProvider is actually instantiated in the app ==="
rg -n "GlobalProvider" apps/ --type ts --type tsx 2>/dev/null

echo
echo "=== Check if there are other layout files or wrappers ==="
find apps/frontend/src -name "*.tsx" -o -name "*.ts" | xargs grep -l "GlobalProvider" 2>/dev/null

echo
echo "=== Look at the full GlobalProvider implementation again ==="
sed -n '64,100p' packages/ui/src/providers/GlobalProvider/GlobalProvider.tsx

Repository: o2sdev/openselfservice

Length of output: 141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for ALL CartStorage calls in the codebase ==="
rg -n "CartStorage\." packages/ --type ts --type tsx 2>/dev/null | grep -v ".stories\|node_modules" | head -30

echo
echo "=== Search for configureCartStorage calls specifically ==="
rg -n "configureCartStorage" . --type ts --type tsx 2>/dev/null | grep -v node_modules

echo
echo "=== Check CheckoutSummary props and where user/config comes from ==="
cat -n packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts

Repository: o2sdev/openselfservice

Length of output: 122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for CartStorage in all files ==="
rg "CartStorage" . 2>/dev/null | grep -v "node_modules" | head -40

echo
echo "=== Check CheckoutSummary types file ==="
cat -n packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.types.ts

echo
echo "=== Look for user/orgId context in CheckoutSummary ==="
rg -n "user\|orgId\|org" packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx | head -20

Repository: o2sdev/openselfservice

Length of output: 4438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Read cart-storage.ts implementation ==="
cat -n packages/utils/frontend/src/utils/cart-storage.ts

echo
echo "=== Check if there's initialization elsewhere in product blocks ==="
rg -B5 "configureCartStorage" packages/blocks/ --type ts --type tsx 2>/dev/null

Repository: o2sdev/openselfservice

Length of output: 2906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Final verification: All layout files in apps/frontend ==="
find apps/frontend/src -name "layout.tsx" -o -name "layout.ts" 2>/dev/null | xargs cat -n 2>/dev/null

echo
echo "=== Check if GlobalProvider is used anywhere in CheckoutSummary flow ==="
rg -n "GlobalProvider\|useGlobalContext" packages/blocks/checkout/ --type ts --type tsx 2>/dev/null

echo
echo "=== Where is user/orgId coming from in CheckoutSummary? ==="
grep -r "CheckoutSummaryPure\|CheckoutSummary" apps/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -10

Repository: o2sdev/openselfservice

Length of output: 3290


CartStorage is never initialized, causing all cart operations to use the fallback 'guest' org bucket.

CartStorage.getCartId() and CartStorage.removeCartId() at lines 47, 72, and 86 will always default to the 'guest' organization key because configureCartStorage() is never called in the application. GlobalProvider (which calls configureCartStorage) is not instantiated in the app layout, so _currentOrgId remains undefined and falls back to DEFAULT_KEY. This causes org-scoped carts to be lost/inaccessible.

Either initialize CartStorage before the app renders (add to root layout), or pass orgId explicitly to CartStorage methods: CartStorage.getCartId(orgId) and CartStorage.removeCartId(orgId).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx`
at line 47, CartStorage is never initialized so Utils.CartStorage.getCartId()
and removeCartId() in CheckoutSummary.client.tsx always fall back to the guest
key; to fix, either ensure configureCartStorage(...) runs before any CartStorage
usage (e.g., call configureCartStorage(orgId) from the app root/layout or
instantiate GlobalProvider early) or change the calls in
CheckoutSummary.client.tsx to pass the orgId explicitly (use
Utils.CartStorage.getCartId(orgId) and Utils.CartStorage.removeCartId(orgId));
locate the usages in CheckoutSummary.client.tsx (the getCartId and removeCartId
calls) and update them to one of these two approaches so _currentOrgId is set
and org-scoped carts are preserved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

GlobalProvider (which wraps all pages) calls configureCartStorage() in a useEffect on mount — this runs before any child block component mounts. _currentOrgId is already set by the time getCartId()/removeCartId() are called.

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.

@lukasz-hycom, thank you for the clarification. There is an important nuance here worth flagging, though.

React fires useEffect hooks children-first, parents-last. This is well-established React behavior:

When a Parent and Child both mount in the same render, the order is: Child committed effect fires before Parent committed effect.

Effects are a different story from rendering. React effectively queues effects from the bottom of the tree upward, so Child's effects are called before Parent's.

This means that if GlobalProvider and CheckoutSummary mount in the same render cycle (e.g., a user lands directly on the checkout summary page for the first time), CheckoutSummaryPure's useEffect (which calls getCartId()) would fire before GlobalProvider's useEffect (which calls configureCartStorage()). At that point, _currentOrgId would not yet be set and the call would fall back to 'guest'.

Where your reasoning holds: If GlobalProvider lives in a persistent Next.js layout (one that stays mounted across client-side navigations), then on any navigation to the checkout page after the initial app load, GlobalProvider is already mounted and configureCartStorage() has already run. In that case, the ordering is fine.

The risk: If a user deep-links directly to the checkout summary URL (e.g., after a redirect from a payment provider), GlobalProvider and CheckoutSummary mount together in the same render, and the child's effect fires first — meaning getCartId() is called before configureCartStorage() sets _currentOrgId. For an authenticated user with an orgId, this would look up the cart under the 'guest' key and return null, triggering the "cart not found" toast and redirect.

Could you confirm whether GlobalProvider is placed in a persistent layout (e.g., apps/frontend/src/app/[locale]/layout.tsx) or co-rendered within the same page component as CheckoutSummary? That distinction determines whether this is a real risk.


✏️ Learnings added
Learnt from: lukasz-hycom
Repo: o2sdev/openselfservice PR: 840
File: packages/blocks/checkout/checkout-summary/src/frontend/CheckoutSummary.client.tsx:47-47
Timestamp: 2026-03-27T12:52:02.496Z
Learning: In the o2sdev/openselfservice repository, React's useEffect fires children-first, parents-last. GlobalProvider calls configureCartStorage() in a useEffect to set _currentOrgId for CartStorage. If GlobalProvider is co-rendered on the same page as a block like CheckoutSummary (not in a persistent Next.js layout), CheckoutSummary's useEffect (calling Utils.CartStorage.getCartId()) fires before GlobalProvider's configureCartStorage(), causing _currentOrgId to be unset and falling back to the 'guest' key on initial page load.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant