Skip to content

Various payments improvements#1116

Merged
N2D4 merged 9 commits intodevfrom
various-payments-improvements
Jan 20, 2026
Merged

Various payments improvements#1116
N2D4 merged 9 commits intodevfrom
various-payments-improvements

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Jan 17, 2026


Note

Delivers major UX for managing products and product lines.

  • New Edit Product page: full form with validation and live preview to manage displayName, pricing (free/include-by-default), included items (add/edit/remove), serverOnly, stackable, add-ons, free trials, and productLineId; dialogs to create product lines/items
  • Product lines view overhaul: read-only cards with actions; drag-and-drop to move products across product lines with compatibility feedback and persisted productLineId; add-product CTAs; duplication now stores data in sessionStorage and opens create flow
  • New product flow: pre-fills form from duplicated product via sessionStorage; respects URL params; add-on selectors now show all existing products
  • Details page tweak: replaces badges with an Edit button linking to the new editor

Written by Cursor Bugbot for commit d81999d. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Full product edit page with comprehensive form, live preview, and dialogs to create product lines and items.
    • Drag-and-drop support to move products between product lines with live preview and compatibility feedback.
    • Session-backed product duplication prefill when creating new products.
  • Improvements

    • Stronger validation for pricing, add-ons, and included items; enhanced included-items management (add/edit/remove).
    • Replaced status badges with a direct Edit action and removed draft-based editing flow.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 17, 2026 01:39
@vercel
Copy link

vercel bot commented Jan 17, 2026

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

Project Deployment Review Updated (UTC)
stack-backend Ready Ready Preview, Comment Jan 20, 2026 4:37am
stack-dashboard Ready Ready Preview, Comment Jan 20, 2026 4:37am
stack-demo Ready Ready Preview, Comment Jan 20, 2026 4:37am
stack-docs Ready Ready Preview, Comment Jan 20, 2026 4:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

Warning

Rate limit exceeded

@N2D4 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4380843 and d81999d.

📒 Files selected for processing (3)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a new product edit page and client with a full EditProductForm, sessionStorage-based duplication/prefill for the new-product flow, drag-and-drop product-line moves replacing draft editing, and an Edit action in product headers replacing status badges.

Changes

Cohort / File(s) Summary
Product Edit Pages
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page.tsx
New client and server page pair. Adds PageClient default export and server Page with metadata. Implements EditProductForm with state init, validation, pricing, included-items dialogs, product-line creation, live preview, navigation and not-found handling.
New Product (duplication prefill)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx
Adds getDuplicateData() and reads duplicate payload from sessionStorage. Prefills new-product form fields (displayName, prices, includedItems, flags, productLine, customer type) from stored payload and clears the key.
Product Lines / ProductCard refactor
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx
Removes draft-based editing and related props; introduces drag-and-drop (DraggableProductCard, DroppableProductLineZone, DragOverlay), onMoveProduct callback, stricter customerType typing, sessionStorage-based duplication navigation, and drag preview/validation across product lines.
Product detail header change
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx
Replaces Add-on/Stackable badges with an Edit button that navigates to the product edit page, changing UI from status display to navigation action.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ProductLinesView
    participant SessionStorage
    participant Router
    participant NewProductPage
    participant EditProductForm

    User->>ProductLinesView: Click "Duplicate" on product
    ProductLinesView->>ProductLinesView: Build duplicate payload
    ProductLinesView->>SessionStorage: Store payload under key
    ProductLinesView->>Router: Navigate to /products/new?duplicateKey=<key>
    Router->>NewProductPage: Init with searchParams
    NewProductPage->>SessionStorage: getDuplicateData(duplicateKey)
    SessionStorage-->>NewProductPage: Return payload (and remove key)
    NewProductPage->>NewProductPage: Pre-fill form state from payload
    User->>EditProductForm: View/modify pre-filled form
    User->>EditProductForm: Submit -> project.updateConfig / save
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • BilalG1

Poem

🐰 I hopped from card to cart and stored a seed,
Prefilled forms sprouted fast indeed,
Drags and dialogs, edits take flight,
Session keys cleared in moonlit night,
Hooray — products bloom both left and right ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Various payments improvements' is vague and generic, using non-descriptive language that doesn't convey specific information about the changeset. Consider a more specific title that highlights the primary changes, such as 'Add product edit page with drag-and-drop product line management' or 'Modernize product management with edit forms and drag-and-drop.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors the payments product management functionality by removing inline editing capabilities from product cards and introducing dedicated pages for creating and editing products. The duplicate functionality has been implemented using sessionStorage for temporary data transfer.

Changes:

  • Removed inline editing state management and UI from ProductCard component, making it view-only
  • Created dedicated edit page at /products/[productId]/edit for editing existing products
  • Added duplication functionality using sessionStorage to transfer product data to the new product creation page

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
page-client-product-lines-view.tsx Simplified ProductCard to view-only mode, removed draft management, updated Edit/Duplicate actions to navigate to dedicated pages
new/page-client.tsx Added support for initializing form from duplicated product data stored in sessionStorage
[productId]/edit/page.tsx New Next.js page component for product editing route
[productId]/edit/page-client.tsx New comprehensive product edit form with validation, preview, and full editing capabilities

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +144 to +146
const effectiveProductLineId = productLineId && paymentsConfig.productLines[productLineId].customerType === customerType
? productLineId
: "";
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The code accesses paymentsConfig.productLines[productLineId] without checking if productLineId exists in the object first. If productLineId is set to a non-existent product line ID, this will throw a runtime error when trying to access the .customerType property of undefined. Consider adding a check like productLineId in paymentsConfig.productLines before accessing the property.

Suggested change
const effectiveProductLineId = productLineId && paymentsConfig.productLines[productLineId].customerType === customerType
? productLineId
: "";
const effectiveProductLineId =
productLineId &&
paymentsConfig.productLines[productLineId]?.customerType === customerType
? productLineId
: "";

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 17, 2026

Greptile Summary

This PR adds product editing and creation pages for the payments feature, along with improvements to the product lines view. The changes split the product management UI into separate pages for creating new products and editing existing ones, while enhancing the product lines view with better organization and inline product management capabilities.

Key Changes:

  • New /payments/products/[productId]/edit/page-client.tsx - Dedicated edit form for existing products with side-by-side preview panel
  • New /payments/products/new/page-client.tsx - Product creation page with customer type selection, product ID auto-generation, duplicate support, and inline product code generation
  • Enhanced /products/page-client-product-lines-view.tsx - Improved product organization with product line grouping, better UI for product management, and support for inline product editing

Issues Found:

  • Async button handlers and form submission handlers inconsistently fail to use runAsynchronouslyWithAlert utility, violating established error handling patterns
  • Manual try/catch blocks in async handlers should be replaced with the standardized error handling utility for consistency

Confidence Score: 3/5

  • PR introduces error handling inconsistencies that violate project standards for async handler management, but functionality appears sound with proper state management and validation patterns.
  • The PR implements the payments feature pages with generally good patterns for form state management, validation, and config updates. However, there are recurring violations of the runAsynchronouslyWithAlert custom rule across multiple async handlers. While the manual try/catch blocks provide basic error handling, the inconsistent approach creates maintenance burden and deviates from established project standards. The issues are straightforward to fix but should be addressed before merging to maintain code consistency.
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx and apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx require async handler pattern updates. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx needs error handling wrapper for async operations.

Important Files Changed

Filename Overview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx New file for editing products. Has async error handling inconsistency - handleCreateProductLine doesn't use runAsynchronouslyWithAlert while handleSave uses manual try/catch. Missing error handling wrapper utility for async button handlers violates custom rule.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page.tsx Simple wrapper page that extracts params and passes to page-client. No issues detected.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx New product creation page. Uses runAsynchronouslyWithAlert for copy-to-clipboard handlers but handleSave is called directly from onClick without error handling wrapper, inconsistent with custom rule. Otherwise well-implemented with good form state management.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx Complex product lines view component. Multiple async handlers not wrapped with runAsynchronouslyWithAlert: handleDeleteProduct, handleDeleteProductLine async operations, onClick handlers for deletion dialogs. Violates custom rule 5e671275-7493-402a-93a8-969537ec4d63. Manual try/catch blocks missing in several async operations.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant CreatePage as Create Product Page
    participant EditPage as Edit Product Page
    participant Config as Project Config
    participant Toast as Toast/Alert

    User->>CreatePage: Select Customer Type
    CreatePage->>CreatePage: Show Product Form
    User->>CreatePage: Fill Form (name, prices, items)
    User->>CreatePage: Click Create Button
    CreatePage->>CreatePage: Validate Form
    alt Validation Fails
        CreatePage->>CreatePage: Show Error Messages
    else Validation Passes
        CreatePage->>Config: updateConfig(product)
        Config-->>CreatePage: Success
        CreatePage->>Toast: Show "Product created"
        CreatePage->>User: Navigate to Products List
    end

    User->>EditPage: Open Edit Product Page
    EditPage->>Config: Load Existing Product
    EditPage->>EditPage: Initialize Form State
    User->>EditPage: Modify Product Fields
    User->>EditPage: Click Save Changes
    EditPage->>EditPage: Validate Form
    alt Validation Fails
        EditPage->>User: Show Error Messages
    else Validation Passes
        EditPage->>Config: updateConfig(updated product)
        Config-->>EditPage: Success
        EditPage->>Toast: Show "Product updated"
        EditPage->>User: Navigate to Products List
    end

    User->>EditPage: Click "Create Product Line"
    EditPage->>EditPage: Open Create Dialog
    User->>EditPage: Fill Dialog
    EditPage->>Config: updateConfig(productLine)
    Config-->>EditPage: Success
    EditPage->>Toast: Show Success
    EditPage->>EditPage: Auto-select Product Line
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (3)

  1. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx, line 570-576 (link)

    style: Async button handler should use runAsynchronouslyWithAlert wrapper for consistency with other async handlers in this file and codebase standards.

    Context Used: Rule from dashboard - Use runAsynchronouslyWithAlert from @stackframe/stack-shared/dist/utils/promises for async butto... (source)

  2. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx, line 1719-1756 (link)

    style: Async function handleDeleteProduct lacks error handling wrapper. Should use runAsynchronouslyWithAlert to ensure errors are shown to users.

    Context Used: Rule from dashboard - Use runAsynchronouslyWithAlert from @stackframe/stack-shared/dist/utils/promises for async butto... (source)

  3. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx, line 1787-1811 (link)

    style: Async operation in onDeleteProductLine handler lacks error handling. Should wrap with runAsynchronouslyWithAlert for user feedback.

    Context Used: Rule from dashboard - Use runAsynchronouslyWithAlert from @stackframe/stack-shared/dist/utils/promises for async butto... (source)

4 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +161 to +169
const handleCreateProductLine = async (productLine: { id: string, displayName: string }) => {
await project.updateConfig({
[`payments.productLines.${productLine.id}`]: {
displayName: productLine.displayName || null,
customerType,
},
});
setProductLineId(productLine.id);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Async handler handleCreateProductLine should use runAsynchronouslyWithAlert instead of exposing raw promise. This ensures consistent error handling and user alerts across the application.

Suggested change
const handleCreateProductLine = async (productLine: { id: string, displayName: string }) => {
await project.updateConfig({
[`payments.productLines.${productLine.id}`]: {
displayName: productLine.displayName || null,
customerType,
},
});
setProductLineId(productLine.id);
};
const handleCreateProductLine = (productLine: { id: string, displayName: string }) => {
runAsynchronouslyWithAlert(async () => {
await project.updateConfig({
[`payments.productLines.${productLine.id}`]: {
displayName: productLine.displayName || null,
customerType,
},
});
setProductLineId(productLine.id);
});
};

Context Used: Rule from dashboard - Use runAsynchronouslyWithAlert from @stackframe/stack-shared/dist/utils/promises for async butto... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx
Line: 161:169

Comment:
**style:** Async handler `handleCreateProductLine` should use `runAsynchronouslyWithAlert` instead of exposing raw promise. This ensures consistent error handling and user alerts across the application.

```suggestion
const handleCreateProductLine = (productLine: { id: string, displayName: string }) => {
  runAsynchronouslyWithAlert(async () => {
    await project.updateConfig({
      [`payments.productLines.${productLine.id}`]: {
        displayName: productLine.displayName || null,
        customerType,
      },
    });
    setProductLineId(productLine.id);
  });
};
```

**Context Used:** Rule from `dashboard` - Use `runAsynchronouslyWithAlert` from `@stackframe/stack-shared/dist/utils/promises` for async butto... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +281 to +286
<Button
onClick={handleSave}
disabled={!canSave || isSaving}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Async button handler handleSave should be wrapped with runAsynchronouslyWithAlert for consistent error handling. Current manual try/catch pattern should be replaced with the utility.

Suggested change
<Button
onClick={handleSave}
disabled={!canSave || isSaving}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
<Button
onClick={() => runAsynchronouslyWithAlert(handleSave)}
disabled={!canSave || isSaving}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>

Context Used: Rule from dashboard - Use runAsynchronouslyWithAlert from @stackframe/stack-shared/dist/utils/promises for async butto... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx
Line: 281:286

Comment:
**style:** Async button handler `handleSave` should be wrapped with `runAsynchronouslyWithAlert` for consistent error handling. Current manual try/catch pattern should be replaced with the utility.

```suggestion
                  <Button
                    onClick={() => runAsynchronouslyWithAlert(handleSave)}
                    disabled={!canSave || isSaving}
                  >
                    {isSaving ? "Saving..." : "Save Changes"}
                  </Button>
```

**Context Used:** Rule from `dashboard` - Use `runAsynchronouslyWithAlert` from `@stackframe/stack-shared/dist/utils/promises` for async butto... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

How can I resolve this? If you propose a fix, please make it concise.

Copy link
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: 1

🤖 Fix all issues with AI agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx:
- Around line 692-700: The onSave async callback passed to ItemDialog should be
wrapped with runAsynchronouslyWithAlert to surface errors to users; replace the
inline async handler used in the ItemDialog prop (the function that calls
project.updateConfig and toast) with a call to runAsynchronouslyWithAlert(() =>
/* same async logic */) so any thrown errors are caught and shown, referencing
the ItemDialog prop onSave, project.updateConfig, toast, and
runAsynchronouslyWithAlert.
🧹 Nitpick comments (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx (2)

178-191: Type assertion without validation could lead to subtle bugs.

The as Product assertion assumes the parsed data matches the Product type. If the sessionStorage data is malformed (e.g., from a version mismatch after deployment), this could cause runtime errors when accessing expected properties.

Consider adding basic shape validation or using a safer fallback pattern.

🔧 Optional: Add minimal shape validation
 function getDuplicateData(key: string | null): Product | null {
   if (!key) return null;
   try {
     const data = sessionStorage.getItem(key);
     if (data) {
       sessionStorage.removeItem(key); // Clean up after reading
-      return JSON.parse(data) as Product;
+      const parsed = JSON.parse(data);
+      // Basic shape validation - ensure required fields exist
+      if (parsed && typeof parsed === 'object' && 'customerType' in parsed) {
+        return parsed as Product;
+      }
     }
   } catch {
     // Ignore parsing errors
   }
   return null;
 }

222-228: Confusing boolean logic for add-on detection.

The condition duplicateData?.isAddOnTo !== false && duplicateData?.isAddOnTo !== undefined essentially checks if isAddOnTo is truthy (an object). The subsequent access pattern is correct but the intent could be clearer.

♻️ Clearer intent expression
-  const duplicateIsAddOn = duplicateData?.isAddOnTo !== false && duplicateData?.isAddOnTo !== undefined;
-  const duplicateIsAddOnTo = duplicateIsAddOn && duplicateData.isAddOnTo
-    ? Object.keys(duplicateData.isAddOnTo as Record<string, boolean>)
-    : [];
+  const duplicateIsAddOn = duplicateData?.isAddOnTo && duplicateData.isAddOnTo !== false;
+  const duplicateIsAddOnTo = duplicateIsAddOn
+    ? Object.keys(duplicateData.isAddOnTo as Record<string, boolean>)
+    : [];
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx (1)

84-116: Significant code duplication with new product page.

The form initialization logic (lines 96-115) and much of the form structure is duplicated from new/page-client.tsx. Consider extracting shared logic into a custom hook or shared component.

Potential extraction targets:

  • Form state initialization and parsing logic
  • Validation logic (validateForm)
  • Included items management functions (addIncludedItem, editIncludedItem, removeIncludedItem)
  • The entire Options section UI (stackable, server-only, add-on, free trial, product line)

This would reduce maintenance burden and ensure consistency between create and edit flows.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx (1)

736-761: Consider removing no-op callbacks for cleaner API.

onSave and onRemove are passed as no-op functions since this is now view-only. If ProductPriceRow supports an optional readOnly mode, these callbacks could be omitted entirely.

Comment on lines +692 to +700
<ItemDialog
open={showNewItemDialog}
onOpenChange={setShowNewItemDialog}
onSave={async (item) => {
await project.updateConfig({ [`payments.items.${item.id}`]: { displayName: item.displayName, customerType: item.customerType } });
toast({ title: "Item created" });
}}
existingItemIds={Object.keys(paymentsConfig.items)}
forceCustomerType={customerType}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Async callback in ItemDialog onSave needs error handling.

Same issue as handleSave - the async function passed to onSave should be wrapped with runAsynchronouslyWithAlert to ensure errors are properly displayed to users.

🐛 Recommended fix
       <ItemDialog
         open={showNewItemDialog}
         onOpenChange={setShowNewItemDialog}
-        onSave={async (item) => {
-          await project.updateConfig({ [`payments.items.${item.id}`]: { displayName: item.displayName, customerType: item.customerType } });
-          toast({ title: "Item created" });
-        }}
+        onSave={(item) => runAsynchronouslyWithAlert(async () => {
+          await project.updateConfig({ [`payments.items.${item.id}`]: { displayName: item.displayName, customerType: item.customerType } });
+          toast({ title: "Item created" });
+        })}
         existingItemIds={Object.keys(paymentsConfig.items)}
         forceCustomerType={customerType}
       />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ItemDialog
open={showNewItemDialog}
onOpenChange={setShowNewItemDialog}
onSave={async (item) => {
await project.updateConfig({ [`payments.items.${item.id}`]: { displayName: item.displayName, customerType: item.customerType } });
toast({ title: "Item created" });
}}
existingItemIds={Object.keys(paymentsConfig.items)}
forceCustomerType={customerType}
<ItemDialog
open={showNewItemDialog}
onOpenChange={setShowNewItemDialog}
onSave={(item) => runAsynchronouslyWithAlert(async () => {
await project.updateConfig({ [`payments.items.${item.id}`]: { displayName: item.displayName, customerType: item.customerType } });
toast({ title: "Item created" });
})}
existingItemIds={Object.keys(paymentsConfig.items)}
forceCustomerType={customerType}
🤖 Prompt for AI Agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx
around lines 692 - 700, The onSave async callback passed to ItemDialog should be
wrapped with runAsynchronouslyWithAlert to surface errors to users; replace the
inline async handler used in the ItemDialog prop (the function that calls
project.updateConfig and toast) with a call to runAsynchronouslyWithAlert(() =>
/* same async logic */) so any thrown errors are caught and shown, referencing
the ItemDialog prop onSave, project.updateConfig, toast, and
runAsynchronouslyWithAlert.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Copy link
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: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx (1)

1070-1093: Wrap clipboard writes with runAsynchronously.

Clipboard writes can reject (permissions/HTTP). Wrapping them avoids unhandled rejections and uses the shared async handler.

As per coding guidelines, ...

🛠️ Suggested fix
- onClick={async () => {
-   await navigator.clipboard.writeText(`const url = await ${customerType}.createCheckoutUrl({ productId: "${id}" });\nwindow.open(url, "_blank");`);
-   toast({ title: "Copied to clipboard" });
- }}
+ onClick={() =>
+   runAsynchronously((async () => {
+     await navigator.clipboard.writeText(
+       `const url = await ${customerType}.createCheckoutUrl({ productId: "${id}" });\nwindow.open(url, "_blank");`
+     );
+     toast({ title: "Copied to clipboard" });
+   })())
+ }

...
- onClick={async () => {
-   const prompt = generateComprehensivePrompt();
-   await navigator.clipboard.writeText(prompt);
-   toast({ title: "Prompt copied to clipboard" });
- }}
+ onClick={() =>
+   runAsynchronously((async () => {
+     const prompt = generateComprehensivePrompt();
+     await navigator.clipboard.writeText(prompt);
+     toast({ title: "Prompt copied to clipboard" });
+   })())
+ }
🤖 Fix all issues with AI agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx:
- Around line 1341-1367: The code assumes
paymentsConfig.productLines[targetProductLineId] exists and uses toast for
blocked moves; update the validation to first verify the target product line
object exists (check paymentsConfig.productLines[targetProductLineId] !==
undefined) before reading customerType or displayName, and if it's missing
(deleted mid-drag) show a blocking alert and return; likewise, when customerType
mismatch between paymentsConfig.productLines[targetProductLineId].customerType
and draggedProduct.customerType, replace the toast with a blocking alert and
return; keep setIsMovingProduct(true) and the await
onMoveProduct(draggedProductId, targetProductLineId) only after these existence
and compatibility checks.
🧹 Nitpick comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx (2)

86-93: Prefer interface for the new prop/config shapes.

These object-shaped type aliases should be interface for consistency in TSX.

As per coding guidelines, ...

♻️ Example conversion (apply similarly to the other new prop types)
-type ProductEditableInputProps = {
+interface ProductEditableInputProps {
   value: string,
   onUpdate?: (value: string) => void,
   readOnly?: boolean,
   placeholder?: string,
   inputClassName?: string,
   transform?: (value: string) => string,
-};
+}

Also applies to: 438-457, 483-489, 1132-1150


985-1002: Use urlString for the new navigation URLs.

The new edit/duplicate routes should use urlString instead of template strings to keep URL construction consistent and safe.

As per coding guidelines, ...

♻️ Suggested change
- onClick={() => router.push(`/projects/${projectId}/payments/products/${id}/edit`)}
+ onClick={() => router.push(urlString`/projects/${projectId}/payments/products/${id}/edit`)}
...
- router.push(`/projects/${projectId}/payments/products/new?duplicate=${duplicateKey}`);
+ router.push(urlString`/projects/${projectId}/payments/products/new?duplicate=${duplicateKey}`);

Comment on lines +1341 to +1367
// Get the target product line's customer type
const targetCustomerType = targetProductLineId
? paymentsConfig.productLines[targetProductLineId].customerType
: undefined;

// Validate customer type compatibility:
// - Can always drop to "No product line"
// - Can drop to a product line if it has the same customer type as the product
if (targetProductLineId && targetCustomerType !== draggedProduct.customerType) {
toast({
title: "Cannot move product",
description: `This product has customer type "${draggedProduct.customerType}" but the target product line is for "${targetCustomerType}" customers.`,
});
return;
}

// Show loading state and update the product's productLineId
setIsMovingProduct(true);
try {
await onMoveProduct(draggedProductId, targetProductLineId);

toast({
title: "Product moved",
description: targetProductLineId
? `Moved to "${paymentsConfig.productLines[targetProductLineId].displayName || targetProductLineId}"`
: "Removed from product line",
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden drag-drop target validation and use alerts for blocked moves.

paymentsConfig.productLines[targetProductLineId] is assumed to exist; if the product line is deleted mid-drag this will throw. Also, the incompatible drop is a blocked action, so use a blocking alert instead of toast.

As per coding guidelines, ...

🛠️ Suggested fix
- const targetCustomerType = targetProductLineId
-   ? paymentsConfig.productLines[targetProductLineId].customerType
-   : undefined;
+ const targetProductLine = targetProductLineId
+   ? paymentsConfig.productLines[targetProductLineId]
+   : undefined;
+ if (targetProductLineId && !targetProductLine) {
+   alert("Target product line no longer exists.");
+   return;
+ }
+ const targetCustomerType = targetProductLine?.customerType;

...
- toast({
-   title: "Cannot move product",
-   description: `This product has customer type "${draggedProduct.customerType}" but the target product line is for "${targetCustomerType}" customers.`,
- });
+ alert(
+   `Cannot move product. This product has customer type "${draggedProduct.customerType}" but the target product line is for "${targetCustomerType}" customers.`
+ );

...
- description: targetProductLineId
-   ? `Moved to "${paymentsConfig.productLines[targetProductLineId].displayName || targetProductLineId}"`
+ description: targetProductLineId
+   ? `Moved to "${targetProductLine?.displayName || targetProductLineId}"`
🤖 Prompt for AI Agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx
around lines 1341 - 1367, The code assumes
paymentsConfig.productLines[targetProductLineId] exists and uses toast for
blocked moves; update the validation to first verify the target product line
object exists (check paymentsConfig.productLines[targetProductLineId] !==
undefined) before reading customerType or displayName, and if it's missing
(deleted mid-drag) show a blocking alert and return; likewise, when customerType
mismatch between paymentsConfig.productLines[targetProductLineId].customerType
and draggedProduct.customerType, replace the toast with a blocking alert and
return; keep setIsMovingProduct(true) and the await
onMoveProduct(draggedProductId, targetProductLineId) only after these existence
and compatibility checks.

Comment on lines +96 to +97
const existingIsAddOn = existingProduct.isAddOnTo !== false;
const existingIsAddOnTo = existingIsAddOn
Copy link

Choose a reason for hiding this comment

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

Suggested change
const existingIsAddOn = existingProduct.isAddOnTo !== false;
const existingIsAddOnTo = existingIsAddOn
const existingIsAddOn = existingProduct.isAddOnTo !== false && existingProduct.isAddOnTo !== undefined;
const existingIsAddOnTo = existingIsAddOn && existingProduct.isAddOnTo

The isAddOnTo field can be undefined according to the schema, but line 96 only checks if it's not false, causing a runtime error when trying to call Object.keys() on undefined.

View Details

Analysis

Missing undefined check for isAddOnTo field causes TypeError

What fails: EditProductForm in apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx crashes when loading a product with an undefined isAddOnTo field.

How to reproduce:

  1. Create a product where the isAddOnTo field is undefined (which is valid according to the product schema - see schema definition)
  2. Navigate to edit that product
  3. The edit page fails with: TypeError: Cannot convert undefined or null to object

What happens: The code at line 96-99 checks existingProduct.isAddOnTo !== false, which evaluates to true when isAddOnTo is undefined (because undefined !== false). It then attempts to call Object.keys(undefined) which throws a TypeError.

Expected behavior: The page should load successfully and treat undefined isAddOnTo as "not an add-on" (same behavior as false), based on the schema definition which allows isAddOnTo to be:

  • false (literal false, not an add-on)
  • Record<string, true> (an add-on with parent products)
  • undefined (optional field)

Root cause: The code pattern was missing the explicit undefined check that exists in the parallel pattern at apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx line 223, which correctly uses: duplicateData?.isAddOnTo !== false && duplicateData?.isAddOnTo !== undefined

Fix applied: Updated the condition to include an explicit undefined check, matching the safe pattern already used in the new product form.

cursor[bot]

This comment was marked as outdated.

Comment on lines +2025 to +2031
const currentProduct = paymentsConfig.products[productId];

// Update the entire product object with the new productLineId
// Using undefined instead of null to properly clear the value
await project.updateConfig({
[`payments.products.${productId}`]: {
...currentProduct,
Copy link

Choose a reason for hiding this comment

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

Missing null check for currentProduct before spreading it in the onMoveProduct callback. If a product is deleted between when the drag starts and when it ends, this will cause a runtime error.

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx
index 265edf30..ed493ea2 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-product-lines-view.tsx
@@ -2023,6 +2023,7 @@ export default function PageClient({ createDraftRequestId, draftCustomerType = '
         paymentsConfig={paymentsConfig}
         onMoveProduct={async (productId, targetProductLineId) => {
           const currentProduct = paymentsConfig.products[productId];
+          if (!currentProduct) return;
 
           // Update the entire product object with the new productLineId
           // Using undefined instead of null to properly clear the value

Analysis

Missing null check for currentProduct in onMoveProduct callback

What fails: The onMoveProduct callback in page-client-product-lines-view.tsx spreads currentProduct without checking if it exists. If a product is deleted between when the drag starts and when it ends (race condition), currentProduct will be undefined, causing updateConfig to fail validation.

How to reproduce:

  1. Start dragging a product to move it to a different product line
  2. Simultaneously delete that product via another operation
  3. Complete the drag and drop before the deletion propagates
  4. The updateConfig call receives an incomplete product object with only productLineId set, missing required fields like displayName, customerType, prices, etc.

Result: Schema validation fails because required fields are missing from the product update. This results in an error being thrown to the user.

Expected: The callback should check if the product exists before attempting to update it, following the defensive programming pattern already used in handleDragEnd (line 1327), where if (!draggedProduct) return; guards against this scenario.

Fixed by: Adding if (!currentProduct) return; check at line 2026, matching the pattern used elsewhere in the same component for handling potentially deleted products during drag-and-drop operations.

cursor[bot]

This comment was marked as outdated.

}));

// Validate that the selected productLineId matches the current customerType
const effectiveProductLineId = productLineId && paymentsConfig.productLines[productLineId].customerType === customerType
Copy link

Choose a reason for hiding this comment

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

Suggested change
const effectiveProductLineId = productLineId && paymentsConfig.productLines[productLineId].customerType === customerType
const effectiveProductLineId = productLineId && productLineId in paymentsConfig.productLines && paymentsConfig.productLines[productLineId].customerType === customerType

Missing existence check when accessing paymentsConfig.productLines[productLineId] could cause a runtime error if the product line is deleted while the edit form is loaded.

View Details

Analysis

Missing existence check for productLineId in edit form allows runtime error

What fails: ProductLineId existence check in EditProductForm() at line 144 of apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/edit/page-client.tsx accesses paymentsConfig.productLines[productLineId].customerType without verifying the key exists in the object.

How to reproduce:

  1. Edit an existing product
  2. Set productLineId to a value that doesn't exist in paymentsConfig.productLines
  3. Trigger the form to re-evaluate the effectiveProductLineId variable

Result: Throws TypeError: "Cannot read properties of undefined (reading 'customerType')" at runtime because paymentsConfig.productLines[productLineId] returns undefined when the key doesn't exist.

Expected: Should safely return empty string when productLineId doesn't exist, matching the pattern used in new/page-client.tsx line 211: const validProductLineId = urlProductLineId && urlProductLineId in paymentsConfig.productLines ? urlProductLineId : null;

Fix: Added in operator check before accessing nested property to verify the key exists in the Record type, preventing access to undefined.customerType.

@N2D4 N2D4 merged commit 73fe6f9 into dev Jan 20, 2026
7 checks passed
@N2D4 N2D4 deleted the various-payments-improvements branch January 20, 2026 04:30
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

// Get the target product line's customer type
const targetCustomerType = targetProductLineId
? paymentsConfig.productLines[targetProductLineId].customerType
: undefined;
Copy link

Choose a reason for hiding this comment

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

Drag-drop handler crashes on deleted product line

Medium Severity

The handleDragEnd function accesses paymentsConfig.productLines[targetProductLineId].customerType and .displayName without verifying the product line exists. If a product line is deleted while a user is actively dragging (e.g., by another browser tab or concurrent API call), this will crash when the user drops on the now-deleted product line.

Additional Locations (1)

Fix in Cursor Fix in Web

@coderabbitai coderabbitai bot mentioned this pull request Jan 21, 2026
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