Skip to content

Feature/medusa integration - product section#662

Merged
marcinkrasowski merged 32 commits intomainfrom
feature/medusa-integration
Feb 17, 2026
Merged

Feature/medusa integration - product section#662
marcinkrasowski merged 32 commits intomainfrom
feature/medusa-integration

Conversation

@michnowak
Copy link
Copy Markdown
Contributor

@michnowak michnowak commented Feb 10, 2026

What does this PR do?

  • My bugfix

Related Ticket(s)

  • Notion Ticket

Key Changes

  • How does the code change address the issue? Describe, at a high level, what was done to affect change.
  • What side effects does this change have? This is the most important question to answer, as it can point out problems where you are making too many changes in one commit or branch. One or two bullet points for related changes may be okay, but five or six are likely indicators of a commit that is doing too many things.

How to test

  • Create a detailed description of what you need to do to set this PR up. ie: Does it need migrations? Do you need to install something?
  • Create a step by step list of what the engineer needs to do to test.

Media (Loom or gif)

  • Insert media here (if applicable)

Summary by CodeRabbit

  • New Features

    • Variant selector on product pages with URL-based navigation and variant-aware links.
  • Updates

    • Product links now include variant context (details URLs).
    • Order subtotal simplified for clearer display.
    • Added one external image domain for remote images.
    • Recommended products limited to ensure six visible items after filtering.
    • New product categories with locale-specific labels.
    • Variant labels and variant data surfaced in product displays.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds end-to-end product variant support (SDK, service, API, mappers, CMS, frontend variant selector and routing), flattens order-list subtotal shape, exposes product detailsUrl, and converts MedusaJS integration to lazy initialization.

Changes

Cohort / File(s) Summary
Next.js config
apps/frontend/next.config.ts
Added new remote image domain pattern for Next.js images.
Order List
packages/blocks/order-list/src/api-harmonization/order-list.mapper.ts, .../order-list.model.ts, .../OrderList.client.stories.tsx
Flattened subtotal shape from { label, value } to a direct value/type and updated story fixtures.
Product Details — API / Service / SDK / Types
packages/blocks/product-details/src/api-harmonization/*, packages/blocks/product-details/src/sdk/product-details.ts, packages/blocks/product-details/src/frontend/ProductDetails.server.tsx, .../ProductDetails.types.ts
Introduced optional variantSlug through request/controller/service/SDK and updated SDK to request /products/{id}/{variantSlug} when present; added variantSlug prop to server/types.
Product Details — Frontend (client)
packages/blocks/product-details/src/frontend/ProductDetails.client.tsx, .../ProductDetails.renderer.tsx
Added variant selection UI, selection helpers, navigation via router.push, and passed variantSlug/productId props into renderer/pure component.
Product List — Links & CMS model
packages/blocks/product-list/src/frontend/ProductList.client.tsx, packages/framework/src/modules/cms/models/blocks/product-list.model.ts
Switched product links to use product.detailsUrl; added detailsUrl to CMS ProductListBlock model.
Framework — Product model
packages/framework/src/modules/products/products.model.ts
Added ProductOptionGroup and ProductVariantOption types; extended Product with optionGroups? and variants?.
Integrations — CMS mappers
packages/integrations/contentful-cms/src/.../cms.product-list.mapper.ts, packages/integrations/strapi-cms/src/.../cms.product-list.mapper.ts
Mappers now include detailsUrl template exposing variant-aware product links.
Integrations — Mocked
packages/integrations/mocked/src/modules/resources/mock/products.mock.ts, .../products/products.mapper.ts, .../products/products.service.ts, .../cms/mappers/blocks/*
Added variants to mocks, mapper/service now accept variantId and apply locale-aware variant overrides; added variantLabel and detailsUrl in mocked CMS mappings.
Integrations — MedusaJS
packages/integrations/medusajs/src/modules/medusajs/medusajs.service.ts, .../products/products.mapper.ts, .../products/products.service.ts, .../products/response.types.ts
Switched MedusaJsService to lazy initialization; replaced raw HTTP with Medusa SDK calls; added slug/variant/SEO mapping, variant resolution, and optional prices on target product.
Recommended Products
packages/blocks/recommended-products/src/api-harmonization/recommended-products.service.ts
Added limit=7 to recommended product fetch to ensure six results after filtering.
Changeset
.changeset/nasty-doors-cut.md
Added changeset and feature notes for Medusa integration and related bumps.
Tests
packages/integrations/medusajs/src/modules/medusajs/medusajs.service.spec.ts, .../products/products.service.spec.ts
Updated tests for lazy SDK init and SDK-driven product flows (mocks adapted).

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Browser)
    participant Client as ProductDetails Client
    participant Router as Next Router
    participant Server as ProductDetails Server
    participant SDK as Blocks SDK
    participant API as Backend API

    User->>Client: Select variant from dropdown
    Client->>Client: compute URL with variantSlug
    Client->>Router: router.push(new URL)
    Router->>Server: Request page with variantSlug
    Server->>SDK: sdk.blocks.getProductDetails({ id, variantSlug })
    SDK->>API: GET /products/{id} or /products/{id}/{variantSlug}
    API-->>SDK: Return product details (variant-aware)
    SDK-->>Server: Return ProductDetailsBlock
    Server-->>Client: Rendered page with variant-specific data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • marcinkrasowski

"I'm a rabbit in the code-filled glen,
hopping slugs and mappers now and then.
Variants bloom where routes entwine,
links and labels snug in line.
Hooray — the product garden's fine! 🐇"

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description uses the template structure but fails to populate required sections with meaningful content; all key fields contain only placeholder text without addressing the actual changes or testing instructions. Replace placeholders in 'Key Changes' and 'How to test' sections with concrete details about Medusa integration, variant selection, and specific testing steps required for this feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 'Feature/medusa integration - product section' is directly related to the main changeset, which implements Medusa integration for product catalog and variant selection features.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/medusa-integration

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/blocks/recommended-products/src/api-harmonization/recommended-products.service.ts (1)

28-55: ⚠️ Potential issue | 🟡 Minor

Hardcoded limit: 7 is fragile — post-fetch filtering can yield fewer or more than 6 results.

The comment on Line 30 assumes exactly one product is removed (the excluded product), but the image filter on Line 42 can remove additional products, leaving fewer than 6. Conversely, when query.excludeProductId is unset, all 7 may pass through.

Consider either:

  1. Over-fetching with a larger limit and slicing after filtering, or
  2. Adding a .slice(0, desiredCount) after filtering to guarantee a consistent count.
Proposed fix: slice after filtering to guarantee count
+                const RECOMMENDED_LIMIT = 6;
+
                 const filteredProducts: Model.ProductSummary[] = products.data
                     .filter((product: Products.Model.Product) => {
                         if (query.excludeProductId && product.id === query.excludeProductId) {
                             return false;
                         }
                         return product.image;
                     })
+                    .slice(0, RECOMMENDED_LIMIT)
                     .map((product: Products.Model.Product) => ({

Also consider increasing the fetch limit to give more headroom for filtered-out products:

-            limit: 7, // Fetch 7 to have 6 after excluding current product
+            limit: 12, // Over-fetch to ensure 6 remain after filtering
packages/integrations/mocked/src/modules/products/products.mapper.ts (1)

273-323: ⚠️ Potential issue | 🟠 Major

Apply variant overrides to related products for consistency with mapProducts

mapProducts applies variant overrides (selecting the first variant and updating variantId and link), but mapRelatedProducts does not. When a product with variants appears in the related products list, it will be missing the variant selection, causing inconsistent behavior between the product list and related products views.

Align mapRelatedProducts with mapProducts by adding the same variant override logic before returning the data.

🤖 Fix all issues with AI agents
In `@apps/frontend/next.config.ts`:
- Around line 41-44: Remove the IANA placeholder by deleting the remotePatterns
entry that lists hostname: 'example.com' (the snippet defining protocol:
'https', hostname: 'example.com') from the Next.js image configuration; update
the remotePatterns array used in your exported Next.js config (e.g., the
remotePatterns property on the exported config object) to only include real,
intended hostnames or leave it empty if no external images are needed, and
ensure no leftover references to 'example.com' remain.

In `@packages/blocks/product-details/src/frontend/ProductDetails.client.tsx`:
- Around line 37-44: The handleVariantChange implementation is fragile because
it uses endsWith to strip currentVariantSlug; instead, parse product.link into
path segments (split by '/'), replace the last segment when it equals
currentVariantSlug (or always replace the last segment if you guarantee the last
segment is the variant), then rejoin and call router.push with the new path;
update the logic in handleVariantChange to operate on segments rather than
substring slicing and keep references to product.link, currentVariantSlug, and
router.push to locate the change.

In `@packages/integrations/medusajs/src/modules/products/products.mapper.ts`:
- Around line 288-290: In mapRelatedProducts, replace using the variant-level id
(targetProduct.id) for the mapped object's id with the product-level id used
elsewhere so filtering like excludeProductId works; update the id assignment in
mapRelatedProducts to use targetProduct.product?.id || targetProduct.id
(matching mapProduct and mapProducts) so downstream logic receives the product
ID consistently.
- Around line 222-229: The client-side category filtering applied to products
(using categoryFilter and filtering data.products into products) breaks
pagination and total calculation; instead, pass the categoryFilter as
category_id to the server when calling Medusa (use AdminProductListParams with
category_id) so the API returns paginated results filtered server-side, stop
filtering data.products in the mapper (remove the products =
products.filter(...) block), and ensure total uses the server's count
(data.count) rather than products.length when category_id is used.

In `@packages/integrations/medusajs/src/modules/products/products.service.ts`:
- Around line 100-142: When getProductByHandle receives a variantSlug but no
matching variant is found (the block that currently falls back to
product.variants[0]), emit a warning so stale/incorrect URLs are visible: after
computing matchingVariant (in the getProductByHandle method), if variantSlug is
truthy and matchingVariant is undefined, call this.logger?.warn(...) or
console.warn(...) with context (handle, variantSlug, product.id and available
variant slugs via this.slugify over product.variants titles/options) before
continuing to fall back to product.variants[0]; keep the remaining flow (setting
variant and calling getVariant) unchanged and ensure the message is concise and
informative.

In
`@packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.product-list.mapper.ts`:
- Line 8: The DE and PL locale entries in the product list mapper are missing
the variantId query param causing variant selection to be lost; update the
detailsUrl property in cms.product-list.mapper.ts for the DE and PL mappings
(the detailsUrl fields at the indicated occurrences) to include
'?variantId={variantId}' (e.g., '/products/{id}?variantId={variantId}'),
matching the EN entry so variant-specific navigation works correctly.

In `@packages/integrations/mocked/src/modules/products/products.mapper.ts`:
- Around line 114-164: VARIANT_OVERRIDES_DE contains price.currency set to 'USD'
for the pro and max variants; update those price objects in VARIANT_OVERRIDES_DE
(keys "pro" and "max") to use 'EUR' instead of 'USD' so the German locale prices
are in euros; search VARIANT_OVERRIDES_DE for any other price.currency entries
and change them to 'EUR' if they also were incorrectly copied as 'USD'.
🧹 Nitpick comments (11)
packages/blocks/order-list/src/frontend/OrderList.client.stories.tsx (1)

265-268: Floating-point artifacts in story data values.

Lines 266 and 410 contain unrounded floating-point values (773.9459999999999, 458.95500000000004). While this is only story/test data, these could surface as visual noise in Storybook rendering. Consider rounding to a reasonable precision (e.g., 2–4 decimal places).

Proposed fix
                     subtotal: {
-                        value: 773.9459999999999,
+                        value: 773.946,
                         currency: 'USD',
                     },
                     subtotal: {
-                        value: 458.95500000000004,
+                        value: 458.955,
                         currency: 'USD',
                     },

Also applies to: 409-412

packages/integrations/mocked/src/modules/products/products.mapper.ts (1)

7-112: Consider extracting variant override data to a separate mock file.

~160 lines of hardcoded variant overrides occupy most of this mapper file. Other mock data (e.g., MOCK_PRODUCTS_EN/PL/DE) is already imported from products.mock.ts. Moving the variant overrides alongside would keep the mapper focused on transformation logic.

packages/blocks/product-details/src/frontend/ProductDetails.client.tsx (2)

25-26: productId is destructured but never used.

The productId prop is extracted from props on line 25 but has no references in the component body. If it's not needed here, remove it from the destructuring to avoid confusion.


75-93: Duplicated variant selector markup.

The variant <Select> block is copy-pasted for the mobile (lines 75–93) and desktop (lines 193–211) layouts. Consider extracting it into a small local component to keep both instances in sync.

Also applies to: 193-211

packages/blocks/product-details/src/api-harmonization/product-details.service.ts (1)

20-33: Naming mismatch: variantSlug is passed as variantId.

The parameter is named variantSlug (line 20) but is forwarded as variantId (line 32) to productsService.getProduct. If the downstream service truly expects an ID, the parameter here should be named consistently (e.g., variantId). If it's actually a slug used for lookup, the field name in the getProduct call should reflect that. Consistent naming avoids confusion about what this value represents.

packages/integrations/medusajs/src/modules/medusajs/medusajs.service.ts (1)

24-26: Config fallback to empty string is intentional but slightly misleading.

this.config.get('MEDUSAJS_BASE_URL') || '' converts both undefined and '' to '', then the !this._medusaBaseUrl check below catches it. This works, but the double-negative pattern (default to empty, then reject empty) could be clearer by simply checking for the raw config value.

♻️ Slightly clearer config retrieval
-        this._medusaBaseUrl = this.config.get('MEDUSAJS_BASE_URL') || '';
-        this._medusaPublishableApiKey = this.config.get('MEDUSAJS_PUBLISHABLE_API_KEY') || '';
-        this._medusaAdminApiKey = this.config.get('MEDUSAJS_ADMIN_API_KEY') || '';
+        this._medusaBaseUrl = this.config.get('MEDUSAJS_BASE_URL') ?? '';
+        this._medusaPublishableApiKey = this.config.get('MEDUSAJS_PUBLISHABLE_API_KEY') ?? '';
+        this._medusaAdminApiKey = this.config.get('MEDUSAJS_ADMIN_API_KEY') ?? '';
packages/integrations/medusajs/src/modules/products/products.mapper.ts (2)

10-18: Duplicated slugify — extract to a shared utility.

This slugify implementation is identical to ProductsService.slugify in products.service.ts (lines 144–151). Extract it into a shared utility module (e.g., utils/slugify.ts) and import in both places.


166-170: Unnecessary optional chaining after null guard.

Line 166 checks if (!productVariant) and throws. Line 170 then uses productVariant?.product — the ?. is redundant since productVariant is guaranteed non-nullish at this point.

♻️ Minor cleanup
-    const product = productVariant?.product;
+    const product = productVariant.product;
packages/integrations/medusajs/src/modules/products/products.service.ts (3)

52-65: Remove redundant .catch blocks that just re-throw.

The .catch((error) => { throw error; }) on lines 58–60 (and similar patterns on lines 82–84, 102–104, 161–163) is a no-op — it catches the error only to re-throw it, which the promise already does by default. The outer RxJS catchError in the pipe handles the error propagation to handleHttpError.

♻️ Example cleanup for getProductList
         return from(
             this.sdk.admin.product
                 .list(params)
                 .then((response) => {
                     return mapProducts(response, this.defaultCurrency, query.category);
-                })
-                .catch((error) => {
-                    throw error;
                 }),
         ).pipe(
             catchError((error) => {
                 return handleHttpError(error);
             }),
         );

Apply the same cleanup to getProductById, getProductByHandle, and getVariant.


24-27: Field specification strings are fragile — consider documenting or structuring.

The productListFields and productDetailFields strings use Medusa-specific prefixes (* for expansion, + for inclusion) that are easy to get wrong. A brief inline comment explaining the syntax or linking to Medusa docs would help future maintainers.


68-78: Handle vs. ID routing relies on prod_ prefix convention.

The heuristic params.id.startsWith('prod_') is reasonable given Medusa's ID convention, but a handle that coincidentally starts with prod_ would be misrouted. This is unlikely but worth a brief inline comment noting the assumption.

Comment thread apps/frontend/next.config.ts Outdated
Comment thread packages/blocks/product-details/src/frontend/ProductDetails.client.tsx Outdated
Comment thread packages/integrations/medusajs/src/modules/products/products.mapper.ts Outdated
Comment thread packages/integrations/medusajs/src/modules/products/products.service.ts Outdated
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 10, 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 Feb 17, 2026 1:34pm

Request Review

@vercel vercel Bot temporarily deployed to Preview – o2s-docs February 10, 2026 11:35 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 10, 2026

Coverage Report for packages/configs/vitest-config

Status Category Percentage Covered / Total
🔵 Lines 79.59% 823 / 1034
🔵 Statements 79.39% 863 / 1087
🔵 Functions 77.17% 284 / 368
🔵 Branches 70.03% 687 / 981
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/integrations/medusajs/src/modules/medusajs/medusajs.service.ts 100% 100% 100% 100%
packages/integrations/medusajs/src/modules/products/products.mapper.ts 79.6% 71.33% 89.18% 81.15% 16, 33, 53, 62, 70, 92, 110-123, 131, 180, 226, 237, 244, 247, 286, 295-296, 299-300, 344-345, 348-349
packages/integrations/medusajs/src/modules/products/products.service.ts 56.6% 46.66% 60.86% 56.6% 83, 94, 123-161, 177, 215
packages/integrations/medusajs/src/modules/resources/resources.service.ts 47.05% 54.54% 39.28% 47.05% 51, 87, 113, 154-253
packages/blocks/product-details/src/api-harmonization/product-details.mapper.ts 6.25% 0% 0% 6.25% 10-74
packages/blocks/product-details/src/api-harmonization/product-details.service.ts 20% 44.44% 25% 20% 24-47
packages/blocks/recommended-products/src/api-harmonization/recommended-products.service.ts 15.38% 33.33% 14.28% 15.38% 22-60
Generated in workflow #302 for commit f7fbe72 by the Vitest Coverage Report Action

@vercel vercel Bot temporarily deployed to Preview – o2s-docs February 10, 2026 11:50 Inactive
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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/integrations/mocked/src/modules/products/products.mapper.ts (1)

273-323: ⚠️ Potential issue | 🟡 Minor

mapRelatedProducts doesn't apply variant overrides, unlike mapProducts.

mapProducts (Lines 225-238) applies first-variant overrides and updates the link for products with variants, but mapRelatedProducts returns them as-is. If a related product has variants, its link won't include the variant slug and variantId will be absent, which could break the frontend variant routing.

🤖 Fix all issues with AI agents
In `@packages/blocks/product-details/src/frontend/ProductDetails.client.tsx`:
- Around line 25-26: The prop productId is being destructured from the component
props but never used; either remove productId from the destructuring or
explicitly document why it's excluded from ...component (e.g., add a short
inline comment near the destructuring in ProductDetails.client.tsx stating
"intentional exclusion of productId from ...component"), and if the prop is
actually needed instead, wire it into the component logic/JSX where appropriate;
update the destructuring to match the chosen approach so no unused variable
remains.
- Line 35: currentVariantSlug can be undefined if product.variantId doesn't
match any variant; update the logic that computes currentVariantSlug (the const
currentVariantSlug = product.variants?.find((v) => v.id ===
product.variantId)?.slug) to provide a safe fallback (e.g., use
product.variants?.[0]?.slug) or ensure the Select shows a placeholder by
rendering <SelectValue placeholder="Select a variant" /> when currentVariantSlug
is undefined; pick one approach and apply it where the Select uses
currentVariantSlug so the UI always shows a value or a clear placeholder.

In `@packages/integrations/medusajs/src/modules/products/products.mapper.ts`:
- Around line 288-292: The mapped variant is using the variant title for the
product name (name: targetProduct.title) which is inconsistent with
mapProduct/mapProducts; change the mapping in the function that produces the
shown diff so that name uses the product-level title (use product?.title as the
primary value, and fall back to targetProduct.title or an empty string if
needed) to match mapProduct and mapProducts behavior.

In `@packages/integrations/medusajs/src/modules/products/products.service.ts`:
- Around line 163-185: Replace the plain Error thrown in getVariant when
response.variant is missing with a proper 404 error so handleHttpError maps it
to NotFound instead of 500: throw a NotFoundException(`Variant ${variantId} not
found for product ${productId}`) (import NotFoundException from `@nestjs/common`)
in the getVariant map block, ensuring getVariant, mapProduct and handleHttpError
continue to be used as before.

In `@packages/integrations/mocked/src/modules/products/products.mapper.ts`:
- Around line 186-200: The code silently falls back to the first variant when a
provided variantId doesn't match; change the selection logic in the block that
computes selectedVariant (where product.variants, variantId and selectedVariant
are used) so that if variantId is defined but no matching variant is found you
raise/return an explicit not-found error (or a 404-equivalent) instead of using
product.variants[0]; keep the existing fallback to the first variant only when
variantId is undefined, and preserve the subsequent usage of
getVariantOverrides(locale, selectedVariant.slug) and the returned object shape
(variantId, sku, link).
🧹 Nitpick comments (9)
packages/integrations/mocked/src/modules/products/products.mapper.ts (1)

173-178: Locale-to-source selection is duplicated three times.

Consider extracting a small helper like getProductsSource(locale) to keep this DRY and avoid drift if more locales are added.

const getProductsSource = (locale?: string) => {
    if (locale === 'pl') return MOCK_PRODUCTS_PL;
    if (locale === 'de') return MOCK_PRODUCTS_DE;
    return MOCK_PRODUCTS_EN;
};

Also applies to: 208-213, 276-281

packages/blocks/product-details/src/frontend/ProductDetails.client.tsx (2)

30-31: createNavigation(routing) is called on every render — consider memoizing or hoisting.

createNavigation is a factory that returns hooks (useRouter, etc.). Invoking it inside the component body means a new set of hook wrappers is allocated on every render. While functionally correct (React identifies hooks by call order), this is unnecessary work. If routing is stable across renders, consider memoizing the result or lifting the call outside the component.

Memoize the navigation factory result
 export const ProductDetailsPure: React.FC<ProductDetailsPureProps> = ({
     locale,
     routing,
     hasPriority,
     productId,
     ...component
 }) => {
     const { product, labels, actionButton } = component;
     const t = useTranslations();
-    const { useRouter } = createNavigation(routing);
-    const router = useRouter();
+    const navigation = React.useMemo(() => createNavigation(routing), [routing]);
+    const router = navigation.useRouter();

75-93: Hardcoded 'Variant' fallback breaks i18n; also, selector JSX is duplicated.

Lines 78 and 196 both use labels.variantLabel || 'Variant' with a hardcoded English string as fallback. Since useTranslations() (t) is already available, prefer a translated fallback like t('product.variantLabel') to keep the component localizable.

Additionally, the variant selector block (lines 75–93 and 193–211) is copy-pasted verbatim. Consider extracting it into a small local component or helper to reduce duplication and simplify future changes.

Extract and fix i18n
+    const VariantSelector = () => (
+        <div className="flex flex-col gap-2">
+            <Typography className="text-sm text-muted-foreground">
+                {labels.variantLabel || t('product.variantLabel')}
+            </Typography>
+            <Select value={currentVariantSlug} onValueChange={handleVariantChange}>
+                <SelectTrigger>
+                    <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                    {product.variants!.map((variant) => (
+                        <SelectItem key={variant.id} value={variant.slug}>
+                            {variant.title}
+                        </SelectItem>
+                    ))}
+                </SelectContent>
+            </Select>
+        </div>
+    );

Then in both locations, replace the duplicated block with:

{product.variants && product.variants.length > 1 && <VariantSelector />}
packages/integrations/medusajs/src/modules/products/products.mapper.ts (2)

77-113: JSON.parse result is not validated to be an array before the type annotation takes effect.

On line 95, JSON.parse(rawKeySpecs) can return any valid JSON value (object, number, boolean, etc.), but it is assigned directly to keySpecs typed as { value?: string; icon?: string }[]. The Array.isArray guard on line 105 catches this at runtime, but between lines 95 and 104 the variable holds an unvalidated value with a misleading type.

A minor safety improvement:

Suggested fix
     if (typeof rawKeySpecs === 'string') {
         try {
-            keySpecs = JSON.parse(rawKeySpecs);
+            const parsed: unknown = JSON.parse(rawKeySpecs);
+            if (!Array.isArray(parsed)) {
+                return undefined;
+            }
+            keySpecs = parsed;
         } catch {
             return undefined;
         }
     } else if (Array.isArray(rawKeySpecs)) {

161-217: Mapping logic is duplicated across mapProduct, mapProducts, and mapRelatedProducts.

Each mapper independently constructs the same shape (id, name, image, images, price, link, type, category, tags) with slight variations and inconsistencies (e.g., shortDescription defaults to '' in mapProduct but undefined in mapRelatedProducts; name source differs as noted above). Consider extracting a shared mapBaseProduct helper that both mappers delegate to, reducing divergence risk.

Also applies to: 280-325

packages/integrations/medusajs/src/modules/products/products.service.ts (4)

154-161: slugify is duplicated between the service and the mapper.

This method (lines 154-161) is character-for-character identical to the module-level slugify in products.mapper.ts (lines 11-18). Extract it into a shared utility (e.g., utils/slugify.ts) and import it in both places to avoid divergence.


58-60: Remove no-op .catch blocks that only re-throw.

Each of these .catch((error) => { throw error; }) blocks is a no-op — the error is caught and immediately re-thrown, adding noise without altering behavior. The catchError in the outer pipe(...) already handles errors.

Example for getProductList (apply similarly to others)
         return from(
             this.sdk.admin.product
                 .list(params)
                 .then((response) => {
                     return mapProducts(response, this.defaultCurrency, query.category);
-                })
-                .catch((error) => {
-                    throw error;
                 }),
         ).pipe(

Also applies to: 82-84, 102-104, 171-173


68-78: Handle-vs-ID dispatch relies on a hardcoded prod_ prefix convention.

Line 70 uses params.id.startsWith('prod_') to distinguish product IDs from handles. If Medusa's ID prefix convention ever changes, this will silently misroute requests. Consider extracting this as a named constant or utility, and adding a comment referencing the Medusa convention for future maintainers.


24-27: Field strings are fragile — consider documenting or validating them.

productListFields and productDetailFields are comma-separated field selector strings specific to the Medusa Admin API. A typo or stale field name after a Medusa SDK upgrade would silently return incomplete data. Consider adding a brief inline comment per field group explaining which model properties each selector targets, to make future SDK upgrades easier to audit.

Comment thread packages/blocks/product-details/src/frontend/ProductDetails.client.tsx Outdated
Comment thread packages/integrations/medusajs/src/modules/products/products.mapper.ts Outdated
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: 5

🤖 Fix all issues with AI agents
In `@packages/blocks/product-details/src/frontend/ProductDetails.client.tsx`:
- Around line 67-68: The component currently calls createNavigation(routing) on
every render (const { useRouter } = createNavigation(routing); const router =
useRouter();), which recreates hook references; hoist the createNavigation call
to module scope by importing the canonical routing config (or a shared
navigation instance) and call createNavigation(...) once outside the component
to get useRouter, then use that stable useRouter inside
ProductDetails.client.tsx; if routing must remain a prop, produce a memoized
navigation object (e.g., memoize createNavigation(routing) by routing identity)
so the returned useRouter reference is stable before calling its hook.
- Line 194: The fallback hardcoded string in ProductDetails.client.tsx uses
labels.variantLabel || 'Variant', which bypasses i18n; replace the literal
fallback with a translation call (e.g. labels.variantLabel ||
t('product.variant')) and ensure the component uses the translation helper (t)
or hook already present in this file so missing labels render localized text;
add the 'product.variant' key to the locale files if it doesn't exist.
- Around line 33-36: The current fallback picks the first variant that matches
only the changed option (via variants.find and withChanged), ignoring other
selections; change resolveVariantSlug to perform a best-effort match instead:
iterate variants and compute a match score by counting how many option keys in
selectedOptions equal variant.options (including the changedOptionId), select
the variant with the highest score (tie-breaker: prefer exact match or keep
current order), and return its slug; if the best score is 0 (no matching
options) return null so the UI can handle an unavailable combination; use the
existing symbols variants, selectedOptions, changedOptionId and return slug or
null accordingly.
- Around line 170-186: The SelectItem list renders unavailable values (computed
via availableValuesPerGroup.get(group.id)?.has(value)) with reduced opacity but
leaves them selectable; update the map loop that renders group.values inside
ProductDetails.client (the block creating <SelectItem key={value} value={value}
...>) to set the SelectItem disabled when !isAvailable so users cannot pick
invalid combinations—use the same isAvailable boolean and add disabled={
!isAvailable } (or the SelectItem equivalent) and keep the opacity class for
visual feedback; this prevents invalid selections that later cause
resolveVariantSlug exact-match failures.

In `@packages/integrations/medusajs/src/modules/products/products.service.ts`:
- Around line 80-100: In getProductById, guard against response.product being
undefined before accessing product.variants: after the SDK retrieve call
resolves in the switchMap, check if response.product exists and if not throw a
clear Error (e.g., "Product not found: <productId>") so the
catchError/handleHttpError path receives a meaningful error; mirror the
existence check used in getProductByHandle and then proceed to validate variants
and call getVariant.
🧹 Nitpick comments (5)
packages/blocks/product-details/src/frontend/ProductDetails.client.tsx (1)

154-211: Variant selector UI is duplicated between mobile and desktop layouts.

Lines 154–211 (mobile) and 311–368 (desktop/sticky panel) contain nearly identical variant selection markup. Extract a shared VariantSelector component to keep the two layouts in sync and reduce maintenance burden.

packages/integrations/medusajs/src/modules/products/products.service.ts (3)

52-65: Remove no-op .catch blocks on SDK promises.

The .catch((error) => { throw error; }) pattern appears on every SDK call (lines 58–60, 84–86, 106–108, 162–164). Re-throwing the same error is a no-op — the rejection already propagates through from() into the RxJS catchError in the pipe. This adds noise without changing behavior.

♻️ Example fix for getProductList
         return from(
             this.sdk.admin.product
                 .list(params)
                 .then((response) => {
                     return mapProducts(response, this.defaultCurrency, query.category);
-                })
-                .catch((error) => {
-                    throw error;
                 }),
         ).pipe(

145-152: Duplicate slugify implementation — consider sharing.

This slugify method is character-for-character identical to the module-level slugify function in products.mapper.ts (lines 11–18). Extract it into a shared utility to avoid drift.


68-78: params.variantId is reinterpreted as a slug when routing by handle — consider documenting this dual meaning.

When id doesn't start with prod_, params.variantId is passed to getProductByHandle as variantSlug. This implicit reinterpretation works for the URL scheme (/products/{handle}/{variantSlug}) but is easy to misunderstand since the field is named variantId. A brief inline comment clarifying this would help future maintainers.

packages/integrations/medusajs/src/modules/products/products.mapper.ts (1)

63-99: mapKeySpecsFromMetadata lacks type validation on parsed array items.

Line 81 parses a JSON string into keySpecs and line 86 assigns rawKeySpecs directly — both assume array items have the shape { value?: string; icon?: string }. Malformed metadata (e.g., items that are numbers or strings instead of objects) would produce silently incorrect KeySpecItem entries with undefined fields rather than being filtered out.

Consider adding a guard on the mapped items:

🛡️ Proposed improvement
     return keySpecs
+        .filter((spec): spec is Record<string, unknown> => spec != null && typeof spec === 'object')
         .map((spec) => ({
-            value: spec.value,
-            icon: spec.icon,
+            value: typeof spec.value === 'string' ? spec.value : undefined,
+            icon: typeof spec.icon === 'string' ? spec.icon : undefined,
         }));

Comment thread packages/blocks/product-details/src/frontend/ProductDetails.client.tsx Outdated
Comment thread packages/blocks/product-details/src/frontend/ProductDetails.client.tsx Outdated
Comment thread packages/blocks/product-details/src/frontend/ProductDetails.client.tsx Outdated
Comment thread packages/integrations/medusajs/src/modules/products/products.service.ts Outdated
Comment thread packages/integrations/medusajs/src/modules/products/products.mapper.ts Outdated
Comment thread packages/integrations/medusajs/src/modules/products/products.mapper.ts Outdated
@vercel vercel Bot temporarily deployed to Preview – o2s-docs February 16, 2026 13:40 Inactive
@vercel vercel Bot temporarily deployed to Preview – o2s-docs February 16, 2026 14:36 Inactive
Comment thread packages/blocks/product-details/src/api-harmonization/product-details.model.ts Outdated
@vercel vercel Bot temporarily deployed to Preview – o2s-docs February 17, 2026 13:34 Inactive
@marcinkrasowski marcinkrasowski merged commit 1f2965c into main Feb 17, 2026
13 checks passed
@marcinkrasowski marcinkrasowski deleted the feature/medusa-integration branch February 17, 2026 14:29
@michnowak michnowak linked an issue Feb 19, 2026 that may be closed by this pull request
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.

[Feature] Medusa.js Integration for Product Catalog

3 participants